What it looks like
<h1>{{ event.name }}</h1>
<f-if test="tickets.size > 0">
<ul>
<f-for each="t in tickets">
<li>{{ t.name }} β {{ t.price_cents | money: 'eur' }}</li>
</f-for>
</ul>
<f-else/>
<p>{{ 'public.event.sold_out' | t }}</p>
</f-if>
Two things to scan for and no more: {{ β¦ }} puts a
value in, <f-β¦> is a directive. Everything else is copied
through untouched β figh never parses your HTML.
What it refuses to do
- Run forever there is no
while, no user functions and so no recursion, and the include graph is static β cycles are a compile error, not a hang. The timeout is defence in depth, not the plan. - Emit raw HTML by accident
{{ }}always escapes. Unescaped output is an element,<f-raw>, never a filter β so it can't hide inside an attribute, and one grep finds every instance in a codebase. - Reach anything no filesystem, no network, no clock, no randomness. Partials resolve only through a loader you supply. A template is a pure function of the data you hand it.
- Disagree with itself implemented twice β Go and JavaScript β and a corpus of shared cases runs through both, asserting byte-identical output including error text and column numbers. Edit in a browser, render on a server, get the same page.
Why not Liquid
Liquid is the obvious answer and it nearly was ours. Three things sent us the other way.
- It doesn't escape
{{ }}emits raw, so every value has to be hand-escaped at the boundary. Miss one and it's an injection, in markup somebody else wrote. - One engine, one runtime we needed the same language in a browser editor with identical semantics. Two independent implementations of a language nobody specified is a bug generator.
- Templates read files the Ruby implementation's default include resolves real paths. Ours has no filesystem to resolve against.
figh is smaller than Liquid on purpose. No assign,
no capture, no arithmetic, fourteen filters. If a template needs to
compute something, that belongs in the data you pass it β where you can test
it.
The name
figh is Irish for weaves β from Old Irish figid, from Proto-Celtic *wegyeti, the same root that gives English weft. It's pronounced roughly fee; the gh is silent. It was called weft until we discovered how many things already are.