Skip to content

Commit 9a5be7e

Browse files
authored
Support writing links in synthesized technology root pages (#980)
* Use a correct root reference for synthesized root pages. Also, avoid overriding existing metadata when promoting articles to root pages. rdar://129282701 rdar://127874910 * Add debug assert about not synthesizing a root page when one exists Add comment about article-only default language
1 parent cffb7da commit 9a5be7e

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

18721902
if articles.count == 1 {
18731903
// This catalog only has one article, so we make that the root.
1874-
var onlyArticle = articles.removeFirst()
1875-
onlyArticle.value = Article(markup: onlyArticle.value.markup, metadata: metadata, redirects: onlyArticle.value.redirects, options: onlyArticle.value.options)
1876-
registerRootPages(from: [onlyArticle], in: bundle)
1904+
registerAsNewRootNode(articles.removeFirst())
18771905
} else if let nameMatchIndex = articles.firstIndex(where: { $0.source.deletingPathExtension().lastPathComponent == title }) {
18781906
// This catalog has an article with the same name as the catalog itself, so we make that the root.
1879-
var nameMatch = articles.remove(at: nameMatchIndex)
1880-
nameMatch.value = Article(markup: nameMatch.value.markup, metadata: metadata, redirects: nameMatch.value.redirects, options: nameMatch.value.options)
1881-
registerRootPages(from: [nameMatch], in: bundle)
1907+
registerAsNewRootNode(articles.remove(at: nameMatchIndex))
18821908
} else {
18831909
// There's no particular article to make into the root. Instead, create a new minimal root page.
18841910
let path = NodeURLGenerator.Path.documentation(path: title).stringValue
@@ -1889,12 +1915,15 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
18891915
let graphNode = TopicGraph.Node(reference: reference, kind: .module, source: .external, title: title)
18901916
topicGraph.addNode(graphNode)
18911917

1892-
// Build up the "full" markup for an empty technology root article
1918+
// Build up the "full" markup for an empty technology root article
1919+
let metadataDirectiveMarkup = BlockDirective(name: "Metadata", children: [
1920+
BlockDirective(name: "TechnologyRoot", children: [])
1921+
])
18931922
let markup = Document(
18941923
Heading(level: 1, Text(title)),
18951924
metadataDirectiveMarkup
18961925
)
1897-
1926+
let metadata = Metadata(from: metadataDirectiveMarkup, for: bundle, in: self)
18981927
let article = Article(markup: markup, metadata: metadata, redirects: nil, options: [:])
18991928
let documentationNode = DocumentationNode(
19001929
reference: reference,

Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift

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

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

0 commit comments

Comments
 (0)