Skip to content

Commit c1ce32f

Browse files
committed
Emit language variants for articles
Articles that document a module available in multiple languages now inherit that module's languages. This ensures that we generate multiple language variants for article pages. rdar://88464797
1 parent 779ca82 commit c1ce32f

File tree

4 files changed

+289
-99
lines changed

4 files changed

+289
-99
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
147147
return node.reference
148148
}
149149
}
150+
151+
/// The topic reference of the root module, if it's the only registered module.
152+
var soleRootModuleReference: ResolvedTopicReference? {
153+
rootModules.count == 1 ? rootModules.first : nil
154+
}
150155

151156
/// Map of document URLs to topic references.
152157
var documentLocationMap = BidirectionalMap<URL, ResolvedTopicReference>()
@@ -1624,9 +1629,23 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
16241629
/// - Parameters:
16251630
/// - articles: Articles to register with the documentation cache.
16261631
/// - bundle: The bundle containing the articles.
1627-
private func registerArticles(_ articles: DocumentationContext.Articles, in bundle: DocumentationBundle) {
1628-
for article in articles {
1629-
guard let (documentation, title) = DocumentationContext.documentationNodeAndTitle(for: article, kind: .article, in: bundle) else { continue }
1632+
/// - Returns: The articles that were registered, with their topic graph node updated to what's been added to the topic graph.
1633+
private func registerArticles(
1634+
_ articles: DocumentationContext.Articles,
1635+
in bundle: DocumentationBundle
1636+
) -> DocumentationContext.Articles {
1637+
articles.map { article in
1638+
guard let (documentation, title) = DocumentationContext.documentationNodeAndTitle(
1639+
for: article,
1640+
1641+
// Articles are available in the same languages the only root module is available in. If there is more
1642+
// than one module, we cannot determine what languages it's available in and default to Swift.
1643+
availableSourceLanguages: soleRootModuleReference?.sourceLanguages,
1644+
kind: .article,
1645+
in: bundle
1646+
) else {
1647+
return article
1648+
}
16301649
let reference = documentation.reference
16311650

16321651
documentationCache[reference] = documentation
@@ -1638,6 +1657,11 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
16381657
for anchor in documentation.anchorSections {
16391658
nodeAnchorSections[anchor.reference] = anchor
16401659
}
1660+
1661+
var article = article
1662+
// Update the article's topic graph node with the one we just added to the topic graph.
1663+
article.topicGraphNode = graphNode
1664+
return article
16411665
}
16421666
}
16431667

@@ -1648,16 +1672,45 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
16481672
/// - kind: The kind that should be used to create the returned documentation node.
16491673
/// - bundle: The documentation bundle this article belongs to.
16501674
/// - Returns: A documentation node and title for the given article semantic result.
1651-
static func documentationNodeAndTitle(for article: DocumentationContext.SemanticResult<Article>, kind: DocumentationNode.Kind, in bundle: DocumentationBundle) -> (node: DocumentationNode, title: String)? {
1675+
static func documentationNodeAndTitle(
1676+
for article: DocumentationContext.SemanticResult<Article>,
1677+
availableSourceLanguages: Set<SourceLanguage>? = nil,
1678+
kind: DocumentationNode.Kind,
1679+
in bundle: DocumentationBundle
1680+
) -> (node: DocumentationNode, title: String)? {
16521681
guard let articleMarkup = article.value.markup else {
16531682
return nil
16541683
}
16551684

16561685
let path = NodeURLGenerator.pathForSemantic(article.value, source: article.source, bundle: bundle)
1657-
let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: path, sourceLanguage: .swift)
1686+
1687+
// If available source languages are provided and it contains Swift, use Swift as the default language of
1688+
// the article.
1689+
let sourceLanguage = availableSourceLanguages.map { availableSourceLanguages in
1690+
if availableSourceLanguages.contains(.swift) {
1691+
return .swift
1692+
} else {
1693+
return availableSourceLanguages.first ?? .swift
1694+
}
1695+
} ?? SourceLanguage.swift
1696+
1697+
let reference = ResolvedTopicReference(
1698+
bundleIdentifier: bundle.identifier,
1699+
path: path,
1700+
sourceLanguages: availableSourceLanguages ?? [.swift]
1701+
)
1702+
16581703
let title = article.topicGraphNode.title
16591704

1660-
let documentationNode = DocumentationNode(reference: reference, kind: kind, sourceLanguage: .swift, name: .conceptual(title: title), markup: articleMarkup, semantic: article.value)
1705+
let documentationNode = DocumentationNode(
1706+
reference: reference,
1707+
kind: kind,
1708+
sourceLanguage: sourceLanguage,
1709+
availableSourceLanguages: availableSourceLanguages,
1710+
name: .conceptual(title: title),
1711+
markup: articleMarkup,
1712+
semantic: article.value
1713+
)
16611714

16621715
return (documentationNode, title)
16631716
}
@@ -1693,11 +1746,29 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
16931746
}
16941747

16951748
let articleReferences = autoCuratedArticles.map(\.topicGraphNode.reference)
1696-
let autoArticlesSection = AutomaticTaskGroupSection(title: "Articles", references: articleReferences, renderPositionPreference: .top)
1749+
1750+
func createAutomaticTaskGroupSection(references: [ResolvedTopicReference]) -> AutomaticTaskGroupSection {
1751+
AutomaticTaskGroupSection(
1752+
title: "Articles",
1753+
references: references,
1754+
renderPositionPreference: .top
1755+
)
1756+
}
16971757

16981758
let node = try entity(with: rootNode.reference)
1699-
if var taskGroupProviding = node.semantic as? AutomaticTaskGroupsProviding {
1700-
taskGroupProviding.automaticTaskGroups = [autoArticlesSection]
1759+
1760+
// If the node we're automatically curating the article under is a symbol, automatically curate the article
1761+
// for each language it's available in.
1762+
if let symbol = node.semantic as? Symbol {
1763+
for sourceLanguage in node.availableSourceLanguages {
1764+
symbol.automaticTaskGroupsVariants[
1765+
.init(interfaceLanguage: sourceLanguage.id)
1766+
] = [createAutomaticTaskGroupSection(references: articleReferences)]
1767+
}
1768+
} else if var taskGroupProviding = node.semantic as? AutomaticTaskGroupsProviding {
1769+
taskGroupProviding.automaticTaskGroups = [
1770+
createAutomaticTaskGroupSection(references: articleReferences)
1771+
]
17011772
}
17021773

17031774
return articleReferences
@@ -1779,7 +1850,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
17791850

17801851
// All discovery went well, process the inputs.
17811852
let (technologies, tutorials, tutorialArticles, allArticles) = result
1782-
let (otherArticles, rootPageArticles) = splitArticles(allArticles)
1853+
var (otherArticles, rootPageArticles) = splitArticles(allArticles)
17831854

17841855
let rootPages = registerRootPages(from: rootPageArticles, in: bundle)
17851856
let (moduleReferences, symbolsURLHierarchy) = try registerSymbols(from: bundle, symbolGraphLoader: symbolGraphLoader)
@@ -1789,9 +1860,9 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
17891860
try shouldContinueRegistration()
17901861

17911862
// Articles that will be automatically curated can be resolved but they need to be pre registered before resolving links.
1792-
let rootNodeForAutomaticCuration = rootModules.count == 1 ? rootModules.first.flatMap(topicGraph.nodeWithReference) : nil
1863+
let rootNodeForAutomaticCuration = soleRootModuleReference.flatMap(topicGraph.nodeWithReference(_:))
17931864
if rootNodeForAutomaticCuration != nil {
1794-
registerArticles(otherArticles, in: bundle)
1865+
otherArticles = registerArticles(otherArticles, in: bundle)
17951866
try shouldContinueRegistration()
17961867
}
17971868

@@ -2550,32 +2621,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
25502621
return results
25512622
}
25522623

2553-
/// Finds the presentation language the symbol is curated under by walking
2554-
/// the documentation graph upwards and finding a parent that has an interface language.
2555-
/// Will return `nil` if no parent with assigned interface language is found
2556-
/// (e.g. in a tutorials only bundle).
2557-
func interfaceLanguageFor(_ reference: ResolvedTopicReference) throws -> SourceLanguage? {
2558-
let node = try entity(with: reference)
2559-
guard !(node.semantic is Symbol) else {
2560-
// For symbols just return the source language
2561-
return node.sourceLanguage
2562-
}
2563-
2564-
// `pathsTo()` returns the canonical path first
2565-
guard let canonical = pathsTo(reference).first else {
2566-
// Uncurated symbol, if not expected a warning will be emitted elsewhere
2567-
return nil
2568-
}
2569-
2570-
// Return the language of the first symbol entity
2571-
return canonical.mapFirst { reference -> SourceLanguage? in
2572-
guard let node = try? entity(with: reference), node.semantic is Symbol else {
2573-
return nil
2574-
}
2575-
return node.sourceLanguage
2576-
}
2577-
}
2578-
25792624
func dumpGraph() -> String {
25802625
return topicGraph.nodes.values
25812626
.filter { parents(of: $0.reference).isEmpty }

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -586,16 +586,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
586586
collectedTopicReferences.append(contentsOf: hierarchyTranslator.collectedTopicReferences)
587587
node.hierarchy = hierarchy
588588

589-
// Find the language of the symbol that curated the article in the graph
590-
// and use it as the interface language for that article.
591-
if let language = try! context.interfaceLanguageFor(identifier)?.id {
592-
let generator = PresentationURLGenerator(context: context, baseURL: bundle.baseURL)
593-
node.variants = [
594-
.init(traits: [.interfaceLanguage(language)], paths: [
595-
generator.presentationURLForReference(identifier).path
596-
])
597-
]
598-
}
589+
node.variants = variants(for: documentationNode)
599590

600591
if let abstract = article.abstractSection,
601592
let abstractContent = visitMarkup(abstract.content) as? [RenderInlineContent] {
@@ -960,25 +951,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
960951
node.metadata.fragmentsVariants = contentRenderer.subHeadingFragments(for: documentationNode)
961952
node.metadata.navigatorTitleVariants = contentRenderer.navigatorFragments(for: documentationNode)
962953

963-
let generator = PresentationURLGenerator(context: context, baseURL: bundle.baseURL)
964-
965-
node.variants = documentationNode.availableSourceLanguages
966-
.sorted(by: { language1, language2 in
967-
// Emit Swift first, then alphabetically.
968-
switch (language1, language2) {
969-
case (.swift, _): return true
970-
case (_, .swift): return false
971-
default: return language1.id < language2.id
972-
}
973-
})
974-
.map { sourceLanguage in
975-
RenderNode.Variant(
976-
traits: [.interfaceLanguage(sourceLanguage.id)],
977-
paths: [
978-
generator.presentationURLForReference(identifier).path
979-
]
980-
)
981-
}
954+
node.variants = variants(for: documentationNode)
982955

983956
collectedTopicReferences.append(identifier)
984957

@@ -1406,6 +1379,28 @@ public struct RenderNodeTranslator: SemanticVisitor {
14061379
}
14071380
}
14081381

1382+
private func variants(for documentationNode: DocumentationNode) -> [RenderNode.Variant] {
1383+
let generator = PresentationURLGenerator(context: context, baseURL: bundle.baseURL)
1384+
1385+
return documentationNode.availableSourceLanguages
1386+
.sorted(by: { language1, language2 in
1387+
// Emit Swift first, then alphabetically.
1388+
switch (language1, language2) {
1389+
case (.swift, _): return true
1390+
case (_, .swift): return false
1391+
default: return language1.id < language2.id
1392+
}
1393+
})
1394+
.map { sourceLanguage in
1395+
RenderNode.Variant(
1396+
traits: [.interfaceLanguage(sourceLanguage.id)],
1397+
paths: [
1398+
generator.presentationURLForReference(identifier).path
1399+
]
1400+
)
1401+
}
1402+
}
1403+
14091404
init(
14101405
context: DocumentationContext,
14111406
bundle: DocumentationBundle,

Tests/SwiftDocCTests/Infrastructure/DocumentationContextTests.swift

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,17 +1339,6 @@ let expected = """
13391339
let canonicalPathFFF = try XCTUnwrap(context.pathsTo(fffNode.reference).first)
13401340
XCTAssertEqual(["/documentation/MyKit"], canonicalPathFFF.map({ $0.path }))
13411341
}
1342-
1343-
func testLanguageForNode() throws {
1344-
let workspace = DocumentationWorkspace()
1345-
let context = try DocumentationContext(dataProvider: workspace)
1346-
let bundle = try testBundle(named: "TestBundle")
1347-
let dataProvider = PrebuiltLocalFileSystemDataProvider(bundles: [bundle])
1348-
try workspace.registerProvider(dataProvider)
1349-
let articleReference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/Test-Bundle/article", sourceLanguage: .swift)
1350-
let articleLanguage = try context.interfaceLanguageFor(articleReference)
1351-
XCTAssertEqual(articleLanguage, SourceLanguage.swift)
1352-
}
13531342

13541343
// Verify that a symbol that has no parents in the symbol graph is automatically curated under the module node.
13551344
func testRootSymbolsAreCureatedInModule() throws {
@@ -3029,6 +3018,91 @@ let expected = """
30293018
"Expected the symbol instances in the documentationCache and symbolIndex dictionaries to be the same"
30303019
)
30313020
}
3021+
3022+
func assertArticleAvailableSourceLanguages(
3023+
moduleAvailableLanguages: Set<SourceLanguage>,
3024+
expectedArticleDefaultLanguage: SourceLanguage,
3025+
file: StaticString = #file,
3026+
line: UInt = #line
3027+
) throws {
3028+
precondition(
3029+
moduleAvailableLanguages.allSatisfy { [.swift, .objectiveC].contains($0) },
3030+
"moduleAvailableLanguages can only contain Swift and Objective-C as languages."
3031+
)
3032+
3033+
let (_, _, context) = try testBundleAndContext(copying: "MixedLanguageFramework") { url in
3034+
try """
3035+
# MyArticle
3036+
3037+
The framework this article is documenting is available in the following languages: \
3038+
\(moduleAvailableLanguages.map(\.name).joined(separator: ",")).
3039+
""".write(to: url.appendingPathComponent("myarticle.md"), atomically: true, encoding: .utf8)
3040+
3041+
func removeSymbolGraph(compiler: String) throws {
3042+
try FileManager.default.removeItem(
3043+
at: url.appendingPathComponent("symbol-graphs").appendingPathComponent(compiler)
3044+
)
3045+
}
3046+
3047+
if !moduleAvailableLanguages.contains(.swift) {
3048+
try removeSymbolGraph(compiler: "swift")
3049+
}
3050+
3051+
if !moduleAvailableLanguages.contains(.objectiveC) {
3052+
try removeSymbolGraph(compiler: "clang")
3053+
}
3054+
}
3055+
3056+
let articleNode = try XCTUnwrap(
3057+
context.documentationCache.first {
3058+
$0.key.path == "/documentation/MixedLanguageFramework/myarticle"
3059+
}?.value,
3060+
file: file,
3061+
line: line
3062+
)
3063+
3064+
XCTAssertEqual(
3065+
articleNode.availableSourceLanguages,
3066+
moduleAvailableLanguages,
3067+
"Expected the article's source languages to have inherited from the module's available source languages.",
3068+
file: file,
3069+
line: line
3070+
)
3071+
3072+
XCTAssertEqual(
3073+
articleNode.sourceLanguage,
3074+
expectedArticleDefaultLanguage,
3075+
file: file,
3076+
line: line
3077+
)
3078+
}
3079+
3080+
func testArticleAvailableSourceLanguagesIsSwiftInSwiftModule() throws {
3081+
enableFeatureFlag(\.isExperimentalObjectiveCSupportEnabled)
3082+
3083+
try assertArticleAvailableSourceLanguages(
3084+
moduleAvailableLanguages: [.swift],
3085+
expectedArticleDefaultLanguage: .swift
3086+
)
3087+
}
3088+
3089+
func testArticleAvailableSourceLanguagesIsMixedLanguageInMixedLanguageModule() throws {
3090+
enableFeatureFlag(\.isExperimentalObjectiveCSupportEnabled)
3091+
3092+
try assertArticleAvailableSourceLanguages(
3093+
moduleAvailableLanguages: [.swift, .objectiveC],
3094+
expectedArticleDefaultLanguage: .swift
3095+
)
3096+
}
3097+
3098+
func testArticleAvailableSourceLanguagesIsObjectiveCInObjectiveCModule() throws {
3099+
enableFeatureFlag(\.isExperimentalObjectiveCSupportEnabled)
3100+
3101+
try assertArticleAvailableSourceLanguages(
3102+
moduleAvailableLanguages: [.objectiveC],
3103+
expectedArticleDefaultLanguage: .objectiveC
3104+
)
3105+
}
30323106
}
30333107

30343108
func assertEqualDumps(_ lhs: String, _ rhs: String, file: StaticString = #file, line: UInt = #line) {

0 commit comments

Comments
 (0)