Building a Handlebars/Mustache Engine with ANTL4 MCP Server
A working parser in an afternoon
Building a Handlebars/Mustache Engine with ANTL4 MCP Server
A working parser in an afternoon
I wanted to build a templating engine.
Not use one — build one. A Mustache-style syntax that could render templates like this:
Hello, {{name}}!
{{#users}}
- {{name}} ({{email}})
{{/users}}
{{^premium}}
Upgrade to premium for more features.
{{/premium}}
The Mustache spec defines the syntax precisely. Variables, sections, inverted sections, partials, comments — all documented in YAML test cases.
Here’s how I built it using the ANTLR4 MCP Server.
Setup ANTLR4 MCP Server
Ensure Docker is running and Pull the Docker image:
docker pull sshailabh1/antlr4-mcp-server:latest
Configure Claude Desktop:
{
"mcpServers": {
"antlr4": {
"command": "docker",
"args": [
"run",
"-i",
" - rm",
"sshailabh1/antlr4-mcp-server:latest"
]
}
}
}
Restart Claude. Done.
The Conversation
I started by describing what I needed:
> **Me:** I want to parse Mustache templates. The syntax has:
> - Variables: `{{name}}`
> - Unescaped: `{{{name}}}` or `{{&name}}`
> - Sections: `{{#items}}…{{/items}}`
> - Inverted sections: `{{^items}}…{{/items}}`
> - Comments: `{{! this is ignored }}`
> - Partials: `{{> header}}`
>
> Plain text outside tags should be captured as-is.
>
> Write a grammar with lexer modes and validate it.
**Claude:**
> I'll create a grammar using lexer modes to switch between text and tag contexts.
ANTLR4 grammar:
lexer grammar MustacheLexer;
// Default mode - capturing text
TEXT : (~[{] | '{' ~[{])+ ;
OPEN_UNESCAPED : '{{{' -> pushMode(TAG_MODE) ;
OPEN_COMMENT : '{{!' -> pushMode(COMMENT_MODE) ;
OPEN : '{{' -> pushMode(TAG_MODE) ;
mode TAG_MODE;
CLOSE_UNESCAPED : '}}}' -> popMode ;
CLOSE : '}}' -> popMode ;
HASH : '#' ;
SLASH : '/' ;
CARET : '^' ;
AMP : '&' ;
GT : '>' ;
DOT : '.' ;
ID : [a-zA-Z_][a-zA-Z0–9_.]* ;
WS_TAG : [ \t]+ -> skip ;
mode COMMENT_MODE;
COMMENT_TEXT : (~[}] | '}' ~[}])* ;
CLOSE_COMMENT : '}}' -> popMode ;
✅ Lexer grammar validates.
ANTLR4 parser:
parser grammar MustacheParser;
options { tokenVocab = MustacheLexer; }
template : element* EOF ;
element
: TEXT
| variable
| unescapedVariable
| section
| invertedSection
| partial
| comment
;
variable : OPEN ID CLOSE ;
unescapedVariable
: OPEN_UNESCAPED ID CLOSE_UNESCAPED
| OPEN AMP ID CLOSE
;
section : OPEN HASH ID CLOSE element* OPEN SLASH ID CLOSE ;
invertedSection : OPEN CARET ID CLOSE element* OPEN SLASH ID CLOSE ;
partial : OPEN GT ID CLOSE ;
comment : OPEN_COMMENT COMMENT_TEXT? CLOSE_COMMENT ;
✅ Parser grammar validates. Let me test with your template…
Input:
Hello, {{name}}!
{{#users}}
- {{name}} ({{email}})
{{/users}}
{{^premium}}
Upgrade to premium.
{{/premium}}
Parse tree:
(template
(element Hello, )
(element (variable {{ name }}))
(element !\n\n)
(element (section {{# users }}
(element \n - )
(element (variable {{ name }}))
(element ()
(element (variable {{ email }}))
(element )\n)
{{/ users }}))
…
)
✅ Parses correctly. Sections are properly nested.
Catching Edge Cases
I pushed further:
> **Me:** What about standalone tags? In Mustache spec, if a tag is alone on a line, the surrounding whitespace should be stripped.
**Claude:**
> That's a semantic rule, not a syntactic one. The grammar captures all whitespace as TEXT. You'd handle standalone detection in a post-processing visitor.
>
> However, let me check if there's any ambiguity with the current rules…
> ⚠️ Running ambiguity detection…
>
> Found: The TEXT rule `(~[{] | '{' ~[{])+` can conflict when input contains a single `{` followed by EOF. Adding boundary handling…
The MCP server caught an edge case automatically.
Handling Triple Braces
The Mustache spec supports {{{unescaped}}} — three braces for raw HTML output.
> **Me:** How do we handle `{{{name}}}` vs `{{name}}`?
**Claude:**
> In the lexer, rule order matters. `OPEN_UNESCAPED : '{{{'` is defined before `OPEN : '{{'`, so it matches first. ANTLR uses longest-match-first, so `{{{` wins over `{{`.
>
> Let me verify with a test case…
**Input:** `Hello {{{raw}}} and {{escaped}}`
**Parse tree:**
(template (element Hello ) (element (unescapedVariable {{{ raw }}})) (element and ) (element (variable {{ escaped }})) )
> ✅ Correctly distinguishes triple from double braces.
Adding Dot Notation
Mustache supports {{user.name}} for nested properties.
> **Me:** Add support for dotted paths like `{{user.address.city}}`
**Claude:**
> For proper AST building, let's make the path explicit:
```antlr
path : ID (DOT ID)* ;
variable : OPEN path CLOSE ;
✅ Validates. Testing
{{user.address.city}}…Parse tree shows:
(path user . address . city)Each segment is now separately accessible in your visitor.
Generating the Parser
When satisfied:
> **Me:** Generate a Python parser with visitor support.
✅ Generated 6 files:
- MustacheLexer.py
- MustacheParser.py
- MustacheListener.py
- MustacheVisitor.py
- MustacheLexer.interp
- MustacheParser.interp
I implemented a simple renderer in Python:
Spec Compliance
The Mustache spec defines test cases in YAML:
- name: Basic variable
template: "Hello, {{name}}!"
data: { name: "World" }
expected: "Hello, World!"
Running my implementation against the spec and passes all tests except whitespace issue ones.
Why Templating Grammars Are Interesting
Templating languages have unique challenges:
-
Lexer modes — Switching between text and tag context
-
Mixed content — Most input is passthrough, some is parsed
-
Overlapping patterns —
{{{vs{{ -
Balanced structures— Sections must match
-
Whitespace sensitivity — Standalone tag rules
The MCP server handles these well because:
-
It validates each piece as you build
-
It catches ambiguities before they become runtime bugs
-
It tests parsing without regenerating code
-
It explains why things work
Beyond Mustache
The same approach works for any templating language:
Handlebars:
{{#each users}}
{{#if active}}
Hello, {{name}}!
{{/if}}
{{/each}}
Jinja2:
{% for user in users %}
{{ user.name | upper }}
{% endfor %}
EJS:
<% users.forEach(function(user) { %>
<li><%= user.name %></li>
<% }); %>
Each has the same fundamental challenge: mixed content with mode switching.
Try It Yourself
- Pull the Docker run image for MCP Server:
docker pull sshailabh1/antlr4-mcp-server:latest
- Add to Claude/ChatGPT/Cursor/Cline config:**
{
"mcpServers": {
"antlr4": {
"command": "docker",
"args": [
"run",
"-i",
" - rm",
"sshailabh1/antlr4-mcp-server:latest"
]
}
}
}
- Start with a simple template and iterate:
“I want to parse templates with
{{variable}}syntax. Write a grammar with lexer modes.”
References
If this helped you, give it a clap 👏 and [⭐ star the ANTLR4 MCP Server] on GitHub. Happy Coding!!
메타데이터
- post_id
- ef7104b3b341
- slug
- building-a-handlebars-mustache-engine-with-antl4-mcp-server-ef7104b3b341
- url
- https://medium.com/@shashankshailabh/building-a-handlebars-mustache-engine-with-antl4-mcp-server-ef7104b3b341
- canonical_url
- https://medium.com/@shashankshailabh/building-a-handlebars-mustache-engine-with-antl4-mcp-server-ef7104b3b341
- author_url
- https://medium.com/@shashankshailabh
- status
- ok
- fetched_at
- 2026-07-13 18:19:34