Plot

repository·master·Indexed 24 days ago

https://github.com/johnsundell/plot

A domain-specific language (DSL) for writing type-safe HTML, XML, and RSS in Swift. Designed for static site generation and web development, Plot provides compile-time semantic validation to prevent invalid document structures. It features a Node-based API for standard elements and a SwiftUI-like Component protocol for reusable UI elements, supporting HTML 5.0, XML 1.0, RSS 2.0, podcast feeds, and Sitemaps.

Tokens
2.8K
Snippets
6
Records
14
Agent score
35%

What's inside Plot

  1. Understand Plot's type safety and design patterns

    master

    Plot employs several Swift language features to ensure a safe and ergonomic developer experience:

    • Phantom Types: Plot uses types as "markers" for the compiler to enforce type safety through generic constraints. DocumentFormat and the Context of nodes, elements, or attributes are phantom types; they are never instantiated but serve to associate values with a specific context or format.
    • Lightweight API Design: The API minimizes external argument labels to reduce syntax noise, creating a clean, DSL-like experience.
    • SwiftUI-like Component API: The Component API leverages Result Builders and Property Wrappers to allow for a declarative syntax similar to SwiftUI.
  2. Create reusable components with the Component protocol

    master

    The Component protocol allows you to define high-level, reusable UI elements using a SwiftUI-like API. Components are defined by a body property that returns a Component.

    Mixing Nodes and Components

    • Components: Best for building reusable UI libraries. They can use modifiers like .class() or .id().
    • Nodes: The standard HTML element API. You can wrap a Component inside a Node using the .component() method.
    • Inlining: You can directly use Node-based elements (e.g., Node.h2(...)) inside a component's body.

    Note: The Component API is intended for elements within the <body> of an HTML page. For <head> elements or non-HTML documents, use the Node-based API. Components also incur a small performance overhead compared to raw Node elements.

    struct NewsArticle: Component {
        var imagePath: String
        var title: String
        var description: String
    
        var body: Component {
            Article {
                Image(url: imagePath, description: "Header image")
                H1(title)
                Span(description).class("description")
            }
            .class("news")
        }
    }
    
    // Integrating a component into a Node hierarchy
    func newsArticlePage(for article: NewsArticle) -> HTML {
        return HTML(.body(
            .div(
                .class("wrapper"),
                .component(article)
            )
        ))
    }
  3. How Plot's type safety works

    master

    Plot uses Swift's generics and context-bound Node types to enforce HTML semantics at compile time. This prevents invalid HTML structures, such as:

    • Adding an attribute to an element that doesn't support it (e.g., adding .href to a <p> tag).
    • Placing invalid child elements inside a parent (e.g., placing a <p> inside a <ul> instead of an <li>).

    This ensures your documents are semantically correct and provides rich autocomplete in IDEs like Xcode.

  4. Use the Environment API to pass values to components

    master

    Similar to SwiftUI, Plot components can pass values down a hierarchy using an environment API. This is useful for setting shared styles or configurations for all child components.

    1. Define an EnvironmentKey: Extend EnvironmentKey with a static property for your custom key.
    2. Enter a value: Use the .environmentValue(_:key:) modifier on a component to inject a value into the hierarchy.
    3. Retrieve a value: Use the @EnvironmentValue property wrapper within a Component to access the value.

    Plot also provides built-in environment keys like listStyle and linkRelationship for customizing standard components.

    // 1. Define the key
    extension EnvironmentKey where Value == String {
        static var actionButtonClass: Self {
            Self(defaultValue: "action-button")
        }
    }
    
    struct Page: Component {
        var body: Component {
            Div {
                InfoView(title: "...", text: "...")
            }
            // 2. Enter the value
            .environmentValue("action-button-large",
                key: .actionButtonClass
            )
        }
    }
    
    struct ActionButton: Component {
        var title: String
    
        // 3. Retrieve the value
        @EnvironmentValue(.actionButtonClass) var className
    
        var body: Component {
            Button(title).class(className)
        }
    }
  5. How Plot's core architecture works

    master

    Plot is built around four core pillars that compose its Domain Specific Language (DSL) and rendering API:

    • Node: The fundamental building block. It represents elements, attributes, text content, or groups of nodes. Every Node is bound to a Context type (e.g., HTML.BodyContext), which dictates which DSL APIs are available for that specific location in the document.
    • Element: Represents a specific HTML/XML element. Elements can be paired (open and close tags like <body></body>) or self-closing (like <img/>). While you can interact with this type directly, it is typically accessed via the DSL.
    • Attribute: Represents an attribute attached to an element (e.g., href or src). These are created via the .attribute() command in the DSL or through its initializer.
    • Component: A protocol used to define reusable UI pieces in a SwiftUI-like manner. Components must implement a body property that returns rendered output using either other components or Node-based elements.
    • Document and DocumentFormat: The entry points for document generation. These represent high-level formats like HTML, RSS, or PodcastFeed and are used to initiate a document building session.
  6. Use inline control flow in Plot

    master

    Plot provides mechanisms for conditional logic and iteration, which differ depending on whether you are using the Node-based API or the Component-based API.

    Conditional Logic

    • Node-based API: Use the .if() command. It supports an optional else: clause for fallback nodes.
    • Component API: Use standard Swift if and else statements within the component's body.

    Unwrapping Optionals

    • Node-based API: Use the .unwrap() command. It takes an optional and a closure to transform the value into a node. It also supports an else: clause if the value is nil.
    • Component API: Use standard Swift if let expressions.

    Iteration

    • Node-based API: Use the .forEach() command to transform a Swift Sequence into a group of nodes.
    • Component API: Use a standard for loop within a component closure, or pass the sequence directly to the built-in List component.
  7. Apply attributes to elements

    master

    Attributes (such as .class(), .href(), or .id()) are applied to elements by adding them as additional comma-separated entries within the element's content list, alongside child elements or text.

    let html = HTML(
        .body(
            .a(.class("link"), .href("https://github.com"), "GitHub")
        )
    )
  8. Define custom elements and attributes

    master

    If Plot does not support a specific HTML element or attribute, you can use "escape hatch" APIs to define them manually.

    One-off Custom Elements

    • Node-based API: Use .element(named:text:) for elements and .attribute(named:value:) for attributes.
    • Component API: Use Element(name:) for elements and .attribute(named:value:) on existing components.

    For long-term stability, extend the relevant document format (e.g., XML) with a new Context and extend Node with custom DSL methods. This allows you to define your own type-safe elements and attributes that behave like built-in ones.

  9. Write HTML using Plot's DSL

    master

    Plot provides a lightweight, type-safe DSL for writing HTML, XML, and RSS in Swift. You build web pages by nesting elements (like HTML, head, body, div, h1, p) as comma-separated arguments in Swift functions. Plot automatically handles metadata like <meta> tags for social media and SEO, and correctly formats attributes like rel="stylesheet" for CSS links.

    let html = HTML(
        .head(
            .title("My website"),
            .stylesheet("styles.css")
        ),
        .body(
            .div(
                .h1("My website"),
                .p("Writing HTML in Swift is pretty great!")
            )
        )
    )
  10. Install Plot via Swift Package Manager

    master

    Plot is distributed via Swift Package Manager. Add it as a dependency in your Package.swift file.

    let package = Package(
        ...
        dependencies: [
            .package(url: "https://github.com/johnsundell/plot.git", from: "0.9.0")
        ],
        ...
    )
  11. Render a document or node to a String

    master

    To convert your constructed Plot DSL into a final string, call the .render() method on an HTML instance, a Node, or a Component.

    Indentation Options

    You can control the output formatting using the indentedBy: parameter:

    • .spaces(n): Indent using a specific number of spaces.
    • .tabs(n): Indent using a specific number of tabs.
    let html = HTML(...) 
    
    let nonIndentedString = html.render()
    let spacesIndentedString = html.render(indentedBy: .spaces(4))
    let tabsIndentedString = html.render(indentedBy: .tabs(1))