Skip to content

Commit 5f57823

Browse files
authored
Make @TechnologyRoot optional in article-only documentation (#778)
1 parent 18561d4 commit 5f57823

File tree

3 files changed

+162
-4
lines changed

3 files changed

+162
-4
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -1874,6 +1874,60 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
18741874
}
18751875
}
18761876

1877+
/// Registers a synthesized root page for a catalog with only non-root articles.
1878+
///
1879+
/// If the catalog only has one article or has an article with the same name as the catalog itself, that article is turned into the root page instead of creating a new article.
1880+
///
1881+
/// - Parameters:
1882+
/// - articles: On input, a list of articles. If an article is used as a root it is removed from this list.
1883+
/// - bundle: The bundle containing the articles.
1884+
private func synthesizeArticleOnlyRootPage(articles: inout [DocumentationContext.SemanticResult<Article>], bundle: DocumentationBundle) {
1885+
let title = bundle.displayName
1886+
let metadataDirectiveMarkup = BlockDirective(name: "Metadata", children: [
1887+
BlockDirective(name: "TechnologyRoot", children: [])
1888+
])
1889+
let metadata = Metadata(from: metadataDirectiveMarkup, for: bundle, in: self)
1890+
1891+
if articles.count == 1 {
1892+
// This catalog only has one article, so we make that the root.
1893+
var onlyArticle = articles.removeFirst()
1894+
onlyArticle.value = Article(markup: onlyArticle.value.markup, metadata: metadata, redirects: onlyArticle.value.redirects, options: onlyArticle.value.options)
1895+
registerRootPages(from: [onlyArticle], in: bundle)
1896+
} else if let nameMatchIndex = articles.firstIndex(where: { $0.source.deletingPathExtension().lastPathComponent == title }) {
1897+
// This catalog has an article with the same name as the catalog itself, so we make that the root.
1898+
var nameMatch = articles.remove(at: nameMatchIndex)
1899+
nameMatch.value = Article(markup: nameMatch.value.markup, metadata: metadata, redirects: nameMatch.value.redirects, options: nameMatch.value.options)
1900+
registerRootPages(from: [nameMatch], in: bundle)
1901+
} else {
1902+
// There's no particular article to make into the root. Instead, create a new minimal root page.
1903+
let path = NodeURLGenerator.Path.documentation(path: title).stringValue
1904+
let sourceLanguage = DocumentationContext.defaultLanguage(in: [])
1905+
1906+
let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: path, sourceLanguages: [sourceLanguage])
1907+
1908+
let graphNode = TopicGraph.Node(reference: reference, kind: .module, source: .external, title: title)
1909+
topicGraph.addNode(graphNode)
1910+
1911+
// Build up the "full" markup for an empty technology root article
1912+
let markup = Document(
1913+
Heading(level: 1, Text(title)),
1914+
metadataDirectiveMarkup
1915+
)
1916+
1917+
let article = Article(markup: markup, metadata: metadata, redirects: nil, options: [:])
1918+
let documentationNode = DocumentationNode(
1919+
reference: reference,
1920+
kind: .collection,
1921+
sourceLanguage: sourceLanguage,
1922+
availableSourceLanguages: [sourceLanguage],
1923+
name: .conceptual(title: title),
1924+
markup: markup,
1925+
semantic: article
1926+
)
1927+
documentationCache[reference] = documentationNode
1928+
}
1929+
}
1930+
18771931
/// Creates a documentation node and title for the given article semantic result.
18781932
///
18791933
/// - Parameters:
@@ -2147,6 +2201,10 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
21472201

21482202
try shouldContinueRegistration()
21492203

2204+
if topicGraph.nodes.isEmpty, !otherArticles.isEmpty, !allowsRegisteringArticlesWithoutTechnologyRoot {
2205+
synthesizeArticleOnlyRootPage(articles: &otherArticles, bundle: bundle)
2206+
}
2207+
21502208
// Keep track of the root modules registered from symbol graph files, we'll need them to automatically
21512209
// curate articles.
21522210
rootModules = topicGraph.nodes.values.compactMap { node in

Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContext+RootPageTests.swift

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021-2022 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -96,4 +96,104 @@ class DocumentationContext_RootPageTests: XCTestCase {
9696
XCTAssertEqual(solution.replacements.first?.range.upperBound.line, 3)
9797
}
9898

99+
func testSingleArticleWithoutTechnologyRootDirective() throws {
100+
let tempFolderURL = try createTempFolder(content: [
101+
Folder(name: "Something.docc", content: [
102+
TextFile(name: "Article.md", utf8Content: """
103+
# My article
104+
105+
A regular article without an explicit `@TechnologyRoot` directive.
106+
""")
107+
]),
108+
])
109+
110+
let workspace = DocumentationWorkspace()
111+
let context = try DocumentationContext(dataProvider: workspace)
112+
let dataProvider = try LocalFileSystemDataProvider(rootURL: tempFolderURL)
113+
try workspace.registerProvider(dataProvider)
114+
115+
XCTAssertEqual(context.knownPages.map(\.absoluteString), ["doc://Something/documentation/Article"])
116+
XCTAssertEqual(context.rootModules.map(\.absoluteString), ["doc://Something/documentation/Article"])
117+
118+
XCTAssertEqual(context.problems.count, 0)
119+
}
120+
121+
func testMultipleArticlesWithoutTechnologyRootDirective() throws {
122+
let tempFolderURL = try createTempFolder(content: [
123+
Folder(name: "Something.docc", content: [
124+
TextFile(name: "First.md", utf8Content: """
125+
# My first article
126+
127+
A regular article without an explicit `@TechnologyRoot` directive.
128+
"""),
129+
130+
TextFile(name: "Second.md", utf8Content: """
131+
# My second article
132+
133+
Another regular article without an explicit `@TechnologyRoot` directive.
134+
"""),
135+
136+
TextFile(name: "Third.md", utf8Content: """
137+
# My third article
138+
139+
Yet another regular article without an explicit `@TechnologyRoot` directive.
140+
"""),
141+
]),
142+
])
143+
144+
let workspace = DocumentationWorkspace()
145+
let context = try DocumentationContext(dataProvider: workspace)
146+
let dataProvider = try LocalFileSystemDataProvider(rootURL: tempFolderURL)
147+
try workspace.registerProvider(dataProvider)
148+
149+
XCTAssertEqual(context.knownPages.map(\.absoluteString).sorted(), [
150+
"doc://Something/documentation/Something", // A synthesized root
151+
"doc://Something/documentation/Something/First",
152+
"doc://Something/documentation/Something/Second",
153+
"doc://Something/documentation/Something/Third",
154+
])
155+
XCTAssertEqual(context.rootModules.map(\.absoluteString), ["doc://Something/documentation/Something"], "If no single article is a clear root, the root page is synthesized")
156+
157+
XCTAssertEqual(context.problems.count, 0)
158+
}
159+
160+
func testMultipleArticlesWithoutTechnologyRootDirectiveWithOneMatchingTheCatalogName() throws {
161+
let tempFolderURL = try createTempFolder(content: [
162+
Folder(name: "Something.docc", content: [
163+
TextFile(name: "Something.md", utf8Content: """
164+
# Some article
165+
166+
A regular article without an explicit `@TechnologyRoot` directive.
167+
168+
The name of this article file matches the name of the catalog.
169+
"""),
170+
171+
TextFile(name: "Second.md", utf8Content: """
172+
# My second article
173+
174+
Another regular article without an explicit `@TechnologyRoot` directive.
175+
"""),
176+
177+
TextFile(name: "Third.md", utf8Content: """
178+
# My third article
179+
180+
Yet another regular article without an explicit `@TechnologyRoot` directive.
181+
"""),
182+
]),
183+
])
184+
185+
let workspace = DocumentationWorkspace()
186+
let context = try DocumentationContext(dataProvider: workspace)
187+
let dataProvider = try LocalFileSystemDataProvider(rootURL: tempFolderURL)
188+
try workspace.registerProvider(dataProvider)
189+
190+
XCTAssertEqual(context.knownPages.map(\.absoluteString).sorted(), [
191+
"doc://Something/documentation/Something", // This article became the root
192+
"doc://Something/documentation/Something/Second",
193+
"doc://Something/documentation/Something/Third",
194+
])
195+
XCTAssertEqual(context.rootModules.map(\.absoluteString), ["doc://Something/documentation/Something"])
196+
197+
XCTAssertEqual(context.problems.count, 0)
198+
}
99199
}

bin/check-source

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# This source file is part of the Swift.org open source project
44
#
5-
# Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
5+
# Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
66
# Licensed under Apache License v2.0 with Runtime Library Exception
77
#
88
# See https://swift.org/LICENSE.txt for license information
@@ -18,7 +18,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
1818

1919
function replace_acceptable_years() {
2020
# this needs to replace all acceptable forms with 'YEARS'
21-
sed -e 's/20[12][7890123]-20[12][890123]/YEARS/' -e 's/20[12][890123]/YEARS/'
21+
sed -e 's/20[12][78901234]-20[12][8901234]/YEARS/' -e 's/20[12][8901234]/YEARS/'
2222
}
2323

2424
printf "=> Checking for unacceptable language… "

0 commit comments

Comments
 (0)