Skip to content

Commit d2bf7d6

Browse files
authored
Allow certain directives (#641)
Allow certain directives in documentation comments rdar://111414385
1 parent b549f1d commit d2bf7d6

File tree

11 files changed

+147
-48
lines changed

11 files changed

+147
-48
lines changed

Sources/SwiftDocC/Model/DocumentationNode.swift

+15-5
Original file line numberDiff line numberDiff line change
@@ -447,10 +447,20 @@ public struct DocumentationNode {
447447
for: SymbolGraph.Symbol.Location.self
448448
)?.url()
449449

450-
for comment in docCommentDirectives {
451-
let range = docCommentMarkup.child(at: comment.indexInParent)?.range
450+
for directive in docCommentDirectives {
451+
let range = docCommentMarkup.child(at: directive.indexInParent)?.range
452452

453-
guard BlockDirective.allKnownDirectiveNames.contains(comment.name) else {
453+
// Only throw warnings for known directive names.
454+
//
455+
// This is important so that we avoid throwing warnings when building
456+
// Objective-C/C documentation that includes doxygen commands.
457+
guard BlockDirective.allKnownDirectiveNames.contains(directive.name) else {
458+
continue
459+
}
460+
461+
// Renderable directives are processed like any other piece of structured markdown (tables, lists, etc.)
462+
// and so are inherently supported in doc comments.
463+
guard DirectiveIndex.shared.renderableDirectives[directive.name] == nil else {
454464
continue
455465
}
456466

@@ -459,8 +469,8 @@ public struct DocumentationNode {
459469
severity: .warning,
460470
range: range,
461471
identifier: "org.swift.docc.UnsupportedDocCommentDirective",
462-
summary: "Directives are not supported in symbol source documentation",
463-
explanation: "Found \(comment.name.singleQuoted) in \(symbol.absolutePath.singleQuoted)"
472+
summary: "The \(directive.name.singleQuoted) directive is not supported in symbol source documentation",
473+
explanation: "Found \(directive.name.singleQuoted) in \(symbol.absolutePath.singleQuoted)"
464474
)
465475

466476
var problem = Problem(diagnostic: diagnostic, possibleSolutions: [])

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

+5-33
Original file line numberDiff line numberDiff line change
@@ -346,40 +346,12 @@ struct RenderContentCompiler: MarkupVisitor {
346346
}
347347

348348
mutating func visitBlockDirective(_ blockDirective: BlockDirective) -> [RenderContent] {
349-
switch blockDirective.name {
350-
case Snippet.directiveName:
351-
guard let snippet = Snippet(from: blockDirective, for: bundle, in: context) else {
352-
return []
353-
}
354-
355-
guard let snippetReference = resolveSymbolReference(destination: snippet.path),
356-
let snippetEntity = try? context.entity(with: snippetReference),
357-
let snippetSymbol = snippetEntity.symbol,
358-
let snippetMixin = snippetSymbol.mixins[SymbolGraph.Symbol.Snippet.mixinKey] as? SymbolGraph.Symbol.Snippet else {
359-
return []
360-
}
361-
362-
if let requestedSlice = snippet.slice,
363-
let requestedLineRange = snippetMixin.slices[requestedSlice] {
364-
// Render only the slice.
365-
let lineRange = requestedLineRange.lowerBound..<min(requestedLineRange.upperBound, snippetMixin.lines.count)
366-
let lines = snippetMixin.lines[lineRange]
367-
let minimumIndentation = lines.map { $0.prefix { $0.isWhitespace }.count }.min() ?? 0
368-
let trimmedLines = lines.map { String($0.dropFirst(minimumIndentation)) }
369-
return [RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: trimmedLines, metadata: nil))]
370-
} else {
371-
// Render the whole snippet with its explanation content.
372-
let docCommentContent = snippetEntity.markup.children.flatMap { self.visit($0) }
373-
let code = RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: snippetMixin.lines, metadata: nil))
374-
return docCommentContent + [code]
375-
}
376-
default:
377-
guard let renderableDirective = DirectiveIndex.shared.renderableDirectives[blockDirective.name] else {
378-
return []
379-
}
380-
381-
return renderableDirective.render(blockDirective, with: &self)
349+
350+
guard let renderableDirective = DirectiveIndex.shared.renderableDirectives[blockDirective.name] else {
351+
return []
382352
}
353+
354+
return renderableDirective.render(blockDirective, with: &self)
383355
}
384356

385357
func defaultVisit(_ markup: Markup) -> [RenderContent] {

Sources/SwiftDocC/Semantics/Snippets/Snippet.swift

+31
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import Foundation
1212
import Markdown
13+
import SymbolKit
1314

1415
public final class Snippet: Semantic, AutomaticDirectiveConvertible {
1516
public let originalMarkup: BlockDirective
@@ -45,3 +46,33 @@ public final class Snippet: Semantic, AutomaticDirectiveConvertible {
4546
return true
4647
}
4748
}
49+
50+
extension Snippet: RenderableDirectiveConvertible {
51+
func render(with contentCompiler: inout RenderContentCompiler) -> [RenderContent] {
52+
guard let snippet = Snippet(from: originalMarkup, for: contentCompiler.bundle, in: contentCompiler.context) else {
53+
return []
54+
}
55+
56+
guard let snippetReference = contentCompiler.resolveSymbolReference(destination: snippet.path),
57+
let snippetEntity = try? contentCompiler.context.entity(with: snippetReference),
58+
let snippetSymbol = snippetEntity.symbol,
59+
let snippetMixin = snippetSymbol.mixins[SymbolGraph.Symbol.Snippet.mixinKey] as? SymbolGraph.Symbol.Snippet else {
60+
return []
61+
}
62+
63+
if let requestedSlice = snippet.slice,
64+
let requestedLineRange = snippetMixin.slices[requestedSlice] {
65+
// Render only the slice.
66+
let lineRange = requestedLineRange.lowerBound..<min(requestedLineRange.upperBound, snippetMixin.lines.count)
67+
let lines = snippetMixin.lines[lineRange]
68+
let minimumIndentation = lines.map { $0.prefix { $0.isWhitespace }.count }.min() ?? 0
69+
let trimmedLines = lines.map { String($0.dropFirst(minimumIndentation)) }
70+
return [RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: trimmedLines, metadata: nil))]
71+
} else {
72+
// Render the whole snippet with its explanation content.
73+
let docCommentContent = snippetEntity.markup.children.flatMap { contentCompiler.visit($0) }
74+
let code = RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: snippetMixin.lines, metadata: nil))
75+
return docCommentContent + [code]
76+
}
77+
}
78+
}

Sources/SwiftDocC/Utility/MarkupExtensions/BlockDirectiveExtensions.swift

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ extension BlockDirective {
4545
TechnologyRoot.directiveName,
4646
TechnologyRoot.directiveName,
4747
Tile.directiveName,
48+
TitleHeading.directiveName,
4849
Tutorial.directiveName,
4950
TutorialArticle.directiveName,
5051
TutorialReference.directiveName,

Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,15 @@ class DiagnosticTests: XCTestCase {
185185
let commentWithKnownDirective = """
186186
Brief description of this method
187187
188-
@Image(source: "my-sloth-image.png", alt: "An illustration of a sleeping sloth.")
188+
@TitleHeading("Fancy Type of Article")
189189
@returns Description of return value
190190
"""
191191
let symbolWithKnownDirective = createTestSymbol(commentText: commentWithKnownDirective)
192192
let engine1 = DiagnosticEngine()
193193

194194
let _ = DocumentationNode.contentFrom(documentedSymbol: symbolWithKnownDirective, documentationExtension: nil, engine: engine1)
195-
196-
// count should 1 for the known directive '@Image'
195+
196+
// count should be 1 for the known directive '@TitleHeading'
197197
// TODO: Consider adding a diagnostic for Doxygen tags (rdar://92184094)
198198
XCTAssertEqual(engine1.problems.count, 1)
199199
XCTAssertEqual(engine1.problems.map { $0.diagnostic.identifier }, ["org.swift.docc.UnsupportedDocCommentDirective"])

Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift

+31
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,37 @@ class RenderNodeTranslatorTests: XCTestCase {
10141014
XCTAssertEqual(l.syntax, "swift")
10151015
XCTAssertEqual(l.code, ["func foo() {}"])
10161016
}
1017+
1018+
func testNestedSnippetSliceToCodeListing() throws {
1019+
let (bundle, context) = try testBundleAndContext(named: "Snippets")
1020+
let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/Snippets/Snippets", sourceLanguage: .swift)
1021+
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
1022+
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference, source: nil)
1023+
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
1024+
let discussion = try XCTUnwrap(renderNode.primaryContentSections.first(where: { $0.kind == .content }) as? ContentRenderSection)
1025+
1026+
let lastTabNavigator = try XCTUnwrap(discussion.content.indices.last {
1027+
guard case .tabNavigator = discussion.content[$0] else {
1028+
return false
1029+
}
1030+
return true
1031+
})
1032+
1033+
guard case let .tabNavigator(t) = discussion.content[lastTabNavigator] else {
1034+
XCTFail("Missing snippet slice code block")
1035+
return
1036+
}
1037+
1038+
let codeListing = t.tabs.last?.content.last
1039+
1040+
guard case let .codeListing(l) = codeListing else {
1041+
XCTFail("Missing nested snippet inside TabNavigator")
1042+
return
1043+
}
1044+
1045+
XCTAssertEqual(l.syntax, "swift")
1046+
XCTAssertEqual(l.code, ["middle()"])
1047+
}
10171048

10181049
func testSnippetSliceTrimsIndentation() throws {
10191050
let (bundle, context) = try testBundleAndContext(named: "Snippets")

Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveIndexTests.swift

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class DirectiveIndexTests: XCTestCase {
6666
"Links",
6767
"Row",
6868
"Small",
69+
"Snippet",
6970
"TabNavigator",
7071
"Video",
7172
]

Tests/SwiftDocCTests/Semantics/Reference/TabNavigatorTests.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,23 @@ class TabNavigatorTests: XCTestCase {
151151
@Small {
152152
Hey but small.
153153
}
154+
155+
@Snippet(path: "Snippets/Snippets/MySnippet")
154156
}
155157
}
156158
"""
157159
}
158160

159161
XCTAssertNotNil(tabNavigator)
160-
XCTAssertEqual(problems, [])
162+
163+
// UnresolvedTopicReference warning expected since the reference to the snippet "Snippets/Snippets/MySnippet"
164+
// should fail to resolve here and then nothing would be added to the content.
165+
XCTAssertEqual(
166+
problems,
167+
["23: warning – org.swift.docc.unresolvedTopicReference"]
168+
)
169+
170+
161171

162172
XCTAssertEqual(renderBlockContent.count, 1)
163173
XCTAssertEqual(

Tests/SwiftDocCTests/Semantics/SymbolTests.swift

+17-5
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ class SymbolTests: XCTestCase {
480480
XCTAssertEqual(withRedirectInArticle.redirects?.map { $0.oldPath.absoluteString }, ["some/previous/path/to/this/symbol"])
481481
}
482482

483-
func testWarningWhenDocCommentContainsDirective() throws {
483+
func testWarningWhenDocCommentContainsUnsupportedDirective() throws {
484484
let (withRedirectInArticle, problems) = try makeDocumentationNodeSymbol(
485485
docComment: """
486486
A cool API to call.
@@ -493,11 +493,25 @@ class SymbolTests: XCTestCase {
493493
)
494494
XCTAssertFalse(problems.isEmpty)
495495
XCTAssertEqual(withRedirectInArticle.redirects, nil)
496-
496+
497497
XCTAssertEqual(problems.first?.diagnostic.identifier, "org.swift.docc.UnsupportedDocCommentDirective")
498498
XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.line, 3)
499499
XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.column, 1)
500500
}
501+
502+
func testNoWarningWhenDocCommentContainsDirective() throws {
503+
let (_, problems) = try makeDocumentationNodeSymbol(
504+
docComment: """
505+
A cool API to call.
506+
507+
@Snippet(from: "Snippets/Snippets/MySnippet")
508+
""",
509+
articleContent: """
510+
# This is my article
511+
"""
512+
)
513+
XCTAssertTrue(problems.isEmpty)
514+
}
501515

502516
func testNoWarningWhenDocCommentContainsDoxygen() throws {
503517
let tempURL = try createTemporaryDirectory()
@@ -1116,9 +1130,7 @@ class SymbolTests: XCTestCase {
11161130

11171131
let engine = DiagnosticEngine()
11181132
let _ = DocumentationNode.contentFrom(documentedSymbol: symbol, documentationExtension: nil, engine: engine)
1119-
XCTAssertEqual(engine.problems.count, 1)
1120-
let problem = try XCTUnwrap(engine.problems.first)
1121-
XCTAssertEqual(problem.diagnostic.source?.path, "/path/to/my file.swift")
1133+
XCTAssertEqual(engine.problems.count, 0)
11221134
}
11231135

11241136
// MARK: - Helpers

Tests/SwiftDocCTests/Test Bundles/BookLikeContent.docc/MyArticle.md

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ This is the abstract of my article. Nice!
3434
}
3535
}
3636

37+
@Snippet(path: "Snippets/Snippets/MySnippet", slice: "foo")
38+
3739
@Small {
3840
Copyright (c) 2022 Apple Inc and the Swift Project authors. All Rights Reserved.
3941
}

Tests/SwiftDocCTests/Test Bundles/Snippets.docc/Snippets.md

+30-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,33 @@ This is a slice of the above snippet, called "foo".
1010

1111
@Snippet(path: "Snippets/Snippets/MySnippet", slice: "foo")
1212

13-
<!-- Copyright (c) 2022 Apple Inc and the Swift Project authors. All Rights Reserved. -->
13+
This is a snippet nested inside a tab navigator.
14+
15+
@TabNavigator {
16+
@Tab("hi") {
17+
@Row {
18+
@Column {
19+
Hello!
20+
}
21+
22+
@Column {
23+
Hello there!
24+
}
25+
}
26+
27+
Hello there.
28+
}
29+
30+
@Tab("hey") {
31+
Hey there.
32+
33+
@Small {
34+
Hey but small.
35+
}
36+
37+
@Snippet(path: "Snippets/Snippets/MySnippet", slice: "middle") {}
38+
}
39+
}
40+
41+
42+
<!-- Copyright (c) 2023 Apple Inc and the Swift Project authors. All Rights Reserved. -->

0 commit comments

Comments
 (0)