From baff176d26d935c104d0fea2ba9e3d97b333e25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sof=C3=ADa=20Rodr=C3=ADguez?= Date: Thu, 11 Jan 2024 13:58:20 +0100 Subject: [PATCH] [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. --- .../Rendering/RenderNodeTranslator.swift | 44 +++++--- .../Rendering/RenderNodeTranslatorTests.swift | 101 ++++++++++++++++++ 2 files changed, 133 insertions(+), 12 deletions(-) diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index 951f735100..27b3904b19 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -598,7 +598,8 @@ public struct RenderNodeTranslator: SemanticVisitor { public mutating func visitArticle(_ article: Article) -> RenderTree? { var node = RenderNode(identifier: identifier, kind: .article) - var contentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: identifier) + // Contains symbol references declared in the Topics section. + var topicSectionContentCompiler = RenderContentCompiler(context: context, bundle: bundle, identifier: identifier) node.metadata.title = article.title!.plainText @@ -674,7 +675,7 @@ public struct RenderNodeTranslator: SemanticVisitor { allowExternalLinks: false, allowedTraits: allowedTraits, availableTraits: documentationNode.availableVariantTraits, - contentCompiler: &contentCompiler + contentCompiler: &topicSectionContentCompiler ) ) } @@ -685,7 +686,7 @@ public struct RenderNodeTranslator: SemanticVisitor { sections.append( contentsOf: renderAutomaticTaskGroupsSection( article.automaticTaskGroups.filter { $0.renderPositionPreference == .top }, - contentCompiler: &contentCompiler + contentCompiler: &topicSectionContentCompiler ) ) } @@ -714,7 +715,7 @@ public struct RenderNodeTranslator: SemanticVisitor { } // Collect all child topic references. - contentCompiler.collectedTopicReferences.append(contentsOf: groups.flatMap(\.references)) + topicSectionContentCompiler.collectedTopicReferences.append(contentsOf: groups.flatMap(\.references)) // Add the final groups to the node. sections.append(contentsOf: groups.map(TaskGroupRenderSection.init(taskGroup:))) } @@ -725,7 +726,7 @@ public struct RenderNodeTranslator: SemanticVisitor { sections.append( contentsOf: renderAutomaticTaskGroupsSection( article.automaticTaskGroups.filter { $0.renderPositionPreference == .bottom }, - contentCompiler: &contentCompiler + contentCompiler: &topicSectionContentCompiler ) ) } @@ -736,11 +737,30 @@ public struct RenderNodeTranslator: SemanticVisitor { node.topicSectionsStyle = topicsSectionStyle(for: documentationNode) if shouldCreateAutomaticRoleHeading(for: documentationNode) { - if node.topicSections.isEmpty { - // Set an eyebrow for articles + + let role = contentRenderer.roleForArticle(article, nodeKind: documentationNode.kind) + node.metadata.role = role.rawValue + + switch role { + case .article: + // If there are no links to other nodes from the article, + // set the eyebrow for articles. node.metadata.roleHeading = "Article" + case .collectionGroup: + // If the article links to other nodes, set the eyebrow for + // API Collections if any linked node is a symbol. + // + // If none of the linked nodes are symbols (it's a plain collection), + // don't display anything as the eyebrow title. + let curatesSymbols = topicSectionContentCompiler.collectedTopicReferences.contains { topicReference in + context.topicGraph.nodeWithReference(topicReference)?.kind.isSymbol ?? false + } + if curatesSymbols { + node.metadata.roleHeading = "API Collection" + } + default: + break } - node.metadata.role = contentRenderer.roleForArticle(article, nodeKind: documentationNode.kind).rawValue } if let pageImages = documentationNode.metadata?.pageImages { @@ -780,7 +800,7 @@ public struct RenderNodeTranslator: SemanticVisitor { allowExternalLinks: true, allowedTraits: allowedTraits, availableTraits: documentationNode.availableVariantTraits, - contentCompiler: &contentCompiler + contentCompiler: &topicSectionContentCompiler ) ) } @@ -794,7 +814,7 @@ public struct RenderNodeTranslator: SemanticVisitor { renderContext: renderContext, renderer: contentRenderer ) { - contentCompiler.collectedTopicReferences.append(contentsOf: seeAlso.references) + topicSectionContentCompiler.collectedTopicReferences.append(contentsOf: seeAlso.references) seeAlsoSections.append(TaskGroupRenderSection( title: seeAlso.title, abstract: nil, @@ -851,7 +871,7 @@ public struct RenderNodeTranslator: SemanticVisitor { node.metadata.roleHeading = titleHeading.heading } - collectedTopicReferences.append(contentsOf: contentCompiler.collectedTopicReferences) + collectedTopicReferences.append(contentsOf: topicSectionContentCompiler.collectedTopicReferences) node.references = createTopicRenderReferences() addReferences(imageReferences, to: &node) @@ -860,7 +880,7 @@ public struct RenderNodeTranslator: SemanticVisitor { addReferences(downloadReferences, to: &node) // See Also can contain external links, we need to separately transfer // link references from the content compiler - addReferences(contentCompiler.linkReferences, to: &node) + addReferences(topicSectionContentCompiler.linkReferences, to: &node) return node } diff --git a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift b/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift index f50440a8bb..90e3395da6 100644 --- a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift @@ -13,6 +13,7 @@ import XCTest @testable import SwiftDocC import SwiftDocCTestUtilities import Markdown +import SymbolKit class RenderNodeTranslatorTests: XCTestCase { private func findDiscussion(forSymbolPath: String, configureBundle: ((URL) throws -> Void)? = nil) throws -> ContentRenderSection? { @@ -1303,4 +1304,104 @@ class RenderNodeTranslatorTests: XCTestCase { XCTAssertEqual(roundTrippedSymbol.metadata.roleHeading, "TestBed Notes") XCTAssertEqual(roundTrippedSymbol.metadata.role, "collection") } + + func testExpectedRoleHeadingIsAssigned() throws { + func renderNodeArticleFromReferencePath( + referencePath: String + ) throws -> RenderNode { + let reference = ResolvedTopicReference( + bundleIdentifier: bundle.identifier, + path: referencePath, + sourceLanguage: .swift + ) + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Article) + var translator = RenderNodeTranslator( + context: context, + bundle: bundle, + identifier: reference, + source: nil + ) + return try XCTUnwrap(translator.visitArticle(symbol) as? RenderNode) + } + + let exampleDocumentation = Folder( + name: "unit-test.docc", + content: [ + TextFile(name: "APICollection.md", utf8Content: """ + # API Collection + My API Collection Abstract. + ## Topics + - ``Symbol`` + - + - + """), + TextFile(name: "Collection.md", utf8Content: """ + # Collection + An abstract with a symbol link: ``MyKit/MyProtocol`` + ## Overview + An overview with a symbol link: ``MyKit/MyProtocol`` + ## Topics + A topic group abstract with a symbol link: ``MyKit/MyProtocol`` + - + - + """), + TextFile(name: "Article.md", utf8Content: """ + # Article + My Article Abstract. + ## Overview + An overview. + """), + TextFile(name: "CustomRole.md", utf8Content: """ + # Article 4 + @Metadata { + @TitleHeading("Custom Role") + } + My Article Abstract. + ## Overview + An overview. + """), + TextFile(name: "SampleCode.md", utf8Content: """ + # Sample Code + @Metadata { + @PageKind(sampleCode) + } + ## Topics + - + """), + JSONFile( + name: "unit-test.symbols.json", + content: makeSymbolGraph( + moduleName: "unit-test", + symbols: [SymbolGraph.Symbol( + identifier: .init(precise: "symbol-id", interfaceLanguage: "swift"), + names: .init(title: "Symbol", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["Symbol"], + docComment: nil, + accessLevel: .public, + kind: .init(parsedIdentifier: .class, displayName: "Kind Display Name"), + mixins: [:] + )] + ) + ), + ] + ) + let tempURL = try createTempFolder(content: [exampleDocumentation]) + let (_, bundle, context) = try loadBundle(from: tempURL) + + // Assert that articles that curates any symbol gets 'API Collection' assigned as the eyebrow title. + var renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/APICollection") + XCTAssertEqual(renderNode.metadata.roleHeading, "API Collection") + // Assert that articles that curates only other articles don't get any value assigned as the eyebrow title. + renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Collection") + XCTAssertEqual(renderNode.metadata.roleHeading, nil) + // Assert that articles that don't curate anything else get 'Article' assigned as the eyebrow title. + renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Article") + XCTAssertEqual(renderNode.metadata.roleHeading, "Article") + // Assert that articles that have a custom title heading the eyebrow title assigned properly. + renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/CustomRole") + XCTAssertEqual(renderNode.metadata.roleHeading, "Custom Role") + // Assert that articles that have a custom page kind the eyebrow title assigned properly. + renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/SampleCode") + XCTAssertEqual(renderNode.metadata.roleHeading, "Sample Code") + } }