Skip to content

Commit 49b9491

Browse files
authored
Support writing links in synthesized technology root pages (#980) (#989)
1 parent 582cfc9 commit 49b9491

File tree

2 files changed

+178
-12
lines changed

2 files changed

+178
-12
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,21 +1871,47 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
18711871
/// - bundle: The bundle containing the articles.
18721872
private func synthesizeArticleOnlyRootPage(articles: inout [DocumentationContext.SemanticResult<Article>], bundle: DocumentationBundle) {
18731873
let title = bundle.displayName
1874-
let metadataDirectiveMarkup = BlockDirective(name: "Metadata", children: [
1875-
BlockDirective(name: "TechnologyRoot", children: [])
1876-
])
1877-
let metadata = Metadata(from: metadataDirectiveMarkup, for: bundle, in: self)
1874+
1875+
// An inner helper function to register a new root node from an article
1876+
func registerAsNewRootNode(_ articleResult: SemanticResult<Article>) {
1877+
uncuratedArticles.removeValue(forKey: articleResult.topicGraphNode.reference)
1878+
let title = articleResult.source.deletingPathExtension().lastPathComponent
1879+
// Create a new root-looking reference
1880+
let reference = ResolvedTopicReference(
1881+
bundleIdentifier: bundle.identifier,
1882+
path: NodeURLGenerator.Path.documentation(path: title).stringValue,
1883+
sourceLanguages: [DocumentationContext.defaultLanguage(in: nil /* article-only content has no source language information */)]
1884+
)
1885+
// Add the technology root to the article's metadata
1886+
let metadataMarkup: BlockDirective
1887+
if let markup = articleResult.value.metadata?.originalMarkup as? BlockDirective {
1888+
assert(!markup.children.contains(where: { ($0 as? BlockDirective)?.name == "TechnologyRoot" }),
1889+
"Nothing should try to synthesize a root page if there's already an explicit authored root page")
1890+
metadataMarkup = markup.withUncheckedChildren(
1891+
markup.children + [BlockDirective(name: "TechnologyRoot", children: [])]
1892+
) as! BlockDirective
1893+
} else {
1894+
metadataMarkup = BlockDirective(name: "Metadata", children: [
1895+
BlockDirective(name: "TechnologyRoot", children: [])
1896+
])
1897+
}
1898+
let article = Article(
1899+
markup: articleResult.value.markup,
1900+
metadata: Metadata(from: metadataMarkup, for: bundle, in: self),
1901+
redirects: articleResult.value.redirects,
1902+
options: articleResult.value.options
1903+
)
1904+
1905+
let graphNode = TopicGraph.Node(reference: reference, kind: .module, source: articleResult.topicGraphNode.source, title: title)
1906+
registerRootPages(from: [.init(value: article, source: articleResult.source, topicGraphNode: graphNode)], in: bundle)
1907+
}
18781908

18791909
if articles.count == 1 {
18801910
// This catalog only has one article, so we make that the root.
1881-
var onlyArticle = articles.removeFirst()
1882-
onlyArticle.value = Article(markup: onlyArticle.value.markup, metadata: metadata, redirects: onlyArticle.value.redirects, options: onlyArticle.value.options)
1883-
registerRootPages(from: [onlyArticle], in: bundle)
1911+
registerAsNewRootNode(articles.removeFirst())
18841912
} else if let nameMatchIndex = articles.firstIndex(where: { $0.source.deletingPathExtension().lastPathComponent == title }) {
18851913
// This catalog has an article with the same name as the catalog itself, so we make that the root.
1886-
var nameMatch = articles.remove(at: nameMatchIndex)
1887-
nameMatch.value = Article(markup: nameMatch.value.markup, metadata: metadata, redirects: nameMatch.value.redirects, options: nameMatch.value.options)
1888-
registerRootPages(from: [nameMatch], in: bundle)
1914+
registerAsNewRootNode(articles.remove(at: nameMatchIndex))
18891915
} else {
18901916
// There's no particular article to make into the root. Instead, create a new minimal root page.
18911917
let path = NodeURLGenerator.Path.documentation(path: title).stringValue
@@ -1896,12 +1922,15 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
18961922
let graphNode = TopicGraph.Node(reference: reference, kind: .module, source: .external, title: title)
18971923
topicGraph.addNode(graphNode)
18981924

1899-
// Build up the "full" markup for an empty technology root article
1925+
// Build up the "full" markup for an empty technology root article
1926+
let metadataDirectiveMarkup = BlockDirective(name: "Metadata", children: [
1927+
BlockDirective(name: "TechnologyRoot", children: [])
1928+
])
19001929
let markup = Document(
19011930
Heading(level: 1, Text(title)),
19021931
metadataDirectiveMarkup
19031932
)
1904-
1933+
let metadata = Metadata(from: metadataDirectiveMarkup, for: bundle, in: self)
19051934
let article = Article(markup: markup, metadata: metadata, redirects: nil, options: [:])
19061935
let documentationNode = DocumentationNode(
19071936
reference: reference,

Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2972,6 +2972,143 @@ let expected = """
29722972
XCTAssertEqual(linkResolutionProblems.first?.diagnostic.identifier, "org.swift.docc.unresolvedTopicReference")
29732973
}
29742974

2975+
func testLinkDiagnosticsInSynthesizedTechnologyRoots() throws {
2976+
// Verify that when synthesizing a technology root, links are resolved in the roots content.
2977+
// Also, if an article is promoted to a root, verify that any existing metadata is preserved.
2978+
2979+
func makeMetadata(root: Bool, color: Bool) -> String {
2980+
guard root || color else {
2981+
return ""
2982+
}
2983+
return """
2984+
@Metadata {
2985+
\(root ? "@TechnologyRoot" : "")
2986+
\(color ? "@PageColor(orange)" : "")
2987+
}
2988+
"""
2989+
}
2990+
2991+
// Only a single article
2992+
for withExplicitTechnologyRoot in [true, false] {
2993+
for withPageColor in [true, false] {
2994+
let catalogURL = try createTempFolder(content: [
2995+
Folder(name: "unit-test.docc", content: [
2996+
TextFile(name: "Root.md", utf8Content: """
2997+
# My root page
2998+
2999+
\(makeMetadata(root: withExplicitTechnologyRoot, color: withPageColor))
3000+
3001+
This implicit technology root links to pages and on-page elements that don't exist.
3002+
3003+
- ``NotFoundSymbol``
3004+
- <doc:NotFoundArticle>
3005+
- <doc:#NotFoundHeading>
3006+
"""),
3007+
])
3008+
])
3009+
let (_, _, context) = try loadBundle(from: catalogURL)
3010+
3011+
XCTAssertEqual(context.problems.map(\.diagnostic.summary), [
3012+
"'NotFoundSymbol' doesn't exist at '/Root'",
3013+
"'NotFoundArticle' doesn't exist at '/Root'",
3014+
"'NotFoundHeading' doesn't exist at '/Root'",
3015+
], withExplicitTechnologyRoot ? "with @TechnologyRoot" : "with synthesized root")
3016+
3017+
let rootReference = try XCTUnwrap(context.soleRootModuleReference)
3018+
let rootPage = try context.entity(with: rootReference)
3019+
XCTAssertNotNil(rootPage.metadata?.technologyRoot)
3020+
if withPageColor {
3021+
XCTAssertEqual(rootPage.metadata?.pageColor?.rawValue, "orange")
3022+
} else {
3023+
XCTAssertNil(rootPage.metadata?.pageColor)
3024+
}
3025+
}
3026+
}
3027+
3028+
// Article that match the bundle's name
3029+
for withExplicitTechnologyRoot in [true, false] {
3030+
for withPageColor in [true, false] {
3031+
let catalogURL = try createTempFolder(content: [
3032+
Folder(name: "CatalogName.docc", content: [
3033+
TextFile(name: "CatalogName.md", utf8Content: """
3034+
# My root page
3035+
3036+
\(makeMetadata(root: withExplicitTechnologyRoot, color: withPageColor))
3037+
3038+
This implicit technology root links to pages and on-page elements that don't exist.
3039+
3040+
- ``NotFoundSymbol``
3041+
- <doc:NotFoundArticle>
3042+
- <doc:#NotFoundHeading>
3043+
"""),
3044+
3045+
TextFile(name: "OtherArticle.md", utf8Content: """
3046+
# Another article
3047+
3048+
This article links to the technology root.
3049+
3050+
- <doc:CatalogName>
3051+
"""),
3052+
])
3053+
])
3054+
let (_, _, context) = try loadBundle(from: catalogURL)
3055+
3056+
XCTAssertEqual(context.problems.map(\.diagnostic.summary), [
3057+
"'NotFoundSymbol' doesn't exist at '/CatalogName'",
3058+
"'NotFoundArticle' doesn't exist at '/CatalogName'",
3059+
"'NotFoundHeading' doesn't exist at '/CatalogName'",
3060+
], withExplicitTechnologyRoot ? "with @TechnologyRoot" : "with synthesized root")
3061+
3062+
let rootReference = try XCTUnwrap(context.soleRootModuleReference)
3063+
let rootPage = try context.entity(with: rootReference)
3064+
XCTAssertNotNil(rootPage.metadata?.technologyRoot)
3065+
if withPageColor {
3066+
XCTAssertEqual(rootPage.metadata?.pageColor?.rawValue, "orange")
3067+
} else {
3068+
XCTAssertNil(rootPage.metadata?.pageColor)
3069+
}
3070+
}
3071+
}
3072+
3073+
// Completely synthesized root
3074+
let catalogURL = try createTempFolder(content: [
3075+
Folder(name: "CatalogName.docc", content: [
3076+
TextFile(name: "First.md", utf8Content: """
3077+
# One article
3078+
3079+
This article links to pages and on-page elements that don't exist.
3080+
3081+
- ``NotFoundSymbol``
3082+
- <doc:#NotFoundHeading>
3083+
3084+
It also links to the technology root.
3085+
3086+
- <doc:CatalogName>
3087+
"""),
3088+
3089+
TextFile(name: "Second.md", utf8Content: """
3090+
# Another article
3091+
3092+
This article links to a page that doesn't exist to the synthesized technology root.
3093+
3094+
- <doc:NotFoundArticle>
3095+
- <doc:CatalogName>
3096+
"""),
3097+
])
3098+
])
3099+
let (_, _, context) = try loadBundle(from: catalogURL)
3100+
3101+
XCTAssertEqual(context.problems.map(\.diagnostic.summary).sorted(), [
3102+
"'NotFoundArticle' doesn't exist at '/CatalogName/Second'",
3103+
"'NotFoundHeading' doesn't exist at '/CatalogName/First'",
3104+
"'NotFoundSymbol' doesn't exist at '/CatalogName/First'",
3105+
])
3106+
3107+
let rootReference = try XCTUnwrap(context.soleRootModuleReference)
3108+
let rootPage = try context.entity(with: rootReference)
3109+
XCTAssertNotNil(rootPage.metadata?.technologyRoot)
3110+
}
3111+
29753112
func testResolvingLinksToHeaders() throws {
29763113
let tempURL = try createTemporaryDirectory()
29773114

0 commit comments

Comments
 (0)