glimr/loom/parser
Template Parser
The lexer produces a flat stream of tokens, but templates are inherently hierarchical — components nest inside each other, conditionals span siblings, and slots cross component boundaries. This module transforms that flat token stream into an AST that preserves these structural relationships so downstream code generation can emit correct, nested output without re-discovering structure.
Types
Different template constructs need distinct code generation strategies — text is emitted verbatim, variables require escaping or lookups, control flow needs branching logic, and components trigger recursive rendering. A sum type lets the code generator pattern match exhaustively on every construct.
pub type Node {
TextNode(String)
VariableNode(name: String, line: Int)
RawVariableNode(name: String, line: Int)
SlotNode(name: option.Option(String), fallback: List(Node))
SlotDefNode(name: option.Option(String), children: List(Node))
AttributesNode(base_attributes: List(lexer.ComponentAttr))
IfNode(
branches: List(#(option.Option(String), Int, List(Node))),
)
EachNode(
collection: String,
items: List(String),
loop_var: option.Option(String),
body: List(Node),
line: Int,
)
ComponentNode(
name: String,
attributes: List(lexer.ComponentAttr),
children: List(Node),
)
ElementNode(
tag: String,
attributes: List(lexer.ComponentAttr),
children: List(Node),
)
}
Constructors
-
TextNode(String) -
VariableNode(name: String, line: Int) -
RawVariableNode(name: String, line: Int) -
SlotNode(name: option.Option(String), fallback: List(Node))SlotNode outputs slot content with optional fallback. Used in component templates:
, fallback -
SlotDefNode(name: option.Option(String), children: List(Node))SlotDefNode defines content to pass to a slot when using a component. Used inside
: content -
AttributesNode(base_attributes: List(lexer.ComponentAttr))AttributesNode holds optional base attributes that will be merged with props.attributes
-
IfNode(branches: List(#(option.Option(String), Int, List(Node)))) -
EachNode( collection: String, items: List(String), loop_var: option.Option(String), body: List(Node), line: Int, ) -
ComponentNode( name: String, attributes: List(lexer.ComponentAttr), children: List(Node), ) -
ElementNode( tag: String, attributes: List(lexer.ComponentAttr), children: List(Node), )
Template authors need actionable error messages when their markup is malformed. Structured error variants let the compiler surface the specific problem — a mismatched closing tag, a dangling else branch, or a directive in the wrong position — rather than a generic parse failure.
pub type ParserError {
UnexpectedLmElse
UnexpectedLmElseIf
LmElseAfterLmElse
LmElseIfAfterLmElse
UnexpectedComponentEnd(name: String)
UnexpectedElementEnd(tag: String)
UnexpectedSlotDefEnd
UnclosedComponent(name: String)
UnclosedElement(tag: String)
UnclosedSlot(name: option.Option(String))
UnexpectedToken(token: lexer.Token)
DirectiveAfterContent(directive: String, line: Int)
DuplicatePropsDirective(line: Int)
}
Constructors
-
UnexpectedLmElse -
UnexpectedLmElseIf -
LmElseAfterLmElse -
LmElseIfAfterLmElse -
UnexpectedComponentEnd(name: String) -
UnexpectedElementEnd(tag: String) -
UnexpectedSlotDefEnd -
UnclosedComponent(name: String) -
UnclosedElement(tag: String) -
UnclosedSlot(name: option.Option(String)) -
UnexpectedToken(token: lexer.Token) -
DirectiveAfterContent(directive: String, line: Int)@props or @import directive appeared after template content
-
DuplicatePropsDirective(line: Int)Multiple @props directives found
Code generation needs a single entry point that carries all the information extracted from a template file — imports, props, content nodes, and liveness — so it can emit a complete module without re-parsing or requiring multiple passes over the token stream.
pub type Template {
Template(
imports: List(String),
props: List(#(String, String)),
nodes: List(Node),
is_live: Bool,
)
}
Constructors
-
Template( imports: List(String), props: List(#(String, String)), nodes: List(Node), is_live: Bool, )Arguments
- imports
-
Import statements from @import directives
- props
-
Props from @props directive (name, type pairs)
- nodes
-
Content nodes
- is_live
-
Whether this template contains l-on:* or l-model attributes and should be treated as a “live” template with WebSocket connection
Values
pub fn parse(
tokens: List(lexer.Token),
) -> Result(Template, ParserError)
Entry point for the parser. Directives are extracted first because they affect how the generated module is structured (imports, function signatures) and must not appear mixed with content. The remaining tokens are then parsed into the node tree that drives code generation.