Giter VIP home page Giter VIP logo

elementary's Introduction

Elementary: HTML Templating in Pure Swift

A modern and efficient HTML rendering library - inspired by SwiftUI, built for the web.

Examples | Motivation | Discussion

struct MainPage: HTMLDocument {
    var title: String = "Elementary"

    var head: some HTML {
        meta(.name(.description), .content("Typesafe HTML in modern Swift"))
    }

    var body: some HTML {
        main {
            h1 { "Features" }

            FeatureList(features: [
                "HTML in pure Swift",
                "SwiftUI-inspired composition",
                "Lightweight and fast",
                "Framework agnostic and unopinionated",
            ])

            a(.href("https://github.com/sliemeobn/elementary"), .class("fancy-style")) {
                "Learn more"
            }
        }
    }
}

struct FeatureList: HTML {
    var features: [String]

    var content: some HTML {
        ul {
            for feature in features {
                li { feature }
            }
        }
    }
}

Play with it

Check out the Hummingbird + Tailwind example app.

For a demo of ElementaryHTMX, see this Hummingbird + HTMX Demo.

For a Vapor example, see the Vapor + HTMX Demo.

Lightweight and fast

Elementary renders straight to text, optimized for serving generated HTML from a Hummingbird or Vapor server app.

Any type conforming to HTML can be rendered individually, ideal for testing or for sending fragments with htmx.

The default rendering mechanism produces chunks of HTML for efficient response streaming, so the browser can start loading a page while the server is still producing the rest of it. Swift concurrency is used to handle back pressure, so you your memory footprint stays low even for large pages.

// Stream HTML, optimized for responsiveness and back pressure-aware
try await MainPage().render(into: responseStreamWriter)

Alternatively, you can simply collect the rendered HTML in a string.

let html: String = div(.class("pretty")) { "Hello" }.render()
// <div class="pretty">Hello</div>

let fragment: String = FeatureList(features: ["Anything conforming to HTML can be rendered"]).render()
// <ul><li>Anything conforming to HTML can be rendered</li></ul>

// For testing purposes, there is also a formatted version
print(
    div {
        p(.class("greeting")) { "Hi mom!" }
        p { "Look how pretty." }
    }.renderFormatted()
)

// <div>
//   <p class="greeting">Hi mom!</p>
//   <p>Look how pretty.</p>
// </div>

Elementary has zero dependencies (not even Foundation) and does not use runtime reflection or existential containers (there is not a single any in the code base).

By design, it does not come with a layout engine, reactive state tracking, or built-in CSS styling: it just renders HTML.

Clean and composable

Structure your HTML with a SwiftUI-inspired composition API.

struct List: HTML {
    var items: [String]
    var importantIndex: Int

    var content: some HTML {
        // conditional rendering
        if items.isEmpty {
            p { "No items" }
        } else {
            ul {
                // list rendering
                for (index, item) in items.enumerated() {
                    // seamless composition of elements
                    ListItem(text: item, isImportant: index == importantIndex)
                }
            }
        }
    }
}

struct ListItem: HTML {
    var text: String
    var isImportant: Bool = false

    var content: some HTML {
        // conditional attributes
        li { text }
            .attributes(.class("important"), when: isImportant)
    }
}

First class attribute handling

Elementary utilizes Swift's powerful generics to provide an attribute system that knows what goes where. Every element knows which Tag it is for.

As in HTML, attributes go right after the "opening tag".

// staying close to HTML syntax really helps
div(.data("hello", value: "there")) {
    a(.href("/swift"), .target(.blank)) {
        img(.src("/swift.png"))
        span(.class("fancy")) { "Click Me" }
    }
}

Attributes can also be altered by using the modifier syntax, this allows for easy handling of conditional attributes.

div {
    p { "Hello" }
        .attributes(.id("maybe-fancy"))
        .attributes(.class("fancy"), when: isFancy)
}

By exposing the tag type of content, attributes will fall through and be applied correctly.

struct Button: HTML {
    var text: String

    // by exposing the HTMLTag type information...
    var content: some HTML<HTMLTag.input> {
        input(.type(.button), .value(text))
    }
}

div {
    // ... Button will know it really is an <input> element ...
    Button(text: "Hello")
        .attributes(.autofocus) // ... and pass on any attributes
}

As a sensible default, class and style attributes are merged (with a blank space or semicolon respectively). All other attributes are overwritten by default.

🚧 Work in progress 🚧

The list of built-in attributes is rather short still, but adding them is really simple (and can be done in external packages as well).

Feel free to open a PR with additional attributes that are missing from the model.

Motivation and other packages

Plot, HTMLKit, and Swim are all excellent packages for doing a similar thing.

My main motivation for Elementary was to create an experience like these (Swift Forums post for more context), but

  • stay true to HTML tag names and conventions (including the choice of lowercase types)
  • avoid allocating an intermedate structure and go straight to streaming HTML
  • using generics to stay away from allocating a ton of lists of existential anys
  • have a list of attributes go before the content block
  • provide attribute fallthrough and merging
  • zero dependencies on other packages

Tokamak is an awesome project and very inspiring. It can produce HTML, but it's main focus is on a very different beast. Check it out!

swift-html and swift-dom will produce HTML nicely, but they use a different syntax for composing HTML elements.

Future directions

  • Experiment with an AsyncHTML type, that can include await in bodies, and a ForEach type that takes an async sequence
  • Experiment with embedded swift for wasm and bolt a lean state tracking/reconciler for reactive DOM manipulation on top

elementary's People

Contributors

sliemeobn avatar

Stargazers

Josh Wright avatar Coen ten Thije Boonkkamp avatar Zane Enders avatar Zaharia Andrei avatar  avatar Roman Mazeev avatar arasan01 avatar Kotaro Suto avatar Joseph Heck avatar Dave Bettin avatar Runar Hummelsund avatar Eden avatar Tiago Canto avatar Franz Fletcher avatar Jordan Howlett avatar Christoph Hagen avatar Satoshi Namai avatar Petr Pavlik avatar Raffe Yang avatar  avatar Vaida avatar Greg Cotten avatar Luca Ban avatar Tomek Kuźma avatar Dmitrii Fomin avatar Rychillie avatar Matthieu Barthelemy avatar Andrejs Agejevs avatar  avatar Lukas Pistrol avatar Wouter Hennen avatar Pat Nakajima avatar Casey Hanley avatar  avatar Bo Weber avatar Ricardo Nogueira avatar Matthew Mannucci avatar Nikola Stojanović avatar Christophe Bronner avatar Graham Burgsma avatar Ben Cousins avatar Tim Kersey avatar  avatar  avatar Timo avatar

Watchers

 avatar  avatar

elementary's Issues

`renderFormatted()` adds empty lines after `img` and `svg`

When rendering HTML with renderFormatted(), there are many empty lines added after img and svg elements.

In my test, there were 12 empty lines after an img element, and 3 lines after each svg element.
The lines are all indented to the previous element.

Option to add trailing slash to void elements

First off, thanks for starting this library. I'm currently experimenting with it to generate HTML pages with swift, and ran across an issue:

I created a custom path element to write inline svg:

public typealias path = HTMLVoidElement<HTMLTag.path>

public extension HTMLTag {

    enum path: HTMLTrait.Unpaired, HTMLTrait.RenderedInline { public static let name = "path" }
}

public extension HTMLAttribute where Tag == HTMLTag.path {

    static func fill(_ value: String) -> HTMLAttribute<HTMLTag.path> {
        .init(name: "fill", value: value)
    }

    static func d(_ value: String) -> HTMLAttribute<HTMLTag.path> {
        .init(name: "d", value: value)
    }
}

I can then write simple svg content:

svg(.id("some")) {
    path(.fill("currentColor"), .d("..."))
    path(.fill("currentColor"), .d("..."))
}

Once I render this, it produces the following HTML:

<svg id="some">
    <path fill="currentColor" d="...">
    <path fill="currentColor" d="...">
</svg>

I think this is valid HTML, but if I try to display the svg in Safari, it doesn't render correctly.
Apparently Safari needs a trailing slash for path, like so:

<svg id="some">
    <path fill="currentColor" d="..." />
    <path fill="currentColor" d="..." />
</svg>

Is there a way to force certain void elements to add a trailing slash when rendering? Or should I use a different approach?

I guess the alternative would be to use HTMLTrait.Paired instead of HTMLTrait.Unpaired, but that would make the HTML less compact.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.