Skip to content

Commit 7fe1b5e

Browse files
ethan-kustersDobromir Hristov
andauthored
Support deviceFrame: parameter in @Image and @Video directives (#462)
When developing UI libraries or apps, it can be useful to show previews of how things look when rendered on a device, like a phone or a tablet. This new experimental feature is designed to allow authors to wrap images and videos in a frame, making it easier to create complex assets and maintain them overtime. Both `@Image` and `@Video` gain a `deviceFrame` parameter that associates them with a device frame defined in the DocC catalog’s `theme-settings.json` file. This is an experimental feature – to use device frames, you must past the following flag to your `docc` invocation: --enable-experimental-device-frame-support This change is discussed on the Swift forums here: https://forums.swift.org/t/support-for-device-frames/62151 rdar://102183073 Co-authored-by: Dobromir Hristov <[email protected]>
1 parent 32e065d commit 7fe1b5e

File tree

16 files changed

+346
-30
lines changed

16 files changed

+346
-30
lines changed

Sources/SwiftDocC/Model/Rendering/Content/RenderContentMetadata.swift

Lines changed: 3 additions & 1 deletion
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-2023 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
@@ -18,6 +18,8 @@ public struct RenderContentMetadata: Equatable, Codable {
1818
public var title: String?
1919
/// An optional custom abstract.
2020
public var abstract: [RenderInlineContent]?
21+
/// An optional identifier for the device frame that should wrap this element.
22+
public var deviceFrame: String?
2123
}
2224

2325
extension RenderContentMetadata {

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 7 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) 2021-2022 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2023 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
@@ -85,22 +85,24 @@ struct RenderContentCompiler: MarkupVisitor {
8585
return visitImage(
8686
source: image.source ?? "",
8787
altText: image.altText,
88-
caption: nil
88+
caption: nil,
89+
deviceFrame: nil
8990
)
9091
}
9192

9293
mutating func visitImage(
9394
source: String,
9495
altText: String?,
95-
caption: [RenderInlineContent]?
96+
caption: [RenderInlineContent]?,
97+
deviceFrame: String?
9698
) -> [RenderContent] {
9799
guard let imageIdentifier = resolveImage(source: source, altText: altText) else {
98100
return []
99101
}
100102

101103
var metadata: RenderContentMetadata?
102-
if let caption = caption {
103-
metadata = RenderContentMetadata(abstract: caption)
104+
if caption != nil || deviceFrame != nil {
105+
metadata = RenderContentMetadata(abstract: caption, deviceFrame: deviceFrame)
104106
}
105107

106108
return [RenderInlineContent.image(identifier: imageIdentifier, metadata: metadata)]

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveArgumentWrapper.swift

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ protocol _DirectiveArgumentProtocol {
1616
var required: Bool { get }
1717
var name: _DirectiveArgumentName { get }
1818
var allowedValues: [String]? { get }
19+
var hiddenFromDocumentation: Bool { get }
1920

2021
var parseArgument: (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Any?) { get }
2122

@@ -63,6 +64,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
6364
let name: _DirectiveArgumentName
6465
let typeDisplayName: String
6566
let allowedValues: [String]?
67+
let hiddenFromDocumentation: Bool
6668

6769
let parseArgument: (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Any?)
6870

@@ -94,7 +96,8 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
9496
name: _DirectiveArgumentName,
9597
transform: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
9698
allowedValues: [String]?,
97-
required: Bool?
99+
required: Bool?,
100+
hiddenFromDocumentation: Bool
98101
) {
99102
self.name = name
100103
self.defaultValue = value
@@ -112,6 +115,7 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
112115

113116
self.parseArgument = transform
114117
self.allowedValues = allowedValues
118+
self.hiddenFromDocumentation = hiddenFromDocumentation
115119
}
116120

117121
@_disfavoredOverload
@@ -120,14 +124,16 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
120124
name: _DirectiveArgumentName = .inferredFromPropertyName,
121125
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
122126
allowedValues: [String]? = nil,
123-
required: Bool? = nil
127+
required: Bool? = nil,
128+
hiddenFromDocumentation: Bool = false
124129
) {
125130
self.init(
126131
value: wrappedValue,
127132
name: name,
128133
transform: parseArgument,
129134
allowedValues: allowedValues,
130-
required: required
135+
required: required,
136+
hiddenFromDocumentation: hiddenFromDocumentation
131137
)
132138
}
133139

@@ -136,14 +142,16 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
136142
name: _DirectiveArgumentName = .inferredFromPropertyName,
137143
parseArgument: @escaping (_ bundle: DocumentationBundle, _ argumentValue: String) -> (Value?),
138144
allowedValues: [String]? = nil,
139-
required: Bool? = nil
145+
required: Bool? = nil,
146+
hiddenFromDocumentation: Bool = false
140147
) {
141148
self.init(
142149
value: nil,
143150
name: name,
144151
transform: parseArgument,
145152
allowedValues: allowedValues,
146-
required: required
153+
required: required,
154+
hiddenFromDocumentation: hiddenFromDocumentation
147155
)
148156
}
149157

@@ -159,20 +167,25 @@ public struct DirectiveArgumentWrapped<Value>: _DirectiveArgumentProtocol {
159167
}
160168

161169
extension DirectiveArgumentWrapped where Value: DirectiveArgumentValueConvertible {
162-
init(name: _DirectiveArgumentName = .inferredFromPropertyName) {
163-
self.init(value: nil, name: name)
170+
init(
171+
name: _DirectiveArgumentName = .inferredFromPropertyName,
172+
hiddenFromDocumentation: Bool = false
173+
) {
174+
self.init(value: nil, name: name, hiddenFromDocumentation: hiddenFromDocumentation)
164175
}
165176

166177
init(
167178
wrappedValue: Value,
168-
name: _DirectiveArgumentName = .inferredFromPropertyName
179+
name: _DirectiveArgumentName = .inferredFromPropertyName,
180+
hiddenFromDocumentation: Bool = false
169181
) {
170-
self.init(value: wrappedValue, name: name)
182+
self.init(value: wrappedValue, name: name, hiddenFromDocumentation: hiddenFromDocumentation)
171183
}
172184

173185
private init(
174186
value: Value?,
175-
name: _DirectiveArgumentName
187+
name: _DirectiveArgumentName,
188+
hiddenFromDocumentation: Bool
176189
) {
177190
self.name = name
178191
self.defaultValue = value
@@ -188,6 +201,7 @@ extension DirectiveArgumentWrapped where Value: DirectiveArgumentValueConvertibl
188201
}
189202
self.allowedValues = Value.allowedValues()
190203
self.required = value == nil
204+
self.hiddenFromDocumentation = hiddenFromDocumentation
191205
}
192206
}
193207

@@ -197,7 +211,8 @@ extension DirectiveArgumentWrapped where Value: OptionallyWrappedDirectiveArgume
197211
init(
198212
wrappedValue: Value,
199213
name: _DirectiveArgumentName = .inferredFromPropertyName,
200-
required: Bool = false
214+
required: Bool = false,
215+
hiddenFromDocumentation: Bool = false
201216
) {
202217
let argumentValueType = Value.baseType() as! DirectiveArgumentValueConvertible.Type
203218

@@ -214,5 +229,6 @@ extension DirectiveArgumentWrapped where Value: OptionallyWrappedDirectiveArgume
214229
}
215230
self.allowedValues = argumentValueType.allowedValues()
216231
self.required = required
232+
self.hiddenFromDocumentation = hiddenFromDocumentation
217233
}
218234
}

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveMirror.swift

Lines changed: 5 additions & 1 deletion
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-2023 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
@@ -157,6 +157,10 @@ extension DirectiveMirror {
157157
}
158158
}
159159

160+
var hiddenFromDocumentation: Bool {
161+
return argument.hiddenFromDocumentation
162+
}
163+
160164
let name: String
161165

162166
let unnamed: Bool

Sources/SwiftDocC/Semantics/Media/ImageMedia.swift

Lines changed: 29 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-2023 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
@@ -28,6 +28,14 @@ public final class ImageMedia: Semantic, Media, AutomaticDirectiveConvertible {
2828
@DirectiveArgumentWrapped(name: .custom("alt"))
2929
public private(set) var altText: String? = nil
3030

31+
32+
/// The name of a device frame that should wrap this image.
33+
///
34+
/// This is an experimental feature – any device frame specified here
35+
/// must be defined in the `theme-settings.json` file of the containing DocC catalog.
36+
@DirectiveArgumentWrapped(hiddenFromDocumentation: true)
37+
public private(set) var deviceFrame: String? = nil
38+
3139
/// An optional caption that should be rendered alongside the image.
3240
@ChildMarkup(numberOfParagraphs: .zeroOrOne)
3341
public private(set) var caption: MarkupContainer
@@ -36,6 +44,7 @@ public final class ImageMedia: Semantic, Media, AutomaticDirectiveConvertible {
3644
"altText" : \ImageMedia._altText,
3745
"source" : \ImageMedia._source,
3846
"caption" : \ImageMedia._caption,
47+
"deviceFrame" : \ImageMedia._deviceFrame,
3948
]
4049

4150
/// Creates a new image with the given parameters.
@@ -51,6 +60,23 @@ public final class ImageMedia: Semantic, Media, AutomaticDirectiveConvertible {
5160
self.source = source
5261
}
5362

63+
func validate(source: URL?, for bundle: DocumentationBundle, in context: DocumentationContext, problems: inout [Problem]) -> Bool {
64+
if !FeatureFlags.current.isExperimentalDeviceFrameSupportEnabled && deviceFrame != nil {
65+
let diagnostic = Diagnostic(
66+
source: source,
67+
severity: .warning, range: originalMarkup.range,
68+
identifier: "org.swift.docc.UnknownArgument",
69+
summary: "Unknown argument 'deviceFrame' in \(Self.directiveName)."
70+
)
71+
72+
problems.append(.init(diagnostic: diagnostic))
73+
74+
deviceFrame = nil
75+
}
76+
77+
return true
78+
}
79+
5480
@available(*, deprecated, message: "Do not call directly. Required for 'AutomaticDirectiveConvertible'.")
5581
init(originalMarkup: BlockDirective) {
5682
self.originalMarkup = originalMarkup
@@ -74,7 +100,8 @@ extension ImageMedia: RenderableDirectiveConvertible {
74100
guard let renderedImage = contentCompiler.visitImage(
75101
source: source.path,
76102
altText: altText,
77-
caption: renderedCaption
103+
caption: renderedCaption,
104+
deviceFrame: deviceFrame
78105
).first as? RenderInlineContent else {
79106
return []
80107
}

Sources/SwiftDocC/Semantics/Media/VideoMedia.swift

Lines changed: 28 additions & 3 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-2023 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
@@ -28,6 +28,13 @@ public final class VideoMedia: Semantic, Media, AutomaticDirectiveConvertible {
2828
@DirectiveArgumentWrapped(name: .custom("alt"))
2929
public private(set) var altText: String? = nil
3030

31+
/// The name of a device frame that should wrap this video.
32+
///
33+
/// This is an experimental feature – any device frame specified here
34+
/// must be defined in the `theme-settings.json` file of the containing DocC catalog.
35+
@DirectiveArgumentWrapped(hiddenFromDocumentation: true)
36+
public private(set) var deviceFrame: String? = nil
37+
3138
/// An optional caption that should be rendered alongside the video.
3239
@ChildMarkup(numberOfParagraphs: .zeroOrOne)
3340
public private(set) var caption: MarkupContainer
@@ -45,6 +52,7 @@ public final class VideoMedia: Semantic, Media, AutomaticDirectiveConvertible {
4552
"poster" : \VideoMedia._poster,
4653
"caption" : \VideoMedia._caption,
4754
"altText" : \VideoMedia._altText,
55+
"deviceFrame" : \VideoMedia._deviceFrame,
4856
]
4957

5058
init(originalMarkup: BlockDirective, source: ResourceReference, poster: ResourceReference?) {
@@ -54,6 +62,23 @@ public final class VideoMedia: Semantic, Media, AutomaticDirectiveConvertible {
5462
self.source = source
5563
}
5664

65+
func validate(source: URL?, for bundle: DocumentationBundle, in context: DocumentationContext, problems: inout [Problem]) -> Bool {
66+
if !FeatureFlags.current.isExperimentalDeviceFrameSupportEnabled && deviceFrame != nil {
67+
let diagnostic = Diagnostic(
68+
source: source,
69+
severity: .warning, range: originalMarkup.range,
70+
identifier: "org.swift.docc.UnknownArgument",
71+
summary: "Unknown argument 'deviceFrame' in \(Self.directiveName)."
72+
)
73+
74+
problems.append(.init(diagnostic: diagnostic))
75+
76+
deviceFrame = nil
77+
}
78+
79+
return true
80+
}
81+
5782
@available(*, deprecated, message: "Do not call directly. Required for 'AutomaticDirectiveConvertible'.")
5883
init(originalMarkup: BlockDirective) {
5984
self.originalMarkup = originalMarkup
@@ -97,8 +122,8 @@ extension VideoMedia: RenderableDirectiveConvertible {
97122
)
98123

99124
var metadata: RenderContentMetadata?
100-
if let renderedCaption = renderedCaption {
101-
metadata = RenderContentMetadata(abstract: renderedCaption)
125+
if renderedCaption != nil || deviceFrame != nil {
126+
metadata = RenderContentMetadata(abstract: renderedCaption, deviceFrame: deviceFrame)
102127
}
103128

104129
let video = RenderBlockContent.Video(

Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,9 @@
10281028
"items": {
10291029
"$ref": "#/components/schemas/RenderInlineContent"
10301030
}
1031+
},
1032+
"deviceFrame": {
1033+
"type": "string"
10311034
}
10321035
}
10331036
}

Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,9 @@
492492
"items": {
493493
"$ref": "#/components/schemas/RenderInlineContent"
494494
}
495+
},
496+
"deviceFrame": {
497+
"type": "string"
495498
}
496499
}
497500
},

0 commit comments

Comments
 (0)