EP 28 · Standalone · Sep 3, 2018 ·Members

Video #28: An HTML DSL

smart_display

Loading stream…

Video #28: An HTML DSL

Episode: Video #28 Date: Sep 3, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep28-an-html-dsl

Episode thumbnail

Description

This week we apply domain-specific languages to a very real-world problem: representing and rendering HTML. We code up a simple but powerful solution that forms the foundation of what we use to build the Point-Free website.

Video

Cloudflare Stream video ID: 6e4fca3a054d2033c6ae907f975c09ef Local file: video_28_an-html-dsl.mp4 *(download with --video 28)*

References

Transcript

0:05

In a couple recent episodes, we introduced the idea of domain specific languages, often abbreviated DSLs, which are languages that are highly tuned to a specific task. We then introduced the idea of embedded DSLs (or EDSLs), which are domain specific languages that are hosted, or written, in a more general purpose language, like Swift.

0:29

To get comfortable with the idea, we embedded some basic arithmetic in the Swift type system. Using a DSL helped us separate the pure, data-driven description of a domain problem from the impure, run-time interpretation of that data.

0:53

But arithmetic was a bit of a toy example, though, so today we’re going to embed a more real-world domain problem in Swift, HTML, by building the same DSL that we use to power the Point-Free web site! HTML in types

1:13

When we sought to describe arithmetic expressions in the Swift type system, we looked at an example in order to break things down into all the units that can make up an expression. Let’s break down the pieces of a simple fragment of HTML into units we can describe. <header> <h1>Point-Free</h1> <p id="blurb"> Functional programming in Swift. <a href="/about">Learn more</a>! </p> <img src="https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg" width="64" height="64"> </header> HTML is a tree and the unit used to build this tree is called a “node”. A node is either: An “element”, which is written with tags using angle brackets. Elements start with a name, like header , h1 , p , a , and img . They contain zero or more “attributes”, which are name-value pairs of associated data—in this example we have id="blurb" , href="/about" , src="..." , width="64" and height="64" . And they can contain zero or more child nodes. The header element here contains h1 , p , and img elements as child nodes. The img element, meanwhile, has no child nodes. There are also “text nodes”, like the Point-Free inside the h1 element. The p element contains a text node, an a element, and a final text node.

2:37

This translates directly into a Swift enum: enum Node { indirect case el(String, [(String, String)], [Node]) case text(String) } Our Node enum has two cases. An el case for our element, which has a few associated values: a String for the element name, an array of (String, String) pairs for attribute names and values, and finally an array of child Node s. Because this data type is recursive— el can contain Node s—we have to annotate it with the indirect keyword.

3:05

We also have a text case, which has a single associated String value for the text it represents.

3:10

This single type is all we need to represent our HTML fragment. Node.el("header", [], [ .el("h1", [], [.text("Point-Free")]), .el("p", [("id", "blurb")], [ .text("Functional programming in Swift. "), .el("a", [("href", "/about")], [.text("Learn more")]), .text("!") ]), .el("img", [("src", "https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg"), ("width", "64"), ("height", "64")], []), ])

4:10

And with that, we have directly translated our fragment of HTML into Swift! We’re done! HTML in functions

4:19

Well, we’re not quite done.

4:26

One problem with our DSL it it’s a lot more difficult to read than plain old HTML. Luckily, we have the whole Swift language at our disposal and can use its features to smooth over some of the rough edges.

4:42

The first thing we can do is reuse a trick from last time and make Node conform to ExpressibleByStringLiteral . This lets us represent text nodes with Swift string literals. extension Node: ExpressibleByStringLiteral { init(stringLiteral value: String) { self = .text(value) } }

5:14

Now we can clean up a lot of extra noise around the text cases. Node.el("header", [], [ .el("h1", [], ["Point-Free"]), .el("p", [("id", "blurb")], [ "Functional programming in Swift. ", .el("a", [("href", "/about")], ["Learn more"]), "!" ]), .el("img", [("src", "https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg"), ("width", "64"), ("height", "64")], []), ])

5:25

This structure is now so succinct that it’s mostly just strings, which is a bit of a red flag. Some of these strings are user-facing, so they should be strings, but a lot of these strings have semantic meaning in HTML, like very specific tag names. We could misspell an element name and we’d have an invalid HTML document.

5:50

In order to prevent these problems, let’s start by wrapping our elements in named functions for static, compile-time safety. func header(_ attrs: [(String, String)], _ children: [Node]) -> Node { return .el("header", attrs, children) }

6:19

And with a little bit of copy-paste and editing, we can cover all the elements we care about so far. func h1(_ attrs: [(String, String)], _ children: [Node]) -> Node { return .el("h1", attrs, children) } func p(_ attrs: [(String, String)], _ children: [Node]) -> Node { return .el("p", attrs, children) } func a(_ attrs: [(String, String)], _ children: [Node]) -> Node { return .el("a", attrs, children) } func img(_ attrs: [(String, String), _ children: [Node]) -> Node { return .el("img", attrs, children) }

6:29

An img element can’t have any children, so we can omit that input and use an empty array inside and directly encode that semantic into our function. func img(_ attrs: [(String, String)]) -> Node { return .el("img", attrs, []) }

6:53

Now our value looks like this: header([], [ h1([], ["Point-Free"]), p([("id", "blurb")], [ "Functional programming in Swift. ", a([("href", "/about")], ["Learn more"]), "!" ]), img([("src", "https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg"), ("width", "64"), ("height", "64")]), ])

7:14

Some of our elements don’t have any attributes. Those empty arrays are kinda noisy, and are hind of hidden in the original, raw HTML. We can write some overloads, though, that don’t take attributes, which cleans things up a bit more. func header(_ children: [Node]) -> Node { return .el("header", [], children) } func h1(_ children: [Node]) -> Node { return .el("h1", [], children) }

7:38

And our example further simplifies: header([ h1(["Point-Free"]), p([("id", "blurb")], [ "Functional programming in Swift. ", a([("href", "/about")], ["Learn more"]), "!" ]), img([("src", "https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg"), ("width", "64"), ("height", "64")]), ])

7:48

Looking good! But our attributes are still raw string pairs that we could misspell and have invalid HTML on our hands. Let’s write some more helper functions that create static interfaces around them. func id(_ value: String) -> (String, String) { return ("id", value) } func href(_ value: String) -> (String, String) { return ("href", value) } func src(_ value: String) -> (String, String) { return ("src", value) } func width(_ value: String) -> (String, String) { return ("width", value) } func height(_ value: String) -> (String, String) { return ("height", value) }

8:33

Plugging in these attribute functions cleans up a bunch more code. header([ h1(["Point-Free"]), p([id("blurb")], [ "Functional programming in Swift. ", a([href("/about")], ["Learn more"]), "!" ]), img([src("https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg"), width("64"), height("64")]), ])

8:56

The width and height attributes are interesting because the HTML specification expects them to be numeric values. This is a detail we can capture in Swift at compile time: our helper functions can take Int s as input and we can convert them to strings inside. func width(_ value: Int) -> (String, String) { return ("width", "\(value)") } func height(_ value: Int) -> (String, String) { return ("height", "\(value)") }

9:23

And now we can provide actual integers to the img tag. img([src("https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg"), width(64), height(64)]),

9:33

It’s all looking pretty good! We’ve gotten rid of a lot of noise and added a whole safety layer, thanks to the Swift compiler, that allows us to use static functions to encode tag names, attribute names, and even attribute values in the Swift type system. The Swift compiler can guide us along to create semantically-correct HTML.

10:09

Let’s take a look at this alongside our original HTML fragment. <header> <h1>Point-Free</h1> <p id="blurb"> Functional programming in Swift. <a href="/about">Learn more</a>! </p> <img src="https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg" width="64" height="64"> </header> header([ h1(["Point-Free"]), p([id("blurb")], [ "Functional programming in Swift. ", a([href("/about")], ["Learn more"]), "!" ]), img([src("https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg"), width(64), height(64)]), ])

10:20

We’ve created a pretty nice interface that looks a lot like the underlying HTML, but it’s all valid Swift that can be checked by the compiler. HTML interpreted

10:42

Now, while the interface we’ve created is feeling pretty usable, it’s not particularly useful just yet. We’ve modeled HTML as a type but we have no way of producing actual HTML that can be rendered in a web browser.

10:57

When we embedded a subset of arithmetic in Swift types, we needed to write functions that could interpret that structure. This included a print function that rendered the arithmetic expression as a string. We can write a similar interpreter for Node that produces an HTML string. func render(_ node: Node) -> String { }

11:20

In order to produce an HTML string from a node, we need to switch on it to handle each case. func render(_ node: Node) -> String { switch node { case .el(_, _, _): case .text(_): } }

11:33

The text case seems the least complicated to implement first. We can merely return the string inside. case let .text(string): return string

11:41

The el case is a bit more complicated and has more moving parts. The tag name is already a string, so let’s format the attributes. We can map over them, joining each name-value pair with an = , making sure to wrap the value in quotation marks. case let .el(tag, attrs, children): let formattedAttrs = attrs .map { key, value in "\(key)=\"\(value)\"" } .joined(separator: " ")

12:30

Now let’s format the children. Well, we have a bunch of Node s we wanna render, so let’s recursively call render on them using map before joining the strings together. let formattedChildren = children.map(render).joined(separator: "")

12:59

Now we can format the tag name with the rendered attributes and children. All together: func render(_ node: Node) -> String { switch node { case let .el(tag, attrs, children): let formattedAttrs = attrs .map { key, value in "\(key)=\"\(value)\"" } .joined(separator: " ") let formattedChildren = children.map(render).joined(separator: "") return "<\(tag) \(formattedAttrs)>\(formattedChildren)</\(tag)>" case let .text(string): return string } }

13:35

So what’s it look like to render our example? let node = header([ h1(["Point-Free"]), p([id("blurb")], [ "Functional programming in Swift. ", a([href("/about")], ["Learn more"]), "!" ]), img([src("https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg"), width(64), height(64)]), ]) print(render(node)) // <header ><h1 >Point-Free</h1><p id="blurb">Functional programming in Swift. <a href="/about">Learn more</a>!</p><img src="https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg" width="64" height="64"></img></header>

13:47

Alright, so it’s not pretty, but it technically works and is renderable in a web browser.

14:09

So let’s do just that! We can create a web view and feed it our rendered HTML to make sure everything looks OK. import WebKit let webView = WKWebView(frame: .init(x: 0, y: 0, width: 320, height: 480)) webView.loadHTMLString(render(node), baseURL: nil) import PlaygroundSupport PlaygroundPage.current.liveView = webView

14:39

Look at that! In next to no time we wrote a render function and had HTML rendering in a browser, right from the comfort of a playground! HTML transformed

15:04

With our algebraic DSL , we explored what it was like to manipulates DSL values and perform transformations. Let’s do the same with our HTML DSL.

15:26

Let’s write a function that traverses and reverses a node’s children and text. func reversed(_ node: Node) -> Node { switch node { case let .el(tag, attrs, children): return .el(tag, attrs, children.reversed().map(reversed)) case let .text(string): return .text(String(string.reversed())) } }

17:00

If we render our node, reversed: webView.loadHTMLString(render(reversed(node)), baseURL: nil) PlaygroundPage.current.liveView = webView

17:13

Everything’s backwards. This was not only possible but really easy since we’re working with a value that we can interpret and change.

17:26

One thing you may have noticed is the image hasn’t flipped, and that’s because we’d need to use CSS to do such a thing. So, let’s do it! We’re not going to even have to add any new features to our DSL. It already supports this kind of thing.

17:47

We can pattern-match directly on image tags and append a CSS transform to the node. case let .el("img", attrs, children): return .el("img", attrs + [("style", "transform: scaleX(-1)")], children)

18:30

Now even the logo is flipped, and all we had to do was make one small change. We could cook up some functions to make things a little safer, like a style function for the attribute, but we didn’t have to do so in order to get things working. What’s the point?

19:04

So this is fun and all, but it’s probably a good time to stop and ask: “what’s the point?” This reversed function is super silly and not really a useful thing. Is this something we should actually be using? It took a lot of boilerplate code to make things safer and nicer-looking, and that was just for the five elements and attributes in this example fragment. Is it worth continuing down this path?

19:45

We think so! While we have to write a lot of boilerplate to make these values nice to construct, it’s boilerplate that we only have to write once and, ideally, it’s boilerplate a community library writes for us.

20:38

And even without the boilerplate, you always have the escape hatch to use the DSL with string tags and attributes.

21:02

Let’s look at the core of our library: it’s just four lines! enum Node { indirect case el(String, [(String, String)], [Node]) case text(String) }

21:12

At it’s core it’s just an enum with two cases. Everything else is just building niceties on top. This enum is what’s responsible for separating the concerns of representing HTML from the concerns of rendering HTML. It’s what gives access to traversing nodes, applying transformations, and getting whole new nodes out of it.

22:06

And even though the reversed function was a little silly, there are a lot of powerful transformations we can make to this DSL, and we do these kinds of things in the pointfreeco repo , like traverse over a page and inline styles for the purpose of rendering in an email. Having a lightweight value representing HTML is what unlocks all of this.

22:40

Now, our render function isn’t quite production-ready yet: it doesn’t handle any kind of HTML escaping, so our output can easily break, and the output itself is kinda ugly. We’ll clean things up and make things safer in a future episode. References Open sourcing swift-html: A Type-Safe Alternative to Templating Languages in Swift Brandon Williams & Stephen Celis • Nov 12, 2018 After developing the ideas of DSLs in a series of episodes ( part 1 and part 2 ), we open sourced our own DSL library for constructing HTML in Swift. We use this library heavily for building every page on this very website, and it unlocks a lot of wonderful transformations and opportunities for code reuse. https://www.pointfree.co/blog/posts/16-open-sourcing-swift-html-a-type-safe-alternative-to-templating-languages-in-swift Downloads Sample code 0028-html-dsl Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .