Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions Sources/SwiftDocC/Model/DocumentationNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -447,10 +447,17 @@ public struct DocumentationNode {
for: SymbolGraph.Symbol.Location.self
)?.url()

for comment in docCommentDirectives {
let range = docCommentMarkup.child(at: comment.indexInParent)?.range
for directive in docCommentDirectives {
let range = docCommentMarkup.child(at: directive.indexInParent)?.range

guard BlockDirective.allKnownDirectiveNames.contains(comment.name) else {
// Don't throw a warning if a directive is not included in allKnownDirectiveNames
guard BlockDirective.allKnownDirectiveNames.contains(directive.name) else {
continue
}

// Renderable directives are processed like any other piece of structured markdown (tables, lists, etc.)
// and so are inherently supported in doc comments.
guard DirectiveIndex.shared.renderableDirectives[directive.name] == nil else {
continue
}

Expand All @@ -459,8 +466,8 @@ public struct DocumentationNode {
severity: .warning,
range: range,
identifier: "org.swift.docc.UnsupportedDocCommentDirective",
summary: "Directives are not supported in symbol source documentation",
explanation: "Found \(comment.name.singleQuoted) in \(symbol.absolutePath.singleQuoted)"
summary: "The \(directive.name.singleQuoted) directive is not supported in symbol source documentation",
explanation: "Found \(directive.name.singleQuoted) in \(symbol.absolutePath.singleQuoted)"
)

var problem = Problem(diagnostic: diagnostic, possibleSolutions: [])
Expand Down
38 changes: 5 additions & 33 deletions Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,40 +346,12 @@ struct RenderContentCompiler: MarkupVisitor {
}

mutating func visitBlockDirective(_ blockDirective: BlockDirective) -> [RenderContent] {
switch blockDirective.name {
case Snippet.directiveName:
guard let snippet = Snippet(from: blockDirective, for: bundle, in: context) else {
return []
}

guard let snippetReference = resolveSymbolReference(destination: snippet.path),
let snippetEntity = try? context.entity(with: snippetReference),
let snippetSymbol = snippetEntity.symbol,
let snippetMixin = snippetSymbol.mixins[SymbolGraph.Symbol.Snippet.mixinKey] as? SymbolGraph.Symbol.Snippet else {
return []
}

if let requestedSlice = snippet.slice,
let requestedLineRange = snippetMixin.slices[requestedSlice] {
// Render only the slice.
let lineRange = requestedLineRange.lowerBound..<min(requestedLineRange.upperBound, snippetMixin.lines.count)
let lines = snippetMixin.lines[lineRange]
let minimumIndentation = lines.map { $0.prefix { $0.isWhitespace }.count }.min() ?? 0
let trimmedLines = lines.map { String($0.dropFirst(minimumIndentation)) }
return [RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: trimmedLines, metadata: nil))]
} else {
// Render the whole snippet with its explanation content.
let docCommentContent = snippetEntity.markup.children.flatMap { self.visit($0) }
let code = RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: snippetMixin.lines, metadata: nil))
return docCommentContent + [code]
}
default:
guard let renderableDirective = DirectiveIndex.shared.renderableDirectives[blockDirective.name] else {
return []
}

return renderableDirective.render(blockDirective, with: &self)

guard let renderableDirective = DirectiveIndex.shared.renderableDirectives[blockDirective.name] else {
return []
}

return renderableDirective.render(blockDirective, with: &self)
}

func defaultVisit(_ markup: Markup) -> [RenderContent] {
Expand Down
31 changes: 31 additions & 0 deletions Sources/SwiftDocC/Semantics/Snippets/Snippet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import Foundation
import Markdown
import SymbolKit

public final class Snippet: Semantic, AutomaticDirectiveConvertible {
public let originalMarkup: BlockDirective
Expand Down Expand Up @@ -45,3 +46,33 @@ public final class Snippet: Semantic, AutomaticDirectiveConvertible {
return true
}
}

extension Snippet: RenderableDirectiveConvertible {
func render(with contentCompiler: inout RenderContentCompiler) -> [RenderContent] {
guard let snippet = Snippet(from: originalMarkup, for: contentCompiler.bundle, in: contentCompiler.context) else {
return []
}

guard let snippetReference = contentCompiler.resolveSymbolReference(destination: snippet.path),
let snippetEntity = try? contentCompiler.context.entity(with: snippetReference),
let snippetSymbol = snippetEntity.symbol,
let snippetMixin = snippetSymbol.mixins[SymbolGraph.Symbol.Snippet.mixinKey] as? SymbolGraph.Symbol.Snippet else {
return []
}

if let requestedSlice = snippet.slice,
let requestedLineRange = snippetMixin.slices[requestedSlice] {
// Render only the slice.
let lineRange = requestedLineRange.lowerBound..<min(requestedLineRange.upperBound, snippetMixin.lines.count)
let lines = snippetMixin.lines[lineRange]
let minimumIndentation = lines.map { $0.prefix { $0.isWhitespace }.count }.min() ?? 0
let trimmedLines = lines.map { String($0.dropFirst(minimumIndentation)) }
return [RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: trimmedLines, metadata: nil))]
} else {
// Render the whole snippet with its explanation content.
let docCommentContent = snippetEntity.markup.children.flatMap { contentCompiler.visit($0) }
let code = RenderBlockContent.codeListing(.init(syntax: snippetMixin.language, code: snippetMixin.lines, metadata: nil))
return docCommentContent + [code]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ extension BlockDirective {
TechnologyRoot.directiveName,
TechnologyRoot.directiveName,
Tile.directiveName,
TitleHeading.directiveName,
Tutorial.directiveName,
TutorialArticle.directiveName,
TutorialReference.directiveName,
Expand Down
6 changes: 3 additions & 3 deletions Tests/SwiftDocCTests/Diagnostics/DiagnosticTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,15 @@ class DiagnosticTests: XCTestCase {
let commentWithKnownDirective = """
Brief description of this method

@Image(source: "my-sloth-image.png", alt: "An illustration of a sleeping sloth.")
@TitleHeading("Fancy Type of Article")
@returns Description of return value
"""
let symbolWithKnownDirective = createTestSymbol(commentText: commentWithKnownDirective)
let engine1 = DiagnosticEngine()

let _ = DocumentationNode.contentFrom(documentedSymbol: symbolWithKnownDirective, documentationExtension: nil, engine: engine1)

// count should 1 for the known directive '@Image'
// count should be 1 for the known directive '@TitleHeading'
// TODO: Consider adding a diagnostic for Doxygen tags (rdar://92184094)
XCTAssertEqual(engine1.problems.count, 1)
XCTAssertEqual(engine1.problems.map { $0.diagnostic.identifier }, ["org.swift.docc.UnsupportedDocCommentDirective"])
Expand Down
42 changes: 42 additions & 0 deletions Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,48 @@ class RenderNodeTranslatorTests: XCTestCase {
XCTAssertEqual(l.syntax, "swift")
XCTAssertEqual(l.code, ["func foo() {}"])
}

func testNestedSnippetSliceToCodeListing() throws {
let (bundle, context) = try testBundleAndContext(named: "Snippets")
let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/Snippets/Snippets", sourceLanguage: .swift)
let article = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference, source: nil)
let renderNode = try XCTUnwrap(translator.visitArticle(article) as? RenderNode)
let discussion = try XCTUnwrap(renderNode.primaryContentSections.first(where: { $0.kind == .content }) as? ContentRenderSection)

let lastCodeListingIndex = try XCTUnwrap(discussion.content.indices.last {
guard case .codeListing = discussion.content[$0] else {
return false
}
return true
})

if case let .paragraph(p) = discussion.content.dropFirst(2).first {
XCTAssertEqual(p.inlineContent, [.text("Does a foo.")])
} else {
XCTFail("Unexpected content where snippet explanation should be.")
}

if case let .codeListing(l) = discussion.content.dropFirst(3).first {
XCTAssertEqual(l.syntax, "swift")
XCTAssertEqual(l.code.joined(separator: "\n"), """
func foo() {}

do {
middle()
}

func bar() {}
""")
} else {
XCTFail("Missing snippet code block")
}

guard case .codeListing(_) = discussion.content[lastCodeListingIndex] else {
XCTFail("Missing snippet TabNavigator code block")
return
}
}

func testSnippetSliceTrimsIndentation() throws {
let (bundle, context) = try testBundleAndContext(named: "Snippets")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class DirectiveIndexTests: XCTestCase {
"Links",
"Row",
"Small",
"Snippet",
"TabNavigator",
"Video",
]
Expand Down
12 changes: 11 additions & 1 deletion Tests/SwiftDocCTests/Semantics/Reference/TabNavigatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,23 @@ class TabNavigatorTests: XCTestCase {
@Small {
Hey but small.
}

@Snippet(path: "Snippets/Snippets/MySnippet")
}
}
"""
}

XCTAssertNotNil(tabNavigator)
XCTAssertEqual(problems, [])

// UnresolvedTopicReference warning expected since the reference to the snippet "Snippets/Snippets/MySnippet"
// should fail to resolve here and then nothing would be added to the content.
XCTAssertEqual(
problems,
["23: warning – org.swift.docc.unresolvedTopicReference"]
)



XCTAssertEqual(renderBlockContent.count, 1)
XCTAssertEqual(
Expand Down
22 changes: 17 additions & 5 deletions Tests/SwiftDocCTests/Semantics/SymbolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ class SymbolTests: XCTestCase {
XCTAssertEqual(withRedirectInArticle.redirects?.map { $0.oldPath.absoluteString }, ["some/previous/path/to/this/symbol"])
}

func testWarningWhenDocCommentContainsDirective() throws {
func testWarningWhenDocCommentContainsUnsupportedDirective() throws {
let (withRedirectInArticle, problems) = try makeDocumentationNodeSymbol(
docComment: """
A cool API to call.
Expand All @@ -493,11 +493,25 @@ class SymbolTests: XCTestCase {
)
XCTAssertFalse(problems.isEmpty)
XCTAssertEqual(withRedirectInArticle.redirects, nil)

XCTAssertEqual(problems.first?.diagnostic.identifier, "org.swift.docc.UnsupportedDocCommentDirective")
XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.line, 3)
XCTAssertEqual(problems.first?.diagnostic.range?.lowerBound.column, 1)
}

func testNoWarningWhenDocCommentContainsDirective() throws {
let (_, problems) = try makeDocumentationNodeSymbol(
docComment: """
A cool API to call.

@Snippet(from: "Snippets/Snippets/MySnippet")
""",
articleContent: """
# This is my article
"""
)
XCTAssertTrue(problems.isEmpty)
}

func testNoWarningWhenDocCommentContainsDoxygen() throws {
let tempURL = try createTemporaryDirectory()
Expand Down Expand Up @@ -1116,9 +1130,7 @@ class SymbolTests: XCTestCase {

let engine = DiagnosticEngine()
let _ = DocumentationNode.contentFrom(documentedSymbol: symbol, documentationExtension: nil, engine: engine)
XCTAssertEqual(engine.problems.count, 1)
let problem = try XCTUnwrap(engine.problems.first)
XCTAssertEqual(problem.diagnostic.source?.path, "/path/to/my file.swift")
XCTAssertEqual(engine.problems.count, 0)
}

// MARK: - Helpers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ This is the abstract of my article. Nice!
}
}

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

@Small {
Copyright (c) 2022 Apple Inc and the Swift Project authors. All Rights Reserved.
}
Expand Down
31 changes: 30 additions & 1 deletion Tests/SwiftDocCTests/Test Bundles/Snippets.docc/Snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,33 @@ This is a slice of the above snippet, called "foo".

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

<!-- Copyright (c) 2022 Apple Inc and the Swift Project authors. All Rights Reserved. -->
This is a snippet nested inside a tab navigator.

@TabNavigator {
@Tab("hi") {
@Row {
@Column {
Hello!
}

@Column {
Hello there!
}
}

Hello there.
}

@Tab("hey") {
Hey there.

@Small {
Hey but small.
}

@Snippet(path: "Snippets/Snippets/MySnippet") {}
}
}


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