Have you ever used Lua?
Itâs a pretty cool language.
It is also one of my favourite programming languages, for which I made a case in a past blog post.
One of my favourite aspects of Luaâs design that I like to preach about is how itâs really tight and small, while also being genuinely really sweet to write.
Today, Iâd like to focus on its Lisp-like aspect: domain specific languages (DSLs)âspecifically, we will use it to build a templating language for HTML.
But first, let me set some background.
As a fellow blogger on the Internet maintaining their own blogging software, Iâve used my fair share of templating engines for generating HTML.
The premise of a templating engine is really simple: you have a bunch of literal text, and into that literal text is sandwiched a bunch of instructions on how to expand that literal text, given some parameters.
Then, a web server can render the template, providing it the parameters to render with.
In other words, a template is a function params -> string.
Parameters go in, string goes out.
On this website, Iâm using Handlebars: a fairly popular templating engine in the JavaScript world, although Iâm using a Rust implementation of it myself.
The syntax looks like this (this is the actual template Iâm using to generate the âBlogâ sidebar on the homepage):
```
{{ page.feed.title }}
{{#each page.feed.entries}}{{{ title }}}
{{ iso_date updated }}-
{{#each tags as |tag|}}
- #{{ tag }} {{/each}}
```
Between the HTML tags, you will find instructions for the template engine, enveloped in curly braces {{ }}.
This is a pretty common syntax among various template engines, although parts of it will vary from engine to engineâsuch as Handlebarsâs non-escaping instructions, which triple the curlies {{{ }}}, or its block helpers {{#each}} and {{/each}}, which Iâll get to in a moment.
The most basic type of instruction is a lookup, which inserts a literal value into your output text.
Letâs zoom in a bit:
```
{{{ title }}}
```
Output
```
The album listener
```
Here, the template engine will look up the fields url and title in the parameters, and insert them into the output text.
Note how I had to use triple curly braces here, because Handlebars will escape the expanded text by default, such that any HTML tokens are presented literally on the page.
My server provides raw HTML in the title parameter, so we want to turn that off.
Note that lookups donât have to be limited to simple field names, as the parameters passed may be an arbitrarily deep data structureâas is seen with the header:
```
{{ page.feed.title }}
```
Output
```
Blog
```
In addition to lookups, Handlebars also has helpers, which is a slightly redundant name for functions that are exposed to the template in addition to the parameters.
(I mean, you could just name them functions!)
With helpers, the template may transform the input parameters in different ways, such as with iso_date in the example above, which is defined in Rust and used like this:
Definition
handlebars_helper!(iso_date: |d: DateTime<Utc>| d.format("%F").to_string());
handlebars.register_helper("iso_date", Box::new(iso_date));
Usage
<time datetime="{{ updated }}">{{ iso_date updated }}</time>
Output
<time datetime="2026-03-19T14:22:00Z">2026-03-19</time>
Finally, we have a special class of helpers, called block helpers.
While regular helpers take single values as parameters, block helpers take in a block in addition, allowing them to execute the block similarly to a closure in a real programming languageâconditionally, in a loop, and so on.
Since block helpers need a start and an end delimiter, Handlebars opts for prefixing the helper name with # and / for the start and the end of the block respectively, like {{#each}} {{/each}}.
Letâs look at the example of {{#each}} from above, though shortened a bit for brevity:
```
{{#each page.feed.entries}}
{{{ title }}}
{{/each}}
```
Output
```
The album listener
The communal chat room
Scheming on a text editor
Out now: along rivers, forever adrift.
```
Here, {{#each}} will expand to as many headings as there are elements in the list provided to it.
An interesting feature (and, in my honest opinion, footgun) of many template engines thatâs demonstrated here is their scoping system, which is akin to JavaScriptâs with statements.
Essentially, lookups are done within a scope of the parameters.
In the beginning, if you use an identifier like url, the template engine will look it up in the parametersâ root.
However, once you enter a loop, that scope walks down to the current loop elementâwhich is why {{ url }} in the example above refers to the feed entryâs URL, rather than the whole pageâs URL.
Because of this, when you want to look up an identifier that isnât within the loop element, you have to walk up the scope chain to get to itâin Handlebars, thatâs written like {{ ../url }}.
I think you can imagine how this can quickly get error-prone and annoying to work with.
Either way, that about sums up the essentials of template engines.
Now, donât get me wrong: I think template engines are great.
They are simple and fast, when done well.
One of my favourite templating engines, Goâs text/templateâwhose design you should totally copy! (though, maybe sans the scopingâ¦)âfits in less than 3000 lines of code as of writing this post.
It is a really comprehensive template engine, featuring basically everything I mentioned above, with a syntax design much nicer to look at than Handlebars, in my humble opinion.
⦠yeah Iâm just dissatisfied with Handlebars, am I not.
Letâs be real, Handlebars doesnât do anything more than Goâs text/template does, yet is 3x larger, at around 9000 lines of code excluding comments! (Meanwhile my estimation of text/templateâs size was done entirely with a web browser and a calculator, no fancy comment exclusions.)
You could blame a lot of this on Rustâs lack of reflection system, but I donât believe you need a reflection system to do templatesâ¦
In fact, youâre probably better off without one, since passing data to templates is like serialising to JSON, which I think should be done imperatively, kept separate from your data structure definitions.
The static type system does not know about your templates, so by exposing your data structures to templates, youâre introducing a contract about your data structuresâ API stabilityâwhich is not ideal, and annoying to enforce with how template engines tend to be dynamically typed.
But even then, Handlebars still has another ergonomic problem.
Remember that triple curly brace {{{ }}}?
```
{{{ title }}}
```
Yeah, that.
Kind of sucks that I have to do that, doesnât it.
What if the data model changes, and the server no longer provides the template with a string thatâs known-good-HTML?
Then weâve got ourselves an XSS vulnerability cooking, if the template is serving anything based on user input (which it will in most dynamic services.)
This is why Go also has a sister module for its text/template, called html/template.
Hereâs how it fixes this problem:
- Any time you want to provide raw HTML to the template, youâre encouraged to do it in your server code, by passing in
HTMLinstead of astring. - To my knowledge, there is no special syntax to tell the template engine, âdonât escape this.â
- The module knows how to render non-HTML strings into templates byâ¦
PARSING THE HTML??
It parses.
The HTML.
It parses.
(My browser chugged hard trying to load that page.)
At this point, it feels like weâre doing something backwards here.
Why are we trying to parse the HTML weâre trying to render, a.k.a. generate, a.k.a. serialise?
Which, I guess, is exactly what the person behind maud was thinking when they made it.
Maud is a different kind of HTML template engine.
Instead of being implemented as a generic language for transforming text, it is implemented as a Rust macro with its own syntax, that always produces valid HTML.
This seems like the more sensible approachâinstead of trying to parse the HTML complexity monster, why donât we rely on the host language to provide us with enough information to generate it correctly?
And the result is actually pretty neat-looking!
I havenât used it personally, but hereâs an example from their website.
html! {
h1 { "Hello, world!" }
p.intro {
"This is an example of the "
a href="https://github.com/lambda-fairy/maud" { "Maud" }
" template language."
}
}
Itâs pretty lightweight, and doesnât lose the structure of the HTML.
I like it.
The one thing I donât like is that itâs a complicated, 3000 line-of-code long Rust macro.
It is an improvement in terms of density of functionality compared to html/template, and doesnât suffer from the same ergonomic annoyances as Handlebars does.
But I donât like that itâs a 3000 lines long Rust macro.
For those of you wondering why Iâm so much against implementing this with Rust macros, hereâs a short breakdown:
- The tooling support sucks.
The language server pretty much stops working inside macros, and therustfmtwill no longer format your code.
That alone is 80% of why I hate doing that kind of stuff with macros.
The remaining 20% isâ¦
- The compilation times.
maud depends on syn to parse Rust expressions, which means that has to get compiled before any other crate does (which takes a bit of time).
The worse part, though, is that once you get to using the macros, the compilerâinstead of just parsing your codeâhas to invoke the macro, which will parse your code, and then spit out a bunch of Rust code for the compiler to parseâincreasing the amount of work that has to be done.
Donât get me started on #[derive] macrosâ¦
maud fortunately isnât one of them, but Iâll just briefly ruin your day by saying that theyâre even worse, because each macro you use has to parse your type definition separately, in addition to the compiler, as well as other #[derive] macros you use.
Shivers.
- And lastly, itâs inventing new syntax.
Iâd love it if an HTML generation thing like maud could just use existing syntax instead of having to invent anything new.
It would result in having less to learn, and less context switches having to look stuff up in the libraryâs documentation after not using it for a month.
For this same reason, I donât really like JSX despite its unquestionable ergonomic benefits.
Itâs bolting on new features to a language in a pretty inelegant way, instead of reusing existing syntax.
And it requires a compilation step.
Fortunately, there exists a programming language that ticks all the boxes.
Enterâ¦
Lua, that ugly language with 1-based indexing, a weird inequality operator, and do..end blocks instead of my beloved curlies?
Look, Iâm also one for curly hair, but I genuinely like Luaâs syntax.
It can be pretty wordy, but in essence it is a bit like a tiny, dynamically typed Go.
A language that gets out of the way and lets you get things done.
At the same time, Lua has a pretty long history having started as a DSL for defining data, which influenced its design towards some really interesting choices that make it perfect for the thing weâre trying to make.
Let me start by introducing Luaâs most awesome feature: its single data structure, the table.
On the surface, theyâre familiar: a table is nothing more than a hash table with dynamically typed keys.
Since there is no other data structure in the language, they feature syntax sugar which lets them double as records or structs.
local cat = {
name = "meowy",
age = 1,
}
print(cat["name"]) print(cat.name)
At the same time, since the language has no dedicated array data structure, thereâs also a syntax sugar for declaring a table with incrementing integer keys, starting at 1.
Simply omit the key:
local words = {"yippy", "foxgirl", "uwu"}
print(words[1])
Itâs also possible to get the number of elements
using the # operator.
print(#words)
Storing elements in this manner activates an optimisation called the tableâs array part.
When a table has a contiguous sequence of elements starting at 1, Lua will store them in a contiguous array in memory, making iteration and indexing more efficient than with a hash table.
Those are the basics, but a cool feature is that you can combine the two syntaxes together in one table initialiser.
This will create a table both with string keys and an array part:
```
local document = {
language = "English",
"Hello! This is a line of text.",
"Meow."
}
print(document.language) print(document[1])
```
Notably, the length of the table is reported as 2 in this case, and thereâs a function ipairs to iterate over only the array part in order of increasing indices, in addition to pairs which iterates over all keys in an arbitrary order.
```
print(#document)
for i, line in ipairs(document) do
print(i, line) end
for k, v in pairs(document) do
print(k, v) end
```
Another neat piece of syntax sugar Lua gained during its DSL roots is the ability to call a function with a table initialiser as its sole argument, without having to add extra parentheses:
print {"hello"}
The rationale was that this allows for validating data very easily, as you can inspect the table, check its fields for correctness, and then return it.
But technically, this can extend to returning a completely different, transformed table, which weâre going to use to great advantage.
Combining these two little syntax sugars with the flexibility of tables allows us to invent pretty sweet DSLs, like this one for constructing GUIs that I had mentioned in my old blog post on Lua:
```
root_window {
width = 800, height = 600,
title = "he is behind the tree",
align {
horz = "center", vert = "center",
vertical_stack {
text "Hello!",
button {
text = "waff",
on_click = function ()
print(":neofox_floof:")
end,
}
},
},
}
```
Here, all the little primitives like root_window, align, vertical_stack, or text, are functions in the global scope that take in a table as an argument, and return a widget.
So naturally, whatâs stopping us from writing something like this for generating HTML?
â¦
Nothing, of course.
So letâs go ahead and do it!
Letâs establish what weâd like to accomplish with our DSL for generating HTML.
The end result weâd like to achieve is for an expression like this:
```
h.Document{
lang = "en",
h.head{
h.meta{charset = "UTF-8"},
h.title{"Hello, world!"},
},
h.body{
h.h1{"Hello, world!"},
h.p{
"This is an example of the little HTML templating framework I wrote ",
"in Lua in like 20 minutes."
},
h.p{
"As you can see, it is fully capable of generating any kind of markup ",
"you'd ever want.", h.br(),
"It can even do things like ", h.b"bold text", "!"
},
h.p"This is some embedded HTML <p></p>"
h.img{
alt = "riki sitting in pink space",
src = "https://riki.house/static/character/riki/sitting.png",
width = 2223, height = 1796,
style = "width: 20%; height: auto;"
},
},
}
```
To generate HTML equivalent to the following:
```
Hello, world!Hello, world!
This is an example of the little HTML templating framework I wrote in Lua in like 20 minutes.
As you can see, it is fully capable of generating any kind of markup you'd ever want.
It can even do things like bold text!
This is some embedded HTML <p></p>
```
I will begin by creating a namespace for our library.
In Lua, you use an ordinary table for that.
In a new file:
```
local html = {}
return html
```
From now on, treat any code examples as sandwiched between these two lines of boilerplate.
Going back to the example for a bitânote how any literal Lua string we try to put into the HTML must be escaped correctly.
For this, we will need a separate data type for differentiating HTML from plain strings.
I will call mine Html, because I like the convention of starting type names with an uppercase letter.
```
local Html = {}
function html.Html(text)
assert(type(text) == "string", "html.Html expects a string")
return setmetatable({text = text}, Html)
end
function Html:__tostring()
return self.text
end
```
We use a metatable to give the table returned by html.Html a prototype, which will help us differentiate our Html-type tables from any others.
I wrote about metatables at length in my article on implementing classes in Lua, so you might want to read that if you donât know what they are.
We also give the metatable a __tostring function, which will allow us to call the standard tostring function on the table to get the rendered HTML.
tostring is also used when printing things to the console, so thatâll also take care of that functionality.
Armed with an Html type, we can write a basic functionâno sugar yetâto produce HTML for us, given a tag and a definition table, containing the elementâs attributes and children.
```
local function write(t, ...)
local n = select('#', ...)
for i = 1, n do
table.insert(t, (select(i, ...)))
end
end
local function write_children(el, def)
for _, child in ipairs(def) do
if type(child) == "string" then
table.insert(el, child)
elseif type(child) == "table" and getmetatable(child) == Html then
table.insert(el, child.text)
elseif type(child) == "table" then
write_children(el, child)
end
end
end
function html.Element(kind, def)
local el = {"<", kind}
for k, v in pairs(def) do
if type(k) == "string" then
write(el, " ", k, '="', v, '"')
end
end
table.insert(el, ">")
write_children(el, def)
write(el, "</", kind, ">")
return html.Html(table.concat(el))
end
```
The function is called Element, again with an uppercase letter, because Iâd like to make space for the syntax sugar weâre about to add later.
HTML is case-insensitive, so weâll reserve uppercase names for specific functions, and lowercase names for element constructorsâsimilar to how JSX works with lowercase names for literal elements, and uppercase for components.
A surprising technique to note here is that we use a table to collect all the HTML text to be rendered, which we then table.concat to obtain our final string.
We do this because strings in Lua are immutable, and using the string concatenation operator a..b incurs a new allocation every time we use it, which can get expensive really quickly.
It can get especially bad if the strings in question are bigâbecause each allocation must copy the old string into the new string, thus getting worse and worse with each use of the operator.
So to avoid writing a Shlemiel the painterâs algorithm, we accumulate all the pieces into a table, and then join them together in one fell swoop.
In other languages, such as Go, this is called a string builder.
Anyways, with that out of the way, we can now construct some HTML!
Letâs try it out.
local html = require "html"
return html.Element("p", {"Hello, world!"})
Output
```
Hello, world!
```
So far, so good! Letâs keep going, and render a user comment.
Iâll use some mock data, because this isnât the time to be writing a comment section backend.
```
local function Comment(c)
return html.Element("article", {
class = "comment",
html.Element("span", {class = "user", comment.user}),
" says: ", comment.text
})
end
return Comment{
user = "riki",
text = "meow"
}
```
Output
```
riki says: meow```
Great! We can use this in pretty much the same way as React components.
Hang on, what are youâ
```
local function Comment(c)
return html.Element("article", {
class = "comment",
html.Element("span", {class = "user", comment.user}),
" says: ", comment.text
})
end
return Comment{
user = "riki",
text = ""
}
```
Output
```
riki says:```
Oh.
We forgot to escape the HTML, didnât we.
Fortunately for us, this seems like a pretty simple thing to do; it involves substituting out all the illegal characters out of our untrusted input strings into nice, safe HTML escape codes.
Letâs write a function that will do the substitution.
local escape_subs = {
["&"] = "&",
["<"] = "<",
[">"] = ">",
['"'] = """,
["'"] = "'",
}
local function escape_html(str)
return (str:gsub("([&<>\"'])", escape_subs))
end
There are a couple things to note here.
First of all, if you want to do this 100% correctly, it is not that simple.
HTML is a weird language because it embeds CSS and JS inside, and those have their own rules as to how values have to be escaped.
Iâm skipping over that because HTML escapes handle 90% of the cases for a simple blog, but youâll have to be wary around CSS and JS.
But regarding the code, whatâs with the oddly formatted string argument we pass in to string.gsub?
local function escape_html(str)
return (str:gsub("([&<>\"'])", escape_subs))
end
A lot of languages have libraries for regular expressions, but Lua aims to be really small.
So small you can put it on a microcontroller!
Meanwhile, regular expression engines tend to be really big, so bundling one with Lua would be a no-go.
Except, text processing is a really useful thing to have in a garbage collected language!
Which is why Lua invented its own small language for matching strings, called patterns.
Patterns are really simple; they offer no backtracking, so you wonât find a | operator like in regular expressions.
Among what they can do however, is matching characters from a setâas seen above.
The pattern [&<>\"'] will match one of &, <, >, \, ", and '.
Used in string.gsub, it finds all occurrences of those characters, and replace them with⦠escape_subs, a table?
local escape_subs = {
["&"] = "&",
["<"] = "<",
[">"] = ">",
['"'] = """,
["'"] = "'",
}
local function escape_html(str)
return (str:gsub("([&<>\"'])", escape_subs))
end
While we could use string.gsub like a simple plain text search-and-replace, and then chain a few of them together to replace all the illegal characters in several passes, it lets us do something a little bit more clever.
string.gsub will interpret a table-typed replacement argument as a lookup table for replacements.
If you wrap your pattern in a capture groupâthose parentheses ([&<>\"'])âstring.gsub will use whatâs matched inside those parentheses as a key to that table you provide, and replace the matched string with a string looked up from that table.
Pretty neat, huh?
This allows us to write a lookup table for all the characters we want to replace, instead of running string.gsub multiple timesâwhich means better performance (as we only scan the string once), but also means less opportunity for bugs to creep in.
For example, the following is not the correct way to implement this function.
Can you tell why?
(Hint: itâs not that any of the substituted characters mean anything special when used in a pattern.)
local function escape_html(str)
return str
:gsub("<", "<")
:gsub(">", ">")
:gsub('"', """)
:gsub("'", "'")
:gsub("&", "&")
end
Going back to the original function for a bit once again, Iâd like to explain another thing that might seem odd:
local function escape_html(str)
return (str:gsub("([&<>\"'])", escape_subs))
end
Notice the extra parentheses around the returned value?
This is because string.gsub returns two values, with one being the string after replacements, and the other being the number of replacements it made.
Therefore, we have to collapse it back to just the output string, which is most conveniently done by wrapping the function call in parentheses.
With that out of the way, we can upgrade our html.Element function to escape untrusted strings and avoid wreaking XSS havoc all over our website.
```
function html.Element(kind, def)
local el = {"<", kind}
for k, v in pairs(def) do
write(el, " ", k, '="', escape_html(v), '"')
end
table.insert(el, ">")
write_children(el, def)
write(el, "</", kind, ">")
return html.Html(table.concat(el))
end
```
Now, if someone tries to post an evil comment, their plans will be foiled:
return Comment{
user = "riki",
text = "<script>alert(1)</script>"
}
Output
```
riki says: <script>alert(1)</script>```
There is still one more important thing we have to implement for our implementation to be fully HTML-compliant, though: void elements.
Some elements in HTML cannot have children, and only have an opening tag.
Those elements include things like <img> or <input>.
We need to filter out those elements, and never emit any children or closing tags for them:
```
local void_elements = {
area = true, base = true,
br = true, col = true,
embed = true, hr = true,
img = true, input = true,
link = true, meta = true,
param = true, source = true,
track = true, wbr = true,
}
function html.Element(kind, def)
local el = {"<", kind}
for k, v in pairs(def) do
write(el, " ", k, '="', escape_html(v), '"') end
table.insert(el, ">")
if void_elements[kind] then
return html.Html(table.concat(el))
end
write_children(el, def)
write(el, "</", kind, ">")
return html.Html(table.concat(el))
end
```
Now if we try to create an <img> element, it will correctly be missing a closing tag.
return html.Element("img", {
alt = "riki sitting in pink space",
src = "https://riki.house/static/character/riki/sitting.png",
})
Output
<img alt="riki sitting in pink space" src="https://riki.house/static/character/riki/sitting.png">
And⦠thatâs pretty much all we need to create well-formed HTML elements!
That said, there is still one correctness improvement Iâd like to do.
Remember how I said that pairsâs iteration order is undefined?
Knowing how hash tables work, there is a risk of it being non-deterministic, depending on the Lua implementation.
Now, I donât know of any implementations of Lua that would randomise hash table order between runs of the program like Rust does, but it does feel kind of awry to be relying on an implementation detail that can change between versions like that.
Who knows what LuaJIT could be doing?
And so, the last improvement Iâd like to make is to sort all the attributes alphabetically to ensure implementation differences cannot introduce any non-determinism.
```
local function attr_cmp(a, b)
return a[1] < b[1]
end
function html.Element(kind, def)
local attr = {}
for k, v in pairs(def) do
if type(k) == "string" then
table.insert(attr, {k, v})
end
end
table.sort(attr, attr_cmp)
local el = {"<", kind}
for _, a in ipairs(attr) do
write(el, " ", a[1], '="', escape_html(a[2]), '"')
end
table.insert(el, ">")
table.insert(el, ">")
end
```
This is the kind of mistake that you only make once in life.
Ask me how I know.
With that out of the way, we can now move onto some ergonomic improvements, because typing html.Element("p", {"This is some text"}) just to display some silly text gets old pretty quick.
So does reading it.
Remember how I hyped up all the cool domain specific language stuff Lua is capable of, only to never follow up on it?
This is that section where I follow up on it.
Letâs start with the most important bit: how do we make it so that we can create elements using the syntax html.p{} instead of html.Element("p", {})?
If youâve read my blog post on implementing classes, you may remember that metatables have an __index metamethod that allows us to override the indexing operator a[b] (and likewise a.b) for keys that are not in the table.
Therefore, letâs override htmlâs __index, to return a function which creates an element with the given nameâaccepting only a single table argument, therefore making it possible to use the shorthand f{} syntax with it.
```
setmetatable(html, html)
function html.__index(html, key)
local function thunk(def)
return html.Element(key, def)
end
html[key] = thunk return thunk
end
```
Note how we save the function in the html namespace for later, so that subsequent calls to it do not recreate itâreducing the amount of meaningless work for the computer to do.
Now, lo and behold, our comment example from before can become much cleaner:
```
local function Comment(c)
return html.Element("article", {
class = "comment",
html.Element("span", {class = "user", comment.user}),
" says: ", comment.text
})
end
local function Comment(c)
return html.article{
class = "comment",
html.span{class = "user", comment.user},
" says: ", comment.text
}
end
```
An annoying thing is that custom elements are still a bit of a bother to use.
For those of you unfamiliar with custom elements, itâs an API that allows you to declare custom HTML elements from JavaScript.
```
class Clock extends HTMLElement {
connectedCallback() {
this.format = this.getAttribute("data-format") ?? "short";
this.update();
setInterval(() => this.update(), 1000);
}
update() {
let now = new Date();
this.innerText = now.toLocaleTimeString([], { timeStyle: this.format });
}
}
customElements.define("riki-clock", Clock);
```
With this script, when we write <riki-clock data-format="short"></riki-clock> in our HTML, the extra behaviour from the Clock class will be attached to it.
A notable thing is that all names of custom elements have to include a dash - in them, to encourage namespacing.
Itâs a pretty useful API, and I use it all the time in my web apps, so itâd be good if constructing those custom elements in our Lua templating engine didnât have to look like this:
return html["riki-clock"]{["data-format"] = "short"}
Output
<riki-clock data-format="short"></riki-clock>
Thatâs not even shorter than the raw HTML!
Fortunately, underscores arenât really used at all in HTML tags and attribute names, so I think the right opinion to take here is to substitute them into dashes automatically.
```
function html.Element(kind, def)
local attr = {}
for k, v in pairs(def) do
if type(k) == "string" then
local k = k:gsub("_", "-")
table.insert(attr, {k, v})
end
end
table.sort(attr, attr_cmp)
local el = {"<", kind}
end
function html.__index(html, key)
key = key:gsub("_", "-")
local function thunk(def)
return html.Element(key, def)
end
html[key] = thunk return thunk
end
```
With that out of the way, the riki clock can now be assembled with much less line noise than before.
return html.riki_clock{data_format = "short"}
Output
<riki-clock data-format="short"></riki-clock>
As another space for improvement, Iâd like to turn your attention to how <img> tags have to be constructed right now.
print(h.img{
alt = "riki sitting in pink space",
src = "https://riki.house/static/character/riki/sitting.png",
width = "2223", height = "1796",
})
If you compare this to the version we wanted to have at the beginning, you will notice one small detail: in this version, the width and height attributes have to be provided as strings!
Thatâs kind of annoying, especially if the sizes are coming in from some outside function that returns them as integers.
To deal with that, we will add one last line to html.Element, to convert all attribute values to strings automatically.
```
function html.Element(kind, def)
local attr = {}
for k, v in pairs(def) do
if type(k) == "string" then
local k = k:gsub("_", "-")
local v = tostring(v)
table.insert(attr, {k, v})
end
end
table.sort(attr, attr_cmp)
local el = {"<", kind}
end
```
To static typing-minded folks, this might look horrible, but I feel like this is one of those cases where being liberal in what you accept and strict in what you produce gives you some really nice results.
Besides, this behaviour is consistent with the rest of Lua, where concatenating strings will call tostring implicitly:
print("Score: "..1)
As one last minor improvement, letâs make the function call itself a little bit more ergonomic, and make it easier to construct tags containing only textâsuch as <b>bold</b>âor nothing at all, such as <br> or <wbr>.
```
function html.Element(kind, def)
if type(def) == "string" then
def = {def}
end
if def == nil then
def = {}
end
local attr = {}
for k, v in pairs(def) do
if type(k) == "string" then
end
```
To tie the library together, letâs make it a bit easier to start a document with a <!doctype html> at the beginning.
function html.Document(def)
return html.Html("<!doctype html>"..tostring(html.Element("html", def)))
end
And there you go!
The whole library can be wrapped up in 117 lines of code.
Hereâs the full code listing, feel free to add it into your projects and tweak it to taste:
```
local html = {}
setmetatable(html, html)
local escape_subs = {
["&"] = "&",
["<"] = "<",
[">"] = ">",
['"'] = """,
["'"] = "'",
}
local function escape_html(str)
return (str:gsub("([&<>\"'])", escape_subs))
end
local function write(t, ...)
local n = select('#', ...)
for i = 1, n do
table.insert(t, (select(i, ...)))
end
end
local Html = {}
function html.Html(text)
assert(type(text) == "string", "html.Html expects a string")
return setmetatable({text = text}, Html)
end
function Html:__tostring()
return self.text
end
local void_elements = {
area = true,
base = true,
br = true,
col = true,
embed = true,
hr = true,
img = true,
input = true,
link = true,
meta = true,
param = true,
source = true,
track = true,
wbr = true,
}
local function attr_cmp(a, b)
return a[1] < b[1]
end
local function write_children(el, def)
for _, child in ipairs(def) do
if type(child) == "string" then
table.insert(el, escape_html(child))
elseif type(child) == "table" and getmetatable(child) == Html then
table.insert(el, child.text)
elseif type(child) == "table" then
write_children(el, child)
end
end
end
function html.Element(kind, def)
if type(def) == "string" then
def = {def}
end
if def == nil then
def = {}
end
local attr = {}
for k, v in pairs(def) do
if type(k) == "string" then
local k = k:gsub("_", "-")
local v = tostring(v)
table.insert(attr, {k, v})
end
end
table.sort(attr, attr_cmp)
local el = {"<", kind}
for _, a in ipairs(attr) do
write(el, " ", a[1], '="', escape_html(a[2]), '"')
end
table.insert(el, ">")
if void_elements[kind] then
return html.Html(table.concat(el))
end
write_children(el, def)
write(el, "</", kind, ">")
return html.Html(table.concat(el))
end
function html.Document(def)
return html.Html("<!doctype html>"..tostring(html.Element("html", def)))
end
function html.__index(html, key)
key = key:gsub("_", "-")
local function thunk(def)
return html.Element(key, def)
end
html[key] = thunk return thunk
end
return html
```
I hear you say, âriki, but this library is incomplete!
It doesnât even have ifs and fors like Handlebars or maud do.â
Here is how you would implement ifs:
```
local content_editable, username = ...
local function iif(cond, t, f)
if cond then return t else f end
end
return html.p{
contenteditable = iif(content_editable, "", nil),
"Hello, "..(username or "guest").."!",
username and html.a{href = "/settings", "settings"} or "",
}
```
In short:
- For conditionally including text or HTML, you can use the Lua
cond and true_value or false_valueidiom.
Note that you donât want any stray nils in the table, because that would break iteration via ipairsâso you have to use an empty string or an empty table.
- For conditionally enabling attributes, you can write an iif (immediate if) function that returns one of two options.
The cond and t or f idiom I mentioned above canât work due to nil being false, and therefore the or operator always taking the left branchâwhich will not work as expected if cond == false.
And here is how you would implement fors:
```
local users = ...
local function map(t, f)
local r = {}
for i, v in ipairs(t) do
r[i] = f(v)
end
return r
end
return html.ul{
map(users, function (user)
return html.li{user.name}
end)
}
```
Of course, an immediately invoked function will also do fine, but having a map function around in your codebase is really handy for transforming all kinds of data.
And thatâs about it, I think.
Hope you enjoyed the post!
It took me many hours to finish.
And I hope you learned something nice about Lua :3
Thank you to my good friend Anya for giving me some thorough feedback on a draft of this post.