Skip to content

Commit c470c5b

Browse files
committed
Emit multi-language symbol data
Emits multi-language symbol data to render JSON. rdar://82967644
1 parent fa53b98 commit c470c5b

38 files changed

+2737
-456
lines changed

Sources/SwiftDocC/Infrastructure/Communication/Foundation/AnyCodable.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ public struct AnyCodable: Codable, CustomDebugStringConvertible {
2222

2323
public init(from decoder: Decoder) throws {
2424
let container = try decoder.singleValueContainer()
25-
value = try container.decode(JSON.self)
25+
if container.decodeNil() {
26+
value = JSON.null
27+
} else {
28+
value = try container.decode(JSON.self)
29+
}
2630
}
2731

2832
public func encode(to encoder: Encoder) throws {

Sources/SwiftDocC/Model/Identifier.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,14 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
8787
}
8888

8989
/// The source language for which this topic is relevant.
90-
public var sourceLanguage: SourceLanguage
90+
public var sourceLanguage: SourceLanguage {
91+
// Return Swift by default to maintain backwards-compatibility.
92+
get { sourceLanguages.contains(.swift) ? .swift : sourceLanguages.first! }
93+
set { sourceLanguages.insert(newValue) }
94+
}
95+
96+
/// The source languages for which this topic is relevant.
97+
public var sourceLanguages: Set<SourceLanguage>
9198

9299
/// The reference cache key
93100
var cacheKey: String {
@@ -108,7 +115,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
108115
self.bundleIdentifier = bundleIdentifier
109116
self.path = urlReadablePath(path)
110117
self.fragment = fragment.map { urlReadableFragment($0) }
111-
self.sourceLanguage = sourceLanguage
118+
self.sourceLanguages = [sourceLanguage]
112119
updateURL()
113120

114121
// Cache the reference
@@ -235,7 +242,21 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
235242
public func encode(to encoder: Encoder) throws {
236243
var container = encoder.container(keyedBy: CodingKeys.self)
237244
try container.encode(url.absoluteString, forKey: .url)
238-
try container.encode(sourceLanguage.id, forKey: .interfaceLanguage)
245+
246+
let sourceLanguageIDVariants = SymbolDataVariants<String>(
247+
values: Dictionary<SymbolDataVariantsTrait, String>(
248+
uniqueKeysWithValues: sourceLanguages.map { language in
249+
(SymbolDataVariantsTrait(interfaceLanguage: language.id), language.id)
250+
}
251+
)
252+
)
253+
254+
try container.encodeVariantCollection(
255+
// Force-unwrapping because resolved topic references should have at least one source language.
256+
VariantCollection<String>(from: sourceLanguageIDVariants)!,
257+
forKey: .interfaceLanguage,
258+
encoder: encoder
259+
)
239260
}
240261

241262
public var description: String {

Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import Foundation
1212
import SymbolKit
13+
import Markdown
1314

1415
public struct RenderReferenceDependencies {
1516
var topicReferences = [ResolvedTopicReference]()
@@ -57,25 +58,44 @@ public class DocumentationContentRenderer {
5758
}
5859

5960
/// For symbol nodes, returns the fragments mixin if any.
60-
func subHeadingFragments(for node: DocumentationNode) -> [DeclarationRenderSection.Token]? {
61-
guard let symbol = (node.semantic as? Symbol),
62-
var fragments = symbol.subHeading?
61+
func subHeadingFragments(for node: DocumentationNode) -> VariantCollection<[DeclarationRenderSection.Token]?> {
62+
guard let symbol = (node.semantic as? Symbol) else {
63+
return .init(defaultValue: nil)
64+
}
65+
66+
return VariantCollection<[DeclarationRenderSection.Token]?>(
67+
from: symbol.subHeadingVariants,
68+
symbol.titleVariants,
69+
symbol.kindVariants
70+
) { _, subHeading, title, kind in
71+
var fragments = subHeading
6372
.map({ fragment -> DeclarationRenderSection.Token in
6473
return DeclarationRenderSection.Token(fragment: fragment, identifier: nil)
65-
}) else { return nil }
66-
if fragments.last?.text == "\n" { fragments.removeLast() }
67-
return Swift.subHeading(for: fragments, symbolTitle: symbol.title, symbolKind: symbol.kind.identifier)
74+
})
75+
if fragments.last?.text == "\n" { fragments.removeLast() }
76+
77+
// TODO: Return an Objective-C subheading for Objective-C symbols (rdar://84195588)
78+
return Swift.subHeading(for: fragments, symbolTitle: title, symbolKind: kind.identifier)
79+
} ?? .init(defaultValue: nil)
6880
}
6981

7082
/// For symbol nodes, returns the navigator title if any.
71-
func navigatorFragments(for node: DocumentationNode) -> [DeclarationRenderSection.Token]? {
72-
guard let symbol = (node.semantic as? Symbol),
73-
var fragments = symbol.navigator?
74-
.map({ fragment -> DeclarationRenderSection.Token in
75-
return DeclarationRenderSection.Token(fragment: fragment, identifier: nil)
76-
}) else { return nil }
77-
if fragments.last?.text == "\n" { fragments.removeLast() }
78-
return Swift.navigatorTitle(for: fragments, symbolTitle: symbol.title)
83+
func navigatorFragments(for node: DocumentationNode) -> VariantCollection<[DeclarationRenderSection.Token]?> {
84+
guard let symbol = (node.semantic as? Symbol) else {
85+
return .init(defaultValue: nil)
86+
}
87+
88+
return VariantCollection<[DeclarationRenderSection.Token]?>(
89+
from: symbol.navigatorVariants
90+
) { _, navigator in
91+
var fragments = navigator.map { fragment -> DeclarationRenderSection.Token in
92+
return DeclarationRenderSection.Token(fragment: fragment, identifier: nil)
93+
}
94+
if fragments.last?.text == "\n" { fragments.removeLast() }
95+
96+
// TODO: Return an Objective-C navigator title for Objective-C symbols (rdar://84195588)
97+
return Swift.navigatorTitle(for: fragments, symbolTitle: symbol.title)
98+
} ?? .init(defaultValue: nil)
7999
}
80100

81101
/// Returns the given amount of minutes as a string, for example: "1hr 10min".
@@ -223,21 +243,29 @@ public class DocumentationContentRenderer {
223243
func renderReference(for reference: ResolvedTopicReference, with overridingDocumentationNode: DocumentationNode? = nil, dependencies: inout RenderReferenceDependencies) -> TopicRenderReference {
224244
let resolver = LinkTitleResolver(context: documentationContext, source: reference.url)
225245

226-
let title: String
246+
let titleVariants: SymbolDataVariants<String>
227247
let kind: RenderNode.Kind
228248
var referenceRole: String?
229249
let node = try? overridingDocumentationNode ?? documentationContext.entity(with: reference)
250+
230251
if let node = node, let resolvedTitle = resolver.title(for: node) {
231-
title = resolvedTitle
252+
titleVariants = resolvedTitle
232253
} else if let anchorSection = documentationContext.nodeAnchorSections[reference] {
233254
// No need to continue, return a section topic reference
234-
return TopicRenderReference(identifier: RenderReferenceIdentifier(reference.absoluteString), title: anchorSection.title, abstract: [], url: urlGenerator.presentationURLForReference(reference, requireRelativeURL: true).absoluteString, kind: .section, estimatedTime: nil)
255+
return TopicRenderReference(
256+
identifier: RenderReferenceIdentifier(reference.absoluteString),
257+
title: anchorSection.title,
258+
abstract: [],
259+
url: urlGenerator.presentationURLForReference(reference, requireRelativeURL: true).absoluteString,
260+
kind: .section,
261+
estimatedTime: nil
262+
)
235263
} else if let topicGraphOnlyNode = documentationContext.topicGraph.nodeWithReference(reference) {
236264
// Some nodes are artificially inserted into the topic graph,
237265
// try resolving that way as a fallback after looking up `documentationCache`.
238-
title = topicGraphOnlyNode.title
266+
titleVariants = .init(defaultVariantValue: topicGraphOnlyNode.title)
239267
} else {
240-
title = reference.absoluteString
268+
titleVariants = .init(defaultVariantValue: reference.absoluteString)
241269
}
242270

243271
switch node?.kind {
@@ -273,7 +301,7 @@ public class DocumentationContentRenderer {
273301
let presentationURL = urlGenerator.presentationURLForReference(reference, requireRelativeURL: true)
274302

275303
var contentCompiler = RenderContentCompiler(context: documentationContext, bundle: bundle, identifier: reference)
276-
let abstractContent: [RenderInlineContent]
304+
let abstractContent: VariantCollection<[RenderInlineContent]>
277305

278306
var abstractedNode = node
279307
if kind == .section {
@@ -282,12 +310,28 @@ public class DocumentationContentRenderer {
282310
abstractedNode = try? documentationContext.entity(with: containerReference)
283311
}
284312

285-
if let abstract = (abstractedNode?.semantic as? Abstracted)?.abstract ?? abstractedNode.map({ DocumentationMarkup(markup: $0.markup, parseUpToSection: .abstract) })?.abstractSection?.paragraph,
286-
let renderedContent = contentCompiler.visit(abstract).first,
287-
case let .paragraph(inlines)? = renderedContent as? RenderBlockContent {
288-
abstractContent = inlines
313+
func extractAbstract(from paragraph: Paragraph?) -> [RenderInlineContent] {
314+
if let abstract = paragraph
315+
?? abstractedNode.map({
316+
DocumentationMarkup(markup: $0.markup, parseUpToSection: .abstract)
317+
})?.abstractSection?.paragraph,
318+
let renderedContent = contentCompiler.visit(abstract).first,
319+
case let .paragraph(inlines)? = renderedContent as? RenderBlockContent
320+
{
321+
return inlines
322+
} else {
323+
return []
324+
}
325+
}
326+
327+
if let symbol = (abstractedNode?.semantic as? Symbol) {
328+
abstractContent = VariantCollection<[RenderInlineContent]>(
329+
from: symbol.abstractVariants
330+
) { _, abstract in
331+
extractAbstract(from: abstract)
332+
} ?? .init(defaultValue: [])
289333
} else {
290-
abstractContent = []
334+
abstractContent = .init(defaultValue: extractAbstract(from: (abstractedNode?.semantic as? Abstracted)?.abstract))
291335
}
292336

293337
// Collect the reference dependencies.
@@ -300,8 +344,8 @@ public class DocumentationContentRenderer {
300344

301345
var renderReference = TopicRenderReference(
302346
identifier: .init(referenceURL),
303-
titleVariants: .init(defaultValue: title),
304-
abstract: abstractContent,
347+
titleVariants: VariantCollection<String>(from: titleVariants) ?? .init(defaultValue: ""),
348+
abstractVariants: abstractContent,
305349
url: presentationURL.absoluteString,
306350
kind: kind,
307351
required: isRequired,
@@ -310,9 +354,9 @@ public class DocumentationContentRenderer {
310354
)
311355

312356
// Store the symbol's display name if present in the render reference
313-
renderReference.fragments = node.flatMap(subHeadingFragments)
357+
renderReference.fragmentsVariants = node.flatMap(subHeadingFragments) ?? .init(defaultValue: [])
314358
// Store the symbol's navigator title if present in the render reference
315-
renderReference.navigatorTitle = node.flatMap(navigatorFragments)
359+
renderReference.navigatorTitleVariants = node.flatMap(navigatorFragments) ?? .init(defaultValue: [])
316360

317361
// Omit the navigator title if it's identical to the fragments
318362
if renderReference.navigatorTitle == renderReference.fragments {

Sources/SwiftDocC/Model/Rendering/LinkTitleResolver.swift

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,35 +25,39 @@ struct LinkTitleResolver {
2525
/// Depending on the page type, semantic parsing may be necessary to determine the title of the page.
2626
///
2727
/// - Parameter page: The page for which to resolve the title.
28-
/// - Returns: The link title for this page, or `nil` if the page doesn't exist in the context.
29-
func title(for page: DocumentationNode) -> String? {
28+
/// - Returns: The variants of the link title for this page, or `nil` if the page doesn't exist in the context.
29+
func title(for page: DocumentationNode) -> SymbolDataVariants<String>? {
3030
if let bundle = context.bundle(identifier: page.reference.bundleIdentifier),
3131
let directive = page.markup.child(at: 0) as? BlockDirective {
3232

3333
var problems = [Problem]()
3434
switch directive.name {
3535
case Tutorial.directiveName:
3636
if let tutorial = Tutorial(from: directive, source: source, for: bundle, in: context, problems: &problems) {
37-
return tutorial.intro.title
37+
return .init(defaultVariantValue: tutorial.intro.title)
3838
}
3939
case Technology.directiveName:
4040
if let overview = Technology(from: directive, source: source, for: bundle, in: context, problems: &problems) {
41-
return overview.name
41+
return .init(defaultVariantValue: overview.name)
4242
}
4343
default: break
4444
}
4545
}
4646

47-
if let symbol = page.symbol {
48-
return symbol.names.title
47+
if case let .conceptual(name) = page.name {
48+
return .init(defaultVariantValue: name)
4949
}
50-
51-
if let article = page.semantic as? Article, let title = article.title?.plainText {
52-
return title
50+
51+
if let symbol = (page.semantic as? Symbol) {
52+
return symbol.titleVariants
5353
}
5454

55-
if case let .conceptual(name) = page.name {
56-
return name
55+
if let symbol = page.symbol {
56+
return .init(defaultVariantValue: symbol.names.title)
57+
}
58+
59+
if let article = page.semantic as? Article, let title = article.title?.plainText {
60+
return .init(defaultVariantValue: title)
5761
}
5862

5963
return nil

0 commit comments

Comments
 (0)