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.

Search Document