Skip to content

Commit d6bfe19

Browse files
authored
Add support for variant overrides in Render JSON (#11)
Adds infrastructure support for adding language-specific overrides in Render JSON. Also, adds support specifying language variants for `RenderMetadata.title` and `TopicRenderReference.title`. rdar://82919099
1 parent 7efcac5 commit d6bfe19

30 files changed

+1274
-85
lines changed

Sources/SwiftDocC/Converter/RenderNode+Coding.swift

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,39 +16,71 @@ import Foundation
1616
let jsonFormattingKey = "DOCC_JSON_PRETTYPRINT"
1717
public let shouldPrettyPrintOutputJSON = NSString(string: ProcessInfo.processInfo.environment[jsonFormattingKey] ?? "NO").boolValue
1818

19-
public extension CodingUserInfoKey {
20-
// A user info key to store topic reference cache in `JSONEncoder`.
21-
static let renderReferenceCache = CodingUserInfoKey(rawValue: "renderReferenceCache")!
19+
extension CodingUserInfoKey {
20+
/// A user info key to indicate that Render JSON references should not be encoded.
21+
static let skipsEncodingReferences = CodingUserInfoKey(rawValue: "skipsEncodingReferences")!
22+
23+
/// A user info key that encapsulates variant overrides.
24+
///
25+
/// This key is used by encoders to accumulate language-specific variants of documentation in a ``VariantOverrides`` value.
26+
static let variantOverrides = CodingUserInfoKey(rawValue: "variantOverrides")!
27+
}
28+
29+
extension Encoder {
30+
/// The variant overrides accumulated as part of the encoding process.
31+
var userInfoVariantOverrides: VariantOverrides? {
32+
userInfo[.variantOverrides] as? VariantOverrides
33+
}
2234
}
2335

2436
/// A namespace for encoders for render node JSON.
25-
enum RenderJSONEncoder {
37+
public enum RenderJSONEncoder {
2638
/// Creates a new JSON encoder for render node values.
2739
///
28-
/// - Parameter prettyPrint: If `true`, the encoder formats its output to make it easy to read; if `false`, the output is compact.
40+
/// Returns an encoder that's configured to encode ``RenderNode`` values.
41+
///
42+
/// > Important: Don't reuse encoders returned by this function to encode multiple render nodes, as the encoder accumulates state during the encoding
43+
/// process which should not be shared in other encoding units. Instead, call this API to create a new encoder for each render node you want to encode.
44+
///
45+
/// - Parameters:
46+
/// - prettyPrint: If `true`, the encoder formats its output to make it easy to read; if `false`, the output is compact.
47+
/// - emitVariantOverrides: Whether the encoder should emit the top-level ``RenderNode/variantOverrides`` property that holds language-
48+
/// specific documentation data.
2949
/// - Returns: The new JSON encoder.
30-
static func encoder(prettyPrint: Bool) -> JSONEncoder {
50+
public static func makeEncoder(
51+
prettyPrint: Bool = shouldPrettyPrintOutputJSON,
52+
emitVariantOverrides: Bool = true
53+
) -> JSONEncoder {
3154
let encoder = JSONEncoder()
55+
3256
if prettyPrint {
3357
if #available(OSX 10.13, *) {
3458
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
3559
} else {
3660
encoder.outputFormatting = [.prettyPrinted]
3761
}
3862
}
63+
64+
if emitVariantOverrides {
65+
encoder.userInfo[.variantOverrides] = VariantOverrides()
66+
}
67+
3968
return encoder
4069
}
4170
}
4271

72+
/// A namespace for decoders for render node JSON.
73+
public enum RenderJSONDecoder {
74+
/// Creates a new JSON decoder for render node values.
75+
///
76+
/// - Returns: The new JSON decoder.
77+
public static func makeDecoder() -> JSONDecoder {
78+
JSONDecoder()
79+
}
80+
}
81+
4382
// This API improves the encoding/decoding to or from JSON with better error messages.
4483
public extension RenderNode {
45-
/// The default decoder for render node JSON.
46-
static var defaultJSONDecoder = JSONDecoder()
47-
/// The default encoder for render node JSON.
48-
static var defaultJSONEncoder = RenderJSONEncoder.encoder(
49-
prettyPrint: shouldPrettyPrintOutputJSON
50-
)
51-
5284
/// An error that describes failures that may occur while encoding or decoding a render node.
5385
enum CodingError: DescribedError {
5486
/// JSON data could not be decoded as a render node value.
@@ -79,7 +111,7 @@ public extension RenderNode {
79111
/// - decoder: The object that decodes the JSON data.
80112
/// - Throws: A ``CodingError`` in case the decoder is unable to find a key or value in the data, the type of a decoded value is wrong, or the data is corrupted.
81113
/// - Returns: The decoded render node value.
82-
static func decode(fromJSON data: Data, with decoder: JSONDecoder = RenderNode.defaultJSONDecoder) throws -> RenderNode {
114+
static func decode(fromJSON data: Data, with decoder: JSONDecoder = RenderJSONDecoder.makeDecoder()) throws -> RenderNode {
83115
do {
84116
return try decoder.decode(RenderNode.self, from: data)
85117
} catch {
@@ -105,26 +137,33 @@ public extension RenderNode {
105137

106138
/// Encodes a render node value as JSON data.
107139
///
108-
/// - Parameter encoder: The object that encodes the render node.
140+
/// - Parameters:
141+
/// - encoder: The object that encodes the render node.
142+
/// - renderReferenceCache: A cache for encoded render reference data. When encoding a large number of render nodes, use the same cache instance
143+
/// to avoid encoding the same reference objects repeatedly.
109144
/// - Throws: A ``CodingError`` in case the encoder couldn't encode the render node.
110145
/// - Returns: The data for the encoded render node.
111-
func encodeToJSON(with encoder: JSONEncoder = RenderNode.defaultJSONEncoder) throws -> Data {
146+
func encodeToJSON(
147+
with encoder: JSONEncoder = RenderJSONEncoder.makeEncoder(),
148+
renderReferenceCache: Synchronized<[String: Data]>? = nil
149+
) throws -> Data {
112150
do {
113151
// If there is no topic reference cache, just encode the reference.
114152
// To skim a little off the duration we first do a quick check if the key is present at all.
115-
guard encoder.userInfo.keys.contains(.renderReferenceCache) else {
153+
guard let renderReferenceCache = renderReferenceCache else {
116154
return try encoder.encode(self)
117155
}
118156

119-
// Encode the render node as usual. `RenderNode` will skip encoding the references itself
120-
// because the `.renderReferenceCache` key is set.
157+
// Since we're using a reference cache, skip encoding the references and encode them separately.
158+
encoder.userInfo[.skipsEncodingReferences] = true
121159
var renderNodeData = try encoder.encode(self)
122160

123161
// Add render references, using the encoder cache.
124162
TopicRenderReferenceEncoder.addRenderReferences(
125163
to: &renderNodeData,
126164
references: references,
127-
encoder: encoder
165+
encoder: encoder,
166+
renderReferenceCache: renderReferenceCache
128167
)
129168

130169
return renderNodeData

Sources/SwiftDocC/Converter/TopicRenderReferenceEncoder.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ enum TopicRenderReferenceEncoder {
1616
/// - renderNodeData: A render node encoded as JSON data.
1717
/// - references: A list of render references.
1818
/// - encoder: A `JSONEncoder` to use for the encoding.
19-
static func addRenderReferences(to renderNodeData: inout Data,
19+
/// - renderReferenceCache: A cache for encoded render reference data. When encoding a large number of render nodes, use the same cache
20+
/// instance to avoid encoding the same reference objects repeatedly.
21+
static func addRenderReferences(
22+
to renderNodeData: inout Data,
2023
references: [String: RenderReference],
21-
encoder: JSONEncoder) {
22-
23-
guard let referenceCache = encoder.userInfo[.renderReferenceCache] as? Synchronized<[String: Data]> else {
24-
fatalError("An unexpected value passed via the .renderReferenceCache key.")
25-
}
24+
encoder: JSONEncoder,
25+
renderReferenceCache referenceCache: Synchronized<[String: Data]>
26+
) {
2627

2728
guard !references.isEmpty else { return }
2829

Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,16 @@ public class DocumentationContentRenderer {
300300

301301
let estimatedTime = (node?.semantic as? Timed)?.durationMinutes.flatMap(formatEstimatedDuration(minutes:))
302302

303-
var renderReference = TopicRenderReference(identifier: .init(referenceURL), title: title, abstract: abstractContent, url: presentationURL.absoluteString, kind: kind, required: isRequired, role: referenceRole, estimatedTime: estimatedTime)
303+
var renderReference = TopicRenderReference(
304+
identifier: .init(referenceURL),
305+
titleVariants: .init(defaultValue: title),
306+
abstract: abstractContent,
307+
url: presentationURL.absoluteString,
308+
kind: kind,
309+
required: isRequired,
310+
role: referenceRole,
311+
estimatedTime: estimatedTime
312+
)
304313

305314
// Store the symbol's display name if present in the render reference
306315
renderReference.fragments = node.flatMap(subHeadingFragments)

Sources/SwiftDocC/Model/Rendering/References/TopicRenderReference.swift

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
/// A reference to another page of documentation in the current context.
12-
public struct TopicRenderReference: RenderReference {
12+
public struct TopicRenderReference: RenderReference, VariantContainer {
1313
/// The type of this reference.
1414
///
1515
/// This value is always `.topic`.
@@ -19,7 +19,14 @@ public struct TopicRenderReference: RenderReference {
1919
public var identifier: RenderReferenceIdentifier
2020

2121
/// The title of the destination page.
22-
public var title: String
22+
public var title: String {
23+
get { getVariantDefaultValue(keyPath: \.titleVariants) }
24+
set { setVariantDefaultValue(newValue, keyPath: \.titleVariants) }
25+
}
26+
27+
/// The variants of the title.
28+
public var titleVariants: VariantCollection<String>
29+
2330
/// The topic url for the destination page.
2431
public var url: String
2532
/// The abstract of the destination page.
@@ -98,9 +105,91 @@ public struct TopicRenderReference: RenderReference {
98105
/// - name: Raw name of a symbol, e.g. "com.apple.enableDataAccess", or `nil` if the referenced page is not a symbol.
99106
/// - ideTitle: The human friendly symbol name, or `nil` if the referenced page is not a symbol.
100107
/// - tags: An optional list of string tags.
101-
public init(identifier: RenderReferenceIdentifier, title: String, abstract: [RenderInlineContent], url: String, kind: RenderNode.Kind, required: Bool = false, role: String? = nil, fragments: [DeclarationRenderSection.Token]? = nil, navigatorTitle: [DeclarationRenderSection.Token]? = nil, estimatedTime: String?, conformance: ConformanceSection? = nil, isBeta: Bool = false, isDeprecated: Bool = false, defaultImplementationCount: Int? = nil, titleStyle: TitleStyle? = nil, name: String? = nil, ideTitle: String? = nil, tags: [RenderNode.Tag]? = nil) {
108+
public init(
109+
identifier: RenderReferenceIdentifier,
110+
title: String,
111+
abstract: [RenderInlineContent],
112+
url: String,
113+
kind: RenderNode.Kind,
114+
required: Bool = false,
115+
role: String? = nil,
116+
fragments: [DeclarationRenderSection.Token]? = nil,
117+
navigatorTitle: [DeclarationRenderSection.Token]? = nil,
118+
estimatedTime: String?,
119+
conformance: ConformanceSection? = nil,
120+
isBeta: Bool = false,
121+
isDeprecated: Bool = false,
122+
defaultImplementationCount: Int? = nil,
123+
titleStyle: TitleStyle? = nil,
124+
name: String? = nil,
125+
ideTitle: String? = nil,
126+
tags: [RenderNode.Tag]? = nil
127+
) {
128+
self.init(
129+
identifier: identifier,
130+
titleVariants: .init(defaultValue: title),
131+
abstract: abstract,
132+
url: url,
133+
kind: kind,
134+
required: required,
135+
role: role,
136+
fragments: fragments,
137+
navigatorTitle: navigatorTitle,
138+
estimatedTime: estimatedTime,
139+
conformance: conformance,
140+
isBeta: isBeta,
141+
isDeprecated: isDeprecated,
142+
defaultImplementationCount: defaultImplementationCount,
143+
titleStyle: titleStyle,
144+
name: name,
145+
ideTitle: ideTitle,
146+
tags: tags
147+
)
148+
}
149+
150+
/// Creates a new topic reference with all its initial values.
151+
///
152+
/// - Parameters:
153+
/// - identifier: The identifier of this reference.
154+
/// - titleVariants: The variants for the title of the destination page.
155+
/// - abstract: The abstract of the destination page.
156+
/// - url: The topic url of the destination page.
157+
/// - kind: The kind of page that's referenced.
158+
/// - required: Whether the reference is required in its parent context.
159+
/// - role: The additional "role" assigned to the symbol, if any.
160+
/// - fragments: The abbreviated declaration of the symbol to display in links, or `nil` if the referenced page is not a symbol.
161+
/// - navigatorTitle: The abbreviated declaration of the symbol to display in navigation, or `nil` if the referenced page is not a symbol.
162+
/// - estimatedTime: The estimated time to complete the topic.
163+
/// - conformance: Information about conditional conformance for the symbol, or `nil` if the referenced page is not a symbol.
164+
/// - isBeta: Whether this symbol is built for a beta platform, or `false` if the referenced page is not a symbol.
165+
/// - isDeprecated: Whether this symbol is deprecated, or `false` if the referenced page is not a symbol.
166+
/// - defaultImplementationCount: Number of default implementations for this symbol, or `nil` if the referenced page is not a symbol.
167+
/// - titleStyle: Information about which title to use in links to this page.
168+
/// - name: Raw name of a symbol, e.g. "com.apple.enableDataAccess", or `nil` if the referenced page is not a symbol.
169+
/// - ideTitle: The human friendly symbol name, or `nil` if the referenced page is not a symbol.
170+
/// - tags: An optional list of string tags.
171+
public init(
172+
identifier: RenderReferenceIdentifier,
173+
titleVariants: VariantCollection<String>,
174+
abstract: [RenderInlineContent],
175+
url: String,
176+
kind: RenderNode.Kind,
177+
required: Bool = false,
178+
role: String? = nil,
179+
fragments: [DeclarationRenderSection.Token]? = nil,
180+
navigatorTitle: [DeclarationRenderSection.Token]? = nil,
181+
estimatedTime: String?,
182+
conformance: ConformanceSection? = nil,
183+
isBeta: Bool = false,
184+
isDeprecated: Bool = false,
185+
defaultImplementationCount: Int? = nil,
186+
titleStyle: TitleStyle? = nil,
187+
name: String? = nil,
188+
ideTitle: String? = nil,
189+
tags: [RenderNode.Tag]? = nil
190+
) {
102191
self.identifier = identifier
103-
self.title = title
192+
self.titleVariants = titleVariants
104193
self.abstract = abstract
105194
self.url = url
106195
self.kind = kind
@@ -120,14 +209,32 @@ public struct TopicRenderReference: RenderReference {
120209
}
121210

122211
enum CodingKeys: String, CodingKey {
123-
case type, identifier, title, url, abstract, kind, required, role, fragments, navigatorTitle, estimatedTime, conformance, beta, deprecated, defaultImplementations, titleStyle, name, ideTitle, tags
212+
case type
213+
case identifier
214+
case titleVariants = "title"
215+
case url
216+
case abstract
217+
case kind
218+
case required
219+
case role
220+
case fragments
221+
case navigatorTitle
222+
case estimatedTime
223+
case conformance
224+
case beta
225+
case deprecated
226+
case defaultImplementations
227+
case titleStyle
228+
case name
229+
case ideTitle
230+
case tags
124231
}
125232

126233
public init(from decoder: Decoder) throws {
127234
let values = try decoder.container(keyedBy: CodingKeys.self)
128235
type = try values.decode(RenderReferenceType.self, forKey: .type)
129236
identifier = try values.decode(RenderReferenceIdentifier.self, forKey: .identifier)
130-
title = try values.decode(String.self, forKey: .title)
237+
titleVariants = try values.decode(VariantCollection<String>.self, forKey: .titleVariants)
131238
url = try values.decode(String.self, forKey: .url)
132239
abstract = try values.decodeIfPresent([RenderInlineContent].self, forKey: .abstract) ?? []
133240
kind = try values.decodeIfPresent(RenderNode.Kind.self, forKey: .kind)
@@ -153,7 +260,7 @@ public struct TopicRenderReference: RenderReference {
153260

154261
try container.encode(type, forKey: .type)
155262
try container.encode(identifier, forKey: .identifier)
156-
try container.encode(title, forKey: .title)
263+
try container.encode(titleVariants, forKey: .titleVariants)
157264
try container.encode(url, forKey: .url)
158265
try container.encode(abstract, forKey: .abstract)
159266
try container.encode(kind, forKey: .kind)

Sources/SwiftDocC/Model/Rendering/RenderNode.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ import Foundation
2828
/// The render node schema constantly evolves to support new documentation features. To help clients maintain compatibility,
2929
/// we associate each schema with a version. See ``schemaVersion`` for more details.
3030
///
31+
/// ### Variants
32+
///
33+
/// Different variants of a documentation page can be represented by a single render node using the ``variantOverrides`` property.
34+
/// This property holds overrides that clients should apply to the render JSON when processing documentation for specific programming languages. The overrides
35+
/// are organized by traits (e.g., language) and it's up to the client to determine which trait is most appropriate for them. For example, a client that wants to
36+
/// process the Objective-C version of documentation should apply the overrides associated with the `interfaceLanguage: objc` trait.
37+
///
38+
/// Use the ``RenderJSONEncoder/makeEncoder(prettyPrint:emitVariantOverrides:)`` API to instantiate a JSON encoder that's configured
39+
/// to accumulate variant overrides and emit them to the ``variantOverrides`` property.
40+
///
41+
/// The overrides are emitted in the [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) format.
42+
///
3143
/// ## Topics
3244
///
3345
/// ### General
@@ -130,6 +142,15 @@ public struct RenderNode {
130142
/// List of variants of the same documentation node for various languages, etc.
131143
public var variants: [RenderNode.Variant]?
132144

145+
/// Language-specific overrides for documentation.
146+
///
147+
/// This property holds overrides that clients should apply to the render JSON when processing documentation for specific languages. The overrides are
148+
/// organized by traits (e.g., language) and it's up to the client to determine which trait is most appropriate for them. For example, a client that wants to
149+
/// process the Objective-C version of documentation should apply the overrides associated with the `interfaceLanguage: objc` trait.
150+
///
151+
/// The overrides are emitted in the [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) format.
152+
public var variantOverrides: VariantOverrides?
153+
133154
/// Information about what API diffs are available for this symbol.
134155
public var diffAvailability: DiffAvailability?
135156

0 commit comments

Comments
 (0)