Skip to content

Commit a253b48

Browse files
authored
Ensure that synthesized landing pages for combined archives display page images and links in abstracts from referenced types (#1179)
* Include page image references dependencies in synthesized landing page rdar://147392201 * Include topic references dependencies from linked abstracts in synthesized landing page * Avoid image collisions for page images from merged archives * Remove stray commented out code * Make inner Information type fileprivate * Resolve unrelated warning (after some deprecated API was removed, these files no longer surface `Foundation.URL` in public API)
1 parent e4d004f commit a253b48

File tree

7 files changed

+305
-24
lines changed

7 files changed

+305
-24
lines changed

Sources/SwiftDocC/Converter/DocumentationContextConverter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11-
public import Foundation
11+
import Foundation
1212

1313
/// A converter from documentation nodes to render nodes.
1414
///

Sources/SwiftDocC/Converter/DocumentationNodeConverter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11-
public import Foundation
11+
import Foundation
1212

1313
/// A converter from documentation nodes to render nodes.
1414
public struct DocumentationNodeConverter {

Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import SymbolKit
1313
import Markdown
1414

1515
public struct RenderReferenceDependencies {
16-
var topicReferences = [ResolvedTopicReference]()
17-
var linkReferences = [LinkReference]()
18-
var imageReferences = [ImageReference]()
16+
public var topicReferences = [ResolvedTopicReference]()
17+
public var linkReferences = [LinkReference]()
18+
public var imageReferences = [ImageReference]()
1919

2020
public init(topicReferences: [ResolvedTopicReference] = [], linkReferences: [LinkReference] = [], imageReferences: [ImageReference] = []) {
2121
self.topicReferences = topicReferences

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public struct TopicImage: Codable, Hashable {
1818
public let type: TopicImageType
1919

2020
/// The reference identifier for the image.
21-
public let identifier: RenderReferenceIdentifier
21+
public var identifier: RenderReferenceIdentifier
2222

2323
/// Create a new topic image with the given type and reference identifier.
2424
public init(

Sources/SwiftDocCTestUtilities/TestFileSystem.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ package class TestFileSystem: FileManagerProtocol {
6464
files["/tmp"] = Self.folderFixtureData
6565

6666
for folder in folders {
67-
try addFolder(folder)
67+
try addFolder(folder, basePath: URL(fileURLWithPath: "/"))
6868
}
6969
}
7070

@@ -127,13 +127,13 @@ package class TestFileSystem: FileManagerProtocol {
127127
}
128128

129129
@discardableResult
130-
func addFolder(_ folder: Folder) throws -> [String] {
130+
package func addFolder(_ folder: Folder, basePath: URL) throws -> [String] {
131131
guard !disableWriting else { return [] }
132132

133133
filesLock.lock()
134134
defer { filesLock.unlock() }
135135

136-
let rootURL = URL(fileURLWithPath: "/\(folder.name)")
136+
let rootURL = basePath.appendingPathComponent(folder.name)
137137
files[rootURL.path] = Self.folderFixtureData
138138
let fileList = try filesIn(folder: folder, at: rootURL)
139139
files.merge(fileList, uniquingKeysWith: +)

Sources/SwiftDocCUtilities/Action/Actions/Merge/MergeAction+SynthesizedLandingPage.swift

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2024 Apple Inc. and the Swift project authors
4+
Copyright (c) 2024-2025 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -13,9 +13,17 @@ import SwiftDocC
1313

1414
extension MergeAction {
1515
struct RootRenderReferences {
16-
var documentation, tutorials: [TopicRenderReference]
16+
fileprivate struct Information {
17+
var reference: TopicRenderReference
18+
var dependencies: [any RenderReference]
19+
20+
var rawIdentifier: String {
21+
reference.identifier.identifier
22+
}
23+
}
24+
fileprivate var documentation, tutorials: [Information]
1725

18-
fileprivate var all: [TopicRenderReference] {
26+
fileprivate var all: [Information] {
1927
documentation + tutorials
2028
}
2129
var isEmpty: Bool {
@@ -27,19 +35,20 @@ extension MergeAction {
2735
}
2836

2937
func readRootNodeRenderReferencesIn(dataDirectory: URL) throws -> RootRenderReferences {
30-
func inner(url: URL) throws -> [TopicRenderReference] {
38+
func inner(url: URL) throws -> [RootRenderReferences.Information] {
3139
try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
3240
.compactMap {
3341
guard $0.pathExtension == "json" else {
3442
return nil
3543
}
3644

3745
let data = try fileManager.contents(of: $0)
38-
return try JSONDecoder().decode(RootNodeRenderReference.self, from: data)
39-
.renderReference
46+
let decoded = try JSONDecoder().decode(RootNodeRenderReference.self, from: data)
47+
48+
return .init(reference: decoded.renderReference, dependencies: decoded.renderDependencies)
4049
}
4150
.sorted(by: { lhs, rhs in
42-
lhs.title < rhs.title
51+
lhs.reference.title < rhs.reference.title
4352
})
4453
}
4554

@@ -72,21 +81,26 @@ extension MergeAction {
7281
if rootRenderReferences.containsBothKinds {
7382
// If the combined archive contains both documentation and tutorial content, create separate topic sections for each.
7483
renderNode.topicSections = [
75-
.init(title: "Modules", abstract: nil, discussion: nil, identifiers: rootRenderReferences.documentation.map(\.identifier.identifier)),
76-
.init(title: "Tutorials", abstract: nil, discussion: nil, identifiers: rootRenderReferences.tutorials.map(\.identifier.identifier)),
84+
.init(title: "Modules", abstract: nil, discussion: nil, identifiers: rootRenderReferences.documentation.map(\.rawIdentifier)),
85+
.init(title: "Tutorials", abstract: nil, discussion: nil, identifiers: rootRenderReferences.tutorials.map(\.rawIdentifier)),
7786
]
7887
} else {
7988
// Otherwise, create a single unnamed topic section
8089
renderNode.topicSections = [
81-
.init(title: nil, abstract: nil, discussion: nil, identifiers: (rootRenderReferences.all).map(\.identifier.identifier)),
90+
.init(title: nil, abstract: nil, discussion: nil, identifiers: (rootRenderReferences.all).map(\.rawIdentifier)),
8291
]
8392
}
8493

8594
for renderReference in rootRenderReferences.documentation {
86-
renderNode.references[renderReference.identifier.identifier] = renderReference
95+
renderNode.references[renderReference.rawIdentifier] = renderReference.reference
96+
97+
for dependencyReference in renderReference.dependencies {
98+
renderNode.references[dependencyReference.identifier.identifier] = dependencyReference
99+
}
87100
}
88101
for renderReference in rootRenderReferences.tutorials {
89-
renderNode.references[renderReference.identifier.identifier] = renderReference
102+
renderNode.references[renderReference.rawIdentifier] = renderReference.reference
103+
// Tutorial pages don't have page images.
90104
}
91105

92106
return renderNode
@@ -97,6 +111,7 @@ extension MergeAction {
97111
private struct RootNodeRenderReference: Decodable {
98112
/// The decoded root node render reference
99113
var renderReference: TopicRenderReference
114+
var renderDependencies: [any RenderReference]
100115

101116
enum CodingKeys: CodingKey {
102117
// The only render node keys that should be needed
@@ -107,7 +122,7 @@ private struct RootNodeRenderReference: Decodable {
107122

108123
struct StringCodingKey: CodingKey {
109124
var stringValue: String
110-
init?(stringValue: String) {
125+
init(stringValue: String) {
111126
self.stringValue = stringValue
112127
}
113128
var intValue: Int? = nil
@@ -122,6 +137,7 @@ private struct RootNodeRenderReference: Decodable {
122137

123138
let identifier = try container.decode(ResolvedTopicReference.self, forKey: .identifier)
124139
let rawIdentifier = identifier.url.absoluteString
140+
let imageReferencePrefix = identifier.bundleID.rawValue + "/"
125141

126142
// Every node should include a reference to the root page.
127143
// For reference documentation, this is because the root appears as a link in the breadcrumbs on every page.
@@ -130,8 +146,21 @@ private struct RootNodeRenderReference: Decodable {
130146
// If the root page has a reference to itself, then that the fastest and easiest way to access the correct topic render reference.
131147
if container.contains(.references) {
132148
let referencesContainer = try container.nestedContainer(keyedBy: StringCodingKey.self, forKey: .references)
133-
if let selfReference = try referencesContainer.decodeIfPresent(TopicRenderReference.self, forKey: .init(stringValue: rawIdentifier)!) {
149+
if var selfReference = try referencesContainer.decodeIfPresent(TopicRenderReference.self, forKey: .init(stringValue: rawIdentifier)) {
150+
renderDependencies = try Self.decodeDependencyReferences(
151+
container: referencesContainer,
152+
images: selfReference.images,
153+
imageReferencePrefix: imageReferencePrefix,
154+
abstract: selfReference.abstract
155+
)
156+
157+
// Image references don't include the bundle ID that they're part of and can collide with other images.
158+
for index in selfReference.images.indices {
159+
selfReference.images[index].identifier.addPrefix(imageReferencePrefix)
160+
}
161+
134162
renderReference = selfReference
163+
135164
return
136165
}
137166
}
@@ -140,13 +169,62 @@ private struct RootNodeRenderReference: Decodable {
140169
// we can create a new topic reference by decoding a little bit more information from the render node.
141170
let metadata = try container.decode(RenderMetadata.self, forKey: .metadata)
142171

172+
var prefixedImages = metadata.images
173+
// Image references don't include the bundle ID that they're part of and can collide with other images.
174+
for index in prefixedImages.indices {
175+
prefixedImages[index].identifier.addPrefix(imageReferencePrefix)
176+
}
177+
143178
renderReference = TopicRenderReference(
144179
identifier: RenderReferenceIdentifier(rawIdentifier),
145180
title: metadata.title ?? identifier.lastPathComponent,
146181
abstract: try container.decodeIfPresent([RenderInlineContent].self, forKey: .abstract) ?? [],
147182
url: identifier.path.lowercased(),
148183
kind: try container.decode(RenderNode.Kind.self, forKey: .kind),
149-
images: metadata.images
184+
images: prefixedImages
150185
)
186+
187+
if container.contains(.references) {
188+
renderDependencies = try Self.decodeDependencyReferences(
189+
container: try container.nestedContainer(keyedBy: StringCodingKey.self, forKey: .references),
190+
images: metadata.images,
191+
imageReferencePrefix: imageReferencePrefix,
192+
abstract: renderReference.abstract
193+
)
194+
195+
} else {
196+
renderDependencies = []
197+
}
198+
}
199+
200+
private static func decodeDependencyReferences(
201+
container: KeyedDecodingContainer<RootNodeRenderReference.StringCodingKey>,
202+
images: [TopicImage],
203+
imageReferencePrefix: String,
204+
abstract: [RenderInlineContent]
205+
) throws -> [any RenderReference] {
206+
var references: [any RenderReference] = []
207+
208+
for image in images {
209+
// Image references don't include the bundle ID that they're part of and can collide with other images.
210+
var imageRef = try container.decode(ImageReference.self, forKey: .init(stringValue: image.identifier.identifier))
211+
imageRef.identifier.addPrefix(imageReferencePrefix)
212+
213+
references.append(imageRef)
214+
}
215+
216+
for case .reference(identifier: let identifier, isActive: _, overridingTitle: _, overridingTitleInlineContent: _) in abstract {
217+
references.append(
218+
try container.decode(TopicRenderReference.self, forKey: .init(stringValue: identifier.identifier))
219+
)
220+
}
221+
222+
return references
223+
}
224+
}
225+
226+
private extension RenderReferenceIdentifier {
227+
mutating func addPrefix(_ prefix: String) {
228+
identifier = prefix + identifier
151229
}
152230
}

0 commit comments

Comments
 (0)