Skip to content

Commit 491d26a

Browse files
Minimal viable merge command (#821)
* Move assets into subdirectory to avoid collisions when merging archives * Add basic merge command to combine documentation archives rdar://114730477 * Publish docs for SwiftDocC and SwiftDocCUtilities as a combined archive * Pass flags to generate source links for published framework docs * Raise an error if the merge output directory already exists Raise an error if the archives to merge has overlapping data Use a temporary directory while merging the archives * Consider all (future) subdirectories in the data directory * Fix to docc merge command: Copy data/documentation and data/tutorials, creating intermediate directories as necessary --------- Co-authored-by: Pat Shaughnessy <[email protected]>
1 parent 31456c4 commit 491d26a

28 files changed

+1434
-179
lines changed

Sources/SwiftDocC/Converter/RenderNode+Coding.swift

Lines changed: 19 additions & 17 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) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 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
@@ -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/Indexing/RenderIndexJSON/RenderIndex.swift

Lines changed: 35 additions & 5 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) 2022 Apple Inc. and the Swift project authors
4+
Copyright (c) 2022-2024 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
@@ -23,45 +23,74 @@ import SymbolKit
2323
/// `Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderIndex.spec.json`.
2424
public struct RenderIndex: Codable, Equatable {
2525
/// The current schema version of the Index JSON spec.
26-
public static let currentSchemaVersion = SemanticVersion(major: 0, minor: 1, patch: 1)
26+
public static let currentSchemaVersion = SemanticVersion(major: 0, minor: 1, patch: 2)
2727

2828
/// The version of the RenderIndex spec that was followed when creating this index.
2929
public let schemaVersion: SemanticVersion
3030

3131
/// A mapping of interface languages to the index nodes they contain.
32-
public let interfaceLanguages: [String: [Node]]
32+
public private(set) var interfaceLanguages: [String: [Node]]
3333

3434
/// The values of the image references used in the documentation index.
3535
public private(set) var references: [String: ImageReference]
3636

37+
/// The unique identifiers of the archives that are included in the documentation index.
38+
public private(set) var includedArchiveIdentifiers: [String]
39+
3740
enum CodingKeys: CodingKey {
3841
case schemaVersion
3942
case interfaceLanguages
4043
case references
44+
case includedArchiveIdentifiers
4145
}
4246

4347
/// Creates a new render index with the given interface language to node mapping.
4448
public init(
4549
interfaceLanguages: [String: [Node]],
46-
references: [String: ImageReference] = [:]
50+
references: [String: ImageReference] = [:],
51+
includedArchiveIdentifiers: [String]
4752
) {
4853
self.schemaVersion = Self.currentSchemaVersion
4954
self.interfaceLanguages = interfaceLanguages
5055
self.references = references
56+
self.includedArchiveIdentifiers = includedArchiveIdentifiers
5157
}
5258

5359
public func encode(to encoder: Encoder) throws {
5460
var container = encoder.container(keyedBy: CodingKeys.self)
5561
try container.encode(self.schemaVersion, forKey: .schemaVersion)
5662
try container.encode(self.interfaceLanguages, forKey: .interfaceLanguages)
5763
try container.encodeIfNotEmpty(self.references, forKey: .references)
64+
try container.encodeIfNotEmpty(self.includedArchiveIdentifiers, forKey: .includedArchiveIdentifiers)
5865
}
5966

6067
public init(from decoder: Decoder) throws {
6168
let container = try decoder.container(keyedBy: CodingKeys.self)
6269
self.schemaVersion = try container.decode(SemanticVersion.self, forKey: .schemaVersion)
6370
self.interfaceLanguages = try container.decode([String : [RenderIndex.Node]].self, forKey: .interfaceLanguages)
6471
self.references = try container.decodeIfPresent([String : ImageReference].self, forKey: .references) ?? [:]
72+
self.includedArchiveIdentifiers = try container.decodeIfPresent([String].self.self, forKey: .includedArchiveIdentifiers) ?? []
73+
}
74+
75+
public mutating func merge(_ other: RenderIndex) throws {
76+
for (languageID, nodes) in other.interfaceLanguages {
77+
interfaceLanguages[languageID, default: []].append(contentsOf: nodes)
78+
}
79+
80+
try references.merge(other.references) { _, new in throw MergeError.referenceCollision(new.identifier.identifier) }
81+
82+
includedArchiveIdentifiers.append(contentsOf: other.includedArchiveIdentifiers)
83+
}
84+
85+
enum MergeError: DescribedError {
86+
case referenceCollision(String)
87+
88+
var errorDescription: String {
89+
switch self {
90+
case .referenceCollision(let reference):
91+
return "Collision merging image references. Reference \(reference.singleQuoted) exists in more than one input archive."
92+
}
93+
}
6594
}
6695
}
6796

@@ -254,7 +283,8 @@ extension RenderIndex {
254283
},
255284
uniquingKeysWith: +
256285
),
257-
references: builder.iconReferences
286+
references: builder.iconReferences,
287+
includedArchiveIdentifiers: [builder.bundleIdentifier]
258288
)
259289
}
260290
}

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

Lines changed: 2 additions & 2 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) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 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
@@ -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: 11 additions & 4 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) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 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
@@ -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: 2 additions & 2 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) 2021-2023 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 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
@@ -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: 4 additions & 4 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) 2021-2023 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 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
@@ -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/SwiftDocC/SwiftDocC.docc/Resources/RenderIndex.spec.json

Lines changed: 7 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 Swift-DocC Index.json file.",
5-
"version": "0.1.0",
5+
"version": "0.1.2",
66
"title": "RenderIndex"
77
},
88
"paths": {},
@@ -33,6 +33,12 @@
3333
"$ref": "#/components/schemas/ImageRenderReference"
3434
}
3535
},
36+
"includedArchiveIdentifiers": {
37+
"type": "array",
38+
"items": {
39+
"type": "string"
40+
}
41+
}
3642
}
3743
},
3844
"Node": {

Sources/SwiftDocC/Utility/FileManagerProtocol.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ public protocol FileManagerProtocol {
5151
func contentsOfDirectory(atPath path: String) throws -> [String]
5252
func contentsOfDirectory(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options mask: FileManager.DirectoryEnumerationOptions) throws -> [URL]
5353

54+
/// The temporary directory for the current user.
55+
var temporaryDirectory: URL { get }
56+
5457
/// Creates a file with the specified `contents` at the specified location.
5558
///
5659
/// - Parameters:

0 commit comments

Comments
 (0)