Skip to content

Commit 95a24ea

Browse files
committed
Move assets into subdirectory to avoid collisions when merging archives
1 parent 95a45d0 commit 95a24ea

File tree

11 files changed

+95
-54
lines changed

11 files changed

+95
-54
lines changed

Sources/SwiftDocC/Converter/RenderNode+Coding.swift

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ extension CodingUserInfoKey {
2727
static let variantOverrides = CodingUserInfoKey(rawValue: "variantOverrides")!
2828

2929
static let baseEncodingPath = CodingUserInfoKey(rawValue: "baseEncodingPath")!
30+
31+
/// A user info key to indicate a base path for local asset URLs.
32+
static let assetPrefixComponent = CodingUserInfoKey(rawValue: "assetPrefixComponent")!
3033
}
3134

3235
extension Encoder {
@@ -45,12 +48,12 @@ extension Encoder {
4548
///
4649
/// These references will then be encoded at a later stage by `TopicRenderReferenceEncoder`.
4750
var skipsEncodingReferences: Bool {
48-
guard let userInfoValue = userInfo[.skipsEncodingReferences] as? Bool else {
49-
// The value doesn't exist so we should encode reference. Return false.
50-
return false
51-
}
52-
53-
return userInfoValue
51+
userInfo[.skipsEncodingReferences] as? Bool ?? false
52+
}
53+
54+
/// A base path to use when creating destination URLs for local assets (images, videos, downloads, etc.)
55+
var assetPrefixComponent: String? {
56+
userInfo[.assetPrefixComponent] as? String
5457
}
5558
}
5659

@@ -81,12 +84,7 @@ extension JSONEncoder {
8184
/// These references will then be encoded at a later stage by `TopicRenderReferenceEncoder`.
8285
var skipsEncodingReferences: Bool {
8386
get {
84-
guard let userInfoValue = userInfo[.skipsEncodingReferences] as? Bool else {
85-
// The value doesn't exist so we should encode reference. Return false.
86-
return false
87-
}
88-
89-
return userInfoValue
87+
userInfo[.skipsEncodingReferences] as? Bool ?? false
9088
}
9189
set {
9290
userInfo[.skipsEncodingReferences] = newValue
@@ -104,13 +102,14 @@ public enum RenderJSONEncoder {
104102
/// 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.
105103
///
106104
/// - Parameters:
107-
/// - prettyPrint: If `true`, the encoder formats its output to make it easy to read; if `false`, the output is compact.
108-
/// - emitVariantOverrides: Whether the encoder should emit the top-level ``RenderNode/variantOverrides`` property that holds language-
109-
/// specific documentation data.
105+
/// - prettyPrint: If `true`, the encoder formats its output to make it easy to read; if `false`, the output is compact.
106+
/// - emitVariantOverrides: Whether the encoder should emit the top-level ``RenderNode/variantOverrides`` property that holds language-specific documentation data.
107+
/// - assetPrefixComponent: A path component to include in destination URLs for local assets (images, videos, downloads, etc.)
110108
/// - Returns: The new JSON encoder.
111109
public static func makeEncoder(
112110
prettyPrint: Bool = shouldPrettyPrintOutputJSON,
113-
emitVariantOverrides: Bool = true
111+
emitVariantOverrides: Bool = true,
112+
assetPrefixComponent: String? = nil
114113
) -> JSONEncoder {
115114
let encoder = JSONEncoder()
116115

@@ -125,6 +124,9 @@ public enum RenderJSONEncoder {
125124
if emitVariantOverrides {
126125
encoder.userInfo[.variantOverrides] = VariantOverrides()
127126
}
127+
if let bundleIdentifier = assetPrefixComponent {
128+
encoder.userInfo[.assetPrefixComponent] = bundleIdentifier
129+
}
128130

129131
return encoder
130132
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public struct ImageReference: MediaReference, URLReference, Equatable {
7474
var result = [VariantProxy]()
7575
// sort assets by URL path for deterministic sorting of images
7676
asset.variants.sorted(by: \.value.path).forEach { (key, value) in
77-
let url = value.isAbsoluteWebURL ? value : destinationURL(for: value.lastPathComponent)
77+
let url = value.isAbsoluteWebURL ? value : destinationURL(for: value.lastPathComponent, prefixComponent: encoder.assetPrefixComponent)
7878
result.append(VariantProxy(url: url, traits: key, svgID: asset.metadata[value]?.svgID))
7979
}
8080
try container.encode(result, forKey: .variants)

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,17 @@ extension URLReference {
6969
///
7070
/// The converter that writes the built documentation to the file system is responsible for copying the referenced file to this destination.
7171
///
72-
/// - Parameter path: The path of the file.
72+
/// - Parameters:
73+
/// - path: The path of the file.
74+
/// - prefixComponent: An optional path component to add before the path of the file.
7375
/// - Returns: The destination URL for the given file path.
74-
func destinationURL(for path: String) -> URL {
75-
return Self.baseURL.appendingPathComponent(path, isDirectory: false)
76+
func destinationURL(for path: String, prefixComponent: String?) -> URL {
77+
var url = Self.baseURL
78+
if let bundleName = prefixComponent {
79+
url.appendPathComponent(bundleName, isDirectory: true)
80+
}
81+
url.appendPathComponent(path, isDirectory: false)
82+
return url
7683
}
7784
}
7885

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public struct VideoReference: MediaReference, URLReference, Equatable {
8181
// convert the data asset to a serializable object
8282
var result = [VariantProxy]()
8383
asset.variants.sorted(by: \.value.path).forEach { (key, value) in
84-
let url = value.isAbsoluteWebURL ? value : destinationURL(for: value.lastPathComponent)
84+
let url = value.isAbsoluteWebURL ? value : destinationURL(for: value.lastPathComponent, prefixComponent: encoder.assetPrefixComponent)
8585
result.append(VariantProxy(url: url, traits: key))
8686
}
8787
try container.encode(result, forKey: .variants)

Sources/SwiftDocC/Model/Rendering/Tutorial/References/DownloadReference.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public struct DownloadReference: RenderReference, URLReference, Equatable {
8686

8787
// Render URL
8888
if !encodeUrlVerbatim {
89-
try container.encode(renderURL(for: url), forKey: .url)
89+
try container.encode(renderURL(for: url, prefixComponent: encoder.assetPrefixComponent), forKey: .url)
9090
} else {
9191
try container.encode(url, forKey: .url)
9292
}
@@ -100,8 +100,8 @@ public struct DownloadReference: RenderReference, URLReference, Equatable {
100100
}
101101

102102
extension DownloadReference {
103-
private func renderURL(for url: URL) -> URL {
104-
url.isAbsoluteWebURL ? url : destinationURL(for: url.lastPathComponent)
103+
private func renderURL(for url: URL, prefixComponent: String?) -> URL {
104+
url.isAbsoluteWebURL ? url : destinationURL(for: url.lastPathComponent, prefixComponent: prefixComponent)
105105
}
106106
}
107107

Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertAction.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,8 @@ public struct ConvertAction: Action, RecreatingContext {
454454
// An optional indexer, if indexing while converting is enabled.
455455
var indexer: Indexer? = nil
456456

457-
if let bundleIdentifier = converter.firstAvailableBundle()?.identifier {
457+
let bundleIdentifier = converter.firstAvailableBundle()?.identifier
458+
if let bundleIdentifier = bundleIdentifier {
458459
// Create an index builder and prepare it to receive nodes.
459460
indexer = try Indexer(outputURL: temporaryFolder, bundleIdentifier: bundleIdentifier)
460461
}
@@ -466,7 +467,8 @@ public struct ConvertAction: Action, RecreatingContext {
466467
context: context,
467468
indexer: indexer,
468469
enableCustomTemplates: experimentalEnableCustomTemplates,
469-
transformForStaticHostingIndexHTML: transformForStaticHosting ? indexHTML : nil
470+
transformForStaticHostingIndexHTML: transformForStaticHosting ? indexHTML : nil,
471+
bundleIdentifier: bundleIdentifier
470472
)
471473

472474
let analysisProblems: [Problem]
@@ -558,7 +560,8 @@ public struct ConvertAction: Action, RecreatingContext {
558560
fileManager: fileManager,
559561
context: context,
560562
indexer: nil,
561-
transformForStaticHostingIndexHTML: nil
563+
transformForStaticHostingIndexHTML: nil,
564+
bundleIdentifier: bundleIdentifier
562565
)
563566

564567
try outputConsumer.consume(benchmarks: Benchmark.main)

Sources/SwiftDocCUtilities/Action/Actions/Convert/ConvertFileWritingConsumer.swift

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer {
1919
var renderNodeWriter: JSONEncodingRenderNodeWriter
2020
var indexer: ConvertAction.Indexer?
2121
let enableCustomTemplates: Bool
22+
var assetPrefixComponent: String?
2223

2324
private enum CustomTemplateIdentifier: String {
2425
case header = "custom-header"
@@ -32,7 +33,8 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer {
3233
context: DocumentationContext,
3334
indexer: ConvertAction.Indexer?,
3435
enableCustomTemplates: Bool = false,
35-
transformForStaticHostingIndexHTML: URL?
36+
transformForStaticHostingIndexHTML: URL?,
37+
bundleIdentifier: BundleIdentifier?
3638
) {
3739
self.targetFolder = targetFolder
3840
self.bundleRootFolder = bundleRootFolder
@@ -45,6 +47,7 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer {
4547
)
4648
self.indexer = indexer
4749
self.enableCustomTemplates = enableCustomTemplates
50+
self.assetPrefixComponent = bundleIdentifier?.split(separator: "/").joined(separator: "-")
4851
}
4952

5053
func consume(problems: [Problem]) throws {
@@ -58,7 +61,7 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer {
5861

5962
func consume(renderNode: RenderNode) throws {
6063
// Write the render node to disk
61-
try renderNodeWriter.write(renderNode)
64+
try renderNodeWriter.write(renderNode, encoder: makeEncoder())
6265

6366
// Index the node, if indexing is enabled.
6467
indexer?.index(renderNode)
@@ -77,11 +80,14 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer {
7780

7881
// TODO: Supporting a single bundle for the moment.
7982
let bundleIdentifier = bundle.identifier
83+
assert(bundleIdentifier == self.assetPrefixComponent, "Unexpectedly encoding assets for a bundle other than the one this output consumer was created for.")
8084

8185
// Create images directory if needed.
82-
let imagesDirectory = targetFolder.appendingPathComponent("images", isDirectory: true)
86+
let imagesDirectory = targetFolder
87+
.appendingPathComponent("images", isDirectory: true)
88+
.appendingPathComponent(bundleIdentifier, isDirectory: true)
8389
if !fileManager.directoryExists(atPath: imagesDirectory.path) {
84-
try fileManager.createDirectory(at: imagesDirectory, withIntermediateDirectories: false, attributes: nil)
90+
try fileManager.createDirectory(at: imagesDirectory, withIntermediateDirectories: true, attributes: nil)
8591
}
8692

8793
// Copy all registered images to the output directory.
@@ -90,9 +96,11 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer {
9096
}
9197

9298
// Create videos directory if needed.
93-
let videosDirectory = targetFolder.appendingPathComponent("videos", isDirectory: true)
99+
let videosDirectory = targetFolder
100+
.appendingPathComponent("videos", isDirectory: true)
101+
.appendingPathComponent(bundleIdentifier, isDirectory: true)
94102
if !fileManager.directoryExists(atPath: videosDirectory.path) {
95-
try fileManager.createDirectory(at: videosDirectory, withIntermediateDirectories: false, attributes: nil)
103+
try fileManager.createDirectory(at: videosDirectory, withIntermediateDirectories: true, attributes: nil)
96104
}
97105

98106
// Copy all registered videos to the output directory.
@@ -101,9 +109,11 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer {
101109
}
102110

103111
// Create downloads directory if needed.
104-
let downloadsDirectory = targetFolder.appendingPathComponent(DownloadReference.locationName, isDirectory: true)
112+
let downloadsDirectory = targetFolder
113+
.appendingPathComponent(DownloadReference.locationName, isDirectory: true)
114+
.appendingPathComponent(bundleIdentifier, isDirectory: true)
105115
if !fileManager.directoryExists(atPath: downloadsDirectory.path) {
106-
try fileManager.createDirectory(at: downloadsDirectory, withIntermediateDirectories: false, attributes: nil)
116+
try fileManager.createDirectory(at: downloadsDirectory, withIntermediateDirectories: true, attributes: nil)
107117
}
108118

109119
// Copy all downloads into the output directory.
@@ -190,7 +200,11 @@ struct ConvertFileWritingConsumer: ConvertOutputConsumer {
190200

191201
/// Encodes the given value using the default render node JSON encoder.
192202
private func encode<E: Encodable>(_ value: E) throws -> Data {
193-
try RenderJSONEncoder.makeEncoder().encode(value)
203+
try makeEncoder().encode(value)
204+
}
205+
206+
private func makeEncoder() -> JSONEncoder {
207+
RenderJSONEncoder.makeEncoder(assetPrefixComponent: assetPrefixComponent)
194208
}
195209

196210
// Injects a <template> tag into the index.html <body> using the contents of

Sources/SwiftDocCUtilities/Action/Actions/Convert/JSONEncodingRenderNodeWriter.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ class JSONEncodingRenderNodeWriter {
4343
/// If the target path to the JSON file includes intermediate folders that don't exist, the writer object will ask the file manager, with which it was created, to
4444
/// create those intermediate folders before writing the JSON file.
4545
///
46-
/// - Parameter renderNode: The node which the writer object writes to a JSON file.
47-
func write(_ renderNode: RenderNode) throws {
46+
/// - Parameters:
47+
/// - renderNode: The node which the writer object writes to a JSON file.
48+
/// - encoder: The encoder to serialize the render node with.
49+
func write(_ renderNode: RenderNode, encoder: JSONEncoder) throws {
4850
let fileSafePath = NodeURLGenerator.fileSafeReferencePath(
4951
renderNode.identifier,
5052
lowercased: true

Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -418,19 +418,19 @@ class ExternalLinkableTests: XCTestCase {
418418
}
419419

420420
// TODO: DataAsset doesn't round-trip encode/decode
421-
summary.references = summary.references?.compactMap {
422-
guard var imageRef = $0 as? ImageReference else { return nil }
421+
summary.references = summary.references?.compactMap { (original: RenderReference) -> RenderReference? in
422+
guard var imageRef = original as? ImageReference else { return nil }
423423
imageRef.asset.variants = imageRef.asset.variants.mapValues { variant in
424-
return imageRef.destinationURL(for: variant.lastPathComponent)
424+
return imageRef.destinationURL(for: variant.lastPathComponent, prefixComponent: bundle.identifier)
425425
}
426426
imageRef.asset.metadata = .init(uniqueKeysWithValues: imageRef.asset.metadata.map { key, value in
427-
return (imageRef.destinationURL(for: key.lastPathComponent), value)
427+
return (imageRef.destinationURL(for: key.lastPathComponent, prefixComponent: bundle.identifier), value)
428428
})
429429
return imageRef as RenderReference
430430
}
431431

432432

433-
let encoded = try JSONEncoder().encode(summary)
433+
let encoded = try RenderJSONEncoder.makeEncoder(assetPrefixComponent: bundle.identifier).encode(summary)
434434
let decoded = try JSONDecoder().decode(LinkDestinationSummary.self, from: encoded)
435435
XCTAssertEqual(decoded, summary)
436436
}

Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,17 @@ class ConvertActionTests: XCTestCase {
102102
// Verify that the following files and folder exist at the output location
103103
let expectedOutput = Folder(name: ".docc-build", content: [
104104
Folder(name: "images", content: [
105-
CopyOfFile(original: imageFile, newName: testImageName),
105+
Folder(name: "com.test.example", content: [
106+
CopyOfFile(original: imageFile, newName: testImageName),
107+
]),
106108
]),
107109
])
108110
expectedOutput.assertExist(at: result.outputs[0], fileManager: testDataProvider)
109111

110112
// Verify that the copied image has the same capitalization as the original
111113
let copiedImageOutput = testDataProvider.files.keys
112-
.filter({ $0.hasPrefix(result.outputs[0].appendingPathComponent("images").path + "/") })
113-
.map({ $0.replacingOccurrences(of: result.outputs[0].appendingPathComponent("images").path + "/", with: "") })
114+
.filter({ $0.hasPrefix(result.outputs[0].appendingPathComponent("images/com.test.example").path + "/") })
115+
.map({ $0.replacingOccurrences(of: result.outputs[0].appendingPathComponent("images/com.test.example").path + "/", with: "") })
114116

115117
XCTAssertEqual(copiedImageOutput, [testImageName])
116118
}
@@ -149,15 +151,17 @@ class ConvertActionTests: XCTestCase {
149151
// Verify that the following files and folder exist at the output location
150152
let expectedOutput = Folder(name: ".docc-build", content: [
151153
Folder(name: "videos", content: [
152-
CopyOfFile(original: videoFile, newName: testVideoName),
154+
Folder(name: "com.test.example", content: [
155+
CopyOfFile(original: videoFile, newName: testVideoName),
156+
]),
153157
]),
154158
])
155159
expectedOutput.assertExist(at: result.outputs[0], fileManager: testDataProvider)
156160

157161
// Verify that the copied video has the same capitalization as the original
158162
let copiedVideoOutput = testDataProvider.files.keys
159-
.filter({ $0.hasPrefix(result.outputs[0].appendingPathComponent("videos").path + "/") })
160-
.map({ $0.replacingOccurrences(of: result.outputs[0].appendingPathComponent("videos").path + "/", with: "") })
163+
.filter({ $0.hasPrefix(result.outputs[0].appendingPathComponent("videos/com.test.example").path + "/") })
164+
.map({ $0.replacingOccurrences(of: result.outputs[0].appendingPathComponent("videos/com.test.example").path + "/", with: "") })
161165

162166
XCTAssertEqual(copiedVideoOutput, [testVideoName])
163167
}
@@ -208,7 +212,9 @@ class ConvertActionTests: XCTestCase {
208212
// Verify that the following files and folder exist at the output location
209213
let expectedOutput = Folder(name: ".docc-build", content: [
210214
Folder(name: "downloads", content: [
211-
CopyOfFile(original: downloadFile),
215+
Folder(name: "com.test.example", content: [
216+
CopyOfFile(original: downloadFile),
217+
]),
212218
]),
213219
])
214220
expectedOutput.assertExist(at: result.outputs[0], fileManager: testDataProvider)
@@ -439,10 +445,14 @@ class ConvertActionTests: XCTestCase {
439445
// Verify that the following files and folder exist at the output location
440446
let expectedOutput = Folder(name: ".docc-build", content: [
441447
Folder(name: "images", content: [
442-
CopyOfFile(original: imageFile, newName: "TEST.png"),
448+
Folder(name: "com.test.example", content: [
449+
CopyOfFile(original: imageFile, newName: "TEST.png"),
450+
]),
443451
]),
444452
Folder(name: "videos", content: [
445-
CopyOfFile(original: imageFile, newName: "VIDEO.mov"),
453+
Folder(name: "com.test.example", content: [
454+
CopyOfFile(original: imageFile, newName: "VIDEO.mov"),
455+
]),
446456
]),
447457
])
448458
expectedOutput.assertExist(at: result.outputs[0], fileManager: testDataProvider)
@@ -636,9 +646,12 @@ class ConvertActionTests: XCTestCase {
636646
// Verify that the following files and folder exist at the output location
637647
let expectedOutput = Folder(name: ".docc-build", content: [
638648
Folder(name: "images", content: [
639-
CopyOfFile(original: imageFile, newName: "referenced-tutorials-image.png"),
649+
Folder(name: "com.test.example", content: [
650+
CopyOfFile(original: imageFile, newName: "referenced-tutorials-image.png"),
651+
]),
640652
]),
641653
Folder(name: "videos", content: [
654+
Folder(name: "com.test.example", content: []),
642655
]),
643656
JSONFile(name: "diagnostics.json", content: [
644657
Digest.Diagnostic(

0 commit comments

Comments
 (0)