Skip to content

Commit 094be94

Browse files
[5.6] Support converting catalogs that include mixed languages (#55)
* Add base support for multiple languages Adds base support for converting DocC Catalogs that include multiple languages by expanding symbol semantic models to hold language-specific variants. Resolves rdar://84169407 Co-authored-by: Franklin Schrans <[email protected]> * Remove extra calls to `ResolvedTopicReference` cache We add ResolvedTopicReferences to the cache when initializing a new reference so we don't need these additional calls. * Make ResolvedTopicReference immutable and copy-on-write Co-authored-by: Franklin Schrans <[email protected]> * Lazy initialize resolved topic reference URL We don't always use the URL property on a resolvedtopicreference and its expensive to compute, so it makes sense to make this a lazy initialization. * Optimize url readable path and fragment logic Because `urlReadablePath` was always call in the topic reference's initializer, when adding and removing path components, we were performing duplicate work. This adds a new private initializer that skips the `urlReadablePath` call when we know we already have an escaped path. * Add `ResolvedTopicReference.withSourceLanguages` API (#59) Introduces a new API that restores the ability for clients to replace a topic reference's source languages value. The existing API that allowed for this was removed in e76ef47. Resolves rdar://86338175. Co-authored-by: Franklin Schrans <[email protected]>
1 parent 5e0729b commit 094be94

File tree

51 files changed

+5186
-396
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+5186
-396
lines changed

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/SwiftDocC/Converter/RenderNode+Coding.swift

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,73 @@ extension CodingUserInfoKey {
2424
///
2525
/// This key is used by encoders to accumulate language-specific variants of documentation in a ``VariantOverrides`` value.
2626
static let variantOverrides = CodingUserInfoKey(rawValue: "variantOverrides")!
27+
28+
static let baseEncodingPath = CodingUserInfoKey(rawValue: "baseEncodingPath")!
2729
}
2830

2931
extension Encoder {
3032
/// The variant overrides accumulated as part of the encoding process.
3133
var userInfoVariantOverrides: VariantOverrides? {
3234
userInfo[.variantOverrides] as? VariantOverrides
3335
}
36+
37+
/// The base path to use when creating dynamic JSON pointers
38+
/// with this encoder.
39+
var baseJSONPatchPath: [String]? {
40+
userInfo[.baseEncodingPath] as? [String]
41+
}
42+
43+
/// A Boolean that is true if this encoder skips the encoding of any render references.
44+
///
45+
/// These references will then be encoded at a later stage by `TopicRenderReferenceEncoder`.
46+
var skipsEncodingReferences: Bool {
47+
guard let userInfoValue = userInfo[.skipsEncodingReferences] as? Bool else {
48+
// The value doesn't exist so we should encode reference. Return false.
49+
return false
50+
}
51+
52+
return userInfoValue
53+
}
54+
}
55+
56+
extension JSONEncoder {
57+
/// The variant overrides accumulated as part of the encoding process.
58+
var userInfoVariantOverrides: VariantOverrides? {
59+
get {
60+
userInfo[.variantOverrides] as? VariantOverrides
61+
}
62+
set {
63+
userInfo[.variantOverrides] = newValue
64+
}
65+
}
66+
67+
/// The base path to use when creating dynamic JSON pointers
68+
/// with this encoder.
69+
var baseJSONPatchPath: [String]? {
70+
get {
71+
userInfo[.baseEncodingPath] as? [String]
72+
}
73+
set {
74+
userInfo[.baseEncodingPath] = newValue
75+
}
76+
}
77+
78+
/// A Boolean that is true if this encoder skips the encoding any render references.
79+
///
80+
/// These references will then be encoded at a later stage by `TopicRenderReferenceEncoder`.
81+
var skipsEncodingReferences: Bool {
82+
get {
83+
guard let userInfoValue = userInfo[.skipsEncodingReferences] as? Bool else {
84+
// The value doesn't exist so we should encode reference. Return false.
85+
return false
86+
}
87+
88+
return userInfoValue
89+
}
90+
set {
91+
userInfo[.skipsEncodingReferences] = newValue
92+
}
93+
}
3494
}
3595

3696
/// A namespace for encoders for render node JSON.
@@ -145,7 +205,7 @@ public extension RenderNode {
145205
/// - Returns: The data for the encoded render node.
146206
func encodeToJSON(
147207
with encoder: JSONEncoder = RenderJSONEncoder.makeEncoder(),
148-
renderReferenceCache: Synchronized<[String: Data]>? = nil
208+
renderReferenceCache: RenderReferenceCache? = nil
149209
) throws -> Data {
150210
do {
151211
// If there is no topic reference cache, just encode the reference.
@@ -155,13 +215,14 @@ public extension RenderNode {
155215
}
156216

157217
// Since we're using a reference cache, skip encoding the references and encode them separately.
158-
encoder.userInfo[.skipsEncodingReferences] = true
218+
encoder.skipsEncodingReferences = true
159219
var renderNodeData = try encoder.encode(self)
160220

161221
// Add render references, using the encoder cache.
162-
TopicRenderReferenceEncoder.addRenderReferences(
222+
try TopicRenderReferenceEncoder.addRenderReferences(
163223
to: &renderNodeData,
164224
references: references,
225+
encodeAccumulatedVariantOverrides: variantOverrides == nil,
165226
encoder: encoder,
166227
renderReferenceCache: renderReferenceCache
167228
)

Sources/SwiftDocC/Converter/TopicRenderReferenceEncoder.swift

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
import Foundation
1212

13+
/// A thread-safe cache for encoded render references.
14+
public typealias RenderReferenceCache = Synchronized<[String: (reference: Data, overrides: [VariantOverride])]>
15+
1316
enum TopicRenderReferenceEncoder {
1417
/// Inserts an encoded list of render references to an already encoded as data render node.
1518
/// - Parameters:
@@ -21,12 +24,17 @@ enum TopicRenderReferenceEncoder {
2124
static func addRenderReferences(
2225
to renderNodeData: inout Data,
2326
references: [String: RenderReference],
27+
encodeAccumulatedVariantOverrides: Bool = false,
2428
encoder: JSONEncoder,
25-
renderReferenceCache referenceCache: Synchronized<[String: Data]>
26-
) {
27-
29+
renderReferenceCache referenceCache: RenderReferenceCache
30+
) throws {
2831
guard !references.isEmpty else { return }
2932

33+
// Because we'll be clearing the encoder's variant overrides field before
34+
// encoding each reference, we need to store any existing values now so that
35+
// when we finally encode the variant overrides we have all relevant values.
36+
var variantOverrides = encoder.userInfoVariantOverrides?.values ?? []
37+
3038
let fragments: Fragments = encoder.outputFormatting.contains(.prettyPrinted) ? .prettyPrinted : .compact
3139

3240
// Remove the final "}"
@@ -49,6 +57,55 @@ enum TopicRenderReferenceEncoder {
4957
let key = reference.identifier.identifier
5058
let value: Data
5159

60+
// Declare a helper function that we'll use to encode any non-cached references
61+
// we encounter
62+
63+
func encodeRenderReference(cacheKey: String? = nil) throws -> Data {
64+
// Because we're encoding these reference ad-hoc and not as part of a full render
65+
// node, the `encodingPath` on the encoder will be incorrect. This means that the
66+
// logic in `VariantEncoder.addVariantsToEncoder(_:pointer:isDefaultValueEncoded:)`
67+
// will incorrectly set the path and the produced JSON patch we use to switch
68+
// between language variants will be incorrect.
69+
//
70+
// To work around this, we set a `baseJSONPatchPath` property in the encoder's
71+
// user info dictionary. Then when `addVariantsToEncoder` is called, it prepends
72+
// this value to the coding path. This way the produced JSON patch will be accurate.
73+
encoder.baseJSONPatchPath = [
74+
"references",
75+
reference.identifier.identifier,
76+
]
77+
78+
// Because we want to cache each render reference with the specific
79+
// variant overrides it produces, we first clear the encoder's user info
80+
// fields before encoding.
81+
//
82+
// This ensures that the whatever override the user info field holds
83+
// _after_ we encode, are the ones for this particular reference.
84+
encoder.userInfoVariantOverrides?.values.removeAll()
85+
86+
// Encode the reference.
87+
let encodedReference = try encoder.encode(CodableRenderReference.init(reference))
88+
89+
// Add the collected variant overrides to the collection of overrides
90+
// we're currently tracking.
91+
if let encodedVariantOverrides = encoder.userInfoVariantOverrides {
92+
variantOverrides.append(contentsOf: encodedVariantOverrides.values)
93+
}
94+
95+
// If a cache key was provided, update the cache with the reference and it's
96+
// overrides.
97+
if let cacheKey = cacheKey {
98+
referenceCache.sync { cache in
99+
cache[cacheKey] = (
100+
encodedReference,
101+
encoder.userInfoVariantOverrides?.values ?? []
102+
)
103+
}
104+
}
105+
106+
return encodedReference
107+
}
108+
52109
if let topicReference = reference as? TopicRenderReference {
53110
if let conformance = topicReference.conformance {
54111
// In case there is a conformance section, adds conformance hash to the cache key.
@@ -60,20 +117,19 @@ enum TopicRenderReferenceEncoder {
60117

61118
let conformanceHash = Checksum.md5(of: Data(conformance.constraints.map({ $0.plainText }).joined().utf8))
62119
let cacheKeyWithConformance = "\(key) : \(conformanceHash)"
63-
if let cached = referenceCache.sync({ $0[cacheKeyWithConformance] }) {
64-
value = cached
120+
if let (reference, overrides) = referenceCache.sync({ $0[cacheKeyWithConformance] }) {
121+
value = reference
122+
variantOverrides.append(contentsOf: overrides)
65123
} else {
66-
value = try! encoder.encode(CodableRenderReference.init(reference))
67-
referenceCache.sync({ $0[cacheKeyWithConformance] = value })
124+
value = try encodeRenderReference(cacheKey: cacheKeyWithConformance)
68125
}
69126

70-
} else if let cached = referenceCache.sync({ $0[key] }) {
127+
} else if let (reference, overrides) = referenceCache.sync({ $0[key] }) {
71128
// Use a cached copy if the reference is already encoded.
72-
value = cached
129+
value = reference
130+
variantOverrides.append(contentsOf: overrides)
73131
} else {
74-
// Encode the reference and add it to the cache.
75-
value = try! encoder.encode(CodableRenderReference.init(reference))
76-
referenceCache.sync({ $0[key] = value })
132+
value = try encodeRenderReference(cacheKey: key)
77133
}
78134
}
79135
else {
@@ -82,7 +138,7 @@ enum TopicRenderReferenceEncoder {
82138
// For example: ![image.png](This is an image) and ![image.png](Another image)
83139
// have the same identifier when encoded in the render node where they are used but the reference
84140
// abstract is not unique within the project.
85-
value = try! encoder.encode(CodableRenderReference.init(reference))
141+
value = try encodeRenderReference()
86142
}
87143

88144
renderNodeData.append(fragments.quote)
@@ -96,32 +152,45 @@ enum TopicRenderReferenceEncoder {
96152
// Remove the last comma from the list
97153
renderNodeData.removeLast(fragments.listDelimiter.count)
98154

99-
// Append closing "}}"
100-
renderNodeData.append(fragments.closingBrackets)
155+
// Append closing "}"
156+
renderNodeData.append(fragments.closingBrace)
157+
158+
if encodeAccumulatedVariantOverrides, !variantOverrides.isEmpty {
159+
// Insert the "variantOverrides" key
160+
renderNodeData.append(fragments.variantOverridesKey)
161+
let variantOverrideData = try encoder.encode(VariantOverrides(values: variantOverrides))
162+
renderNodeData.append(variantOverrideData)
163+
}
164+
165+
// Append closing "}"
166+
renderNodeData.append(fragments.closingBrace)
101167
}
102168

103169
/// Data fragments to use to build a reference list.
104170
private struct Fragments {
105171

172+
let variantOverridesKey: Data
106173
let referencesKey: Data
107-
let closingBrackets: Data
174+
let closingBrace: Data
108175
let listDelimiter: Data
109176
let quote: Data
110177
let colon: Data
111178

112179
// Compact fragments
113180
static let compact = Fragments(
181+
variantOverridesKey: Data(",\"variantOverrides\":".utf8),
114182
referencesKey: Data(",\"references\":{".utf8),
115-
closingBrackets: Data("}}".utf8),
183+
closingBrace: Data("}".utf8),
116184
listDelimiter: Data(",".utf8),
117185
quote: Data("\"".utf8),
118186
colon: Data(":".utf8)
119187
)
120188

121189
// Pretty printed fragments
122190
static let prettyPrinted = Fragments(
191+
variantOverridesKey: Data(", \n\"variantOverrides\":".utf8),
123192
referencesKey: Data(", \n\"references\": {\n".utf8),
124-
closingBrackets: Data("\n}\n}".utf8),
193+
closingBrace: Data("\n}".utf8),
125194
listDelimiter: Data(",\n".utf8),
126195
quote: Data("\"".utf8),
127196
colon: Data(": ".utf8)

0 commit comments

Comments
 (0)