Skip to content

Commit

Permalink
Adopt Plot's new Component-based API (#111)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
JohnSundell committed May 11, 2021
1 parent 9490cc8 commit 1402af3
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 171 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
"repositoryURL": "https://github.com/johnsundell/plot.git",
"state": {
"branch": null,
"revision": "61e828949ca6f84071bde65c8fc046cf74b7d1a2",
"version": "0.8.0"
"revision": "80612b34252188edbef280e5375e2fc5249ac770",
"version": "0.9.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let package = Package(
],
dependencies: [
.package(name: "Ink", url: "https://github.com/johnsundell/ink.git", from: "0.2.0"),
.package(name: "Plot", url: "https://github.com/johnsundell/plot.git", from: "0.4.0"),
.package(name: "Plot", url: "https://github.com/johnsundell/plot.git", from: "0.9.0"),
.package(name: "Files", url: "https://github.com/johnsundell/files.git", from: "4.0.0"),
.package(name: "Codextended", url: "https://github.com/johnsundell/codextended.git", from: "0.1.0"),
.package(name: "ShellOut", url: "https://github.com/johnsundell/shellout.git", from: "2.3.0"),
Expand Down
22 changes: 22 additions & 0 deletions Sources/Publish/API/Content.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,37 @@ public struct Content: Hashable, ContentProtocol {
}

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

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

/// Initialize an instance with a Plot `Node`.
/// - parameter node: The node to render. See `Node` for more information.
/// - parameter indentation: Any indentation to apply when rendering the node.
public init(node: Node<HTML.BodyContext>,
indentation: Indentation.Kind? = nil) {
html = node.render(indentedBy: indentation)
}

/// Initialize an instance using Plot's `Component` API.
/// - parameter indentation: Any indentation to apply when rendering the components.
/// - parameter components: The components that should make up this instance's content.
public init(indentation: Indentation.Kind? = nil,
@ComponentBuilder components: () -> Component) {
self.init(node: .component(components()),
indentation: indentation)
}
}
}

Expand All @@ -68,3 +86,7 @@ extension Content.Body: ExpressibleByStringInterpolation {
self.init(html: value)
}
}

extension Content.Body: Component {
public var body: Component { node }
}
127 changes: 100 additions & 27 deletions Sources/Publish/API/PlotComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Plot
import Ink
import Sweep

// MARK: - Nodes and Attributes

public extension Node where Context == HTML.DocumentContext {
/// Add an HTML `<head>` tag within the current context, based
/// on inferred information from the current location and `Website`
Expand Down Expand Up @@ -97,35 +99,28 @@ public extension Node where Context: HTML.BodyContext {
}

/// Add an inline audio player within the current context.
/// - Parameter audio: The audio to add a player for.
/// - Parameter showControls: Whether playback controls should be shown to the user.
/// - parameter audio: The audio to add a player for.
/// - parameter showControls: Whether playback controls should be shown to the user.
static func audioPlayer(for audio: Audio,
showControls: Bool = true) -> Node {
return .audio(
.controls(showControls),
.source(.type(audio.format), .src(audio.url))
AudioPlayer(
audio: audio,
showControls: showControls
)
.convertToNode()
}

/// Add an inline video player within the current context.
/// - Parameter video: The video to add a player for.
/// - Parameter showControls: Whether playback controls should be shown to the user.
/// - parameter video: The video to add a player for.
/// - parameter showControls: Whether playback controls should be shown to the user.
/// Note that this parameter is only relevant for hosted videos.
static func videoPlayer(for video: Video,
showControls: Bool = true) -> Node {
switch video {
case .hosted(let url, let format):
return .video(
.controls(showControls),
.source(.type(format), .src(url))
)
case .youTube(let id):
let url = "https://www.youtube-nocookie.com/embed/" + id
return .iframeVideoPlayer(forURL: url)
case .vimeo(let id):
let url = "https://player.vimeo.com/video/" + id
return .iframeVideoPlayer(forURL: url)
}
VideoPlayer(
video: video,
showControls: showControls
)
.convertToNode()
}
}

Expand Down Expand Up @@ -201,13 +196,91 @@ internal extension Node where Context == PodcastFeed.ItemContext {
}
}

private extension Node where Context: HTML.BodyContext {
static func iframeVideoPlayer(forURL url: String) -> Node {
return .iframe(
.frameborder(false),
.allow("accelerometer", "encrypted-media", "gyroscope", "picture-in-picture"),
.allowfullscreen(true),
.src(url)
// MARK: - Extensions to Plot's built-in components

public extension AudioPlayer {
/// Create an inline player for an `Audio` model.
/// - parameter audio: The audio to create a player for.
/// - parameter showControls: Whether playback controls should be shown to the user.
init(audio: Audio, showControls: Bool = true) {
self.init(
source: Source(url: audio.url, format: audio.format),
showControls: showControls
)
}
}

// MARK: - New Component implementations

/// Component that can be used to parse a Markdown string into HTML
/// that's then rendered as the body of the component.
///
/// You can control what `MarkdownParser` that's used for parsing
/// using the `markdownParser` environment key, or by applying the
/// `markdownParser` modifier to a component.
public struct Markdown: Component {
/// The Markdown string to render.
public var string: String

@EnvironmentValue(.markdownParser) private var parser

/// Initialize an instance of this component with a Markdown string.
/// - parameter string: The Markdown string to render.
public init(_ string: String) {
self.string = string
}

public var body: Component {
Node.markdown(string, using: parser)
}
}

/// Component that can be used to render an inline video player, using either
/// the `<video>` element (for hosted videos), or by embedding either a YouTube
/// or Vimeo player using an `<iframe>`.
public struct VideoPlayer: Component {
/// The video to create a player for.
public var video: Video
/// Whether playback controls should be shown to the user. Note that this
/// property is ignored when rendering a video hosted by a service like YouTube.
public var showControls: Bool

/// Create an inline player for a `Video` model.
/// - parameter video: The video to create a player for.
/// - parameter showControls: Whether playback controls should be shown to the user.
/// Note that this parameter is only relevant for hosted videos.
public init(video: Video, showControls: Bool = true) {
self.video = video
self.showControls = showControls
}

public var body: Component {
switch video {
case .hosted(let url, let format):
return Node.video(
.controls(showControls),
.source(.type(format), .src(url))
)
case .youTube(let id):
let url = "https://www.youtube-nocookie.com/embed/" + id
return iframeVideoPlayer(for: url)
case .vimeo(let id):
let url = "https://player.vimeo.com/video/" + id
return iframeVideoPlayer(for: url)
}
}

private func iframeVideoPlayer(for url: URLRepresentable) -> Component {
IFrame(
url: url,
addBorder: false,
allowFullScreen: true,
enabledFeatureNames: [
"accelerometer",
"encrypted-media",
"gyroscope",
"picture-in-picture"
]
)
}
}
8 changes: 8 additions & 0 deletions Sources/Publish/API/PlotEnvironmentKeys.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Plot
import Ink

public extension EnvironmentKey where Value == MarkdownParser {
/// Environment key that can be used to pass what `MarkdownParser` that
/// should be used when rendering `Markdown` components.
static var markdownParser: Self { .init(defaultValue: .init()) }
}
13 changes: 13 additions & 0 deletions Sources/Publish/API/PlotModifiers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Ink
import Plot

public extension Component {
/// Assign what `MarkdownParser` to use when rendering `Markdown` components
/// within this component hierarchy. This value is placed in the environment,
/// and is thus inherited by all child components. Note that this modifier
/// does not affect nodes rendered using the `.markdown` API.
/// - parameter parser: The parser to assign.
func markdownParser(_ parser: MarkdownParser) -> Component {
environmentValue(parser, key: .markdownParser)
}
}

0 comments on commit 1402af3

Please sign in to comment.