-
Notifications
You must be signed in to change notification settings - Fork 137
/
Copy pathDataAssetManager.swift
369 lines (303 loc) · 15.1 KB
/
DataAssetManager.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
/*
This source file is part of the Swift.org open source project
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/
public import Foundation
/// A container for a collection of data. Each data can have multiple variants.
struct DataAssetManager {
var storage = [String: DataAsset]()
// A "file name with no extension" to "file name with extension" index
var fuzzyKeyIndex = [String: String]()
/**
Returns the data that is registered to a data asset with the specified trait collection.
If no data is registered that exactly matches the trait collection, the data with the trait
collection that best matches the requested trait collection is returned.
*/
func data(named name: String, bestMatching traitCollection: DataTraitCollection) -> BundleData? {
return bestKey(forAssetName: name)
.flatMap({ storage[$0]?.data(bestMatching: traitCollection) })
}
/// Finds the best matching storage key for a given asset name.
/// `name` is one of the following formats:
/// - "image" - asset name without extension
/// - "image.png" - asset name including extension
func bestKey(forAssetName name: String) -> String? {
guard !storage.keys.contains(name) else { return name }
// Try the fuzzy index
return fuzzyKeyIndex[name]
}
/**
Returns all the data objects for a given name, respective of Bundle rules.
If multiple data objects are registered, the first one will be returned in a non-deterministic way.
For example if figure1 is asked and the bundle has figure1.png and figure1.jpg, one of the two will be returned.
Returns `nil` if there is no asset registered with the `name` name.
*/
func allData(named name: String) -> DataAsset? {
return bestKey(forAssetName: name).flatMap({ storage[$0] })
}
private let darkSuffix = "~dark"
// These static regular expressions should always build successfully & therefore we used `try!`.
private lazy var darkSuffixRegex: NSRegularExpression = {
try! NSRegularExpression(pattern: "(?!^)\(self.darkSuffix)(?=[.$]|@)")
}()
private lazy var displayScaleRegex: NSRegularExpression = {
try! NSRegularExpression(pattern: "(?!^)(?<=@)[1|2|3]x(?=\\.\\w*$)")
}()
private mutating func referenceMetaInformationForDataURL(_ dataURL: URL) throws -> (reference: String, traits: DataTraitCollection, metadata: DataAsset.Metadata) {
var dataReference = dataURL.path
var traitCollection = DataTraitCollection()
var metadata = DataAsset.Metadata()
if DocumentationContext.isFileExtension(dataURL.pathExtension, supported: .video) {
// In case of a video read its traits: dark/light variants.
let userInterfaceStyle: UserInterfaceStyle = darkSuffixRegex.matches(in: dataReference) ? .dark : .light
// Remove the dark suffix from the image reference.
if userInterfaceStyle == .dark {
dataReference = dataReference.replacingOccurrences(of: darkSuffix, with: "")
}
traitCollection = .init(userInterfaceStyle: userInterfaceStyle, displayScale: nil)
} else if DocumentationContext.isFileExtension(dataURL.pathExtension, supported: .image) {
// Process dark variants.
let userInterfaceStyle: UserInterfaceStyle = darkSuffixRegex.matches(in: dataReference) ? .dark : .light
// Process variants with different scale if a file name modifier is found.
let displayScale = displayScaleRegex.firstMatch(in: dataReference)
.flatMap(DisplayScale.init(rawValue:)) ?? .standard
// Remove traits information from the image reference to store multiple variants.
// Remove the dark suffix from the image reference.
if userInterfaceStyle == .dark {
dataReference = dataReference.replacingOccurrences(of: darkSuffix, with: "")
}
// Remove the display scale information from the image reference.
dataReference = dataReference.replacingOccurrences(of: "@\(displayScale.rawValue)", with: "")
traitCollection = .init(userInterfaceStyle: userInterfaceStyle, displayScale: displayScale)
if dataURL.pathExtension.lowercased() == "svg" {
metadata.svgID = SVGIDExtractor.extractID(from: dataURL)
}
}
return (reference: dataReference, traits: traitCollection, metadata: metadata)
}
/// Registers a collection of data and determines their trait collection.
///
/// Data objects which have a file name ending with '~dark' are associated to their light variant.
mutating func register(data datas: some Collection<URL>) throws {
for dataURL in datas {
let meta = try referenceMetaInformationForDataURL(dataURL)
let referenceURL = URL(fileURLWithPath: meta.reference, isDirectory: false)
// Store the image with given scale information and display scale.
let name = referenceURL.lastPathComponent
storage[name, default: DataAsset()]
.register(dataURL, with: meta.traits, metadata: meta.metadata)
if name.contains(".") {
let nameNoExtension = referenceURL.deletingPathExtension().lastPathComponent
fuzzyKeyIndex[nameNoExtension] = name
}
}
}
mutating func register(dataAsset: DataAsset, forName name: String) {
storage[name] = dataAsset
}
/// Replaces an existing asset with a new one.
mutating func update(name: String, asset: DataAsset) {
bestKey(forAssetName: name).flatMap({ storage[$0] = asset })
}
}
/// A container for a collection of data that represent multiple ways to describe a single asset.
///
/// Assets can be media files, source code files, or files for download.
/// A ``DataAsset`` instance represents one bundle asset, which might be represented by multiple files. For example, a single image
/// asset might have a light and dark variants, and 1x, 2x, and 3x image sizes.
///
/// Each variant of an asset is identified by a ``DataTraitCollection`` and represents the best asset file for the given
/// combination of traits, e.g. a 2x scale image when rendered for Dark Mode.
///
/// ## Topics
///
/// ### Asset Traits
/// - ``DisplayScale``
/// - ``UserInterfaceStyle``
public struct DataAsset: Codable, Equatable {
/// A context in which you intend clients to use a data asset.
public enum Context: String, CaseIterable, Codable {
/// An asset that a user intends to view alongside documentation content.
case display
/// An asset that a user intends to download.
case download
}
/// The variants associated with the resource.
///
/// An asset can have multiple variants which you can use in different environments.
/// For example, an image asset can have distinct light and dark variants, so a renderer can select the appropriate variant
/// depending on the system's appearance.
public var variants = [DataTraitCollection: URL]()
/// The metadata associated with each variant.
public var metadata = [URL : Metadata]()
/// The context in which you intend to use the data asset.
public var context = Context.display
/// Creates an empty asset.
public init() {}
init(
variants: [DataTraitCollection : URL] = [DataTraitCollection: URL](),
metadata: [URL : DataAsset.Metadata] = [URL : Metadata](),
context: DataAsset.Context = Context.display
) {
self.variants = variants
self.metadata = metadata
self.context = context
}
/// Registers a variant of the asset.
/// - Parameters:
/// - url: The location of the variant.
/// - traitCollection: The trait collection associated with the variant.
/// - metadata: Metadata specific to this variant of the asset.
public mutating func register(_ url: URL, with traitCollection: DataTraitCollection, metadata: Metadata = Metadata()) {
variants[traitCollection] = url
self.metadata[url] = metadata
}
/// Returns the data that is registered to the data asset that best matches the given trait collection.
///
/// If no variant with the exact given trait collection is found, the variant that has the largest trait collection overlap with the
/// provided one is returned.
public func data(bestMatching traitCollection: DataTraitCollection) -> BundleData {
guard let variant = variants[traitCollection] else {
// FIXME: If we can't find a variant that matches the given trait collection exactly,
// we should return the variant that has the largest trait collection overlap with the
// provided one. (rdar://68632024)
let first = variants.first!
return BundleData(url: first.value, traitCollection: first.key)
}
return BundleData(url: variant, traitCollection: traitCollection)
}
}
extension DataAsset {
/// Metadata specific to this data asset.
public struct Metadata: Codable, Equatable {
/// The first ID found in the SVG asset.
///
/// This value is nil if the data asset is not an SVG or if it is an SVG that does not contain an ID.
public var svgID: String?
/// Create a new data asset metadata with the given SVG ID.
public init(svgID: String? = nil) {
self.svgID = svgID
}
}
}
/// A collection of environment traits for an asset variant.
///
/// Traits describe properties of a rendering environment, such as a user-interface style (light or dark mode) and display-scale
/// (1x, 2x, or 3x). A trait collection is a combination of traits and describes the rendering environment in which an asset variant is best
/// suited for, e.g., an environment that uses the dark mode user-interface style and a display-scale of 3x.
public struct DataTraitCollection: Hashable, Codable {
/// The style associated with the user-interface.
public var userInterfaceStyle: UserInterfaceStyle?
/// The display-scale of the trait collection.
public var displayScale: DisplayScale?
/// Creates a new trait collection with traits set to their default, unspecified, values.
public init() {
self.userInterfaceStyle = nil
self.displayScale = nil
}
/// Returns a new trait collection consisting of traits merged from a specified array of trait collections.
public init(traitsFrom traitCollections: [DataTraitCollection]) {
for trait in traitCollections {
userInterfaceStyle = trait.userInterfaceStyle ?? userInterfaceStyle
displayScale = trait.displayScale ?? displayScale
}
}
/// Creates a trait collection from an array of raw values.
public init(from rawValues: [String]) {
for value in rawValues {
if let validUserInterfaceStyle = UserInterfaceStyle(rawValue: value) {
userInterfaceStyle = validUserInterfaceStyle
} else if let validDisplayScale = DisplayScale(rawValue: value) {
displayScale = validDisplayScale
}
}
}
/// Creates a trait collection that contains only the specified user-interface style and display-scale traits.
public init(userInterfaceStyle: UserInterfaceStyle? = nil, displayScale: DisplayScale? = nil) {
self.userInterfaceStyle = userInterfaceStyle
self.displayScale = displayScale
}
/// Returns an array of raw values associated with the trait collection.
public func toArray() -> [String] {
var result = [String]()
result.append((displayScale ?? .standard).rawValue)
if let rawUserInterfaceStyle = userInterfaceStyle?.rawValue {
result.append(rawUserInterfaceStyle)
}
return result
}
/// Returns all the asset's registered variants.
public static var allCases: [DataTraitCollection] = {
return UserInterfaceStyle.allCases.flatMap { style in DisplayScale.allCases.map { .init(userInterfaceStyle: style, displayScale: $0)}}
}()
}
/// The interface style for a rendering context.
public enum UserInterfaceStyle: String, CaseIterable, Codable {
/// The light interface style.
case light = "light"
/// The dark interface style.
case dark = "dark"
}
/// The display-scale factor of a rendering environment.
///
/// ## See Also
/// - [Image size and resolution](https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/)
/// - [`UIScreen.scale` documentation](https://developer.apple.com/documentation/uikit/uiscreen/1617836-scale)
public enum DisplayScale: String, CaseIterable, Codable {
/// The 1x scale factor.
case standard = "1x"
/// The 2x scale factor.
case double = "2x"
/// The 3x scale factor.
case triple = "3x"
/// The scale factor as an integer.
var scaleFactor: Int {
switch self {
case .standard:
return 1
case .double:
return 2
case .triple:
return 3
}
}
}
fileprivate extension NSRegularExpression {
/// Returns a boolean indicating if a match has been found in the given string.
func matches(in string: String) -> Bool {
return firstMatch(in: string) != nil
}
/// Returns a substring containing the first match found in a given string.
func firstMatch(in string: String) -> String? {
guard let match = firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) else {
return nil
}
return (string as NSString).substring(with: match.range)
}
}
/// A reference to an asset.
public struct AssetReference: Hashable, Codable {
/// The name of the asset.
public var assetName: String
@available(*, deprecated, renamed: "bundleID", message: "Use 'bundleID' instead. This deprecated API will be removed after 6.2 is released")
public var bundleIdentifier: String {
bundleID.rawValue
}
/// The identifier of the bundle the asset is apart of.
public let bundleID: DocumentationBundle.Identifier
/// Creates a reference from a given asset name and the bundle it is apart of.
public init(assetName: String, bundleID: DocumentationBundle.Identifier) {
self.assetName = assetName
self.bundleID = bundleID
}
@available(*, deprecated, renamed: "init(assetName:bundleID:)", message: "Use 'init(assetName:bundleID:)' instead. This deprecated API will be removed after 6.2 is released")
public init(assetName: String, bundleIdentifier: String) {
self.init(
assetName: assetName,
bundleID: .init(rawValue: bundleIdentifier)
)
}
}