Wanted to know how hard it is to do like the Handlebars folks and make a prettier plugin for Liquid.
Our problem: Liquid is a templating language. As such, its Abstract Syntax Tree (AST) has no notion of what its text nodes contain.
And since prettier is really a function(ast) -> string
, you can't make pretty Liquid + HTML if the AST you have has no notion of HTML.
It's possible though:
prettier.mp4
First, we need to make a Liquid/HTML parser that supports a stricter form of Liquid—One that can form a tree.
This works:
{% for product in all_products %}
<img
src="{{ product.featured_image | image_url }}"
loading="lazy"
>
{% endfor %}
Since it can be represented as this tree:
But this doesn't because the div is closed before the if tag was closed:
{% if A %}<div>{% endif %}</div>
- We parse the liquid source code into an Liquid/HTML AST.
- Our harc/ohm grammar tokenizes the source code.(Link to LiquidHTML grammar.)
- From Ohm's tokens, we build a Concrete Syntax Tree (CST). (Link to Grammar->CST code.)
- From the nodes in the CST, we build an AST. (Link to CST->AST code.)
- From the AST, we build a
Doc
that prettier then prints for us.(Link to LiquidHTML printer)
Here's a flowchart that roughly illustrates the process.
%%{init: { 'theme': 'dark', 'themeVariables': { 'fontFamily': 'monospace'} } }%%
flowchart TB
subgraph INPUT
s1["#lt;ul#gt;{% for el in col %}<br>#lt;li class=#quot;{% cycle 'odd', 'even' %}#quot;#gt;<br> {{ el }}<br>#lt;/li#gt;<br>{%endfor%}<br>#lt;/ul#gt;"]
end
subgraph TOKENS ["OHM TOKENS"]
direction TB
t1["#lt;ul#gt;"] -->
t2["{% for el in col %}"] --> t3
subgraph t3 ["#lt;li class=#quot;{% cycle 'odd', 'even' %}#quot;#gt;"]
t3.1["#lt;li"] ---
t3.2["class="] ---
t3.3["{% cycle 'odd', 'even' %}"] ---
t3.4["#gt;"]
end
t3 -->
t4["{{ el }}"] -->
t5["#lt;/li#gt;"] -->
t6["{%endfor%}"]
end
subgraph CST
direction TB
c1["HtmlTagOpen#ul"] --->
c2["LiquidTagOpen#for el in col"] --->
c3["HtmlTagOpen#li"]
c3 -->
c4["LiquidDrop#el"] -->
c5["HtmlTagClose#li"] -->
c6["LiquidTagClose#for"]
c3 .- attributes .-
c3a["HtmlAttribute"]
c3a .- name .- c3an["class"]
c3a .- value .- c3av
c3av["LiquidTag#cycle 'odd', 'even'"]
end
subgraph AST
direction TB
a1["HtmlElement#ul"] -- children -->
a2["LiquidTag#for el in col"] -- children -->
a3["HtmlElement#li"]
a3 -- children -->
a4["LiquidDrop#el"]
a3 -- attributes -->
a3a["HtmlAttribute"]
a3a -- name --> a3an["class"]
a3a -- value --> a3av
a3av["LiquidTag#cycle 'odd', 'even'"]
end
subgraph OUTPUT
o1["#lt;ul#gt;<br> {% for el in col %}<br> #lt;li class=#quot;{% cycle 'odd', 'even' %}#quot;#gt;<br> {{ el }}<br> #lt;/li#gt;<br> {% endfor %}<br>#lt;/ul#gt;"]
end
INPUT -- "ohmGrammar.match(input)" --> TOKENS
TOKENS -- "toCST(tokens)" --> CST
CST -- "toAST(cst)" --> AST
AST -- "prettier.print(ast, options)" --> OUTPUT
style TOKENS text-align: left;
style s1 text-align:left
style o1 text-align:left