Skip to content

The template language

Grammar

The whole syntax, in EBNF.

This is the whole syntax of a template, in EBNF. { x } repeats x zero or more times, [ x ] makes it optional, ( a | b ) is a choice, and quoted text is literal. The lexical structure defines name, number and string.

Templates

template     = { declaration | node } ;

Declarations and nodes can be mixed at the top level, though declarations conventionally come first. Only the top level holds declarations.

Declarations

See Declarations and Data and effects.

declaration  = state | let | on | load | timer ;

state        = "state" name "=" expr ;
let          = "let" name "=" expr ;
on           = "on" name [ "(" [ name { "," name } ] ")" ] "=" steps ;
load         = "load" name "=" request ;
timer        = ( "every" ( "frame" | expr ) | "after" expr )
               [ "when" expr ] "=" steps ;

request      = "get" "(" expr [ "," expr ] ")"
             | "post" "(" expr "," expr [ "," expr ] ")" ;

steps        = step { "," step } ;
step         = name "<-" expr | command ;
command      = "emit" "(" expr [ "," expr ] ")"
             | "navigate" "(" expr ")"
             | ( "get" | "delete" ) "(" expr [ "," name [ "," name ] ] ")"
             | ( "post" | "put" | "patch" ) "(" expr "," expr [ "," name [ "," name ] ] ")"
             | "reload" "(" name ")" ;

Nodes

See Nodes and Attributes and values.

node         = element | string | if | for ;

element      = name [ "(" [ attribute { [ "," ] attribute } [ "," ] ] ")" ]
               [ string | block ] ;
block        = "{" { node } "}" ;

attribute    = name [ ":" expr ]
             | "@" name ":" name [ "(" [ expr { "," expr } [ "," ] ] ")" ] ;

if           = "if" expr block [ "else" ( if | block ) ] ;
for          = "for" name "in" expr block ;
  • An element's string must start on the element's line (the line rule).
  • An element has text or children, not both.
  • In an if or for head, a { starts the block, so a record literal there needs parentheses.

Expressions

See Expressions. Each level binds tighter than the one above it, and every binary operator groups from the left: a - b - c is (a - b) - c.

expr         = conditional ;
conditional  = "if" expr "then" expr "else" expr | coalesce ;
coalesce     = or { "??" or } ;
or           = and { "||" and } ;
and          = compare { "&&" compare } ;
compare      = pipe { ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) pipe } ;
pipe         = additive { "|>" call } ;
additive     = term { ( "+" | "-" ) term } ;
term         = unary { ( "*" | "/" | "%" ) unary } ;
unary        = ( "!" | "-" ) unary | postfix ;
postfix      = primary { "." name } ;

primary      = number | string | "true" | "false" | "null"
             | name                                  (* a path: post.title *)
             | call | list | record | update
             | conditional
             | "(" expr ")" ;

call         = name "(" [ argument { "," argument } [ "," ] ] ")" ;
argument     = expr | lambda ;
lambda       = name "->" expr
             | "(" [ name { "," name } ] ")" "->" expr ;

list         = "[" [ expr { "," expr } [ "," ] ] "]" ;
record       = "{" [ field { "," field } [ "," ] ] "}" ;
field        = name [ ":" expr ] ;                   (* { title } is { title: title } *)
update       = "{" expr "|" field { "," field } [ "," ] "}" ;
  • .name after a primary is a field. A path's dots are part of its name.
  • A lambda is only allowed as a call's argument.
  • An if expression extends as far right as it can: 1 + if c then 1 else 2 + 3 adds 2 + 3 in its else.
  • !a == b is rejected: write a != b, or (!a) == b.