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 Nov 22, 2024
1 parent 10fa24f commit 14d85bf
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 13 deletions.
44 changes: 31 additions & 13 deletions Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand Down
74 changes: 74 additions & 0 deletions Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
])
}
}

0 comments on commit 14d85bf

Please sign in to comment.