From abe7160e8c1583ecc2ab364cfcf484f1d64fde27 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:12:52 +0000 Subject: [PATCH] Render alternate representations as node variants --- .../Rendering/RenderNodeTranslator.swift | 44 +++++++---- .../Rendering/RenderNodeTranslatorTests.swift | 74 +++++++++++++++++++ 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index adad3d7d5..952ac9cc8 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -1852,23 +1852,41 @@ public struct RenderNodeTranslator: SemanticVisitor { private func variants(for documentationNode: DocumentationNode) -> [RenderNode.Variant] { let generator = PresentationURLGenerator(context: context, baseURL: bundle.baseURL) - return documentationNode.availableSourceLanguages - .sorted(by: { language1, language2 in + var allVariants: [SourceLanguage: [ResolvedTopicReference]] = Dictionary(uniqueKeysWithValues: documentationNode.availableSourceLanguages.map { ($0, [identifier]) }) + + // Apply alternate representations + documentationNode.metadata?.alternateRepresentations.forEach { alternateRepresentation in + // Only counterparts which were able to be resolved to a reference should be included as an alternate representation. + // Unresolved counterparts can be ignored, as they would have been reported during link resolution. + guard case .resolved(.success(let counterpartReference)) = alternateRepresentation.reference else { + return + } + + // Add all of the variants of the counterpart as additional variants for the current symbol + // If the current symbol and its counterpart share source languages, the list of variants for that language will contain multiple symbol references. + // Only the first symbol reference will be respected by Swift-DocC Render. + counterpartReference.sourceLanguages.forEach { + allVariants[$0, default: []].append(counterpartReference) + } + } + + return allVariants + .sorted(by: { variant1, variant2 in // Emit Swift first, then alphabetically. - switch (language1, language2) { + switch (variant1.key, variant2.key) { case (.swift, _): return true case (_, .swift): return false - default: return language1.id < language2.id - } - }) - .map { sourceLanguage in - RenderNode.Variant( - traits: [.interfaceLanguage(sourceLanguage.id)], - paths: [ - generator.presentationURLForReference(identifier).path - ] - ) + default: return variant1.key.id < variant2.key.id } + }) + .map { sourceLanguage, references in + RenderNode.Variant( + traits: [.interfaceLanguage(sourceLanguage.id)], + paths: references.map { reference in + generator.presentationURLForReference(reference).path + } + ) + } } private mutating func convertFragments(_ fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment]) -> [DeclarationRenderSection.Token] { diff --git a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift b/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift index 167405ca0..6cf8be297 100644 --- a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift @@ -1497,4 +1497,78 @@ class RenderNodeTranslatorTests: XCTestCase { } } } + + func testAlternateRepresentationsRenderedAsVariants() throws { + let exampleDocumentation = Folder( + name: "unit-test.docc", + content: [ + TextFile(name: "Symbol.md", utf8Content: """ + # ``Symbol`` + @Metadata { + @AlternateRepresentation(``CounterpartSymbol``) + } + A symbol extension file defining an alternate representation. + """), + TextFile(name: "OtherSymbol.md", utf8Content: """ + # ``OtherSymbol`` + @Metadata { + @AlternateRepresentation(``MissingCounterpart``) + } + A symbol extension file defining an alternate representation which doesn't exist. + """), + TextFile(name: "MultipleSwiftVariantsSymbol.md", utf8Content: """ + # ``MultipleSwiftVariantsSymbol`` + @Metadata { + @AlternateRepresentation(``Symbol``) + } + A symbol extension file defining an alternate representation which is also in Swift. + """), + JSONFile( + name: "unit-test.symbols.json", + content: makeSymbolGraph( + moduleName: "unit-test", + symbols: [ + makeSymbol(id: "symbol-id", kind: .class, pathComponents: ["Symbol"]), + makeSymbol(id: "counterpart-symbol-id", language: .objectiveC, kind: .class, pathComponents: ["CounterpartSymbol"]), + makeSymbol(id: "other-symbol-id", kind: .class, pathComponents: ["OtherSymbol"]), + makeSymbol(id: "multiple-swift-variants-symbol-id", kind: .class, pathComponents: ["MultipleSwiftVariantsSymbol"]), + ] + ) + ), + ] + ) + let tempURL = try createTempFolder(content: [exampleDocumentation]) + let (_, bundle, context) = try loadBundle(from: tempURL) + + func renderNodeArticleFromReferencePath( + referencePath: String + ) throws -> RenderNode { + let reference = ResolvedTopicReference(bundleID: bundle.id, path: referencePath, sourceLanguage: .swift) + let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol) + var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference) + return try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode) + } + + // Assert that CounterpartSymbol's source languages have been added as source languages of Symbol + var renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Symbol") + XCTAssertEqual(renderNode.variants?.count, 2) + XCTAssertEqual(renderNode.variants, [ + .init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unit-test/symbol"]), + .init(traits: [.interfaceLanguage("occ")], paths: ["/documentation/unit-test/counterpartsymbol"]) + ]) + + // Assert that alternate representations which can't be resolved are ignored + renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/OtherSymbol") + XCTAssertEqual(renderNode.variants?.count, 1) + XCTAssertEqual(renderNode.variants, [ + .init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unit-test/othersymbol"]), + ]) + + // Assert that multiple alternate representations are represented as variants + renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/MultipleSwiftVariantsSymbol") + XCTAssertEqual(renderNode.variants?.count, 1) + XCTAssertEqual(renderNode.variants, [ + .init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unit-test/multipleswiftvariantssymbol", "/documentation/unit-test/symbol"]), + ]) + } }