Skip to content

Commit 3fe2154

Browse files
Fix problem where API Collections had no roleHeading assigned (#754)
rdar://90789460 The eyebrow title for articles curating a list of symbols should display 'API Collection'. Prior to this fix, the eyebrow did not show anything. 'API Collection' now appears whenever one of the curated items in the Topics section is a symbol. If the entire list does not contain a symbol (i.e., it's a plain collection), we maintain the old behavior of not displaying anything as the eyebrow title. Tests have been added to verify the correct assignment of an eyebrow title for the following cases: Articles, API Collections, and Collections. Renamed `contentCompiler` to `topicSectionContentCompiler` in the `visitArticle` method.
1 parent 97ce0c9 commit 3fe2154

File tree

2 files changed

+133
-12
lines changed

2 files changed

+133
-12
lines changed

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,8 @@ public struct RenderNodeTranslator: SemanticVisitor {
598598

599599
public mutating func visitArticle(_ article: Article) -> RenderTree? {
600600
var node = RenderNode(identifier: identifier, kind: .article)
601-
var contentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: identifier)
601+
// Contains symbol references declared in the Topics section.
602+
var topicSectionContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: identifier)
602603

603604
node.metadata.title = article.title!.plainText
604605

@@ -674,7 +675,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
674675
allowExternalLinks: false,
675676
allowedTraits: allowedTraits,
676677
availableTraits: documentationNode.availableVariantTraits,
677-
contentCompiler: &contentCompiler
678+
contentCompiler: &topicSectionContentCompiler
678679
)
679680
)
680681
}
@@ -685,7 +686,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
685686
sections.append(
686687
contentsOf: renderAutomaticTaskGroupsSection(
687688
article.automaticTaskGroups.filter { $0.renderPositionPreference == .top },
688-
contentCompiler: &contentCompiler
689+
contentCompiler: &topicSectionContentCompiler
689690
)
690691
)
691692
}
@@ -714,7 +715,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
714715
}
715716

716717
// Collect all child topic references.
717-
contentCompiler.collectedTopicReferences.append(contentsOf: groups.flatMap(\.references))
718+
topicSectionContentCompiler.collectedTopicReferences.append(contentsOf: groups.flatMap(\.references))
718719
// Add the final groups to the node.
719720
sections.append(contentsOf: groups.map(TaskGroupRenderSection.init(taskGroup:)))
720721
}
@@ -725,7 +726,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
725726
sections.append(
726727
contentsOf: renderAutomaticTaskGroupsSection(
727728
article.automaticTaskGroups.filter { $0.renderPositionPreference == .bottom },
728-
contentCompiler: &contentCompiler
729+
contentCompiler: &topicSectionContentCompiler
729730
)
730731
)
731732
}
@@ -736,11 +737,30 @@ public struct RenderNodeTranslator: SemanticVisitor {
736737
node.topicSectionsStyle = topicsSectionStyle(for: documentationNode)
737738

738739
if shouldCreateAutomaticRoleHeading(for: documentationNode) {
739-
if node.topicSections.isEmpty {
740-
// Set an eyebrow for articles
740+
741+
let role = DocumentationContentRenderer.roleForArticle(article, nodeKind: documentationNode.kind)
742+
node.metadata.role = role.rawValue
743+
744+
switch role {
745+
case .article:
746+
// If there are no links to other nodes from the article,
747+
// set the eyebrow for articles.
741748
node.metadata.roleHeading = "Article"
749+
case .collectionGroup:
750+
// If the article links to other nodes, set the eyebrow for
751+
// API Collections if any linked node is a symbol.
752+
//
753+
// If none of the linked nodes are symbols (it's a plain collection),
754+
// don't display anything as the eyebrow title.
755+
let curatesSymbols = topicSectionContentCompiler.collectedTopicReferences.contains { topicReference in
756+
context.topicGraph.nodeWithReference(topicReference)?.kind.isSymbol ?? false
757+
}
758+
if curatesSymbols {
759+
node.metadata.roleHeading = "API Collection"
760+
}
761+
default:
762+
break
742763
}
743-
node.metadata.role = DocumentationContentRenderer.roleForArticle(article, nodeKind: documentationNode.kind).rawValue
744764
}
745765

746766
if let pageImages = documentationNode.metadata?.pageImages {
@@ -780,7 +800,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
780800
allowExternalLinks: true,
781801
allowedTraits: allowedTraits,
782802
availableTraits: documentationNode.availableVariantTraits,
783-
contentCompiler: &contentCompiler
803+
contentCompiler: &topicSectionContentCompiler
784804
)
785805
)
786806
}
@@ -794,7 +814,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
794814
renderContext: renderContext,
795815
renderer: contentRenderer
796816
) {
797-
contentCompiler.collectedTopicReferences.append(contentsOf: seeAlso.references)
817+
topicSectionContentCompiler.collectedTopicReferences.append(contentsOf: seeAlso.references)
798818
seeAlsoSections.append(TaskGroupRenderSection(taskGroup: seeAlso))
799819
}
800820

@@ -845,7 +865,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
845865
node.metadata.roleHeading = titleHeading.heading
846866
}
847867

848-
collectedTopicReferences.append(contentsOf: contentCompiler.collectedTopicReferences)
868+
collectedTopicReferences.append(contentsOf: topicSectionContentCompiler.collectedTopicReferences)
849869
node.references = createTopicRenderReferences()
850870

851871
addReferences(imageReferences, to: &node)
@@ -854,7 +874,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
854874
addReferences(downloadReferences, to: &node)
855875
// See Also can contain external links, we need to separately transfer
856876
// link references from the content compiler
857-
addReferences(contentCompiler.linkReferences, to: &node)
877+
addReferences(topicSectionContentCompiler.linkReferences, to: &node)
858878

859879
return node
860880
}

Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import XCTest
1313
@testable import SwiftDocC
1414
import SwiftDocCTestUtilities
1515
import Markdown
16+
import SymbolKit
1617

1718
class RenderNodeTranslatorTests: XCTestCase {
1819
private func findDiscussion(forSymbolPath: String, configureBundle: ((URL) throws -> Void)? = nil) throws -> ContentRenderSection? {
@@ -1300,4 +1301,104 @@ class RenderNodeTranslatorTests: XCTestCase {
13001301
XCTAssertEqual(roundTrippedSymbol.metadata.roleHeading, "TestBed Notes")
13011302
XCTAssertEqual(roundTrippedSymbol.metadata.role, "collection")
13021303
}
1304+
1305+
func testExpectedRoleHeadingIsAssigned() throws {
1306+
func renderNodeArticleFromReferencePath(
1307+
referencePath: String
1308+
) throws -> RenderNode {
1309+
let reference = ResolvedTopicReference(
1310+
bundleIdentifier: bundle.identifier,
1311+
path: referencePath,
1312+
sourceLanguage: .swift
1313+
)
1314+
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Article)
1315+
var translator = RenderNodeTranslator(
1316+
context: context,
1317+
bundle: bundle,
1318+
identifier: reference,
1319+
source: nil
1320+
)
1321+
return try XCTUnwrap(translator.visitArticle(symbol) as? RenderNode)
1322+
}
1323+
1324+
let exampleDocumentation = Folder(
1325+
name: "unit-test.docc",
1326+
content: [
1327+
TextFile(name: "APICollection.md", utf8Content: """
1328+
# API Collection
1329+
My API Collection Abstract.
1330+
## Topics
1331+
- ``Symbol``
1332+
- <doc:article2>
1333+
- <doc:article3>
1334+
"""),
1335+
TextFile(name: "Collection.md", utf8Content: """
1336+
# Collection
1337+
An abstract with a symbol link: ``MyKit/MyProtocol``
1338+
## Overview
1339+
An overview with a symbol link: ``MyKit/MyProtocol``
1340+
## Topics
1341+
A topic group abstract with a symbol link: ``MyKit/MyProtocol``
1342+
- <doc:article4>
1343+
- <doc:article5>
1344+
"""),
1345+
TextFile(name: "Article.md", utf8Content: """
1346+
# Article
1347+
My Article Abstract.
1348+
## Overview
1349+
An overview.
1350+
"""),
1351+
TextFile(name: "CustomRole.md", utf8Content: """
1352+
# Article 4
1353+
@Metadata {
1354+
@TitleHeading("Custom Role")
1355+
}
1356+
My Article Abstract.
1357+
## Overview
1358+
An overview.
1359+
"""),
1360+
TextFile(name: "SampleCode.md", utf8Content: """
1361+
# Sample Code
1362+
@Metadata {
1363+
@PageKind(sampleCode)
1364+
}
1365+
## Topics
1366+
- <doc:article>
1367+
"""),
1368+
JSONFile(
1369+
name: "unit-test.symbols.json",
1370+
content: makeSymbolGraph(
1371+
moduleName: "unit-test",
1372+
symbols: [SymbolGraph.Symbol(
1373+
identifier: .init(precise: "symbol-id", interfaceLanguage: "swift"),
1374+
names: .init(title: "Symbol", navigator: nil, subHeading: nil, prose: nil),
1375+
pathComponents: ["Symbol"],
1376+
docComment: nil,
1377+
accessLevel: .public,
1378+
kind: .init(parsedIdentifier: .class, displayName: "Kind Display Name"),
1379+
mixins: [:]
1380+
)]
1381+
)
1382+
),
1383+
]
1384+
)
1385+
let tempURL = try createTempFolder(content: [exampleDocumentation])
1386+
let (_, bundle, context) = try loadBundle(from: tempURL)
1387+
1388+
// Assert that articles that curates any symbol gets 'API Collection' assigned as the eyebrow title.
1389+
var renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/APICollection")
1390+
XCTAssertEqual(renderNode.metadata.roleHeading, "API Collection")
1391+
// Assert that articles that curates only other articles don't get any value assigned as the eyebrow title.
1392+
renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Collection")
1393+
XCTAssertEqual(renderNode.metadata.roleHeading, nil)
1394+
// Assert that articles that don't curate anything else get 'Article' assigned as the eyebrow title.
1395+
renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Article")
1396+
XCTAssertEqual(renderNode.metadata.roleHeading, "Article")
1397+
// Assert that articles that have a custom title heading the eyebrow title assigned properly.
1398+
renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/CustomRole")
1399+
XCTAssertEqual(renderNode.metadata.roleHeading, "Custom Role")
1400+
// Assert that articles that have a custom page kind the eyebrow title assigned properly.
1401+
renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/SampleCode")
1402+
XCTAssertEqual(renderNode.metadata.roleHeading, "Sample Code")
1403+
}
13031404
}

0 commit comments

Comments
 (0)