Skip to content

Commit 1402af3

Browse files
authored
Adopt Plot's new Component-based API (#111)
- Bump Plot to version `0.9.0`. - Publish now ships with a few implementations of Plot's new `Component` protocol - specifically `Markdown` (for rendering Markdown inline within a component hierarchy), `VideoPlayer` (for rendering an inline video player), and an extension that makes it possible to directly use Plot's `AudioPlayer` component with Publish's `Audio` model. - The `Content.Body` type now also conforms to `Component`, which makes it possible to place such instances directly within a component hierarchy. That type has now also been fully documented, since it was previously missing documentation for some of its properties and initializers. - The built-in Foundation theme as been rewritten using the new component API. While it remains functionally identical to the previous implementation, it should act as a nice example of how this new API can be used. - Because Publish now ships with a type called `Markdown`, it's possible that some API users might need to disambiguate between this new type and Ink's `Markdown` type. However, that tradeoff was considered worth it, since using the new `Markdown` component will likely be a much more common use case.
1 parent 9490cc8 commit 1402af3

9 files changed

+317
-171
lines changed

Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"repositoryURL": "https://github.com/johnsundell/plot.git",
3434
"state": {
3535
"branch": null,
36-
"revision": "61e828949ca6f84071bde65c8fc046cf74b7d1a2",
37-
"version": "0.8.0"
36+
"revision": "80612b34252188edbef280e5375e2fc5249ac770",
37+
"version": "0.9.0"
3838
}
3939
},
4040
{

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ let package = Package(
1616
],
1717
dependencies: [
1818
.package(name: "Ink", url: "https://github.com/johnsundell/ink.git", from: "0.2.0"),
19-
.package(name: "Plot", url: "https://github.com/johnsundell/plot.git", from: "0.4.0"),
19+
.package(name: "Plot", url: "https://github.com/johnsundell/plot.git", from: "0.9.0"),
2020
.package(name: "Files", url: "https://github.com/johnsundell/files.git", from: "4.0.0"),
2121
.package(name: "Codextended", url: "https://github.com/johnsundell/codextended.git", from: "0.1.0"),
2222
.package(name: "ShellOut", url: "https://github.com/johnsundell/shellout.git", from: "2.3.0"),

Sources/Publish/API/Content.swift

+22
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,37 @@ public struct Content: Hashable, ContentProtocol {
4747
}
4848

4949
public extension Content {
50+
/// Type that represents the main renderable body of a piece of content.
5051
struct Body: Hashable {
52+
/// The content's renderable HTML.
5153
public var html: String
54+
/// A node that can be used to embed the content in a Plot hierarchy.
5255
public var node: Node<HTML.BodyContext> { .raw(html) }
56+
/// Whether this value doesn't contain any content.
5357
public var isEmpty: Bool { html.isEmpty }
5458

59+
/// Initialize an instance with a ready-made HTML string.
60+
/// - parameter html: The content HTML that the instance should cointain.
5561
public init(html: String) {
5662
self.html = html
5763
}
5864

65+
/// Initialize an instance with a Plot `Node`.
66+
/// - parameter node: The node to render. See `Node` for more information.
67+
/// - parameter indentation: Any indentation to apply when rendering the node.
5968
public init(node: Node<HTML.BodyContext>,
6069
indentation: Indentation.Kind? = nil) {
6170
html = node.render(indentedBy: indentation)
6271
}
72+
73+
/// Initialize an instance using Plot's `Component` API.
74+
/// - parameter indentation: Any indentation to apply when rendering the components.
75+
/// - parameter components: The components that should make up this instance's content.
76+
public init(indentation: Indentation.Kind? = nil,
77+
@ComponentBuilder components: () -> Component) {
78+
self.init(node: .component(components()),
79+
indentation: indentation)
80+
}
6381
}
6482
}
6583

@@ -68,3 +86,7 @@ extension Content.Body: ExpressibleByStringInterpolation {
6886
self.init(html: value)
6987
}
7088
}
89+
90+
extension Content.Body: Component {
91+
public var body: Component { node }
92+
}

Sources/Publish/API/PlotComponents.swift

+100-27
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import Plot
99
import Ink
1010
import Sweep
1111

12+
// MARK: - Nodes and Attributes
13+
1214
public extension Node where Context == HTML.DocumentContext {
1315
/// Add an HTML `<head>` tag within the current context, based
1416
/// on inferred information from the current location and `Website`
@@ -97,35 +99,28 @@ public extension Node where Context: HTML.BodyContext {
9799
}
98100

99101
/// Add an inline audio player within the current context.
100-
/// - Parameter audio: The audio to add a player for.
101-
/// - Parameter showControls: Whether playback controls should be shown to the user.
102+
/// - parameter audio: The audio to add a player for.
103+
/// - parameter showControls: Whether playback controls should be shown to the user.
102104
static func audioPlayer(for audio: Audio,
103105
showControls: Bool = true) -> Node {
104-
return .audio(
105-
.controls(showControls),
106-
.source(.type(audio.format), .src(audio.url))
106+
AudioPlayer(
107+
audio: audio,
108+
showControls: showControls
107109
)
110+
.convertToNode()
108111
}
109112

110113
/// Add an inline video player within the current context.
111-
/// - Parameter video: The video to add a player for.
112-
/// - Parameter showControls: Whether playback controls should be shown to the user.
114+
/// - parameter video: The video to add a player for.
115+
/// - parameter showControls: Whether playback controls should be shown to the user.
113116
/// Note that this parameter is only relevant for hosted videos.
114117
static func videoPlayer(for video: Video,
115118
showControls: Bool = true) -> Node {
116-
switch video {
117-
case .hosted(let url, let format):
118-
return .video(
119-
.controls(showControls),
120-
.source(.type(format), .src(url))
121-
)
122-
case .youTube(let id):
123-
let url = "https://www.youtube-nocookie.com/embed/" + id
124-
return .iframeVideoPlayer(forURL: url)
125-
case .vimeo(let id):
126-
let url = "https://player.vimeo.com/video/" + id
127-
return .iframeVideoPlayer(forURL: url)
128-
}
119+
VideoPlayer(
120+
video: video,
121+
showControls: showControls
122+
)
123+
.convertToNode()
129124
}
130125
}
131126

@@ -201,13 +196,91 @@ internal extension Node where Context == PodcastFeed.ItemContext {
201196
}
202197
}
203198

204-
private extension Node where Context: HTML.BodyContext {
205-
static func iframeVideoPlayer(forURL url: String) -> Node {
206-
return .iframe(
207-
.frameborder(false),
208-
.allow("accelerometer", "encrypted-media", "gyroscope", "picture-in-picture"),
209-
.allowfullscreen(true),
210-
.src(url)
199+
// MARK: - Extensions to Plot's built-in components
200+
201+
public extension AudioPlayer {
202+
/// Create an inline player for an `Audio` model.
203+
/// - parameter audio: The audio to create a player for.
204+
/// - parameter showControls: Whether playback controls should be shown to the user.
205+
init(audio: Audio, showControls: Bool = true) {
206+
self.init(
207+
source: Source(url: audio.url, format: audio.format),
208+
showControls: showControls
209+
)
210+
}
211+
}
212+
213+
// MARK: - New Component implementations
214+
215+
/// Component that can be used to parse a Markdown string into HTML
216+
/// that's then rendered as the body of the component.
217+
///
218+
/// You can control what `MarkdownParser` that's used for parsing
219+
/// using the `markdownParser` environment key, or by applying the
220+
/// `markdownParser` modifier to a component.
221+
public struct Markdown: Component {
222+
/// The Markdown string to render.
223+
public var string: String
224+
225+
@EnvironmentValue(.markdownParser) private var parser
226+
227+
/// Initialize an instance of this component with a Markdown string.
228+
/// - parameter string: The Markdown string to render.
229+
public init(_ string: String) {
230+
self.string = string
231+
}
232+
233+
public var body: Component {
234+
Node.markdown(string, using: parser)
235+
}
236+
}
237+
238+
/// Component that can be used to render an inline video player, using either
239+
/// the `<video>` element (for hosted videos), or by embedding either a YouTube
240+
/// or Vimeo player using an `<iframe>`.
241+
public struct VideoPlayer: Component {
242+
/// The video to create a player for.
243+
public var video: Video
244+
/// Whether playback controls should be shown to the user. Note that this
245+
/// property is ignored when rendering a video hosted by a service like YouTube.
246+
public var showControls: Bool
247+
248+
/// Create an inline player for a `Video` model.
249+
/// - parameter video: The video to create a player for.
250+
/// - parameter showControls: Whether playback controls should be shown to the user.
251+
/// Note that this parameter is only relevant for hosted videos.
252+
public init(video: Video, showControls: Bool = true) {
253+
self.video = video
254+
self.showControls = showControls
255+
}
256+
257+
public var body: Component {
258+
switch video {
259+
case .hosted(let url, let format):
260+
return Node.video(
261+
.controls(showControls),
262+
.source(.type(format), .src(url))
263+
)
264+
case .youTube(let id):
265+
let url = "https://www.youtube-nocookie.com/embed/" + id
266+
return iframeVideoPlayer(for: url)
267+
case .vimeo(let id):
268+
let url = "https://player.vimeo.com/video/" + id
269+
return iframeVideoPlayer(for: url)
270+
}
271+
}
272+
273+
private func iframeVideoPlayer(for url: URLRepresentable) -> Component {
274+
IFrame(
275+
url: url,
276+
addBorder: false,
277+
allowFullScreen: true,
278+
enabledFeatureNames: [
279+
"accelerometer",
280+
"encrypted-media",
281+
"gyroscope",
282+
"picture-in-picture"
283+
]
211284
)
212285
}
213286
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Plot
2+
import Ink
3+
4+
public extension EnvironmentKey where Value == MarkdownParser {
5+
/// Environment key that can be used to pass what `MarkdownParser` that
6+
/// should be used when rendering `Markdown` components.
7+
static var markdownParser: Self { .init(defaultValue: .init()) }
8+
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Ink
2+
import Plot
3+
4+
public extension Component {
5+
/// Assign what `MarkdownParser` to use when rendering `Markdown` components
6+
/// within this component hierarchy. This value is placed in the environment,
7+
/// and is thus inherited by all child components. Note that this modifier
8+
/// does not affect nodes rendered using the `.markdown` API.
9+
/// - parameter parser: The parser to assign.
10+
func markdownParser(_ parser: MarkdownParser) -> Component {
11+
environmentValue(parser, key: .markdownParser)
12+
}
13+
}

0 commit comments

Comments
 (0)