/ Miscellaneous

A DSL for writing type-safe HTML and RSS in Swift

A DSL for writing type-safe HTML and RSS in Swift

Plot

Welcome to Plot, a domain-specific language (DSL) for writing type-safe HTML, XML and RSS in Swift. It can be used to build websites, documents and feeds, as a templating tool, or as a renderer for higher-level components and tools. It’s primary focus is on static site generation and Swift-based web development.

Write HTML — in Swift!

Plot enables you to write HTML using native, fully compiled Swift code, by modeling the HTML5 standard’s various elements as Swift APIs. The result is a very lightweight DSL that lets you build complete web pages in a highly expressive way:

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

Looking at the above, it may at first seem like Plot simply maps each function call directly to an equivalent HTML element — and while that’s the case for some elements, Plot also inserts many kinds of highly valuable metadata automatically. For example, the above expression will result in this HTML:

<!DOCTYPE html>
<html>
    <head>
        <title>My website</title>
        <meta name="twitter:title" content="My website"/>
        <meta name="og:title" content="My website"/>
        <link rel="stylesheet" href="styles.css" type="text/css"/>
    </head>
    <body>
        <div>
            <h1>My website</h1>
            <p>Writing HTML in Swift is pretty great!</p>
        </div>
    </body>
</html>

As you can see above, Plot added both all of the necessary attributes to load the requested CSS stylesheet, along with additional metadata for the page’s title as well — improving page rendering, social media sharing, and search engine optimization.

Plot ships with a very wide coverage of the HTML5 standard, enabling all sorts of elements to be defined using the same lightweight syntax — such as tables, lists, and inline text styling:

let html = HTML(
    .body(
        .h2("Countries and their capitals"),
        .table(
            .tr(.th("Country"), .th("Capital")),
            .tr(.td("Sweden"), .td("Stockholm")),
            .tr(.td("Japan"), .td("Tokyo"))
        ),
        .h2("List of ", .strong("programming languages")),
        .ul(
            .li("Swift"),
            .li("Objective-C"),
            .li("C")
        )
    )
)

Above we’re also using Plot’s powerful composition capabilities, which lets us express all sorts of HTML hierarchies by simply adding new elements as comma-separated values.

Applying attributes

Attributes can also be applied the exact same way as child elements are added, by simply adding another entry to an element’s comma-separated list of content. For example, here’s how an anchor element with both a CSS class and a URL can be defined:

let html = HTML(
    .body(
        .a(.class("link"), .href("https://github.com"), "GitHub")
    )
)

The fact that attributes, elements and inline text are all defined the same way both makes Plot’s API easier to learn, and also enables a really fast and fluid typing experience — as you can simply type . within any context to keep defining new attributes and elements.

Type safety built-in

Plot makes heavy use of Swift’s advanced generics capabilities to not only make it possible to write HTML and XML using native code, but to also make that process completely type-safe as well.

All of Plot’s elements and attributes are implemented as context-bound nodes, which both enforces valid HTML semantics, and also enables Xcode and other IDEs to provide rich autocomplete suggestions when writing code using Plot’s DSL.

For example, above the href attribute was added to an <a> element, which is completely valid. However, if we instead attempted to add that same attribute to a <p> element, we’d get a compiler error:

let html = HTML(.body(
    // Compiler error: Referencing static method 'href' on
    // 'Node' requires that 'HTML.BodyContext' conform to
    // 'HTMLLinkableContext'.
    .p(.href("https://github.com"))
))

Plot also leverages the Swift type system to verify each document’s element structure as well. For example, within HTML lists (such as <ol> and <ul>), it’s only valid to place <li> elements — and if we break that rule, we’ll again get a compiler error:

let html = HTML(.body(
    // Compiler error: Member 'p' in 'Node<HTML.ListContext>'
    // produces result of type 'Node<Context>', but context
    // expects 'Node<HTML.ListContext>'.
    .ul(.p("Not allowed"))
))

This high degree of type safety both results in a really pleasant development experience, and that the HTML and XML documents created using Plot will have a much higher chance of being semantically correct — especially when compared to writing documents and markup using raw strings.

Defining custom components

The same context-bound Node architecture that gives Plot its high degree of type safety also enables more higher-level components to be defined, which can then be mixed and composed the exact same way as elements defined within Plot itself.

For example, let’s say that we’re building a news website using Plot, and that we’re rendering NewsArticle models in multiple places. Here’s how we could define a reusable newsArticle component that’s bound to the context of an HTML document’s <body>:

extension Node where Context: HTML.BodyContext {
    static func newsArticle(_ article: NewsArticle) -> Self {
        return .article(
            .class("news"),
            .img(.src(article.imagePath)),
            .h1(.text(article.title)),
            .span(
                .class("description"),
                .text(article.description)
            )
        )
    }
}

With the above in place, we can now render any of our NewsArticle models using the exact same syntax as we use for built-in elements:

func newsArticlePage(for article: NewsArticle) -> HTML {
    return HTML(.body(
        .div(
            .class("wrapper"),
            .newsArticle(article)
        )
    ))
}

It’s highly recommended that you use the above component-based approach as much as possible when building websites and documents with Plot — as doing so will let you build up a growing library of reusable components, which will most likely accelerate your overall workflow over time.

Inline control flow

Since Plot is focused on static site generation, it also ships with several control flow mechanisms that let you inline logic when using its DSL. For example, using the .if() command, you can optionally add a node only when a given condition is true:

let rating: Rating = ...

let html = HTML(.body(
    .if(rating.hasEnoughVotes,
        .span("Average score: \(rating.averageScore)")
    )
))

You can also attach an else clause to the .if() command as well, which will act as a fallback node to be displayed when the condition is false:

let html = HTML(.body(
    .if(rating.hasEnoughVotes,
        .span("Average score: \(rating.averageScore)"),
        else: .span("Not enough votes yet.")
    )
))

Optional values can also be unwrapped inline using the .unwrap() command, which takes an optional to unwrap, and a closure used to transform its value into a node — for example to conditionally display a part of an HTML page only if a user is logged in:

let user: User? = loadUser()

let html = HTML(.body(
    .unwrap(user) {
        .p("Hello, \($0.name)")
    }
))

Finally, the .forEach() command can be used to transform any Swift Sequence into a group of nodes, which is incredibly useful when constructing lists:

let names: [String] = ...

let html = HTML(.body(
    .h2("People"),
    .ul(.forEach(names) {
        .li(.class("name"), .text($0))
    })
))

Using the above control flow mechanisms, especially when combined with the approach of defining custom components, lets you build really flexible templates, documents and HTML pages — all in a completely type-safe way.

GitHub