Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow certain directives #641

Merged
merged 16 commits into from Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 15 additions & 5 deletions Sources/SwiftDocC/Model/DocumentationNode.swift
Expand Up @@ -447,10 +447,20 @@ 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 {
// Only throw warnings for known directive names.
//
// This is important so that we avoid throwing warnings when building
// Objective-C/C documentation that includes doxygen commands.
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 {
emilyychenn marked this conversation as resolved.
Show resolved Hide resolved
continue
}

Expand All @@ -459,8 +469,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
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
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]
}
}
}
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
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
31 changes: 31 additions & 0 deletions Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift
Expand Up @@ -1014,6 +1014,37 @@ 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 lastTabNavigator = try XCTUnwrap(discussion.content.indices.last {
guard case .tabNavigator = discussion.content[$0] else {
return false
}
return true
})

guard case let .tabNavigator(t) = discussion.content[lastTabNavigator] else {
XCTFail("Missing snippet slice code block")
return
}

let codeListing = t.tabs.last?.content.last

guard case let .codeListing(l) = codeListing else {
XCTFail("Missing nested snippet inside TabNavigator")
return
}

XCTAssertEqual(l.syntax, "swift")
XCTAssertEqual(l.code, ["middle()"])
}

func testSnippetSliceTrimsIndentation() throws {
let (bundle, context) = try testBundleAndContext(named: "Snippets")
Expand Down
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
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"]
emilyychenn marked this conversation as resolved.
Show resolved Hide resolved
)



XCTAssertEqual(renderBlockContent.count, 1)
XCTAssertEqual(
Expand Down
22 changes: 17 additions & 5 deletions Tests/SwiftDocCTests/Semantics/SymbolTests.swift
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
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
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", slice: "middle") {}
}
}


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