Skip to content

Commit

Permalink
Render alternate representations as node variants
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
anferbui committed Dec 13, 2024
1 parent 711469a commit 74f3f2c
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 13 deletions.
48 changes: 35 additions & 13 deletions Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1852,23 +1852,45 @@ 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] = documentationNode.availableSourceLanguages.reduce(into: [:]) { partialResult, language in
partialResult[language] = identifier
}

// Apply alternate representations
if let alternateRepresentations = documentationNode.metadata?.alternateRepresentations {
for alternateRepresentation in alternateRepresentations {
// Only alternate representations which were able to be resolved to a reference should be included as an alternate representation.
// Unresolved alternate representations can be ignored, as they would have been reported during link resolution.
guard case .resolved(.success(let alternateRepresentationReference)) = alternateRepresentation.reference else {
continue
}

// Add the language representations of the alternate symbol as additional variants for the current symbol.
// Symbols can only specify custom alternate language representations for languages that the documented symbol doesn't already have a representation for.
// If the current symbol and its custom alternate representation share language representations, the custom language representation is ignored.
allVariants.merge(
alternateRepresentationReference.sourceLanguages.map { ($0, alternateRepresentationReference) }
) { existing, _ in existing }
}
}

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, reference in
RenderNode.Variant(
traits: [.interfaceLanguage(sourceLanguage.id)],
paths: [
generator.presentationURLForReference(reference).path
]
)
}
}

private mutating func convertFragments(_ fragments: [SymbolGraph.Symbol.DeclarationFragments.Fragment]) -> [DeclarationRenderSection.Token] {
Expand Down
80 changes: 80 additions & 0 deletions Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1495,4 +1495,84 @@ class RenderNodeTranslatorTests: XCTestCase {
}
}
}

func testAlternateRepresentationsRenderedAsVariants() throws {
let (bundle, context) = try loadBundle(catalog: 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.swift.symbols.json",
content: makeSymbolGraph(
moduleName: "unit-test",
symbols: [
makeSymbol(id: "symbol-id", kind: .class, pathComponents: ["Symbol"]),
makeSymbol(id: "other-symbol-id", kind: .class, pathComponents: ["OtherSymbol"]),
makeSymbol(id: "multiple-swift-variants-symbol-id", kind: .class, pathComponents: ["MultipleSwiftVariantsSymbol"]),
]
)
),
JSONFile(
name: "unit-test.occ.symbols.json",
content: makeSymbolGraph(
moduleName: "unit-test",
symbols: [
makeSymbol(id: "counterpart-symbol-id", language: .objectiveC, kind: .class, pathComponents: ["CounterpartSymbol"]),
]
)
),
]
))

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 duplicate alternate representations are not added 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"]),
])
}
}

0 comments on commit 74f3f2c

Please sign in to comment.