Skip to content

Commit b16e565

Browse files
committed
Add page image directive
Add custom icon support to navigator index Extract SVG IDs Update Render Node and Index spec for `@PageImage` Fix failing tests # Conflicts: # Sources/SwiftDocC/Model/DocumentationNode.swift # Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift # Tests/SwiftDocCTests/Semantics/DirectiveInfrastructure/DirectiveIndexTests.swift
1 parent 3099317 commit b16e565

27 files changed

+567
-50
lines changed

Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -753,11 +753,14 @@ extension NavigatorIndex {
753753
}
754754
}
755755

756-
let navigationItem = NavigatorItem(pageType: renderNode.navigatorPageType().rawValue,
757-
languageID: language.mask,
758-
title: title,
759-
platformMask: platformID,
760-
availabilityID: UInt64(availabilityID))
756+
let navigationItem = NavigatorItem(
757+
pageType: renderNode.navigatorPageType().rawValue,
758+
languageID: language.mask,
759+
title: title,
760+
platformMask: platformID,
761+
availabilityID: UInt64(availabilityID),
762+
icon: renderNode.icon
763+
)
761764
navigationItem.path = identifierPath
762765

763766
// Index the USR for the given identifier
@@ -894,7 +897,8 @@ extension NavigatorIndex {
894897
public func finalize(
895898
estimatedCount: Int? = nil,
896899
emitJSONRepresentation: Bool = true,
897-
emitLMDBRepresentation: Bool = true
900+
emitLMDBRepresentation: Bool = true,
901+
context: DocumentationContext? = nil
898902
) {
899903
precondition(!isCompleted, "Finalizing an already completed index build multiple times is not possible.")
900904

@@ -1020,7 +1024,7 @@ extension NavigatorIndex {
10201024
}
10211025

10221026
if emitJSONRepresentation {
1023-
let renderIndex = RenderIndex.fromNavigatorIndex(navigatorIndex, with: self)
1027+
let renderIndex = RenderIndex.fromNavigatorIndex(navigatorIndex, with: self, context: context)
10241028

10251029
let jsonEncoder = JSONEncoder()
10261030
if shouldPrettyPrintOutputJSON {

Sources/SwiftDocC/Indexing/Navigator/NavigatorItem.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString
4747
/// If available, a hashed USR of this entry and its language information.
4848
internal var usrIdentifier: String? = nil
4949

50+
var icon: RenderReferenceIdentifier? = nil
51+
5052
/**
5153
Initialize a `NavigatorItem` with the given data.
5254

@@ -58,13 +60,14 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString
5860
- availabilityID: The identifier of the availability information of the page.
5961
- path: The path to load the content.
6062
*/
61-
init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, path: String) {
63+
init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, path: String, icon: RenderReferenceIdentifier? = nil) {
6264
self.pageType = pageType
6365
self.languageID = languageID
6466
self.title = title
6567
self.platformMask = platformMask
6668
self.availabilityID = availabilityID
6769
self.path = path
70+
self.icon = icon
6871
}
6972

7073
/**
@@ -77,12 +80,13 @@ public final class NavigatorItem: Serializable, Codable, Equatable, CustomString
7780
- platformMask: The mask indicating for which platform the page is available.
7881
- availabilityID: The identifier of the availability information of the page.
7982
*/
80-
public init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64) {
83+
public init(pageType: UInt8, languageID: UInt8, title: String, platformMask: UInt64, availabilityID: UInt64, icon: RenderReferenceIdentifier? = nil) {
8184
self.pageType = pageType
8285
self.languageID = languageID
8386
self.title = title
8487
self.platformMask = platformMask
8588
self.availabilityID = availabilityID
89+
self.icon = icon
8690
}
8791

8892
// MARK: - Serialization and Deserialization

Sources/SwiftDocC/Indexing/IndexJSON/Index.swift renamed to Sources/SwiftDocC/Indexing/RenderIndexJSON/RenderIndex.swift

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,46 @@ 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: 0)
26+
public static let currentSchemaVersion = SemanticVersion(major: 0, minor: 1, patch: 1)
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.
3232
public let interfaceLanguages: [String: [Node]]
3333

34+
/// The values of the image references used in the documentation index.
35+
public private(set) var references: [String: ImageReference]
36+
37+
enum CodingKeys: CodingKey {
38+
case schemaVersion
39+
case interfaceLanguages
40+
case references
41+
}
42+
3443
/// Creates a new render index with the given interface language to node mapping.
3544
public init(
36-
interfaceLanguages: [String: [Node]]
45+
interfaceLanguages: [String: [Node]],
46+
references: [String: ImageReference] = [:]
3747
) {
3848
self.schemaVersion = Self.currentSchemaVersion
3949
self.interfaceLanguages = interfaceLanguages
50+
self.references = references
51+
}
52+
53+
public func encode(to encoder: Encoder) throws {
54+
var container = encoder.container(keyedBy: CodingKeys.self)
55+
try container.encode(self.schemaVersion, forKey: .schemaVersion)
56+
try container.encode(self.interfaceLanguages, forKey: .interfaceLanguages)
57+
58+
try container.encodeIfNotEmpty(self.references, forKey: .references)
59+
}
60+
61+
public init(from decoder: Decoder) throws {
62+
let container = try decoder.container(keyedBy: CodingKeys.self)
63+
self.schemaVersion = try container.decode(SemanticVersion.self, forKey: .schemaVersion)
64+
self.interfaceLanguages = try container.decode([String : [RenderIndex.Node]].self, forKey: .interfaceLanguages)
65+
self.references = try container.decodeIfPresent([String : ImageReference].self, forKey: .references) ?? [:]
4066
}
4167
}
4268

@@ -73,6 +99,9 @@ extension RenderIndex {
7399
///
74100
/// Allows renderers to use a specific design treatment for render index nodes that mark the node as in beta.
75101
public let isBeta: Bool
102+
103+
/// Reference to the image that should be used to represent this node.
104+
public let icon: RenderReferenceIdentifier?
76105

77106
enum CodingKeys: String, CodingKey {
78107
case title
@@ -82,6 +111,7 @@ extension RenderIndex {
82111
case deprecated
83112
case external
84113
case beta
114+
case icon
85115
}
86116

87117
public func encode(to encoder: Encoder) throws {
@@ -107,6 +137,8 @@ extension RenderIndex {
107137
if isBeta {
108138
try container.encode(isBeta, forKey: .beta)
109139
}
140+
141+
try container.encodeIfPresent(icon, forKey: .icon)
110142
}
111143

112144
public init(from decoder: Decoder) throws {
@@ -126,6 +158,8 @@ extension RenderIndex {
126158

127159
// `isBeta` defaults to false if it's not specified
128160
isBeta = try values.decodeIfPresent(Bool.self, forKey: .beta) ?? false
161+
162+
icon = try values.decodeIfPresent(RenderReferenceIdentifier.self, forKey: .icon)
129163
}
130164

131165
/// Creates a new node with the given title, path, type, and children.
@@ -146,7 +180,8 @@ extension RenderIndex {
146180
children: [Node]?,
147181
isDeprecated: Bool,
148182
isExternal: Bool,
149-
isBeta: Bool
183+
isBeta: Bool,
184+
icon: RenderReferenceIdentifier? = nil
150185
) {
151186
self.title = title
152187
self.path = path
@@ -155,14 +190,16 @@ extension RenderIndex {
155190
self.isDeprecated = isDeprecated
156191
self.isExternal = isExternal
157192
self.isBeta = isBeta
193+
self.icon = nil
158194
}
159195

160196
init(
161197
title: String,
162198
path: String,
163199
pageType: NavigatorIndex.PageType?,
164200
isDeprecated: Bool,
165-
children: [Node]
201+
children: [Node],
202+
icon: RenderReferenceIdentifier?
166203
) {
167204
self.title = title
168205
self.children = children.isEmpty ? nil : children
@@ -174,6 +211,7 @@ extension RenderIndex {
174211
self.isExternal = false
175212

176213
self.isBeta = false
214+
self.icon = icon
177215

178216
guard let pageType = pageType else {
179217
self.type = nil
@@ -193,14 +231,20 @@ extension RenderIndex {
193231
}
194232

195233
extension RenderIndex {
196-
static func fromNavigatorIndex(_ navigatorIndex: NavigatorIndex, with builder: NavigatorIndex.Builder) -> RenderIndex {
234+
static func fromNavigatorIndex(
235+
_ navigatorIndex: NavigatorIndex,
236+
with builder: NavigatorIndex.Builder,
237+
context: DocumentationContext?
238+
) -> RenderIndex {
197239
// The immediate children of the root represent the interface languages
198240
// described in this navigator tree.
199241
let interfaceLanguageRoots = navigatorIndex.navigatorTree.root.children
200242

201243
let languageMaskToLanguage = navigatorIndex.languageMaskToLanguage
202244

203-
return RenderIndex(
245+
var iconIdentifiers = [RenderReferenceIdentifier]()
246+
247+
var renderIndex = RenderIndex(
204248
interfaceLanguages: Dictionary(
205249
interfaceLanguageRoots.compactMap { interfaceLanguageRoot in
206250
// If an interface language in the given navigator tree does not exist
@@ -210,19 +254,51 @@ extension RenderIndex {
210254

211255
return (
212256
language: languageID,
213-
children: interfaceLanguageRoot.children.map {
214-
RenderIndex.Node.fromNavigatorTreeNode($0, in: navigatorIndex, with: builder)
257+
children: interfaceLanguageRoot.children.map { node in
258+
RenderIndex.Node.fromNavigatorTreeNode(
259+
node,
260+
in: navigatorIndex,
261+
with: builder,
262+
iconIdentifiers: &iconIdentifiers
263+
)
215264
}
216265
)
217266
},
218267
uniquingKeysWith: +
219268
)
220269
)
270+
271+
guard let context = context else {
272+
return renderIndex
273+
}
274+
275+
let icons = iconIdentifiers.lazy.compactMap { iconReference -> ImageReference? in
276+
guard let dataAsset = context.resolveAsset(
277+
named: iconReference.identifier,
278+
bundleIdentifier: builder.bundleIdentifier
279+
) else {
280+
return nil
281+
}
282+
283+
return ImageReference(identifier: iconReference, imageAsset: dataAsset)
284+
}
285+
286+
let mappedReferences = Dictionary(
287+
uniqueKeysWithValues: icons.map { ($0.identifier.identifier, $0) }
288+
)
289+
290+
renderIndex.references = mappedReferences
291+
return renderIndex
221292
}
222293
}
223294

224295
extension RenderIndex.Node {
225-
static func fromNavigatorTreeNode(_ node: NavigatorTree.Node, in navigatorIndex: NavigatorIndex, with builder: NavigatorIndex.Builder) -> RenderIndex.Node {
296+
fileprivate static func fromNavigatorTreeNode(
297+
_ node: NavigatorTree.Node,
298+
in navigatorIndex: NavigatorIndex,
299+
with builder: NavigatorIndex.Builder,
300+
iconIdentifiers: inout [RenderReferenceIdentifier]
301+
) -> RenderIndex.Node {
226302
// If this node was deprecated on any platform version mark it as deprecated.
227303
let isDeprecated: Bool
228304

@@ -235,14 +311,19 @@ extension RenderIndex.Node {
235311
isDeprecated = false
236312
}
237313

314+
if let iconIdentifier = node.item.icon {
315+
iconIdentifiers.append(iconIdentifier)
316+
}
317+
238318
return RenderIndex.Node(
239319
title: node.item.title,
240320
path: node.item.path,
241321
pageType: NavigatorIndex.PageType(rawValue: node.item.pageType),
242322
isDeprecated: isDeprecated,
243323
children: node.children.map {
244-
RenderIndex.Node.fromNavigatorTreeNode($0, in: navigatorIndex, with: builder)
245-
}
324+
RenderIndex.Node.fromNavigatorTreeNode($0, in: navigatorIndex, with: builder, iconIdentifiers: &iconIdentifiers)
325+
},
326+
icon: node.item.icon
246327
)
247328
}
248329
}

Sources/SwiftDocC/Infrastructure/Bundle Assets/DataAssetManager.swift

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,11 @@ struct DataAssetManager {
7171
try! NSRegularExpression(pattern: "(?!^)(?<=@)[1|2|3]x(?=\\.\\w*$)")
7272
}()
7373

74-
private mutating func referenceMetaInformationForDataURL(_ dataURL: URL, dataProvider: DocumentationContextDataProvider? = nil, bundle documentationBundle: DocumentationBundle? = nil) throws -> (reference: String, traits: DataTraitCollection) {
74+
private mutating func referenceMetaInformationForDataURL(_ dataURL: URL, dataProvider: DocumentationContextDataProvider? = nil, bundle documentationBundle: DocumentationBundle? = nil) throws -> (reference: String, traits: DataTraitCollection, metadata: DataAsset.Metadata) {
7575
var dataReference = dataURL.path
7676
var traitCollection = DataTraitCollection()
7777

78+
var metadata = DataAsset.Metadata()
7879
if DocumentationContext.isFileExtension(dataURL.pathExtension, supported: .video) {
7980
// In case of a video read its traits: dark/light variants.
8081

@@ -103,9 +104,13 @@ struct DataAssetManager {
103104
// Remove the display scale information from the image reference.
104105
dataReference = dataReference.replacingOccurrences(of: "@\(displayScale.rawValue)", with: "")
105106
traitCollection = .init(userInterfaceStyle: userInterfaceStyle, displayScale: displayScale)
107+
108+
if dataURL.pathExtension.lowercased() == "svg" {
109+
metadata.svgID = SVGIDExtractor.extractID(from: dataURL)
110+
}
106111
}
107112

108-
return (reference: dataReference, traits: traitCollection)
113+
return (reference: dataReference, traits: traitCollection, metadata: metadata)
109114
}
110115

111116
/**
@@ -123,7 +128,7 @@ struct DataAssetManager {
123128
// Store the image with given scale information and display scale.
124129
let name = referenceURL.lastPathComponent
125130
storage[name, default: DataAsset()]
126-
.register(dataURL, with: meta.traits)
131+
.register(dataURL, with: meta.traits, metadata: meta.metadata)
127132

128133
if name.contains(".") {
129134
let nameNoExtension = referenceURL.deletingPathExtension().lastPathComponent
@@ -156,7 +161,7 @@ struct DataAssetManager {
156161
/// ### Asset Traits
157162
/// - ``DisplayScale``
158163
/// - ``UserInterfaceStyle``
159-
public struct DataAsset: Codable {
164+
public struct DataAsset: Codable, Equatable {
160165
/// A context in which you intend clients to use a data asset.
161166
public enum Context: String, CaseIterable, Codable {
162167
/// An asset that a user intends to view alongside documentation content.
@@ -172,6 +177,9 @@ public struct DataAsset: Codable {
172177
/// depending on the system's appearance.
173178
public var variants = [DataTraitCollection: URL]()
174179

180+
/// The metadata associated with each variant.
181+
public var metadata = [URL : Metadata]()
182+
175183
/// The context in which you intend to use the data asset.
176184
public var context = Context.display
177185

@@ -182,8 +190,9 @@ public struct DataAsset: Codable {
182190
/// - Parameters:
183191
/// - url: The location of the variant.
184192
/// - traitCollection: The trait collection associated with the variant.
185-
public mutating func register(_ url: URL, with traitCollection: DataTraitCollection) {
193+
public mutating func register(_ url: URL, with traitCollection: DataTraitCollection, metadata: Metadata = Metadata()) {
186194
variants[traitCollection] = url
195+
self.metadata[url] = metadata
187196
}
188197

189198
/// Returns the data that is registered to the data asset that best matches the given trait collection.
@@ -210,6 +219,21 @@ public struct DataAsset: Codable {
210219

211220
}
212221

222+
extension DataAsset {
223+
/// Metadata specific to this data asset.
224+
public struct Metadata: Codable, Equatable {
225+
/// The first ID found in the SVG asset.
226+
///
227+
/// This value is nil if the data asset is not an SVG or if it is an SVG that does not contain an ID.
228+
public var svgID: String?
229+
230+
/// Create a new data asset metadata with the given SVG ID.
231+
public init(svgID: String? = nil) {
232+
self.svgID = svgID
233+
}
234+
}
235+
}
236+
213237
/// A collection of environment traits for an asset variant.
214238
///
215239
/// Traits describe properties of a rendering environment, such as a user-interface style (light or dark mode) and display-scale

0 commit comments

Comments
 (0)