// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Parser state
struct Parser {
tokens : Array[Token]
mut pos : Int
}
///|
pub fn Parser::new(tokens : Array[Token]) -> Parser {
{ tokens, pos: 0 }
}
///|
fn Parser::peek(self : Parser) -> Token? {
if self.pos >= self.tokens.length() {
None
} else {
Some(self.tokens[self.pos])
}
}
///|
fn Parser::advance(self : Parser) -> Token? {
if self.pos >= self.tokens.length() {
None
} else {
let tok = self.tokens[self.pos]
self.pos += 1
Some(tok)
}
}
///|
fn Parser::skip_newlines(self : Parser) -> Unit {
while self.peek() is Some(Newline) {
let _ = self.advance()
}
}
///|
/// Parse tag interpolation content (e.g., "strong important" or "a(href=\"/\") here")
fn parse_tag_interpolation(content : String) -> Node? {
// Tokenize the content and parse as an inline element
let tokens = tokenize(content)
// Filter out Indent and Newline/EOF tokens
let filtered : Array[Token] = []
for tok in tokens {
match tok {
Indent(_) | Newline | EOF => ()
_ => filtered.push(tok)
}
}
if filtered.is_empty() {
return None
}
filtered.push(EOF)
let parser = Parser::new(filtered)
parser.parse_inline_element()
}
///|
/// Parse an inline element (for block expansion, no Indent token)
fn Parser::parse_inline_element(self : Parser) -> Node? {
let mut tag = "div"
let mut id = ""
let classes : Array[String] = []
let attributes : Array[Attribute] = []
let mut has_explicit_tag = false
// Parse tag name
if self.peek() is Some(Tag(t)) {
tag = t
has_explicit_tag = true
let _ = self.advance()
}
// Parse id and classes
while true {
match self.peek() {
Some(Id(i)) => {
id = i
let _ = self.advance()
}
Some(Class(c)) => {
classes.push(c)
let _ = self.advance()
}
_ => break
}
}
// Parse attributes
if self.peek() is Some(LParen) {
let _ = self.advance()
while true {
match self.peek() {
Some(AttrName(name)) => {
let _ = self.advance()
let mut value : String? = None
let mut unescaped = false
if self.peek() is Some(UnescapedEquals) {
let _ = self.advance()
unescaped = true
if self.peek() is Some(AttrValue(v)) {
value = Some(v)
let _ = self.advance()
}
} else if self.peek() is Some(Equals) {
let _ = self.advance()
if self.peek() is Some(AttrValue(v)) {
value = Some(v)
let _ = self.advance()
}
}
attributes.push({ name, value, unescaped })
}
Some(RParen) => {
let _ = self.advance()
break
}
_ => break
}
}
}
let children : Array[Node] = []
// Handle chained block expansion
while self.peek() is Some(BlockExpand) {
let _ = self.advance()
let nested = self.parse_inline_element()
match nested {
Some(node) => children.push(node)
None => ()
}
}
// Parse text, interpolation, and tag interpolation
while true {
match self.peek() {
Some(Text(t)) => {
children.push(Text(t))
let _ = self.advance()
}
Some(Token::Interpolation(name)) => {
children.push(Node::Interpolation(name))
let _ = self.advance()
}
Some(Token::UnescapedInterpolation(name)) => {
children.push(Node::UnescapedInterpolation(name))
let _ = self.advance()
}
Some(Token::TagInterpolation(content)) => {
match parse_tag_interpolation(content) {
Some(node) => children.push(node)
None => ()
}
let _ = self.advance()
}
_ => break
}
}
// If no content at all, return None
if not(has_explicit_tag) &&
id == "" &&
classes.is_empty() &&
attributes.is_empty() &&
children.is_empty() {
return None
}
Some(Element(tag~, id~, classes~, attributes~, children~, self_closing=false))
}
///|
/// Parse a line of tokens into a Node (at current position after Indent)
fn Parser::parse_element(self : Parser) -> Node? {
let mut tag = "div"
let mut id = ""
let mut self_closing = false
let classes : Array[String] = []
let attributes : Array[Attribute] = []
let mut has_explicit_tag = false
// Check for special tokens first
match self.peek() {
Some(Comment(text)) => {
let _ = self.advance()
return Some(Comment(text~, render=true))
}
Some(UnbufferedComment(_)) => {
let _ = self.advance()
return None // Unbuffered comments are not rendered
}
Some(Doctype(value)) => {
let _ = self.advance()
return Some(Doctype(value))
}
Some(PipedText(text)) => {
let _ = self.advance()
return Some(Text(text))
}
Some(Token::If(condition)) => {
let _ = self.advance()
// Children will be added by build_tree
return Some(
Conditional(condition~, if_true=[], if_false=[], is_unless=false),
)
}
Some(Token::Unless(condition)) => {
let _ = self.advance()
return Some(
Conditional(condition~, if_true=[], if_false=[], is_unless=true),
)
}
Some(Token::ElseIf(condition)) => {
let _ = self.advance()
// ElseIf is treated as a new conditional that will be nested in if_false
return Some(
Conditional(condition~, if_true=[], if_false=[], is_unless=false),
)
}
Some(Token::Else) => {
let _ = self.advance()
// Else is a marker - we'll use a special condition "else" to identify it
return Some(
Conditional(
condition="__else__",
if_true=[],
if_false=[],
is_unless=false,
),
)
}
Some(Token::Each(item_var, index_var, collection)) => {
let _ = self.advance()
// Children will be added by build_tree
return Some(
Each(item_var~, index_var~, collection~, body=[], else_body=[]),
)
}
Some(Token::While(condition)) => {
let _ = self.advance()
// Body will be added by build_tree
return Some(While(condition~, body=[]))
}
Some(Token::Case(expr)) => {
let _ = self.advance()
// Children (When/Default) will be added by build_tree
return Some(Case(expr~, cases=[], default=[]))
}
Some(Token::When(value)) => {
let _ = self.advance()
let when_body : Array[Node] = []
// Check for block expansion - inline content after :
if self.peek() is Some(BlockExpand) {
let _ = self.advance()
match self.parse_inline_element() {
Some(node) => when_body.push(node)
None => ()
}
}
return Some(When(value~, body=when_body))
}
Some(Token::Default) => {
let _ = self.advance()
let default_body : Array[Node] = []
// Check for block expansion - inline content after :
if self.peek() is Some(BlockExpand) {
let _ = self.advance()
match self.parse_inline_element() {
Some(node) => default_body.push(node)
None => ()
}
}
return Some(Default(body=default_body))
}
Some(Token::MixinDef(name, params)) => {
let _ = self.advance()
// Body will be added by build_tree
return Some(MixinDef(name~, params~, body=[]))
}
Some(Token::MixinCall(name, args, attrs)) => {
let _ = self.advance()
// Block content will be added via add_child in build_tree
return Some(MixinCall(name~, args~, block=[], attrs~))
}
Some(Token::Block) => {
let _ = self.advance()
return Some(Node::Block)
}
Some(Token::VarAssign(name, value)) => {
let _ = self.advance()
return Some(Node::VarAssign(name~, value~))
}
Some(Token::NamedBlock(name)) => {
let _ = self.advance()
// Body will be added by build_tree
return Some(Node::NamedBlock(name~, body=[], mode="replace"))
}
Some(Token::AppendBlock(name)) => {
let _ = self.advance()
return Some(Node::NamedBlock(name~, body=[], mode="append"))
}
Some(Token::PrependBlock(name)) => {
let _ = self.advance()
return Some(Node::NamedBlock(name~, body=[], mode="prepend"))
}
Some(Token::Include(path)) => {
let _ = self.advance()
return Some(Node::Include(path~))
}
Some(Token::IncludeFiltered(filter, path)) => {
let _ = self.advance()
return Some(Node::IncludeFiltered(filter~, path~))
}
Some(Token::Extends(path)) => {
let _ = self.advance()
return Some(Node::Extends(path~))
}
Some(Token::Filter(name, content)) => {
let _ = self.advance()
return Some(Node::Filter(name~, content~))
}
_ => ()
}
// Parse tag name
if self.peek() is Some(Tag(t)) {
tag = t
has_explicit_tag = true
let _ = self.advance()
}
// Parse id and classes
while true {
match self.peek() {
Some(Id(i)) => {
id = i
let _ = self.advance()
}
Some(Class(c)) => {
classes.push(c)
let _ = self.advance()
}
_ => break
}
}
// Parse attributes
if self.peek() is Some(LParen) {
let _ = self.advance() // consume LParen
while true {
match self.peek() {
Some(AttrName(name)) => {
let _ = self.advance()
let mut value : String? = None // Boolean attribute by default
let mut unescaped = false
if self.peek() is Some(UnescapedEquals) {
let _ = self.advance()
unescaped = true
if self.peek() is Some(AttrValue(v)) {
value = Some(v)
let _ = self.advance()
}
} else if self.peek() is Some(Equals) {
let _ = self.advance()
if self.peek() is Some(AttrValue(v)) {
value = Some(v) // Has explicit value (even if empty)
let _ = self.advance()
}
}
attributes.push({ name, value, unescaped })
}
Some(RParen) => {
let _ = self.advance()
break
}
_ => break
}
}
}
// Build children array for inline content
let children : Array[Node] = []
// Handle block expansion - parse nested element
while self.peek() is Some(BlockExpand) {
let _ = self.advance() // consume BlockExpand
// Parse the nested element inline
let nested = self.parse_inline_element()
match nested {
Some(node) => children.push(node)
None => ()
}
}
// Check for block text marker
let mut is_block_text = false
if self.peek() is Some(BlockTextMarker) {
let _ = self.advance()
is_block_text = true
}
// Parse text, interpolation, buffered output, and tag interpolation tokens as inline children
while true {
match self.peek() {
Some(Text(t)) => {
children.push(Text(t))
let _ = self.advance()
}
Some(Token::Interpolation(name)) => {
children.push(Node::Interpolation(name))
let _ = self.advance()
}
Some(Token::UnescapedInterpolation(name)) => {
children.push(Node::UnescapedInterpolation(name))
let _ = self.advance()
}
Some(Token::BufferedOutput(variable)) => {
// Buffered output = variable outputs the variable value as content (escaped)
children.push(Node::Interpolation(variable))
let _ = self.advance()
}
Some(Token::UnescapedBufferedOutput(variable)) => {
// Unescaped buffered output != variable outputs the variable value as content (raw)
children.push(Node::UnescapedInterpolation(variable))
let _ = self.advance()
}
Some(Token::TagInterpolation(content)) => {
match parse_tag_interpolation(content) {
Some(node) => children.push(node)
None => ()
}
let _ = self.advance()
}
_ => break
}
}
// Check for self-closing marker
if self.peek() is Some(SelfClose) {
let _ = self.advance()
self_closing = true
}
// Skip newline/EOF at end of element line
match self.peek() {
Some(Newline) | Some(EOF) => {
let _ = self.advance()
}
_ => ()
}
// If block text mode, get the block text content
if is_block_text {
if self.peek() is Some(BlockText(content)) {
let _ = self.advance()
children.push(Text(content))
}
}
// If we have no explicit tag, id, class, or attributes, this might be text-only or empty
if not(has_explicit_tag) &&
id == "" &&
classes.is_empty() &&
attributes.is_empty() {
if children.length() == 1 {
return Some(children[0])
} else if children.length() > 1 {
// Multiple inline children without a tag - wrap in implicit div
return Some(
Element(
tag="div",
id="",
classes=[],
attributes=[],
children~,
self_closing=false,
),
)
} else {
return None
}
}
Some(Element(tag~, id~, classes~, attributes~, children~, self_closing~))
}
///|
/// Represents a parsed line with its indentation level
priv struct ParsedLine {
indent : Int
node : Node
}
///|
/// Parse all lines into flat list with indentation info
fn Parser::parse_lines(self : Parser) -> Array[ParsedLine] {
let lines : Array[ParsedLine] = []
while self.pos < self.tokens.length() {
self.skip_newlines()
if self.peek() is Some(EOF) || self.peek() is None {
break
}
// Get indentation
let indent = match self.peek() {
Some(Indent(n)) => {
let _ = self.advance()
n
}
_ => 0
}
// Skip empty lines (just indent + newline)
if self.peek() is Some(Newline) || self.peek() is Some(EOF) {
let _ = self.advance()
continue
}
// Parse the element
match self.parse_element() {
Some(node) => lines.push({ indent, node })
None => () // Skip unbuffered comments, etc.
}
}
lines
}
///|
/// Build tree from flat list using indentation
fn build_tree(lines : Array[ParsedLine]) -> Array[Node] {
if lines.is_empty() {
return []
}
let result : Array[Node] = []
let stack : Array[(Int, Node)] = [] // (indent, node) pairs
// Track recent conditionals at each indent level for else linking
let recent_conditionals : Map[Int, Node] = {}
// Track recent Each nodes at each indent level for else linking
let recent_each : Map[Int, Node] = {}
for line in lines {
let indent = line.indent
let node = line.node
// Pop stack until we find a parent with less indentation
while stack.length() > 0 {
let (parent_indent, _) = stack[stack.length() - 1]
if parent_indent >= indent {
let (popped_indent, completed) = stack.pop().unwrap()
// Track completed conditionals for else linking
if is_conditional(completed) && not(is_else_marker(completed)) {
recent_conditionals[popped_indent] = completed
}
// Track completed Each nodes for else linking
if is_each_node(completed) {
recent_each[popped_indent] = completed
}
if stack.length() > 0 {
// Add to parent's children
let (_, parent) = stack[stack.length() - 1]
add_child(parent, completed)
} else {
// Add to root
result.push(completed)
}
} else {
break
}
}
// Handle else/else if - link to previous conditional or Each at same indent
if is_else_marker(node) {
// First check for recent Each at this indent (each ... else for empty collection)
match recent_each.get(indent) {
Some(_) =>
// The else marker's children will be collected, then added to Each's else_body
stack.push((indent, node))
None =>
// Otherwise check for recent conditional at this indent
match recent_conditionals.get(indent) {
Some(_) =>
// The else marker's children will be collected, then added to if_false
stack.push((indent, node))
None =>
// No matching if/each - just ignore
()
}
}
continue
}
// Handle else if - similar to else but it's a new conditional
match node {
Conditional(condition~, ..) if condition != "__else__" =>
// Check if there's a recent conditional at this indent to link to
match recent_conditionals.get(indent) {
Some(prev_cond) =>
// This else-if should be added to prev_cond's if_false
// But first process it as a child collector
match prev_cond {
Conditional(..) =>
// The else-if becomes nested in if_false
// Push to stack to collect its children
stack.push((indent, node))
// Mark this indent as having a new conditional
// It will be properly linked when popped
_ => stack.push((indent, node))
}
None =>
// No previous if - this is a standalone if
stack.push((indent, node))
}
_ =>
// Regular node - push to stack
stack.push((indent, node))
}
}
// Pop remaining items from stack
while stack.length() > 0 {
let (popped_indent, completed) = stack.pop().unwrap()
// Handle else marker - add its children to the recent conditional's if_false or Each's else_body
if is_else_marker(completed) {
// Get the children collected under the else marker
let else_children = match completed {
Conditional(if_true~, ..) => if_true
_ => []
}
// First check for recent Each at this indent
match recent_each.get(popped_indent) {
Some(prev_each) => add_each_else_branch(prev_each, else_children)
None =>
// Otherwise check for conditional
match recent_conditionals.get(popped_indent) {
Some(prev_cond) => add_else_branch(prev_cond, else_children)
None => ()
}
}
continue
}
// Track completed conditionals
if is_conditional(completed) {
recent_conditionals[popped_indent] = completed
}
// Track completed Each nodes
if is_each_node(completed) {
recent_each[popped_indent] = completed
}
if stack.length() > 0 {
let (_, parent) = stack[stack.length() - 1]
add_child(parent, completed)
} else {
result.push(completed)
}
}
result
}
///|
/// Add a child to an Element, Conditional, Each, When, Default, Case, MixinDef, or MixinCall node
fn add_child(parent : Node, child : Node) -> Unit {
match parent {
Element(children~, ..) => children.push(child)
Conditional(if_true~, ..) => if_true.push(child)
Each(body~, ..) => body.push(child)
While(body~, ..) => body.push(child)
NamedBlock(body~, ..) => body.push(child)
When(body~, ..) => body.push(child)
Default(body~) => body.push(child)
MixinDef(body~, ..) => body.push(child)
MixinCall(block~, ..) => block.push(child)
Case(cases~, default~, ..) =>
// Add When/Default as case branches
match child {
When(value~, body~) => cases.push((value, body))
Default(body~) =>
for node in body {
default.push(node)
}
_ => () // Can't add other children to Case directly
}
_ => () // Can't add children to other node types
}
}
///|
/// Check if a node is an else marker (Conditional with condition "__else__")
fn is_else_marker(node : Node) -> Bool {
match node {
Conditional(condition~, ..) => condition == "__else__"
_ => false
}
}
///|
/// Check if a node is a Conditional (if/else if)
fn is_conditional(node : Node) -> Bool {
node is Conditional(_)
}
///|
/// Check if a node is an Each loop
fn is_each_node(node : Node) -> Bool {
node is Each(_)
}
///|
/// Add else branch to a conditional
fn add_else_branch(cond : Node, else_children : Array[Node]) -> Unit {
match cond {
Conditional(if_false~, ..) =>
for child in else_children {
if_false.push(child)
}
_ => ()
}
}
///|
/// Add else branch to an Each loop (for empty collection fallback)
fn add_each_else_branch(each_node : Node, else_children : Array[Node]) -> Unit {
match each_node {
Each(else_body~, ..) =>
for child in else_children {
else_body.push(child)
}
_ => ()
}
}
///|
/// Parse tokens into a Document
pub fn Parser::parse(self : Parser) -> Document {
let lines = self.parse_lines()
let nodes = build_tree(lines)
let doc = Document::new()
for node in nodes {
doc.push(node)
}
doc
}
///|
/// Convenience function to parse a Pug string
pub fn parse(input : String) -> Document {
let tokens = tokenize(input)
Parser::new(tokens).parse()
}