Introducing swift-dom: a cross-platform HTML and SVG DSL

The swift-dom library is a cross-platform, efficient, expressive DSL for generating HTML (and related formats) with few intermediate allocations.

$0[.li] 
{ 
    $0[.p] { $0.class = "album" } = "Tortured Poets Department" 
}

Why another HTML DSL?

Many HTML libraries are built around the idea of an Abstract Syntax Tree (AST). You build up a tree of nodes that represents the structure of the document, and then a rendering engine performs a tree traversal to generate the HTML. This is an intuitive mental model for many developers, and dovetails well with Swift result builders and Swift’s general ethos of immutable value semantics.

On the other hand, AST-based rendering can use a lot of memory, as it involves a large number of intermediate allocations. AST-based data structures also struggle with static typing, as they must balance the internal need to model the polymorphic nature of HTML nodes with the external need to present a type-safe API.

Many AST-based HTML libraries mitigate this problem by providing voluminous convenience APIs and multiple levels of abstraction, but these APIs do not always compose well, and can be difficult to learn. The large number of “concise” spellings also tends to trip up AI code generation tools who struggle to distinguish between real and imaginary APIs.

Philosophy

Swift DOM is designed to be composable and easy to learn, with a small number of patterns that developers must internalize. Instead of outlawing as many invalid states as possible at compile-time, Swift DOM relies on a small number of high-impact static typing rules to eliminate common mistakes without confusing developers with too many highly-specialized guardrails. The tradeoff is that some understanding of the HTML format is required to use Swift DOM correctly, as the library makes no effort to abstract away the underlying markup language.

Tutorials

We suggest reading the library tutorials to get started with the DSL.

Who is using it?

The library has been in use on Swiftinit for over one year, and produces all of the HTML and SVG graphics on the site. Swift DOM 1.0 is our first attempt to open-source the DSL.

What platforms does it support?

Swift DOM is written in pure Swift, and works on Linux, macOS, iOS, watchOS, and tvOS. The HTML product does not link Foundation. You can view the full compatibility matrix on The Swift Package Index.

Swift DOM requires Swift 5.10.

8 Likes

Neat! Can I ask why you went with subscript-assignment instead of something more conventional like methods or result builders? (Maybe you’ve already described this elsewhere.)

the shortest answer is that you can’t write $0(.p) = "foo".

but i assume what you were really asking is why it is not spelled $0.p("foo"), and that is because node elision is a big part of the DSL, and i felt that $0.p(nil) wasn’t very natural API to vend.

  • should there be a non-optional overload?
  • if you pass nil, does it still emit an empty <p></p> element?
  • what if you want to emit an empty element?
  • where do you fit the attributes?

in contrast, i felt that $0[.p] = nil benefits from good intuition because when you assign nil to bracket-shaped things, it is more plausible that that it will have no effects. if you want to emit an empty element, you can do $0[.p] { _ in }, or as the tutorial explains, just $0[.p].

finally, i found that = is just way easier to format than (), at least in VSCode, which matters if you have a lot of HTML to edit.

2 Likes

Wow. I have made a quick look and it seems attractive. It's great that it has also been battle tested during development of Swiftinit.
One more interesting thing I found for myself is small amount of code in the library sources. It is easy to understand API and implementation details. Overall library design looks great for me.

Do you plan to add some runtime checks that can be opted in for debug mode or enabled by special build flags?

1 Like

i think it is reasonable to add some build flag-dependent sanitization structures to HTML.AttributeEncoder to catch things like duplicated attributes.

but i think in practice what you want to be doing is running your generated HTML through a markup validator because that will catch many more kinds of mistakes than the generator can as that is what a markup validator is designed to do. the validator can also report errors back to you in a developer-friendly way, whereas the only thing the generator can do is crash.

if you have a large site with a lot of pages, another thing you could do is setup an autocrawler that explores the site naturally and validates the markup as it travels.

1 Like

Thanks for pointing this out, it make sense.

One of the moments that pleased me is clean code without crash-risk operations like force casts, force try, force unwrapping or fatalError's. So crashes are felt like wrong direction.
Instead, I mean something like console warnings for handling weird cases or anomalies. My experience with html is small, so this suggestion may be naive I guess.

the problem here is that logging warnings for this sort of thing wouldn’t include any information about where the problem in your code is. you would just get a source location that points to somewhere inside subscript(dynamicMember:), which probably wouldn’t be very helpful.

we could propogate source location information by passing #file and #line and friends as defaulted arguments, but we wouldn’t want those to appear at all when the library is compiled for production. which means that the subscripts would have different signatures depending on whether you are compiling in debug or release mode.

alternatively, we could dump the partially-encoded HTML document whenever the encoder detects something weird going on. but the minified HTML fragment could still be very long, and would not be very readable at all. as it is just an HTML fragment, you probably couldn’t open it in an HTML AST viewer either.