Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnSundell authored Mar 31, 2023
2 parents 723d75b + d8fa91a commit 9f93057
Show file tree
Hide file tree
Showing 64 changed files with 3,256 additions and 628 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.2
// swift-tools-version:5.4

/**
* Plot
Expand Down
227 changes: 200 additions & 27 deletions README.md

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions Sources/Plot/API/Attribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,23 @@ public struct Attribute<Context> {
public var name: String
/// The attribute's value
public var value: String?
/// Whether the attribute should be completely ignored if it has no value
/// Whether the attribute's value should replace any existing one that has
/// already been added to a given element for the same attribute name.
public var replaceExisting: Bool
/// Whether the attribute should be completely ignored if it has no value.
public var ignoreIfValueIsEmpty: Bool

/// Create a new `Attribute` instance with a name and a value, and optionally
/// opt out of ignoring the attribute if its value is empty.
/// opt out of ignoring the attribute if its value is empty, and decide whether the
/// attribute should replace any existing one that's already been added to an element
/// for the same name.
public init(name: String,
value: String?,
replaceExisting: Bool = true,
ignoreIfValueIsEmpty: Bool = true) {
self.name = name
self.value = value
self.replaceExisting = replaceExisting
self.ignoreIfValueIsEmpty = ignoreIfValueIsEmpty
}
}
Expand All @@ -51,23 +58,16 @@ internal extension Attribute where Context == Any {
}
}

internal protocol AnyAttribute {
var name: String { get }
func render() -> String
extension Attribute: NodeConvertible {
public var node: Node<Context> { .attribute(self) }
}

extension Attribute: AnyAttribute {
func render() -> String {
guard let value = value, !value.isEmpty else {
guard let value = nonEmptyValue else {
return ignoreIfValueIsEmpty ? "" : name
}

return "\(name)=\"\(value)\""
}
}

extension Attribute: NodeConvertible {
func asNode() -> AnyNode {
Node<Context>.attribute(self)
}
}
144 changes: 144 additions & 0 deletions Sources/Plot/API/Component.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Plot
* Copyright (c) John Sundell 2021
* MIT license, see LICENSE file for details
*/

import Foundation

/// Protocol used to define components that can be rendered into HTML.
///
/// Implement custom types conforming to this protocol to create your own
/// HTML components that can then be rendered using either the built-in
/// component types that Plot ships with, or using the `Node`-based API.
///
/// You can freely mix and match components and nodes when implementing
/// a component, and any component can be converted into a `Node`, either
/// by creating a `.component` node, or by calling `convertToNode()`
/// on a component.
///
/// Modifiers can be applied to components to change attributes like `class`
/// and `id`, and using the `EnvironmentValue` property wrapper and the
/// `EnvironmentKey` type, you can propagate environmental values through
/// a hierarchy of nodes and components.
public protocol Component: Renderable {
/// The underlying component that should be used to render this component.
/// Can either be a `Node`, another `Component`, or a group of components
/// created using the `ComponentGroup` type.
var body: Component { get }
}

public extension Component {
/// A convenience type alias for a closure that creates the contents of a
/// given component. Closures of this type are typically marked with the
/// `@ComponentBuilder` attribute to enable Plot's DSL to be used when
/// implementing them.
typealias ContentProvider = () -> ComponentGroup

/// Add an attribute to the HTML element used to render this component.
/// - parameter name: The name of the attribute to add.
/// - parameter value: The value that the attribute should have.
/// - parameter replaceExisting: Whether any existing attribute with the
/// same name should be replaced by the new attribute. Defaults to `true`,
/// and if set to `false`, this attribute's value will instead be appended
/// to any existing one, separated by a space.
/// - parameter ignoreValueIfEmpty: Whether the attribute should be ignored if
/// its value is `nil` or empty. Defaults to `true`, and if set to `false`,
/// only the attribute's name will be rendered if its value is empty.
func attribute(named name: String,
value: String?,
replaceExisting: Bool = true,
ignoreValueIfEmpty: Bool = true) -> Component {
attribute(Attribute<Any>(
name: name,
value: value,
replaceExisting: replaceExisting,
ignoreIfValueIsEmpty: ignoreValueIfEmpty
))
}

/// Add an attribute to the HTML element used to render this component.
/// - parameter attribute: The attribute to add. See the documentation for
/// the `Attribute` type for more information.
func attribute<T>(_ attribute: Attribute<T>) -> Component {
if let group = self as? ComponentGroup {
return ComponentGroup(members: group.members.map {
$0.attribute(attribute)
})
}

if var modified = self as? ModifiedComponent {
modified.deferredAttributes.append(attribute)
return modified
}

return ModifiedComponent(
base: self,
deferredAttributes: [attribute]
)
}

/// Place a value into the environment used to render this component and any
/// of its child components. An environment value will be passed downwards
/// through a component/node hierarchy until its overridden by another value
/// for the same key.
/// - parameter value: The value to add. Must match the type of the key that
/// it's being added for. This value will override any value that was assigned
/// by a parent component for the same key, or the key's default value.
/// - parameter key: The key to associate the value with. You can either use any
/// of the built-in key definitions that Plot ships with, or define your own.
/// See `EnvironmentKey` for more information.
func environmentValue<T>(_ value: T, key: EnvironmentKey<T>) -> Component {
let override = Environment.Override(key: key, value: value)

if var modified = self as? ModifiedComponent {
modified.environmentOverrides.append(override)
return modified
}

return ModifiedComponent(
base: self,
environmentOverrides: [override]
)
}

/// Convert this component into a `Node`, with either an inferred or explicit
/// context. Use this API when you want to embed a component into a `Node`-based
/// hierarchy. Calling this method is equivalent to creating a `.component` node
/// using this component.
/// - parameter context: The context of the returned node (can typically be
/// inferred by the compiler based on the call site).
func convertToNode<T>(withContext context: T.Type = T.self) -> Node<T> {
.component(self)
}

func render(indentedBy indentationKind: Indentation.Kind?) -> String {
var renderer = Renderer(indentationKind: indentationKind)
renderer.renderComponent(self)
return renderer.result
}
}

internal extension Component {
func wrappedInElement(named wrappingElementName: String) -> Component {
wrapped(using: ElementWrapper(
wrappingElementName: wrappingElementName
))
}

func wrapped(using wrapper: ElementWrapper) -> Component {
guard !(self is EmptyComponent) else {
return self
}

if let group = self as? ComponentGroup {
return ComponentGroup(
members: group.members.map {
$0.wrapped(using: wrapper)
}
)
}

return Node.wrappingComponent(self, using: wrapper)
}
}
99 changes: 99 additions & 0 deletions Sources/Plot/API/ComponentAttributes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Plot
* Copyright (c) John Sundell 2021
* MIT license, see LICENSE file for details
*/

import Foundation

public extension Component {
/// Assign an accessibility label to this component's element, which
/// is used by assistive technologies to get a text representation of it.
/// - parameter label: The label to assign.
func accessibilityLabel(_ label: String) -> Component {
attribute(named: "aria-label", value: label)
}

/// Assign a class name to this component's element. May also be a list
/// of space-separated class names.
/// - parameter className: The class or list of classes to assign.
/// - parameter replaceExisting: Whether the new class name should replace
/// any existing one. Defaults to `false`, which will instead cause the
/// new class name to be appended to any existing one, separated by a space.
func `class`(_ className: String, replaceExisting: Bool = false) -> Component {
attribute(named: "class",
value: className,
replaceExisting: replaceExisting)
}

/// Add a `data-` attribute to this component's element.
/// - parameter name: The name of the attribute to add. The name will be
/// prefixed with `data-`.
/// - parameter value: The attribute's string value.
func data(named name: String, value: String) -> Component {
attribute(named: "data-" + name, value: value)
}

/// Assign an ID attribute to this component's element.
/// - parameter id: The ID to assign.
func id(_ id: String) -> Component {
attribute(named: "id", value: id)
}

/// Assign a directionality to this component's element.
/// - parameter directionality: The directionality to assign.
func directionality(_ directionality: Directionality) -> Component {
attribute(named: "dir", value: directionality.rawValue)
}

/// Assign whether this component hierarchy's `Input` components should have
/// autocomplete turned on or off. This value is placed in the environment, and
/// is thus inherited by all child components. Note that this modifier only
/// affects components, not elements created using the `Node.input` API, or
/// manually created input elements.
/// - parameter isEnabled: Whether autocomplete should be enabled.
func autoComplete(_ isEnabled: Bool) -> Component {
environmentValue(isEnabled, key: .isAutoCompleteEnabled)
}

/// Assign a given `HTMLAnchorRelationship` to all `Link` components within
/// this component hierarchy. Affects the `rel` attribute on the generated
/// `<a>` elements. This value is placed in the environment, and is thus
/// inherited by all child components. Note that this modifier only affects
/// components, not elements created using the `Node.a` API, or manually
/// created anchor elements.
/// - parameter relationship: The relationship to assign.
func linkRelationship(_ relationship: HTMLAnchorRelationship?) -> Component {
environmentValue(relationship, key: .linkRelationship)
}

/// Assign a given `HTMLAnchorTarget` to all `Link` components within this
/// component hierarchy. Affects the `target` attribute on the generated
/// `<a>` elements. This value is placed in the environment, and is thus
/// inherited by all child components. Note that this modifier only affects
/// components, not elements created using the `Node.a` API, or manually
/// created anchor elements.
/// - parameter target: The target to assign.
func linkTarget(_ target: HTMLAnchorTarget?) -> Component {
environmentValue(target, key: .linkTarget)
}

/// Assign a given `HTMLListStyle` to all `List` components within this
/// component hierarchy. You can use this modifier to decide whether lists
/// should be rendered as ordered or unordered, or even use a completely
/// custom style. This value is placed in the environment, and is thus
/// inherited by all child components. Note that this modifier only affects
/// components, not elements created using the `Node.ul` or `Node.ol` APIs,
/// or manually created list elements.
/// - parameter style: The style to assign.
func listStyle(_ style: HTMLListStyle) -> Component {
environmentValue(style, key: .listStyle)
}

/// Assign a given set of inline CSS styles to this component's element.
/// - parameter css: A string containing the CSS code that should be assigned
/// to this component's `style` attribute.
func style(_ css: String) -> Component {
attribute(named: "style", value: css)
}
}
46 changes: 46 additions & 0 deletions Sources/Plot/API/ComponentBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Plot
* Copyright (c) John Sundell 2021
* MIT license, see LICENSE file for details
*/

import Foundation

/// Result builder used to combine all of the `Component` expressions that appear
/// within a given attributed scope into a single `ComponentGroup`.
///
/// You can annotate any function or closure with the `@ComponentBuilder` attribute
/// to have its contents be processed by this builder. Note that you never have to
/// call any of the methods defined within this type directly. Instead, the Swift
/// compiler will automatically map your expressions to calls into this builder type.
@resultBuilder public enum ComponentBuilder {
/// Build a `ComponentGroup` from a list of components.
/// - parameter components: The components that should be included in the group.
public static func buildBlock(_ components: Component...) -> ComponentGroup {
ComponentGroup(members: components)
}

/// Build a flattened `ComponentGroup` from an array of component groups.
/// - parameter groups: The component groups to flatten into a single group.
public static func buildArray(_ groups: [ComponentGroup]) -> ComponentGroup {
ComponentGroup(members: groups.flatMap { $0 })
}

/// Pick the first `ComponentGroup` within a conditional statement.
/// - parameter component: The component to pick.
public static func buildEither(first component: ComponentGroup) -> ComponentGroup {
component
}

/// Pick the second `ComponentGroup` within a conditional statement.
/// - parameter component: The component to pick.
public static func buildEither(second component: ComponentGroup) -> ComponentGroup {
component
}

/// Build a `ComponentGroup` from an optional group.
/// - parameter component: The optional to transform into a concrete group.
public static func buildOptional(_ component: ComponentGroup?) -> ComponentGroup {
component ?? ComponentGroup(members: [])
}
}
46 changes: 46 additions & 0 deletions Sources/Plot/API/ComponentContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Plot
* Copyright (c) John Sundell 2021
* MIT license, see LICENSE file for details
*/

import Foundation

/// Protocol adopted by components that can act as a container for
/// other components. Plot ships with a number of implementations of
/// this protocol (such as `Div`, `List`, `Article`, and so on), and
/// you can easily create your own as well by implementing the required
/// initializer.
public protocol ComponentContainer: Component {
/// Initialize this component with a closure that defines its content.
/// - parameter content: The component content that should be contained
/// within this component.
init(@ComponentBuilder content: @escaping ContentProvider)
}

public extension ComponentContainer {
/// Initialize this component without any content.
init() {
self.init {}
}

/// Initialize this container with a single content component.
/// - parameter component: The component to include as content.
init(_ component: Component) {
self.init { component }
}

/// Initialize this container with a string as its content.
/// - parameter string: The text that this component should contain.
/// Any special characters that can't be rendered as-is will be escaped.
init(_ string: String) {
self.init { Node<Any>.text(string) }
}

/// Initialize this container with a raw HTML string.
/// - parameter html: The HTML that this component should contain.
/// Won't be processed in any way, and will instead be rendered as-is.
init(html: String) {
self.init { Node<Any>.raw(html) }
}
}
Loading

0 comments on commit 9f93057

Please sign in to comment.