Skip to content

Commit 31ba814

Browse files
Label Sample-Code Call-to-Action buttons with "View Source" (#566)
Updates the logic for `@CallToAction` buttons that are attached to `sampleCode` pages and use the `link` purpose to render with a more specific “View Source” title instead of the more generic “Visit” title. This makes the intent of the button more clear. Resolves rdar://108013602
1 parent 88d0fd6 commit 31ba814

File tree

7 files changed

+122
-82
lines changed

7 files changed

+122
-82
lines changed

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
795795
action: .reference(
796796
identifier: downloadIdentifier,
797797
isActive: true,
798-
overridingTitle: callToAction.buttonLabel,
798+
overridingTitle: callToAction.buttonLabel(for: article.metadata?.pageKind?.kind),
799799
overridingTitleInlineContent: nil))
800800
externalLocationReferences[url.description] = ExternalLocationReference(identifier: downloadIdentifier)
801801
} else if let fileReference = callToAction.file,
@@ -804,7 +804,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
804804
node.sampleDownload = .init(action: .reference(
805805
identifier: downloadIdentifier,
806806
isActive: true,
807-
overridingTitle: callToAction.buttonLabel,
807+
overridingTitle: callToAction.buttonLabel(for: article.metadata?.pageKind?.kind),
808808
overridingTitleInlineContent: nil
809809
))
810810
}

Sources/SwiftDocC/Semantics/Metadata/CallToAction.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import Markdown
2525
/// The link text can also be specified in one of two ways:
2626
/// - The `purpose` parameter can be used to use a default button label. There are two valid values:
2727
/// - `download` indicates that the link is to a downloadable file. The button will be labeled "Download".
28-
/// - `link` indicates that the link is to an external webpage. The button will be labeled "Visit".
28+
/// - `link` indicates that the link is to an external webpage.
29+
///
30+
/// The button will be labeled "Visit" when used on article pages and "View Source" when used on sample code pages.
2931
/// - The `label` parameter specifies the literal text to use as the button label.
3032
///
3133
/// `@CallToAction` requires one of `url` or `path`, and one of `purpose` or `label`. Specifying both
@@ -79,11 +81,20 @@ public final class CallToAction: Semantic, AutomaticDirectiveConvertible {
7981

8082
/// The computed label for this Call to Action, whether provided directly via ``label`` or
8183
/// indirectly via ``purpose``.
84+
@available(*, deprecated, renamed: "buttonLabel(for:)")
8285
public var buttonLabel: String {
86+
return buttonLabel(for: nil)
87+
}
88+
89+
/// The label that should be used when rendering the user-interface for this call to action button.
90+
///
91+
/// This can be provided directly via the ``label`` parameter or indirectly via the given ``purpose`` and
92+
/// associated page kind.
93+
public func buttonLabel(for pageKind: Metadata.PageKind.Kind?) -> String {
8394
if let label = label {
8495
return label
8596
} else if let purpose = purpose {
86-
return purpose.defaultLabel
97+
return purpose.defaultLabel(for: pageKind)
8798
} else {
8899
// The `validate()` method ensures that this type should never be constructed without
89100
// one of the above.
@@ -178,12 +189,22 @@ extension CallToAction {
178189
extension CallToAction.Purpose {
179190
/// The label that will be applied to a Call to Action with this purpose if it doesn't provide
180191
/// a separate label.
192+
@available(*, deprecated, message: "Replaced with 'CallToAction.buttonLabel(for:)'.")
181193
public var defaultLabel: String {
194+
return defaultLabel(for: nil)
195+
}
196+
197+
fileprivate func defaultLabel(for pageKind: Metadata.PageKind.Kind?) -> String {
182198
switch self {
183199
case .download:
184200
return "Download"
185201
case .link:
186-
return "Visit"
202+
switch pageKind {
203+
case .article, .none:
204+
return "Visit"
205+
case .sampleCode:
206+
return "View Source"
207+
}
187208
}
188209
}
189210
}

Sources/docc/DocCDocumentation.docc/DocC.symbols.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,13 @@
817817
"text" : " - `download` indicates that the link is to a downloadable file. The button will be labeled \"Download\"."
818818
},
819819
{
820-
"text" : " - `link` indicates that the link is to an external webpage. The button will be labeled \"Visit\"."
820+
"text" : " - `link` indicates that the link is to an external webpage."
821+
},
822+
{
823+
"text" : ""
824+
},
825+
{
826+
"text" : " The button will be labeled \"Visit\" when used on article pages and \"View Source\" when used on sample code pages."
821827
},
822828
{
823829
"text" : "- The `label` parameter specifies the literal text to use as the button label."

Tests/SwiftDocCTests/Rendering/SampleDownloadTests.swift

Lines changed: 40 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,8 @@ class SampleDownloadTests: XCTestCase {
7575
}
7676

7777
func testParseSampleDownload() throws {
78-
let (bundle, context) = try testBundleAndContext(named: "SampleBundle")
79-
let reference = ResolvedTopicReference(
80-
bundleIdentifier: bundle.identifier,
81-
path: "/documentation/SampleBundle/MySample",
82-
sourceLanguage: .swift
83-
)
84-
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
85-
var translator = RenderNodeTranslator(
86-
context: context,
87-
bundle: bundle,
88-
identifier: reference,
89-
source: nil
90-
)
91-
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
78+
let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MySample")
79+
9280
let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload)
9381
guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else {
9482
XCTFail("Unexpected action in callToAction")
@@ -98,20 +86,8 @@ class SampleDownloadTests: XCTestCase {
9886
}
9987

10088
func testParseSampleLocalDownload() throws {
101-
let (bundle, context) = try testBundleAndContext(named: "SampleBundle")
102-
let reference = ResolvedTopicReference(
103-
bundleIdentifier: bundle.identifier,
104-
path: "/documentation/SampleBundle/MyLocalSample",
105-
sourceLanguage: .swift
106-
)
107-
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
108-
var translator = RenderNodeTranslator(
109-
context: context,
110-
bundle: bundle,
111-
identifier: reference,
112-
source: nil
113-
)
114-
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
89+
let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MyLocalSample")
90+
11591
let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload)
11692
guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else {
11793
XCTFail("Unexpected action in callToAction")
@@ -121,20 +97,7 @@ class SampleDownloadTests: XCTestCase {
12197
}
12298

12399
func testSampleDownloadRoundtrip() throws {
124-
let (bundle, context) = try testBundleAndContext(named: "SampleBundle")
125-
let reference = ResolvedTopicReference(
126-
bundleIdentifier: bundle.identifier,
127-
path: "/documentation/SampleBundle/MySample",
128-
sourceLanguage: .swift
129-
)
130-
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
131-
var translator = RenderNodeTranslator(
132-
context: context,
133-
bundle: bundle,
134-
identifier: reference,
135-
source: nil
136-
)
137-
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
100+
let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MySample")
138101

139102
let encoder = JSONEncoder()
140103
let decoder = JSONDecoder()
@@ -161,12 +124,12 @@ class SampleDownloadTests: XCTestCase {
161124

162125
XCTAssertEqual(origIdent, decodedIdent)
163126
}
164-
165-
func testSampleDownloadRelativeURL() throws {
127+
128+
private func renderNodeFromSampleBundle(at referencePath: String) throws -> RenderNode {
166129
let (bundle, context) = try testBundleAndContext(named: "SampleBundle")
167130
let reference = ResolvedTopicReference(
168131
bundleIdentifier: bundle.identifier,
169-
path: "/documentation/SampleBundle/RelativeURLSample",
132+
path: referencePath,
170133
sourceLanguage: .swift
171134
)
172135
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
@@ -176,7 +139,11 @@ class SampleDownloadTests: XCTestCase {
176139
identifier: reference,
177140
source: nil
178141
)
179-
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
142+
return try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
143+
}
144+
145+
func testSampleDownloadRelativeURL() throws {
146+
let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/RelativeURLSample")
180147
let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload)
181148
guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else {
182149
XCTFail("Unexpected action in callToAction")
@@ -197,20 +164,7 @@ class SampleDownloadTests: XCTestCase {
197164
}
198165

199166
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)
167+
let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/RelativeURLSample")
214168
let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload)
215169
guard case .reference(identifier: let ident, isActive: true, overridingTitle: "Download", overridingTitleInlineContent: nil) = sampleCodeDownload.action else {
216170
XCTFail("Unexpected action in callToAction")
@@ -260,4 +214,30 @@ class SampleDownloadTests: XCTestCase {
260214
XCTAssertEqual(firstJson, finalJson)
261215
}
262216
}
217+
218+
func testExternalLinkOnSampleCodePage() throws {
219+
let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MyExternalSample")
220+
let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload)
221+
guard case .reference(identifier: let identifier, isActive: true, overridingTitle: "View Source", overridingTitleInlineContent: nil) = sampleCodeDownload.action else {
222+
XCTFail("Unexpected action in callToAction")
223+
return
224+
}
225+
226+
XCTAssertEqual(identifier.identifier, "https://www.example.com/source-repository.git")
227+
let reference = try XCTUnwrap(renderNode.references[identifier.identifier])
228+
XCTAssert(reference is ExternalLocationReference)
229+
}
230+
231+
func testExternalLinkOnRegularArticlePage() throws {
232+
let renderNode = try renderNodeFromSampleBundle(at: "/documentation/SampleBundle/MyArticle")
233+
let sampleCodeDownload = try XCTUnwrap(renderNode.sampleDownload)
234+
guard case .reference(identifier: let identifier, isActive: true, overridingTitle: "Visit", overridingTitleInlineContent: nil) = sampleCodeDownload.action else {
235+
XCTFail("Unexpected action in callToAction")
236+
return
237+
}
238+
239+
XCTAssertEqual(identifier.identifier, "https://www.example.com")
240+
let reference = try XCTUnwrap(renderNode.references[identifier.identifier])
241+
XCTAssert(reference is ExternalLocationReference)
242+
}
263243
}

Tests/SwiftDocCTests/Semantics/CallToActionTests.swift

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,33 +137,47 @@ class CallToActionTests: XCTestCase {
137137
}
138138

139139
func testDefaultLabel() throws {
140-
func assertExpectedLabel(source: String, expectedLabel: String) throws {
140+
func assertExpectedLabel(source: String, expectedDefaultLabel: String, expectedSampleCodeLabel: String) throws {
141141
let document = Document(parsing: source, options: .parseBlockDirectives)
142-
let directive = document.child(at: 0) as? BlockDirective
143-
XCTAssertNotNil(directive)
142+
let directive = try XCTUnwrap(document.child(at: 0) as? BlockDirective)
144143

145144
let (bundle, context) = try testBundleAndContext(named: "SampleBundle")
146145

147-
directive.map { directive in
148-
var problems = [Problem]()
149-
XCTAssertEqual(CallToAction.directiveName, directive.name)
150-
let callToAction = CallToAction(from: directive, source: nil, for: bundle, in: context, problems: &problems)
151-
XCTAssertNotNil(callToAction)
152-
XCTAssert(problems.isEmpty)
153-
XCTAssertEqual(callToAction?.buttonLabel, expectedLabel)
154-
}
146+
var problems = [Problem]()
147+
XCTAssertEqual(CallToAction.directiveName, directive.name)
148+
let callToAction = try XCTUnwrap(CallToAction(from: directive, source: nil, for: bundle, in: context, problems: &problems))
149+
XCTAssert(problems.isEmpty)
150+
151+
XCTAssertEqual(callToAction.buttonLabel(for: nil), expectedDefaultLabel)
152+
XCTAssertEqual(callToAction.buttonLabel(for: .article), expectedDefaultLabel)
153+
XCTAssertEqual(callToAction.buttonLabel(for: .sampleCode), expectedSampleCodeLabel)
155154
}
156155

157-
var validLabels: [(arg: String, label: String)] = []
156+
var validLabels: [(arg: String, defaultLabel: String, sampleCodeLabel: String)] = []
158157
for buttonKind in CallToAction.Purpose.allCases {
159-
validLabels.append(("purpose: \(buttonKind)", buttonKind.defaultLabel))
158+
let expectedDefaultLabel: String
159+
let expectedSampleCodeLabel: String
160+
switch buttonKind {
161+
case .download:
162+
expectedDefaultLabel = "Download"
163+
expectedSampleCodeLabel = "Download"
164+
case .link:
165+
expectedDefaultLabel = "Visit"
166+
expectedSampleCodeLabel = "View Source"
167+
}
168+
169+
validLabels.append(("purpose: \(buttonKind)", expectedDefaultLabel, expectedSampleCodeLabel))
160170
// Ensure that adding a label argument overrides the kind's default label
161-
validLabels.append(("purpose: \(buttonKind), label: \"Button\"", "Button"))
171+
validLabels.append(("purpose: \(buttonKind), label: \"Button\"", "Button", "Button"))
162172
}
163173

164-
for (arg, label) in validLabels {
174+
for (arg, defaultLabel, sampleCodeLabel) in validLabels {
165175
let directive = "@CallToAction(file: \"Downloads/plus.svg\", \(arg))"
166-
try assertExpectedLabel(source: directive, expectedLabel: label)
176+
try assertExpectedLabel(
177+
source: directive,
178+
expectedDefaultLabel: defaultLabel,
179+
expectedSampleCodeLabel: sampleCodeLabel
180+
)
167181
}
168182
}
169183
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# MyArticle
2+
3+
@Metadata {
4+
@CallToAction(url: "https://www.example.com", purpose: link)
5+
}
6+
7+
Check out this cool website.
8+
9+
<!-- Copyright (c) 2023 Apple Inc and the Swift Project authors. All Rights Reserved. -->
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# MyExternalSample
2+
3+
@Metadata {
4+
@CallToAction(url: "https://www.example.com/source-repository.git", purpose: link)
5+
@PageKind(sampleCode)
6+
}
7+
8+
Check out my cool sample code project.
9+
10+
<!-- Copyright (c) 2023 Apple Inc and the Swift Project authors. All Rights Reserved. -->

0 commit comments

Comments
 (0)