Skip to content

Commit

Permalink
Render alternate representations as node variants
Browse files Browse the repository at this point in the history
  • Loading branch information
anferbui committed Nov 20, 2024
1 parent 6ba0559 commit 2638a80
Show file tree
Hide file tree
Showing 2 changed files with 137 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
106 changes: 106 additions & 0 deletions Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1497,4 +1497,110 @@ 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: [
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: [:]
),
SymbolGraph.Symbol(
identifier: .init(precise: "counterpart-symbol-id", interfaceLanguage: "occ"),
names: .init(title: "CounterpartSymbol", navigator: nil, subHeading: nil, prose: nil),
pathComponents: ["CounterpartSymbol"],
docComment: nil,
accessLevel: .public,
kind: .init(parsedIdentifier: .class, displayName: "Kind Display Name"),
mixins: [:]
),
SymbolGraph.Symbol(
identifier: .init(precise: "other-symbol-id", interfaceLanguage: "swift"),
names: .init(title: "OtherSymbol", navigator: nil, subHeading: nil, prose: nil),
pathComponents: ["OtherSymbol"],
docComment: nil,
accessLevel: .public,
kind: .init(parsedIdentifier: .class, displayName: "Kind Display Name"),
mixins: [:]
),
SymbolGraph.Symbol(
identifier: .init(precise: "multiple-swift-variants-symbol-id", interfaceLanguage: "swift"),
names: .init(title: "MultipleSwiftVariantsSymbol", navigator: nil, subHeading: nil, prose: nil),
pathComponents: ["MultipleSwiftVariantsSymbol"],
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)

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? 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 2638a80

Please sign in to comment.