‘Iacta alea est’
I wrote about building a static site generator, Sew, with Mote as a templating library. Now that I have reached a templating API with Hypertext DSL that I am happy with I’d like to have a go at replacing Mote with Hypertext in Sew.
Sew is itself a single 97 SLOC file. It contains a Context
class which handles the rendering of templates with layouts and partials using Mote. I have reduced the snippet below to just the rendering aspects. (@site
/site
) is an OpenStruct
object containing an array of pages and an array of data files.)
class Context
…
def render(page)
@site.page = page
@site.content = mote(page.body)
partial("_layout")
end
def mote(content)
Mote.parse(content, self, site.each_pair.map(&:first))[site]
end
def partial(template)
localized = sprintf("%s.%s.mote", template, site.page.locale)
if File.exist?(localized)
mote(File.read(localized))
else
mote(File.read(sprintf("%s.mote", template)))
end
end
end
As a short refresher, the Context
takes the site struct on initialization and then each page is rendered one by one with this site object as the params. The site object is modified slightly for each page with @site.page
(the current page details) and @site.content
(the rendered template from that page for inserting into the layout).
This is how the context is used as part of the Sew::build
command:
context = Context.new(site)
…
site.pages.each do |page|
File.open(page.destination, "w") do |file|
file.write context.render(page)
end
end
So basically one context object is used and each page uses the same object but with a different page. With Hypertext we’ll need one object per page so we can rejig things around a little:
- context = Context.new(site)
…
+ site.layout = File.read("_layout.ht")
site.pages.each do |page|
+ site.page = page
File.open(page.destination, "w") do |file|
- file.write context.render(page)
+ file.write ???
end
end
We set the layout globally, assuming one layout for the whole site. We know that with Hypertext we won’t need a double pass, so we can remove the assignment of site.content
and rely on site
with site.page
. We assign site.page
to the current page (defined by the loop).
And now we need to dovetail our Hypertext DSL Template class into our rendering flow. For now, we can ditch the Sew::Context
and replace it with Sew::Template
.
We know we’re going to need the fragment rendering versus the file rendering, since we have already loaded the file contents into memory.
And since site
is an OpenStruct we’ll need to use OpenStruct#each_pair
instead of Hash#each
to do the variable assignment.
(As an aside, this is a perfect example of knowing your tools well and knowing the problem space well since you can match the two easily. The alternative is to write a hodge-podge of decision logic to cater for all cases.)
class Sew
…
class Template < Hypertext::DSL
def initialize(site)
site.each_pair do |key, val|
instance_variable_set(sprintf("@%s", key), val)
end
@ht = Hypertext.new
render @layout
end
def render(string)
instance_eval string
end
end
end
I’ve opted for render
rather than fragment
or partial
here because I think it reads better in this context.
If we want partials then we’ll have to read from file explicitly (which we’re not doing with layout and template) so I’ll introduce the partial
method to do just that
def partial(filename)
render File.read(filename)
end
As you may have noticed I’m still not enamoured by Frankenstein’s method which switches on (non-)existence of a file.
And for those familiar with Sew I’m leaving localisation out for now to keep things simple.
If we now return to the Sew::build
command then we can insert our Template
class for rendering pages, replacing our ???
placeholder.
site.layout = File.read("_layout.ht")
site.pages.each do |page|
site.page = page
File.open(page.destination, "w") do |file|
file.write Template.new(site).to_s
end
end
Given a layout and template and a partial in Hypertext DSL (and Sew) form:
# _layout.ht
html lang: "en-GB" do
head do
title do
text @page.title
end
end
body do
render @page.body
end
partial "_footer.ht"
end
# index.ht
---
title: Sew Me
---
h1 do
text "Welcome"
end
p do
text "A paragraph sewn together."
end
# _footer.ht
footer do
text "You put your foot-er in it."
end
Then running $ sew build
, lo and behold, produces the following output:
<html lang="en-GB">
<head>
<title>
Sew Me
</title>
</head>
<body>
<h1>
Welcome
</h1>
<p>
A paragraph sewn together.
</p>
</body>
<footer>
You put your foot-er in it.
</footer>
</html>
Wonderful. A full gist shows all of the code together. Sew is down to 72 SLOC. I can live with that.
—Thursday 8th April 2021.