Skip to content

Commit bc30e78

Browse files
make CallToAction url arguments encode verbatim in Render JSON (#467)
rdar://104558809
1 parent 6bc9eb8 commit bc30e78

File tree

5 files changed

+167
-7
lines changed

5 files changed

+167
-7
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2023 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
/// A specialized ``DownloadReference`` used for references to external links.
14+
///
15+
/// `@CallToAction` directives can link either to a local file or to a URL, whether relative or
16+
/// absolute. Directives that use the `file` argument will create a ``DownloadReference`` and copy
17+
/// the file from the catalog into the resulting archive.
18+
///
19+
/// An `ExternalLocationReference` is intended to encode to Render JSON compatible with a
20+
/// ``DownloadReference``, but with the `url` set to the text given in the `@CallToAction`'s `url`
21+
/// argument.
22+
public struct ExternalLocationReference: RenderReference, URLReference {
23+
public static var baseURL: URL = DownloadReference.baseURL
24+
25+
public private(set) var type: RenderReferenceType = .download
26+
27+
public var identifier: RenderReferenceIdentifier
28+
29+
enum CodingKeys: String, CodingKey {
30+
case type
31+
case identifier
32+
case url
33+
}
34+
35+
public init(identifier: RenderReferenceIdentifier) {
36+
self.identifier = identifier
37+
}
38+
39+
public init(from decoder: Decoder) throws {
40+
let container = try decoder.container(keyedBy: CodingKeys.self)
41+
42+
self.identifier = try container.decode(RenderReferenceIdentifier.self, forKey: .identifier)
43+
self.type = try container.decode(RenderReferenceType.self, forKey: .type)
44+
}
45+
46+
public func encode(to encoder: Encoder) throws {
47+
var container = encoder.container(keyedBy: CodingKeys.self)
48+
try container.encode(type.rawValue, forKey: .type)
49+
try container.encode(identifier, forKey: .identifier)
50+
51+
// Enter the given URL verbatim into the Render JSON
52+
try container.encode(identifier.identifier, forKey: .url)
53+
}
54+
}

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -789,11 +789,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
789789
isActive: true,
790790
overridingTitle: callToAction.buttonLabel,
791791
overridingTitleInlineContent: nil))
792-
downloadReferences[url.description] = DownloadReference(
793-
identifier: downloadIdentifier,
794-
renderURL: url,
795-
checksum: nil
796-
)
792+
externalLocationReferences[url.description] = ExternalLocationReference(identifier: downloadIdentifier)
797793
} else if let fileReference = callToAction.file {
798794
let downloadIdentifier = createAndRegisterRenderReference(forMedia: fileReference, assetContext: .download)
799795
node.sampleDownload = .init(action: .reference(
@@ -830,6 +826,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
830826
addReferences(videoReferences, to: &node)
831827
addReferences(linkReferences, to: &node)
832828
addReferences(downloadReferences, to: &node)
829+
addReferences(externalLocationReferences, to: &node)
833830
// See Also can contain external links, we need to separately transfer
834831
// link references from the content compiler
835832
addReferences(contentCompiler.linkReferences, to: &node)
@@ -1626,6 +1623,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
16261623
var linkReferences: [String: LinkReference] = [:]
16271624
var requirementReferences: [String: XcodeRequirementReference] = [:]
16281625
var downloadReferences: [String: DownloadReference] = [:]
1626+
var externalLocationReferences: [String: ExternalLocationReference] = [:]
16291627

16301628
private var bundleAvailability: [BundleModuleIdentifier: [AvailabilityRenderItem]] = [:]
16311629

Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,103 @@ class SampleDownloadTests: XCTestCase {
161161

162162
XCTAssertEqual(origIdent, decodedIdent)
163163
}
164-
164+
165+
func testSampleDownloadRelativeURL() throws {
166+
let (bundle, context) = try testBundleAndContext(named: "SampleBundle")
167+
let reference = ResolvedTopicReference(
168+
bundleIdentifier: bundle.identifier,
169+
path: "/documentation/SampleBundle/RelativeURLSample",
170+
sourceLanguage: .swift
171+
)
172+
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
173+
var translator = RenderNodeTranslator(
174+
context: context,
175+
bundle: bundle,
176+
identifier: reference,
177+
source: nil
178+
)
179+
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
180+
let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload)
181+
guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else {
182+
XCTFail("Unexpected action in callToAction")
183+
return
184+
}
185+
XCTAssertEqual(ident.identifier, "files/ExternalSample.zip")
186+
187+
// Ensure that the encoded URL still references the entered URL
188+
let downloadReference = try XCTUnwrap(renderNode.references[ident.identifier] as? ExternalLocationReference)
189+
190+
let encoder = JSONEncoder()
191+
let decoder = JSONDecoder()
192+
193+
let encodedReference = try encoder.encode(downloadReference)
194+
let decodedReference = try decoder.decode(DownloadReference.self, from: encodedReference)
195+
196+
XCTAssertEqual(decodedReference.url.description, "files/ExternalSample.zip")
197+
}
198+
199+
func testExternalLocationRoundtrip() throws {
200+
let (bundle, context) = try testBundleAndContext(named: "SampleBundle")
201+
let reference = ResolvedTopicReference(
202+
bundleIdentifier: bundle.identifier,
203+
path: "/documentation/SampleBundle/RelativeURLSample",
204+
sourceLanguage: .swift
205+
)
206+
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
207+
var translator = RenderNodeTranslator(
208+
context: context,
209+
bundle: bundle,
210+
identifier: reference,
211+
source: nil
212+
)
213+
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
214+
let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload)
215+
guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else {
216+
XCTFail("Unexpected action in callToAction")
217+
return
218+
}
219+
XCTAssertEqual(ident.identifier, "files/ExternalSample.zip")
220+
221+
// Make sure that the ExternalLocationReference we get can round-trip as itself as well as through a DownloadReference
222+
let downloadReference = try XCTUnwrap(renderNode.references[ident.identifier] as? ExternalLocationReference)
223+
224+
let encoder = JSONEncoder()
225+
encoder.outputFormatting.insert(.sortedKeys)
226+
let decoder = JSONDecoder()
227+
228+
let encodedReference = try encoder.encode(downloadReference)
229+
230+
// ExternalLocationReference -> ExternalLocationReference
231+
// The encoded JSON should be the same before and after re-encoding.
232+
do {
233+
let decodedReference = try decoder.decode(ExternalLocationReference.self, from: encodedReference)
234+
let reEncodedReference = try encoder.encode(decodedReference)
235+
236+
let firstJson = String(data: encodedReference, encoding: .utf8)
237+
let finalJson = String(data: reEncodedReference, encoding: .utf8)
238+
239+
XCTAssertEqual(firstJson, finalJson)
240+
}
241+
242+
// ExternalLocationReference -> DownloadReference -> ExternalLocationReference
243+
// The reference identifier should be the same all throughout, and the final ExternalLocationReference
244+
// should encode to the same JSON as the initial reference.
245+
do {
246+
let decodedReference = try decoder.decode(DownloadReference.self, from: encodedReference)
247+
248+
XCTAssertEqual(decodedReference.identifier, downloadReference.identifier)
249+
250+
let encodedDownload = try encoder.encode(decodedReference)
251+
let reDecodedReference = try decoder.decode(ExternalLocationReference.self, from: encodedDownload)
252+
253+
XCTAssertEqual(reDecodedReference.identifier, downloadReference.identifier)
254+
255+
let reEncodedReference = try encoder.encode(reDecodedReference)
256+
257+
let firstJson = String(data: encodedReference, encoding: .utf8)
258+
let finalJson = String(data: reEncodedReference, encoding: .utf8)
259+
260+
XCTAssertEqual(firstJson, finalJson)
261+
}
262+
}
165263
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Relative URL Sample
2+
3+
@Metadata {
4+
@CallToAction(url: "files/ExternalSample.zip", purpose: download)
5+
}
6+
7+
This sample references a file on the web server.
8+
9+
<!-- Copyright (c) 2023 Apple Inc and the Swift Project authors. All Rights Reserved. -->

Tests/SwiftDocCTests/Test Bundles/SampleBundle.docc/SomeSample.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ This is a great framework, I tell you what.
1212

1313
- <doc:MySample>
1414
- <doc:MyLocalSample>
15+
- <doc:RelativeURLSample>
1516

16-
<!-- Copyright (c) 2022 Apple Inc and the Swift Project authors. All Rights Reserved. -->
17+
<!-- Copyright (c) 2022-2023 Apple Inc and the Swift Project authors. All Rights Reserved. -->

0 commit comments

Comments
 (0)