Skip to content

Commit 77d5b38

Browse files
committed
Render alternate representations as node variants
When rendering the variants of a node, use the topic references from the `@AlternateRepresentation` directives to populate more variants. There is no need to report diagnostics as they would have been reported during bundle registration. Link resolution would have already been performed at that point. Unresolved topic references are ignored, but all resolved references are added as variants. If there are multiple symbols per variant, Swift-DocC-Render prefers the first one that was added, which will always be the current node's symbol. There should be no breakage and change of behaviour for anyone not using `@AlternateRepresentation`, and the current symbol's variants will always be preferred over any other.
1 parent dc6a487 commit 77d5b38

File tree

2 files changed

+115
-13
lines changed

2 files changed

+115
-13
lines changed

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,23 +1852,45 @@ public struct RenderNodeTranslator: SemanticVisitor {
18521852
private func variants(for documentationNode: DocumentationNode) -> [RenderNode.Variant] {
18531853
let generator = PresentationURLGenerator(context: context, baseURL: bundle.baseURL)
18541854

1855-
return documentationNode.availableSourceLanguages
1856-
.sorted(by: { language1, language2 in
1855+
var allVariants: [SourceLanguage: ResolvedTopicReference] = documentationNode.availableSourceLanguages.reduce(into: [:]) { partialResult, language in
1856+
partialResult[language] = identifier
1857+
}
1858+
1859+
// Apply alternate representations
1860+
if let alternateRepresentations = documentationNode.metadata?.alternateRepresentations {
1861+
for alternateRepresentation in alternateRepresentations {
1862+
// Only alternate representations which were able to be resolved to a reference should be included as an alternate representation.
1863+
// Unresolved alternate representations can be ignored, as they would have been reported during link resolution.
1864+
guard case .resolved(.success(let alternateRepresentationReference)) = alternateRepresentation.reference else {
1865+
continue
1866+
}
1867+
1868+
// Add the language representations of the alternate symbol as additional variants for the current symbol.
1869+
// Symbols can only specify custom alternate language representations for languages that the documented symbol doesn't already have a representation for.
1870+
// If the current symbol and its custom alternate representation share language representations, the custom language representation is ignored.
1871+
allVariants.merge(
1872+
alternateRepresentationReference.sourceLanguages.map { ($0, alternateRepresentationReference) }
1873+
) { existing, _ in existing }
1874+
}
1875+
}
1876+
1877+
return allVariants
1878+
.sorted(by: { variant1, variant2 in
18571879
// Emit Swift first, then alphabetically.
1858-
switch (language1, language2) {
1880+
switch (variant1.key, variant2.key) {
18591881
case (.swift, _): return true
18601882
case (_, .swift): return false
1861-
default: return language1.id < language2.id
1862-
}
1863-
})
1864-
.map { sourceLanguage in
1865-
RenderNode.Variant(
1866-
traits: [.interfaceLanguage(sourceLanguage.id)],
1867-
paths: [
1868-
generator.presentationURLForReference(identifier).path
1869-
]
1870-
)
1883+
default: return variant1.key.id < variant2.key.id
18711884
}
1885+
})
1886+
.map { sourceLanguage, reference in
1887+
RenderNode.Variant(
1888+
traits: [.interfaceLanguage(sourceLanguage.id)],
1889+
paths: [
1890+
generator.presentationURLForReference(reference).path
1891+
]
1892+
)
1893+
}
18721894
}
18731895

18741896
private mutating func convertFragments(_ fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment]) -> [DeclarationRenderSection.Token] {

Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,4 +1495,84 @@ class RenderNodeTranslatorTests: XCTestCase {
14951495
}
14961496
}
14971497
}
1498+
1499+
func testAlternateRepresentationsRenderedAsVariants() throws {
1500+
let (bundle, context) = try loadBundle(catalog: Folder(
1501+
name: "unit-test.docc",
1502+
content: [
1503+
TextFile(name: "Symbol.md", utf8Content: """
1504+
# ``Symbol``
1505+
@Metadata {
1506+
@AlternateRepresentation(``CounterpartSymbol``)
1507+
}
1508+
A symbol extension file defining an alternate representation.
1509+
"""),
1510+
TextFile(name: "OtherSymbol.md", utf8Content: """
1511+
# ``OtherSymbol``
1512+
@Metadata {
1513+
@AlternateRepresentation(``MissingCounterpart``)
1514+
}
1515+
A symbol extension file defining an alternate representation which doesn't exist.
1516+
"""),
1517+
TextFile(name: "MultipleSwiftVariantsSymbol.md", utf8Content: """
1518+
# ``MultipleSwiftVariantsSymbol``
1519+
@Metadata {
1520+
@AlternateRepresentation(``Symbol``)
1521+
}
1522+
A symbol extension file defining an alternate representation which is also in Swift.
1523+
"""),
1524+
JSONFile(
1525+
name: "unit-test.swift.symbols.json",
1526+
content: makeSymbolGraph(
1527+
moduleName: "unit-test",
1528+
symbols: [
1529+
makeSymbol(id: "symbol-id", kind: .class, pathComponents: ["Symbol"]),
1530+
makeSymbol(id: "other-symbol-id", kind: .class, pathComponents: ["OtherSymbol"]),
1531+
makeSymbol(id: "multiple-swift-variants-symbol-id", kind: .class, pathComponents: ["MultipleSwiftVariantsSymbol"]),
1532+
]
1533+
)
1534+
),
1535+
JSONFile(
1536+
name: "unit-test.occ.symbols.json",
1537+
content: makeSymbolGraph(
1538+
moduleName: "unit-test",
1539+
symbols: [
1540+
makeSymbol(id: "counterpart-symbol-id", language: .objectiveC, kind: .class, pathComponents: ["CounterpartSymbol"]),
1541+
]
1542+
)
1543+
),
1544+
]
1545+
))
1546+
1547+
func renderNodeArticleFromReferencePath(
1548+
referencePath: String
1549+
) throws -> RenderNode {
1550+
let reference = ResolvedTopicReference(bundleID: bundle.id, path: referencePath, sourceLanguage: .swift)
1551+
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
1552+
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference)
1553+
return try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
1554+
}
1555+
1556+
// Assert that CounterpartSymbol's source languages have been added as source languages of Symbol
1557+
var renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/Symbol")
1558+
XCTAssertEqual(renderNode.variants?.count, 2)
1559+
XCTAssertEqual(renderNode.variants, [
1560+
.init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unit-test/symbol"]),
1561+
.init(traits: [.interfaceLanguage("occ")], paths: ["/documentation/unit-test/counterpartsymbol"])
1562+
])
1563+
1564+
// Assert that alternate representations which can't be resolved are ignored
1565+
renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/OtherSymbol")
1566+
XCTAssertEqual(renderNode.variants?.count, 1)
1567+
XCTAssertEqual(renderNode.variants, [
1568+
.init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unit-test/othersymbol"]),
1569+
])
1570+
1571+
// Assert that duplicate alternate representations are not added as variants
1572+
renderNode = try renderNodeArticleFromReferencePath(referencePath: "/documentation/unit-test/MultipleSwiftVariantsSymbol")
1573+
XCTAssertEqual(renderNode.variants?.count, 1)
1574+
XCTAssertEqual(renderNode.variants, [
1575+
.init(traits: [.interfaceLanguage("swift")], paths: ["/documentation/unit-test/multipleswiftvariantssymbol"]),
1576+
])
1577+
}
14981578
}

0 commit comments

Comments
 (0)