Skip to content

Commit baff176

Browse files
[5.10] Fix problem where API Collections had no roleHeading assigned (#795)
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 c47f599 commit baff176

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 = contentRenderer.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 = contentRenderer.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(
799819
title: seeAlso.title,
800820
abstract: nil,
@@ -851,7 +871,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
851871
node.metadata.roleHeading = titleHeading.heading
852872
}
853873

854-
collectedTopicReferences.append(contentsOf: contentCompiler.collectedTopicReferences)
874+
collectedTopicReferences.append(contentsOf: topicSectionContentCompiler.collectedTopicReferences)
855875
node.references = createTopicRenderReferences()
856876

857877
addReferences(imageReferences, to: &node)
@@ -860,7 +880,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
860880
addReferences(downloadReferences, to: &node)
861881
// See Also can contain external links, we need to separately transfer
862882
// link references from the content compiler
863-
addReferences(contentCompiler.linkReferences, to: &node)
883+
addReferences(topicSectionContentCompiler.linkReferences, to: &node)
864884

865885
return node
866886
}

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

0 commit comments

Comments
 (0)