Skip to content

Commit 2566321

Browse files
committed
Merge branch 'main' into ci-test-separation
2 parents 8c614df + 9808092 commit 2566321

File tree

4 files changed

+264
-17
lines changed

4 files changed

+264
-17
lines changed

Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift

Lines changed: 122 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ import SymbolKit
7171
/// - In a paragraph of text, a link to this element will use the ``title`` as the link text and style the tile in code font if the ``kind`` is a type of symbol.
7272
/// - In a task group, the the ``title`` and ``abstract-swift.property`` is displayed together to give more context about this element and the element may be marked as deprecated
7373
/// based on the values of its ``platforms`` and other metadata about the current versions of the platforms.
74+
///
75+
/// The summary may include content that vary based on the source language. The content that is different in another source language is specified in a ``Variant``. Any property on the variant that is `nil` has the same value as the summarized element's value.
7476
public struct LinkDestinationSummary: Codable, Equatable {
7577
/// The kind of the summarized element.
7678
public let kind: DocumentationNode.Kind
@@ -142,6 +144,54 @@ public struct LinkDestinationSummary: Codable, Equatable {
142144
///
143145
/// A web server can use this list of URLs to redirect to the current URL.
144146
public let redirects: [URL]?
147+
148+
/// A variant of content for a summarized element.
149+
///
150+
/// - Note: All properties except for ``traits`` are optional. If a property is `nil` it means that the value is the same as the summarized element's value.
151+
public struct Variant: Codable, Equatable {
152+
/// The traits of the variant.
153+
public let traits: [RenderNode.Variant.Trait]
154+
155+
/// A wrapper for variant values that can either be specified, meaning the variant has a custom value, or not, meaning the variant has the same value as the summarized element.
156+
///
157+
/// This alias is used to make the property declarations more explicit while at the same time offering the convenient syntax of optionals.
158+
public typealias VariantValue = Optional
159+
160+
/// The kind of the variant or `nil` if the kind is the same as the summarized element.
161+
public let kind: VariantValue<DocumentationNode.Kind>
162+
163+
/// The source language of the variant or `nil` if the kind is the same as the summarized element.
164+
public let language: VariantValue<SourceLanguage>
165+
166+
/// The relative path of the variant or `nil` if the relative is the same as the summarized element.
167+
public let path: VariantValue<String>
168+
169+
/// The title of the variant or `nil` if the title is the same as the summarized element.
170+
public let title: VariantValue<String?>
171+
172+
/// The abstract of the variant or `nil` if the abstract is the same as the summarized element.
173+
///
174+
/// If the summarized element has an abstract but the variant doesn't, this property will be `Optional.some(nil)`.
175+
public let abstract: VariantValue<Abstract?>
176+
177+
/// The taskGroups of the variant or `nil` if the taskGroups is the same as the summarized element.
178+
///
179+
/// If the summarized element has task groups but the variant doesn't, this property will be `Optional.some(nil)`.
180+
public let taskGroups: VariantValue<[TaskGroup]?>
181+
182+
/// The precise symbol identifier of the variant or `nil` if the precise symbol identifier is the same as the summarized element.
183+
///
184+
/// If the summarized element has a precise symbol identifier but the variant doesn't, this property will be `Optional.some(nil)`.
185+
public let usr: VariantValue<String?>
186+
187+
/// The declaration of the variant or `nil` if the declaration is the same as the summarized element.
188+
///
189+
/// If the summarized element has a declaration but the variant doesn't, this property will be `Optional.some(nil)`.
190+
public let declarationFragments: VariantValue<DeclarationFragments?>
191+
}
192+
193+
/// The variants of content (kind, title, abstract, path, urs, declaration, and task groups) for this summarized element.
194+
public let variants: [Variant]
145195
}
146196

147197
// MARK: - Accessing the externally linkable elements
@@ -205,11 +255,7 @@ extension LinkDestinationSummary {
205255
/// - taskGroups: The task groups that lists the children of this page.
206256
/// - compiler: The content compiler that's used to render the node's abstract.
207257
init(documentationNode: DocumentationNode, path: String, taskGroups: [TaskGroup], platforms: [PlatformAvailability]?, compiler: inout RenderContentCompiler) {
208-
let declaration = (documentationNode.semantic as? Symbol)?.subHeading.map { declaration in
209-
return declaration.map { fragment in
210-
DeclarationRenderSection.Token(fragment: fragment, identifier: nil)
211-
}
212-
}
258+
let symbol = documentationNode.semantic as? Symbol
213259

214260
self.init(
215261
kind: documentationNode.kind,
@@ -221,9 +267,10 @@ extension LinkDestinationSummary {
221267
availableLanguages: documentationNode.availableSourceLanguages,
222268
platforms: platforms,
223269
taskGroups: taskGroups,
224-
usr: (documentationNode.semantic as? Symbol)?.externalID,
225-
declarationFragments: declaration,
226-
redirects: (documentationNode.semantic as? Redirected)?.redirects?.map { $0.oldPath }
270+
usr: symbol?.externalID,
271+
declarationFragments: symbol?.subHeading?.map { .init(fragment: $0, identifier: nil) },
272+
redirects: (documentationNode.semantic as? Redirected)?.redirects?.map { $0.oldPath },
273+
variants: []
227274
)
228275
}
229276
}
@@ -240,13 +287,13 @@ extension LinkDestinationSummary {
240287
init(landmark: Landmark, basePath: String, page: DocumentationNode, platforms: [PlatformAvailability]?, compiler: inout RenderContentCompiler) {
241288
let anchor = urlReadableFragment(landmark.title)
242289

243-
let abstract: Abstract
290+
let abstract: Abstract?
244291
if let abstracted = landmark as? Abstracted {
245292
abstract = abstracted.renderedAbstract(using: &compiler) ?? []
246293
} else if let paragraph = landmark.markup.children.lazy.compactMap({ $0 as? Paragraph }).first, case RenderBlockContent.paragraph(let inlineContent)? = compiler.visitParagraph(paragraph).first {
247294
abstract = inlineContent
248295
} else {
249-
abstract = []
296+
abstract = nil
250297
}
251298

252299
self.init(
@@ -261,7 +308,8 @@ extension LinkDestinationSummary {
261308
taskGroups: [], // Landmarks have no children
262309
usr: nil, // Only symbols have a USR
263310
declarationFragments: nil, // Only symbols have declarations
264-
redirects: (landmark as? Redirected)?.redirects?.map { $0.oldPath }
311+
redirects: (landmark as? Redirected)?.redirects?.map { $0.oldPath },
312+
variants: []
265313
)
266314
}
267315
}
@@ -271,7 +319,7 @@ extension LinkDestinationSummary {
271319
// Add Codable methods—which include an initializer—in an extension so that it doesn't override the member-wise initializer.
272320
extension LinkDestinationSummary {
273321
enum CodingKeys: String, CodingKey {
274-
case kind, path, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects
322+
case kind, path, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, variants
275323
case declarationFragments = "fragments"
276324
}
277325

@@ -289,6 +337,9 @@ extension LinkDestinationSummary {
289337
try container.encodeIfPresent(usr, forKey: .usr)
290338
try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments)
291339
try container.encodeIfPresent(redirects, forKey: .redirects)
340+
if !variants.isEmpty {
341+
try container.encode(variants, forKey: .variants)
342+
}
292343
}
293344

294345
public init(from decoder: Decoder) throws {
@@ -320,5 +371,64 @@ extension LinkDestinationSummary {
320371
usr = try container.decodeIfPresent(String.self, forKey: .usr)
321372
declarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .declarationFragments)
322373
redirects = try container.decodeIfPresent([URL].self, forKey: .redirects)
374+
375+
variants = try container.decodeIfPresent([Variant].self, forKey: .variants) ?? []
376+
}
377+
}
378+
379+
extension LinkDestinationSummary.Variant {
380+
enum CodingKeys: String, CodingKey {
381+
case traits, kind, path, title, abstract, language, usr, declarationFragments = "fragments", taskGroups
382+
}
383+
384+
public func encode(to encoder: Encoder) throws {
385+
var container = encoder.container(keyedBy: CodingKeys.self)
386+
try container.encode(traits, forKey: .traits)
387+
try container.encodeIfPresent(kind?.id, forKey: .kind)
388+
try container.encodeIfPresent(path, forKey: .path)
389+
try container.encodeIfPresent(title, forKey: .title)
390+
try container.encodeIfPresent(abstract, forKey: .abstract)
391+
try container.encodeIfPresent(language, forKey: .language)
392+
try container.encodeIfPresent(usr, forKey: .usr)
393+
try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments)
394+
try container.encodeIfPresent(taskGroups, forKey: .taskGroups)
395+
}
396+
397+
public init(from decoder: Decoder) throws {
398+
let container = try decoder.container(keyedBy: CodingKeys.self)
399+
400+
let traits = try container.decode([RenderNode.Variant.Trait].self, forKey: .traits)
401+
for case .interfaceLanguage(let languageID) in traits {
402+
guard SourceLanguage.knownLanguages.contains(where: { $0.id == languageID }) else {
403+
throw DecodingError.dataCorruptedError(forKey: .traits, in: container, debugDescription: "Unknown SourceLanguage identifier: '\(languageID)'.")
404+
}
405+
}
406+
self.traits = traits
407+
408+
let kindID = try container.decodeIfPresent(String.self, forKey: .kind)
409+
if let kindID = kindID {
410+
guard let foundKind = DocumentationNode.Kind.allKnownValues.first(where: { $0.id == kindID }) else {
411+
throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown DocumentationNode.Kind identifier: '\(kindID)'.")
412+
}
413+
kind = foundKind
414+
} else {
415+
kind = nil
416+
}
417+
418+
let languageID = try container.decodeIfPresent(String.self, forKey: .language)
419+
if let languageID = languageID {
420+
guard let foundLanguage = SourceLanguage.knownLanguages.first(where: { $0.id == languageID }) else {
421+
throw DecodingError.dataCorruptedError(forKey: .language, in: container, debugDescription: "Unknown SourceLanguage identifier: '\(languageID)'.")
422+
}
423+
language = foundLanguage
424+
} else {
425+
language = nil
426+
}
427+
path = try container.decodeIfPresent(String.self, forKey: .path)
428+
title = try container.decodeIfPresent(String?.self, forKey: .title)
429+
abstract = try container.decodeIfPresent(LinkDestinationSummary.Abstract?.self, forKey: .abstract)
430+
usr = try container.decodeIfPresent(String?.self, forKey: .title)
431+
declarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments)
432+
taskGroups = try container.decodeIfPresent([LinkDestinationSummary.TaskGroup]?.self, forKey: .taskGroups)
323433
}
324434
}

Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"openapi": "3.0.0",
33
"info": {
44
"description": "Specification of the DocC linkable-entities.json digest file.",
5-
"version": "0.1.0",
5+
"version": "0.2.0",
66
"title": "Linkable Entities"
77
},
88
"paths": { },
@@ -78,6 +78,12 @@
7878
"items": {
7979
"type": "string"
8080
}
81+
},
82+
"variants": {
83+
"type": "array",
84+
"items": {
85+
"$ref": "#/components/schemas/LinkDestinationSummaryVariant"
86+
}
8187
}
8288
}
8389
},
@@ -96,6 +102,71 @@
96102
}
97103
}
98104
},
105+
"RenderNodeVariantTrait": {
106+
"oneOf": [
107+
{
108+
"$ref": "#/components/schemas/TraitInterfaceLanguage"
109+
}
110+
]
111+
},
112+
"TraitInterfaceLanguage": {
113+
"required": [
114+
"interfaceLanguage"
115+
],
116+
"type": "object",
117+
"properties": {
118+
"interfaceLanguage": {
119+
"type": "string"
120+
}
121+
}
122+
},
123+
"LinkDestinationSummaryVariant": {
124+
"type": "object",
125+
"required": [
126+
"traits",
127+
],
128+
"properties": {
129+
"traits": {
130+
"type": "array",
131+
"items": {
132+
"$ref": "#/components/schemas/RenderNodeVariantTrait"
133+
}
134+
},
135+
"kind": {
136+
"type": "string"
137+
},
138+
"language": {
139+
"type": "string"
140+
},
141+
"path": {
142+
"type": "string"
143+
},
144+
"title": {
145+
"type": "string"
146+
},
147+
"abstract": {
148+
"type": "array",
149+
"items": {
150+
"$ref": "#/components/schemas/RenderInlineContent"
151+
}
152+
},
153+
"usr": {
154+
"type": "string"
155+
},
156+
"declarationFragments": {
157+
"type": "array",
158+
"items": {
159+
"$ref": "#/components/schemas/DeclarationToken"
160+
}
161+
},
162+
"taskGroups": {
163+
"type": "array",
164+
"items": {
165+
"$ref": "#/components/schemas/TaskGroup"
166+
}
167+
}
168+
}
169+
},
99170
"RenderInlineContent": {
100171
"oneOf": [
101172
{

Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ class ExternalLinkableTests: XCTestCase {
105105

106106
let node = try context.entity(with: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/tutorials/TestBundle/Tutorial", sourceLanguage: .swift))
107107
let renderNode = try converter.convert(node, at: nil)
108-
let pageSummary = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode)[0]
108+
109+
let summaries = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode)
110+
let pageSummary = summaries[0]
109111
XCTAssertEqual(pageSummary.title, "Basic Augmented Reality App")
110112
XCTAssertEqual(pageSummary.path, "/tutorials/testbundle/tutorial")
111113
XCTAssertEqual(pageSummary.referenceURL.absoluteString, "doc://com.test.example/tutorials/TestBundle/Tutorial")
@@ -123,7 +125,7 @@ class ExternalLinkableTests: XCTestCase {
123125
XCTAssertNil(pageSummary.declarationFragments, "Only symbols have declaration fragments")
124126
XCTAssertNil(pageSummary.abstract, "There is no text to use as an abstract for the tutorial page")
125127

126-
let sectionSummary = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode)[1]
128+
let sectionSummary = summaries[1]
127129
XCTAssertEqual(sectionSummary.title, "Create a New AR Project")
128130
XCTAssertEqual(sectionSummary.path, "/tutorials/testbundle/tutorial#Create-a-New-AR-Project")
129131
XCTAssertEqual(sectionSummary.referenceURL.absoluteString, "doc://com.test.example/tutorials/TestBundle/Tutorial#Create-a-New-AR-Project")
@@ -142,6 +144,11 @@ class ExternalLinkableTests: XCTestCase {
142144
.text(" "),
143145
.text("ut labore et dolore magna aliqua. Phasellus faucibus scelerisque eleifend donec pretium."),
144146
])
147+
148+
// Test that the summaries can be decoded from the encoded data
149+
let encoded = try JSONEncoder().encode(summaries)
150+
let decoded = try JSONDecoder().decode([LinkDestinationSummary].self, from: encoded)
151+
XCTAssertEqual(summaries, decoded)
145152
}
146153

147154
func testSymbolSummaries() throws {
@@ -270,4 +277,62 @@ class ExternalLinkableTests: XCTestCase {
270277
])
271278
}
272279
}
280+
func testDecodingLegacyData() throws {
281+
let legacyData = """
282+
{
283+
"title": "ClassName",
284+
"referenceURL": "doc://org.swift.docc.example/documentation/MyKit/ClassName",
285+
"language": "swift",
286+
"path": "documentation/MyKit/ClassName",
287+
"availableLanguages": [
288+
"swift"
289+
],
290+
"kind": "org.swift.docc.kind.class",
291+
"abstract": [
292+
{
293+
"type": "text",
294+
"text": "A brief explanation of my class."
295+
}
296+
],
297+
"platforms": [
298+
{
299+
"name": "PlatformName",
300+
"introducedAt": "1.0"
301+
},
302+
],
303+
"fragments": [
304+
{
305+
"kind": "keyword",
306+
"text": "class"
307+
},
308+
{
309+
"kind": "text",
310+
"text": " "
311+
},
312+
{
313+
"kind": "identifier",
314+
"text": "ClassName"
315+
}
316+
]
317+
}
318+
""".data(using: .utf8)!
319+
320+
let decoded = try JSONDecoder().decode(LinkDestinationSummary.self, from: legacyData)
321+
322+
XCTAssertEqual(decoded.referenceURL, ResolvedTopicReference(bundleIdentifier: "org.swift.docc.example", path: "/documentation/MyKit/ClassName", sourceLanguage: .swift).url)
323+
XCTAssertEqual(decoded.platforms?.count, 1)
324+
XCTAssertEqual(decoded.platforms?.first?.name, "PlatformName")
325+
XCTAssertEqual(decoded.platforms?.first?.introduced, "1.0")
326+
XCTAssertEqual(decoded.kind, .class)
327+
XCTAssertEqual(decoded.title, "ClassName")
328+
XCTAssertEqual(decoded.abstract?.plainText, "A brief explanation of my class.")
329+
XCTAssertEqual(decoded.path, "documentation/MyKit/ClassName")
330+
XCTAssertEqual(decoded.declarationFragments, [
331+
.init(text: "class", kind: .keyword, identifier: nil),
332+
.init(text: " ", kind: .text, identifier: nil),
333+
.init(text: "ClassName", kind: .identifier, identifier: nil),
334+
])
335+
336+
XCTAssert(decoded.variants.isEmpty)
337+
}
273338
}

0 commit comments

Comments
 (0)