‘Iacta alea est’
Over the last few days I have been exploring a way to write a small library (script?) that would give me a nice Ruby DSL for writing HTML documents.
The inspiration comes from the Elm HTML library which I believe forms a key part of Elm’s delightful usability as a language for the front-end. For me, the main draw is avoiding the context switch between the programming language and a templating language.
There are side benefits, like having a thin and faithful abstraction layer over the underlying HTML document, but the primary benefit is being able to deal with only one programming model in your head.
The goal was to arrive at something like this:
HTMLDocument.new do |d|
d.el :html, lang: "en-GB" do
d.el :head do
d.el :title do
d.el :text do
"Hello, World!"
end
end
end
end
end.to_s
It is in many ways also inspired by Markaby but eschews the magic (e.g. special syntax for class and id definition) in favour of a more faithful modelling of HTML tags and attributes.
And of course, the desire for simplicity is inspired by that which I have seen in the Ruby libraries written by Michel Martens.
I did manage to arrive at a solution for generating HTML from a document written in the syntax above. However, I was very unhappy with the approach I took.
Nevertheless, sometimes making it work is the first step. Then you can make it beautiful.
There are serious benefits to sleeping on problems. A couple of days ago I resigned myself to what felt like defeat. The next morning I broke through the last few barriers to arrive at a solution.
Today, in a flash of inspiration, I ripped out most of the code that I wrote yesterday based on a growing hunch:
The nested block “grandparent” wrapping I stumbled upon makes me think we can generate the HTML string on the fly instead of some flattened tree or AST. It would greatly reduce the surface area of the code and get rid of the need for an underlying data abstraction completely. I’d like to try that next.
So this is what I ended up with in just a couple of minutes:
class HTMLDocument
def initialize
@doc = ""
yield self if block_given?
end
def el(type, attrs={})
case type
when :text
@doc << yield
when :input
@doc << opening_tag(type, attrs)
else
@doc << opening_tag(type, attrs)
yield
@doc << closing_tag(type)
end
end
def opening_tag(name, attrs)
if attrs.empty?
"<#{name}>"
else
"<#{name} #{attributes(attrs)}>"
end
end
def closing_tag(name)
"</#{name}>"
end
def attributes(attrs)
attrs.map do |key, value|
%Q(#{key}="#{value}")
end.join(" ")
end
def to_s
@doc
end
end
That’s it. From 75 down to 36 significant lines of code. No data structures, no complicated tree surgery, just some straightforward string concatenation. It doesn’t even need any explanation.
I think it’s simple. I think it’s beautiful.
What do you think?
—Tuesday 9th March 2021.