rtmlast API

rtmlast

package

API reference for the rtmlast package.

F
function

TestRenderNodesEscapesValues

RenderNodes follows the same escape-by-default policy as the production
renderer in core: {{var}}, @prop, @store and @signal output is text.

Parameters

rtmlast/renderer_test.go:14-37
func TestRenderNodesEscapesValues(t *testing.T)

{
	mgr := state.NewStoreManager()
	st := mgr.NewStore("s", state.WithModule("app"))
	st.Set("v", "<i>store</i>")

	nodes, err := Parse("{{name}}\n@prop:name\n@store:app.s.v\n")
	if err != nil {
		t.Fatal(err)
	}
	ctx := &RenderContext{
		Props:    map[string]any{"name": "<b>x</b>"},
		StoreMgr: mgr,
	}
	out := RenderNodes(nodes, ctx)
	if strings.Contains(out, "<b>x</b>") || strings.Contains(out, "<i>store</i>") {
		t.Fatalf("markup injected: %s", out)
	}
	if !strings.Contains(out, "&lt;b&gt;x&lt;/b&gt;") {
		t.Fatalf("var/prop not escaped: %s", out)
	}
	if !strings.Contains(out, "&lt;i&gt;store&lt;/i&gt;") {
		t.Fatalf("store not escaped: %s", out)
	}
}
F
function

TestRenderNodesEscapesSignals

Parameters

rtmlast/renderer_test.go:39-49
func TestRenderNodesEscapesSignals(t *testing.T)

{
	sig := state.NewSignal("<img src=x>")
	nodes, err := Parse("@signal:v")
	if err != nil {
		t.Fatal(err)
	}
	out := RenderNodes(nodes, &RenderContext{Props: map[string]any{"v": sig}})
	if !strings.Contains(out, "&lt;img src=x&gt;") {
		t.Fatalf("signal not escaped: %s", out)
	}
}
I
interface

Node

Node is the root of the AST.

rtmlast/ast.go:12-14
type Node interface

Methods

node
Method
func node(...)
S
struct
Implements: Node

TextNode

TextNode is literal text.

rtmlast/ast.go:27-29
type TextNode struct

Methods

node
Method
func (TextNode) node()
{}

Fields

Name Type Description
Text string
S
struct
Implements: Node

ExprNode

ExprNode is a standalone reactive expression @expr:expression.

rtmlast/ast.go:32-34
type ExprNode struct

Methods

node
Method
func (ExprNode) node()
{}

Fields

Name Type Description
Expr Expr
S
struct
Implements: Node

VarNode

VarNode is a reactive interpolation {expr}.

rtmlast/ast.go:37-39
type VarNode struct

Methods

node
Method
func (VarNode) node()
{}

Fields

Name Type Description
Expr Expr
S
struct
Implements: Node

ElementNode

ElementNode is a rendered HTML element. It holds the tag name,
attributes (plain + bound), children, and optional [ref] / [key].

rtmlast/ast.go:43-51
type ElementNode struct

Methods

node
Method
func (ElementNode) node()
{}

Fields

Name Type Description
Tag string
Attrs []Attr
BoundAttrs []BoundAttr
Children []Node
Ref string
Key Expr
IsVoid bool
S
struct

Attr

Attr is a static HTML attribute.

rtmlast/ast.go:54-57
type Attr struct

Fields

Name Type Description
Name string
Value string
S
struct

BoundAttr

BoundAttr is an attribute whose value is computed from a reactive Expr.

rtmlast/ast.go:60-64
type BoundAttr struct

Fields

Name Type Description
Name string
Expr Expr
Bool bool
S
struct
Implements: Node

CommandNode

CommandNode is a top-level command or constructor that does not produce markup.

rtmlast/ast.go:67-70
type CommandNode struct

Methods

node
Method
func (CommandNode) node()
{}

Fields

Name Type Description
Kind string
Value string
S
struct
Implements: Node

IfNode

IfNode is a conditional block.

rtmlast/ast.go:73-78
type IfNode struct

Methods

node
Method
func (IfNode) node()
{}

Fields

Name Type Description
Cond Expr
Then []Node
ElseIf []ElseIfBranch
Else []Node
S
struct

ElseIfBranch

ElseIfBranch is an @else-if branch.

rtmlast/ast.go:81-84
type ElseIfBranch struct

Fields

Name Type Description
Cond Expr
Body []Node
S
struct
Implements: Node

ForNode

ForNode is a list loop.

rtmlast/ast.go:87-92
type ForNode struct

Methods

node
Method
func (ForNode) node()
{}

Fields

Name Type Description
Alias string
KeyAlias string
Expr Expr
Body []Node
S
struct
Implements: Node

SlotNode

SlotNode is a named/placeholder slot.

rtmlast/ast.go:95-98
type SlotNode struct

Methods

node
Method
func (SlotNode) node()
{}

Fields

Name Type Description
Name string
Fallback []Node
S
struct
Implements: Node

IncludeNode

IncludeNode is a component inclusion with inline props.

rtmlast/ast.go:101-104
type IncludeNode struct

Methods

node
Method
func (IncludeNode) node()
{}

Fields

Name Type Description
Name string
Props map[string]Expr
I
interface

Expr

Expr represents a reactive expression.
It is later evaluated against component scope.

rtmlast/ast.go:108-110
type Expr interface

Methods

expr
Method
func expr(...)
S
struct
Implements: Expr

IdentExpr

IdentExpr is a variable reference by name.

rtmlast/ast.go:121-123
type IdentExpr struct

Methods

expr
Method
func (IdentExpr) expr()
{}

Fields

Name Type Description
Name string
S
struct
Implements: Expr

LiteralExpr

LiteralExpr is a string, number, or bool literal.

rtmlast/ast.go:126-128
type LiteralExpr struct

Methods

expr
Method
func (LiteralExpr) expr()
{}

Fields

Name Type Description
Value any
S
struct
Implements: Expr

BinaryExpr

BinaryExpr is lhs op rhs.

rtmlast/ast.go:131-135
type BinaryExpr struct

Methods

expr
Method
func (BinaryExpr) expr()
{}

Fields

Name Type Description
Op BinOp
LHS Expr
RHS Expr
T
type

BinOp

BinOp is a binary operator.

rtmlast/ast.go:138-138
type BinOp int
S
struct
Implements: Expr

UnaryExpr

UnaryExpr is !expr or -expr.

rtmlast/ast.go:170-173
type UnaryExpr struct

Methods

expr
Method
func (UnaryExpr) expr()
{}

Fields

Name Type Description
Op UnaryOp
Expr Expr
T
type

UnaryOp

UnaryOp is a unary operator.

rtmlast/ast.go:176-176
type UnaryOp int
S
struct
Implements: Expr

CallExpr

CallExpr is function(args).

rtmlast/ast.go:188-191
type CallExpr struct

Methods

expr
Method
func (CallExpr) expr()
{}

Fields

Name Type Description
Fn string
Args []Expr
S
struct
Implements: Expr

FieldExpr

FieldExpr is obj.Field (dotted access).

rtmlast/ast.go:194-197
type FieldExpr struct

Methods

expr
Method
func (FieldExpr) expr()
{}

Fields

Name Type Description
Obj Expr
Field string
S
struct
Implements: Expr

TernaryExpr

TernaryExpr is cond ? then : else (rare, but supported).

rtmlast/ast.go:200-204
type TernaryExpr struct

Methods

expr
Method
func (TernaryExpr) expr()
{}

Fields

Name Type Description
Cond Expr
Then Expr
Else Expr
T
type

TokenType

TokenType identifies a lexer token.

rtmlast/parser.go:10-10
type TokenType int
S
struct

Token

Token is one lexer result.

rtmlast/parser.go:26-29
type Token struct

Fields

Name Type Description
Type TokenType
Value string
S
struct

Lexer

Lexer tokenizes an RTML template.

rtmlast/parser.go:32-35
type Lexer struct

Methods

Lex
Method

Lex returns all tokens in the input.

Returns

[]Token
func (*Lexer) Lex() []Token
{
	var tokens []Token
	var text strings.Builder

	for l.pos < len(l.input) {
		ch := l.input[l.pos]

		if ch == '{' && l.pos+1 < len(l.input) && l.input[l.pos+1] == '{' {
			if text.Len() > 0 {
				tokens = append(tokens, Token{Type: TokenText, Value: text.String()})
				text.Reset()
			}
			l.pos += 2
			var expr strings.Builder
			for l.pos < len(l.input) {
				if l.pos+1 < len(l.input) && l.input[l.pos] == '}' && l.input[l.pos+1] == '}' {
					l.pos += 2
					break
				}
				expr.WriteByte(l.input[l.pos])
				l.pos++
			}
			tokens = append(tokens, Token{Type: TokenVarOpen, Value: strings.TrimSpace(expr.String())})
			tokens = append(tokens, Token{Type: TokenVarClose, Value: "}}"})

		} else if ch == '@' && isCommandPrefix(l.input[l.pos+1:]) {
			if text.Len() > 0 {
				tokens = append(tokens, Token{Type: TokenText, Value: text.String()})
				text.Reset()
			}
			l.pos++
			start := l.pos
			for l.pos < len(l.input) && !isNewline(l.input[l.pos]) && l.input[l.pos] != '<' {
				l.pos++
			}
			cmdText := strings.TrimSpace(l.input[start:l.pos])
			tokens = append(tokens, Token{Type: TokenCommand, Value: cmdText})

		} else {
			text.WriteByte(ch)
			l.pos++
		}
	}

	if text.Len() > 0 {
		tokens = append(tokens, Token{Type: TokenText, Value: text.String()})
	}
	tokens = append(tokens, Token{Type: TokenEOF})
	return tokens
}

Fields

Name Type Description
input string
pos int
F
function

NewLexer

NewLexer creates a lexer for input.

Parameters

input
string

Returns

rtmlast/parser.go:38-40
func NewLexer(input string) *Lexer

{
	return &Lexer{input: input}
}
F
function

isCommandPrefix

Parameters

s
string

Returns

bool
rtmlast/parser.go:94-105
func isCommandPrefix(s string) bool

{
	prefixes := []string{"if:", "else-if:", "else", "endif", "for:", "endfor", "include:", "slot:", "endslot", "on:", "store:", "signal:", "prop:", "h:", "plugin:", "expr:"}
	for _, p := range prefixes {
		if len(s) > len(p) && s[:len(p)] == p {
			return true
		}
		if s == p {
			return true
		}
	}
	return false
}
F
function

isNewline

Parameters

ch
byte

Returns

bool
rtmlast/parser.go:107-109
func isNewline(ch byte) bool

{
	return ch == '\n' || ch == '\r'
}
F
function

Parse

Parse builds an RTML syntax tree.

Parameters

template
string

Returns

error
rtmlast/parser.go:112-117
func Parse(template string) ([]Node, error)

{
	lex := NewLexer(template)
	tokens := lex.Lex()
	p := &parser{tokens: tokens, pos: 0}
	return p.parseNodes()
}
S
struct

parser

rtmlast/parser.go:119-122
type parser struct

Methods

peek
Method

Returns

func (*parser) peek() Token
{
	if p.pos >= len(p.tokens) {
		return Token{Type: TokenEOF}
	}
	return p.tokens[p.pos]
}
parseNodes
Method

Returns

[]Node
error
func (*parser) parseNodes() ([]Node, error)
{
	var nodes []Node
	for {
		t := p.peek()
		if t.Type == TokenEOF {
			break
		}
		switch t.Type {
		case TokenText:
			p.next()
			if t.Value != "" {
				nodes = append(nodes, TextNode{Text: t.Value})
			}
		case TokenVarOpen:
			p.next()
			expr := ParseExpr(t.Value)
			nodes = append(nodes, VarNode{Expr: expr})
			if p.peek().Type == TokenVarClose {
				p.next()
			}
		case TokenCommand:
			p.next()
			cmdText := t.Value
			node, consumed, err := p.parseCommand(cmdText)
			if err != nil {
				return nodes, err
			}
			if node != nil {
				nodes = append(nodes, node)
			}
			_ = consumed
		default:
			p.next()
		}
	}
	return nodes, nil
}
parseCommand
Method

Parameters

cmdText string

Returns

bool
error
func (*parser) parseCommand(cmdText string) (Node, bool, error)
{
	colonIdx := strings.Index(cmdText, ":")
	if colonIdx < 0 {
		switch cmdText {
		case "else":
			return nil, false, nil
		case "endif":
			return nil, false, nil
		case "endfor":
			return nil, false, nil
		case "endslot":
			return nil, false, nil
		}
		return CommandNode{Kind: cmdText, Value: ""}, true, nil
	}

	kind := cmdText[:colonIdx]
	rest := cmdText[colonIdx+1:]

	switch kind {
	case "if":
		node, err := p.parseIf(rest)
		return node, true, err
	case "for":
		node, err := p.parseFor(rest)
		return node, true, err
	case "include":
		return IncludeNode{Name: rest, Props: nil}, true, nil
	case "slot":
		return p.parseSlot(rest)
	case "on":
		return CommandNode{Kind: "on", Value: rest}, true, nil
	case "store", "signal", "prop":
		return CommandNode{Kind: kind, Value: rest}, true, nil
	case "h":
		return CommandNode{Kind: "h", Value: rest}, true, nil
	case "plugin":
		return CommandNode{Kind: "plugin", Value: rest}, true, nil
	case "expr":
		return ExprNode{Expr: ParseExpr(rest)}, true, nil
	default:
		return CommandNode{Kind: kind, Value: rest}, true, nil
	}
}
parseIf
Method

Parameters

condStr string

Returns

error
func (*parser) parseIf(condStr string) (Node, error)
{
	thenNodes, _ := p.parseUntilCommands("else-if", "else", "endif")
	node := IfNode{Cond: ParseExpr(condStr), Then: thenNodes}

	for p.peek().Type == TokenCommand && strings.HasPrefix(p.peek().Value, "else-if:") {
		cmdText := p.next().Value
		elseCond := strings.TrimPrefix(cmdText, "else-if:")
		body, _ := p.parseUntilCommands("else-if", "else", "endif")
		node.ElseIf = append(node.ElseIf, ElseIfBranch{Cond: ParseExpr(elseCond), Body: body})
	}

	if p.peek().Type == TokenCommand && p.peek().Value == "else" {
		p.next()
		elseBody, _ := p.parseUntilCommands("endif")
		node.Else = elseBody
	}

	if p.peek().Type == TokenCommand && p.peek().Value == "endif" {
		p.next()
	}
	return node, nil
}
parseFor
Method

Parameters

detail string

Returns

error
func (*parser) parseFor(detail string) (Node, error)
{
	body, _ := p.parseUntilCommands("endfor")
	if p.peek().Type == TokenCommand && p.peek().Value == "endfor" {
		p.next()
	}

	parts := strings.SplitN(detail, " in ", 2)
	alias := strings.TrimSpace(parts[0])
	exprStr := ""
	if len(parts) > 1 {
		exprStr = strings.TrimSpace(parts[1])
	}
	keyAlias := ""
	if commaIdx := strings.Index(alias, ","); commaIdx != -1 {
		keyAlias = strings.TrimSpace(alias[commaIdx+1:])
		alias = strings.TrimSpace(alias[:commaIdx])
	}
	return ForNode{Alias: alias, KeyAlias: keyAlias, Expr: ParseExpr(exprStr), Body: body}, nil
}
parseSlot
Method

Parameters

name string

Returns

bool
error
func (*parser) parseSlot(name string) (Node, bool, error)
{
	body, _ := p.parseUntilCommands("endslot")
	if p.peek().Type == TokenCommand && p.peek().Value == "endslot" {
		p.next()
	}
	return SlotNode{Name: name, Fallback: body}, true, nil
}

Parameters

commands ...string

Returns

[]Node
error
func (*parser) parseUntilCommands(commands ...string) ([]Node, error)
{
	var nodes []Node
	for {
		t := p.peek()
		if t.Type == TokenEOF {
			break
		}
		if t.Type == TokenCommand {
			for _, cmd := range commands {
				if t.Value == cmd || strings.HasPrefix(t.Value, cmd+":") || strings.HasPrefix(t.Value, cmd+" ") {
					return nodes, nil
				}
			}
		}
		p.next()
		switch t.Type {
		case TokenText:
			if t.Value != "" {
				nodes = append(nodes, TextNode{Text: t.Value})
			}
		case TokenVarOpen:
			nodes = append(nodes, VarNode{Expr: ParseExpr(t.Value)})
			if p.peek().Type == TokenVarClose {
				p.next()
			}
		case TokenCommand:
			node, _, err := p.parseCommand(t.Value)
			if err != nil {
				return nodes, err
			}
			if node != nil {
				nodes = append(nodes, node)
			}
		}
	}
	return nodes, nil
}

Fields

Name Type Description
tokens []Token
pos int
F
function

ParseExpr

ParseExpr parses a reactive RTML expression.

Parameters

s
string

Returns

rtmlast/parser.go:313-385
func ParseExpr(s string) Expr

{
	s = strings.TrimSpace(s)
	if s == "" {
		return LiteralExpr{Value: ""}
	}
	// Ternary: cond then X else Y (preferred) or cond ? X : Y (legacy)
	if idx, ok := findTernaryThen(s); ok {
		condStr := strings.TrimSpace(s[:idx])
		rest := s[idx+5:] // len(" then") = 5
		elseIdx, ok2 := findElse(rest)
		if ok2 {
			thenStr := strings.TrimSpace(rest[:elseIdx])
			elseStr := strings.TrimSpace(rest[elseIdx+5:]) // len(" else") = 5
			return TernaryExpr{Cond: ParseExpr(condStr), Then: ParseExpr(thenStr), Else: ParseExpr(elseStr)}
		}
	}
	if idx, ok := findTernarySymbol(s); ok {
		condStr := strings.TrimSpace(s[:idx])
		rest := s[idx+1:]
		colonIdx := findTernaryColon(rest)
		if colonIdx >= 0 {
			thenStr := strings.TrimSpace(rest[:colonIdx])
			elseStr := strings.TrimSpace(rest[colonIdx+1:])
			return TernaryExpr{Cond: ParseExpr(condStr), Then: ParseExpr(thenStr), Else: ParseExpr(elseStr)}
		}
	}
	if result, ok := tryParseBinary(s); ok {
		return result
	}
	if strings.HasPrefix(s, "!") || strings.HasPrefix(s, "not ") {
		var inner string
		if strings.HasPrefix(s, "not ") {
			inner = strings.TrimSpace(s[4:])
		} else {
			inner = strings.TrimSpace(s[1:])
		}
		return UnaryExpr{Op: UnaryNot, Expr: ParseExpr(inner)}
	}
	if strings.HasPrefix(s, "-") && len(s) > 1 {
		return UnaryExpr{Op: UnaryNeg, Expr: ParseExpr(s[1:])}
	}
	if strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) {
		return LiteralExpr{Value: s[1 : len(s)-1]}
	}
	if strings.HasPrefix(s, "'") && strings.HasSuffix(s, "'") && len(s) >= 2 {
		return LiteralExpr{Value: s[1 : len(s)-1]}
	}
	if n, ok := tryParseNumber(s); ok {
		return LiteralExpr{Value: n}
	}
	if strings.HasPrefix(s, "store:") || strings.HasPrefix(s, "signal:") || strings.HasPrefix(s, "prop:") {
		return IdentExpr{Name: s}
	}
	if strings.Contains(s, ".") && !strings.Contains(s, "(") {
		parts := strings.SplitN(s, ".", 2)
		if isIdent(parts[0]) && isIdent(parts[1]) {
			return FieldExpr{Obj: ParseExpr(parts[0]), Field: parts[1]}
		}
	}
	if strings.Contains(s, "(") {
		parenIdx := strings.Index(s, "(")
		fnName := s[:parenIdx]
		argsStr := s[parenIdx+1 : len(s)-1]
		var args []Expr
		if argsStr != "" {
			for _, a := range strings.Split(argsStr, ",") {
				args = append(args, ParseExpr(strings.TrimSpace(a)))
			}
		}
		return CallExpr{Fn: fnName, Args: args}
	}
	return IdentExpr{Name: s}
}
F
function

findTernaryThen

findTernaryThen finds “ then ” outside strings and parens.

Parameters

s
string

Returns

int
bool
rtmlast/parser.go:388-413
func findTernaryThen(s string) (int, bool)

{
	depth := 0
	inStr := false
	for i := 0; i < len(s); i++ {
		ch := s[i]
		if ch == '"' || ch == '\'' {
			inStr = !inStr
		}
		if inStr {
			continue
		}
		if ch == '(' {
			depth++
		}
		if ch == ')' {
			depth--
		}
		if depth > 0 {
			continue
		}
		if i+5 <= len(s) && s[i:i+5] == " then" && (i+5 >= len(s) || s[i+5] == ' ') {
			return i, true
		}
	}
	return -1, false
}
F
function

findElse

findElse finds “ else ” outside strings and parens.

Parameters

s
string

Returns

int
bool
rtmlast/parser.go:416-441
func findElse(s string) (int, bool)

{
	depth := 0
	inStr := false
	for i := 0; i < len(s); i++ {
		ch := s[i]
		if ch == '"' || ch == '\'' {
			inStr = !inStr
		}
		if inStr {
			continue
		}
		if ch == '(' {
			depth++
		}
		if ch == ')' {
			depth--
		}
		if depth > 0 {
			continue
		}
		if i+5 <= len(s) && s[i:i+5] == " else" && (i+5 >= len(s) || s[i+5] == ' ') {
			return i, true
		}
	}
	return -1, false
}
F
function

findTernarySymbol

findTernarySymbol finds “?” outside strings and parens.

Parameters

s
string

Returns

int
bool
rtmlast/parser.go:444-469
func findTernarySymbol(s string) (int, bool)

{
	depth := 0
	inStr := false
	for i := 0; i < len(s); i++ {
		ch := s[i]
		if ch == '"' || ch == '\'' {
			inStr = !inStr
		}
		if inStr {
			continue
		}
		if ch == '(' {
			depth++
		}
		if ch == ')' {
			depth--
		}
		if depth > 0 {
			continue
		}
		if ch == '?' {
			return i, true
		}
	}
	return -1, false
}
F
function

findTernaryColon

findTernaryColon finds “:” outside strings and parens, used for legacy ? : ternary.

Parameters

s
string

Returns

int
rtmlast/parser.go:472-497
func findTernaryColon(s string) int

{
	depth := 0
	inStr := false
	for i := 0; i < len(s); i++ {
		ch := s[i]
		if ch == '"' || ch == '\'' {
			inStr = !inStr
		}
		if inStr {
			continue
		}
		if ch == '(' {
			depth++
		}
		if ch == ')' {
			depth--
		}
		if depth > 0 {
			continue
		}
		if ch == ':' {
			return i
		}
	}
	return -1
}
F
function

isIdent

Parameters

s
string

Returns

bool
rtmlast/parser.go:499-509
func isIdent(s string) bool

{
	if s == "" {
		return false
	}
	for _, ch := range s {
		if !unicode.IsLetter(ch) && ch != '_' && !unicode.IsDigit(ch) {
			return false
		}
	}
	return true
}
F
function

tryParseNumber

Parameters

s
string

Returns

any
bool
rtmlast/parser.go:511-520
func tryParseNumber(s string) (any, bool)

{
	var f float64
	if _, err := fmt.Sscanf(s, "%f", &f); err == nil {
		if f == float64(int(f)) {
			return int(f), true
		}
		return f, true
	}
	return nil, false
}
F
function

tryParseBinary

Parameters

s
string

Returns

bool
rtmlast/parser.go:522-554
func tryParseBinary(s string) (Expr, bool)

{
	type opInfo struct {
		op   BinOp
		syms []string
	}
	ops := []opInfo{
		{OpOr, []string{" or ", "||"}},
		{OpAnd, []string{" and ", "&&"}},
		{OpEq, []string{" is ", "=="}},
		{OpNeq, []string{" is not ", "!="}},
		{OpLte, []string{"<="}},
		{OpGte, []string{">="}},
		{OpLt, []string{"<"}},
		{OpGt, []string{">"}},
		{OpAdd, []string{"+"}},
		{OpSub, []string{"-"}},
		{OpMul, []string{"*"}},
		{OpDiv, []string{"/"}},
	}
	for _, o := range ops {
		for _, sym := range o.syms {
			idx := findBinaryOp(s, sym)
			if idx >= 0 {
				lhs := strings.TrimSpace(s[:idx])
				rhs := strings.TrimSpace(s[idx+len(sym):])
				if lhs != "" && rhs != "" {
					return BinaryExpr{Op: o.op, LHS: ParseExpr(lhs), RHS: ParseExpr(rhs)}, true
				}
			}
		}
	}
	return nil, false
}
F
function

findBinaryOp

Parameters

s
string
op
string

Returns

int
rtmlast/parser.go:556-591
func findBinaryOp(s, op string) int

{
	depth := 0
	inStr := false
	inVar := false
	for i := 0; i < len(s); i++ {
		ch := s[i]
		if ch == '"' || ch == '\'' {
			inStr = !inStr
		}
		if inStr {
			continue
		}
		if ch == '{' {
			inVar = true
		}
		if ch == '}' {
			inVar = false
		}
		if inVar {
			continue
		}
		if ch == '(' {
			depth++
		}
		if ch == ')' {
			depth--
		}
		if depth > 0 {
			continue
		}
		if i+len(op) <= len(s) && s[i:i+len(op)] == op {
			return i
		}
	}
	return -1
}
F
function

TestParseText

Parameters

rtmlast/parser_test.go:7-22
func TestParseText(t *testing.T)

{
	nodes, err := Parse("hello world")
	if err != nil {
		t.Fatal(err)
	}
	if len(nodes) != 1 {
		t.Fatalf("expected 1 node, got %d", len(nodes))
	}
	tn, ok := nodes[0].(TextNode)
	if !ok {
		t.Fatalf("expected TextNode, got %T", nodes[0])
	}
	if tn.Text != "hello world" {
		t.Fatalf("expected 'hello world', got '%s'", tn.Text)
	}
}
F
function

TestParseVarInterpolation

Parameters

rtmlast/parser_test.go:24-44
func TestParseVarInterpolation(t *testing.T)

{
	input := "hello {{name}} world"
	nodes, err := Parse(input)
	if err != nil {
		t.Fatal(err)
	}
	if len(nodes) != 3 {
		t.Fatalf("expected 3 nodes, got %d", len(nodes))
	}
	vn, ok := nodes[1].(VarNode)
	if !ok {
		t.Fatalf("expected VarNode, got %T", nodes[1])
	}
	ident, ok := vn.Expr.(IdentExpr)
	if !ok {
		t.Fatalf("expected IdentExpr, got %T", vn.Expr)
	}
	if ident.Name != "name" {
		t.Fatalf("expected 'name', got '%s'", ident.Name)
	}
}
F
function

TestParseIfConditional

Parameters

rtmlast/parser_test.go:46-69
func TestParseIfConditional(t *testing.T)

{
	input := "@if:active\nhello\n@else\nworld\n@endif"
	nodes, err := Parse(input)
	if err != nil {
		t.Fatal(err)
	}
	ifn, ok := nodes[0].(IfNode)
	if !ok {
		t.Fatalf("expected IfNode, got %T", nodes[0])
	}
	ident, ok := ifn.Cond.(IdentExpr)
	if !ok {
		t.Fatalf("expected IdentExpr condition, got %T", ifn.Cond)
	}
	if ident.Name != "active" {
		t.Fatalf("expected condition 'active', got '%s'", ident.Name)
	}
	if len(ifn.Then) == 0 {
		t.Fatal("expected Then branch")
	}
	if len(ifn.Else) == 0 {
		t.Fatal("expected Else branch")
	}
}
F
function

TestParseForLoop

Parameters

rtmlast/parser_test.go:71-84
func TestParseForLoop(t *testing.T)

{
	input := "@for:item in items\n<div>{{item}}</div>\n@endfor"
	nodes, err := Parse(input)
	if err != nil {
		t.Fatal(err)
	}
	fn, ok := nodes[0].(ForNode)
	if !ok {
		t.Fatalf("expected ForNode, got %T", nodes[0])
	}
	if fn.Alias != "item" {
		t.Fatalf("expected alias 'item', got '%s'", fn.Alias)
	}
}
F
function

TestParseInclude

Parameters

rtmlast/parser_test.go:86-99
func TestParseInclude(t *testing.T)

{
	input := "@include:MyComponent"
	nodes, err := Parse(input)
	if err != nil {
		t.Fatal(err)
	}
	inc, ok := nodes[0].(IncludeNode)
	if !ok {
		t.Fatalf("expected IncludeNode, got %T", nodes[0])
	}
	if inc.Name != "MyComponent" {
		t.Fatalf("expected 'MyComponent', got '%s'", inc.Name)
	}
}
F
function

TestParseBinaryExpr

Parameters

rtmlast/parser_test.go:101-110
func TestParseBinaryExpr(t *testing.T)

{
	expr := ParseExpr("count > 0")
	bin, ok := expr.(BinaryExpr)
	if !ok {
		t.Fatalf("expected BinaryExpr, got %T", expr)
	}
	if bin.Op != OpGt {
		t.Fatalf("expected OpGt, got %d", bin.Op)
	}
}
F
function

TestParseBinaryExprEq

Parameters

rtmlast/parser_test.go:112-121
func TestParseBinaryExprEq(t *testing.T)

{
	expr := ParseExpr("status == \"active\"")
	bin, ok := expr.(BinaryExpr)
	if !ok {
		t.Fatalf("expected BinaryExpr, got %T", expr)
	}
	if bin.Op != OpEq {
		t.Fatalf("expected OpEq, got %d", bin.Op)
	}
}
F
function

TestParseUnaryNot

Parameters

rtmlast/parser_test.go:123-132
func TestParseUnaryNot(t *testing.T)

{
	expr := ParseExpr("!visible")
	un, ok := expr.(UnaryExpr)
	if !ok {
		t.Fatalf("expected UnaryExpr, got %T", expr)
	}
	if un.Op != UnaryNot {
		t.Fatalf("expected UnaryNot, got %d", un.Op)
	}
}
F
function

TestParseFieldExpr

Parameters

rtmlast/parser_test.go:134-143
func TestParseFieldExpr(t *testing.T)

{
	expr := ParseExpr("user.name")
	field, ok := expr.(FieldExpr)
	if !ok {
		t.Fatalf("expected FieldExpr, got %T", expr)
	}
	if field.Field != "name" {
		t.Fatalf("expected 'name', got '%s'", field.Field)
	}
}
F
function

TestParseCallExpr

Parameters

rtmlast/parser_test.go:145-154
func TestParseCallExpr(t *testing.T)

{
	expr := ParseExpr("format(date)")
	call, ok := expr.(CallExpr)
	if !ok {
		t.Fatalf("expected CallExpr, got %T", expr)
	}
	if call.Fn != "format" {
		t.Fatalf("expected 'format', got '%s'", call.Fn)
	}
}
F
function

TestParseElseIf

Parameters

rtmlast/parser_test.go:156-172
func TestParseElseIf(t *testing.T)

{
	input := "@if:a\nA\n@else-if:b\nB\n@else\nC\n@endif"
	nodes, err := Parse(input)
	if err != nil {
		t.Fatal(err)
	}
	ifn, ok := nodes[0].(IfNode)
	if !ok {
		t.Fatalf("expected IfNode, got %T", nodes[0])
	}
	if len(ifn.ElseIf) != 1 {
		t.Fatalf("expected 1 else-if, got %d", len(ifn.ElseIf))
	}
	if len(ifn.Else) == 0 {
		t.Fatal("expected else branch")
	}
}
F
function

TestParseSlot

Parameters

rtmlast/parser_test.go:174-187
func TestParseSlot(t *testing.T)

{
	input := "@slot:header\n<h1>fallback</h1>\n@endslot"
	nodes, err := Parse(input)
	if err != nil {
		t.Fatal(err)
	}
	sn, ok := nodes[0].(SlotNode)
	if !ok {
		t.Fatalf("expected SlotNode, got %T", nodes[0])
	}
	if sn.Name != "header" {
		t.Fatalf("expected 'header', got '%s'", sn.Name)
	}
}
F
function

TestParseStoreIdent

Parameters

rtmlast/parser_test.go:189-198
func TestParseStoreIdent(t *testing.T)

{
	expr := ParseExpr("store:app.user.name")
	ident, ok := expr.(IdentExpr)
	if !ok {
		t.Fatalf("expected IdentExpr, got %T", expr)
	}
	if ident.Name != "store:app.user.name" {
		t.Fatalf("expected 'store:app.user.name', got '%s'", ident.Name)
	}
}
F
function

TestParseSignalIdent

Parameters

rtmlast/parser_test.go:200-209
func TestParseSignalIdent(t *testing.T)

{
	expr := ParseExpr("signal:count")
	ident, ok := expr.(IdentExpr)
	if !ok {
		t.Fatalf("expected IdentExpr, got %T", expr)
	}
	if ident.Name != "signal:count" {
		t.Fatalf("expected 'signal:count', got '%s'", ident.Name)
	}
}
F
function

TestParseComplexTemplate

Parameters

rtmlast/parser_test.go:211-220
func TestParseComplexTemplate(t *testing.T)

{
	input := "<div>{{user.name}}@if:admin\nadmin panel\n@endif</div>"
	nodes, err := Parse(input)
	if err != nil {
		t.Fatal(err)
	}
	if len(nodes) == 0 {
		t.Fatal("expected nodes")
	}
}
F
function

escapeValue

escapeValue renders a substituted value HTML-escaped, matching the
escape-by-default policy of the production renderer in core.

Parameters

v
any

Returns

string
rtmlast/renderer.go:16-18
func escapeValue(v any) string

{
	return html.EscapeString(fmt.Sprintf("%v", v))
}
I
interface

HTMLComponent

HTMLComponent exposes component data required by the AST renderer.

rtmlast/renderer.go:21-25
type HTMLComponent interface

Methods

GetID
Method

Returns

string
func GetID(...)
GetProps
Method

Returns

map[string]any
func GetProps(...)

Parameters

fn func()
func AddUnsubscribe(...)
S
struct

RenderContext

RenderContext contains component, property and store data for rendering.

rtmlast/renderer.go:28-32
type RenderContext struct

Fields

Name Type Description
Component HTMLComponent
Props map[string]any
StoreMgr *state.StoreManager
F
function

RenderNodes

RenderNodes renders a sequence of AST nodes.

Parameters

nodes

Returns

string
rtmlast/renderer.go:35-41
func RenderNodes(nodes []Node, ctx *RenderContext) string

{
	var sb strings.Builder
	for _, n := range nodes {
		sb.WriteString(renderNode(n, ctx))
	}
	return sb.String()
}
F
function

renderNode

Parameters

Returns

string
rtmlast/renderer.go:43-64
func renderNode(n Node, ctx *RenderContext) string

{
	switch v := n.(type) {
	case TextNode:
		return v.Text
	case ExprNode:
		return renderExprNode(v, ctx)
	case VarNode:
		return renderVar(v, ctx)
	case IfNode:
		return renderIf(v, ctx)
	case ForNode:
		return renderFor(v, ctx)
	case IncludeNode:
		return renderInclude(v, ctx)
	case SlotNode:
		return renderSlot(v, ctx)
	case CommandNode:
		return renderCommand(v, ctx)
	default:
		return ""
	}
}
F
function

renderVar

Parameters

Returns

string
rtmlast/renderer.go:66-69
func renderVar(v VarNode, ctx *RenderContext) string

{
	val := evalExpr(v.Expr, ctx)
	return fmt.Sprintf(`<span data-var>%s</span>`, escapeValue(val))
}
F
function

renderExprNode

Parameters

Returns

string
rtmlast/renderer.go:71-74
func renderExprNode(v ExprNode, ctx *RenderContext) string

{
	val := evalExpr(v.Expr, ctx)
	return fmt.Sprintf(`<span data-expr>%s</span>`, escapeValue(val))
}
F
function

renderIf

Parameters

Returns

string
rtmlast/renderer.go:76-90
func renderIf(v IfNode, ctx *RenderContext) string

{
	condVal := evalBool(v.Cond, ctx)
	if condVal {
		return RenderNodes(v.Then, ctx)
	}
	for _, branch := range v.ElseIf {
		if evalBool(branch.Cond, ctx) {
			return RenderNodes(branch.Body, ctx)
		}
	}
	if len(v.Else) > 0 {
		return RenderNodes(v.Else, ctx)
	}
	return ""
}
F
function

renderFor

Parameters

Returns

string
rtmlast/renderer.go:92-123
func renderFor(v ForNode, ctx *RenderContext) string

{
	collection := evalExpr(v.Expr, ctx)
	var items []any
	switch c := collection.(type) {
	case []any:
		items = c
	case map[string]any:
		for k, val := range c {
			items = append(items, map[string]any{"key": k, "value": val})
		}
	default:
		return ""
	}
	var sb strings.Builder
	for i, item := range items {
		childCtx := *ctx
		if childCtx.Props == nil {
			childCtx.Props = map[string]any{}
		}
		childCtx.Props[v.Alias] = item
		if v.KeyAlias != "" {
			switch k := item.(type) {
			case map[string]any:
				childCtx.Props[v.KeyAlias] = k["key"]
			default:
				childCtx.Props[v.KeyAlias] = i
			}
		}
		sb.WriteString(RenderNodes(v.Body, &childCtx))
	}
	return sb.String()
}
F
function

renderInclude

Parameters

Returns

string
rtmlast/renderer.go:125-127
func renderInclude(v IncludeNode, _ *RenderContext) string

{
	return fmt.Sprintf(`@include:%s`, v.Name)
}
F
function

renderSlot

Parameters

Returns

string
rtmlast/renderer.go:129-136
func renderSlot(v SlotNode, ctx *RenderContext) string

{
	if content, ok := ctx.Props["slot:"+v.Name]; ok {
		if s, ok := content.(string); ok {
			return s
		}
	}
	return RenderNodes(v.Fallback, ctx)
}
F
function

renderCommand

Parameters

Returns

string
rtmlast/renderer.go:138-153
func renderCommand(v CommandNode, ctx *RenderContext) string

{
	switch v.Kind {
	case "store":
		return renderStoreCmd(v.Value, ctx)
	case "signal":
		return renderSignalCmd(v.Value, ctx)
	case "prop":
		return renderPropCmd(v.Value, ctx)
	case "on":
		return renderEventCmd(v.Value)
	case "h":
		return fmt.Sprintf(`<span data-host-var="%s" data-host-expected=""></span>`, v.Value)
	default:
		return fmt.Sprintf(`@%s:%s`, v.Kind, v.Value)
	}
}
F
function

renderStoreCmd

Parameters

val
string

Returns

string
rtmlast/renderer.go:155-174
func renderStoreCmd(val string, ctx *RenderContext) string

{
	parts := strings.Split(val, ".")
	isW := strings.HasSuffix(val, ":w")
	if isW && len(parts) >= 3 {
		cleanVal := strings.TrimSuffix(val, ":w")
		cparts := strings.Split(cleanVal, ".")
		if len(cparts) == 3 {
			placeholder := fmt.Sprintf("@store:%s:w", cleanVal)
			return placeholder
		}
	}
	if len(parts) == 3 && !isW {
		store := ctx.StoreMgr.GetStore(parts[0], parts[1])
		if store != nil {
			v := store.Get(parts[2])
			return fmt.Sprintf(`<span data-store="%s">%s</span>`, val, escapeValue(v))
		}
	}
	return fmt.Sprintf(`@store:%s`, val)
}
F
function

renderSignalCmd

Parameters

val
string

Returns

string
rtmlast/renderer.go:176-190
func renderSignalCmd(val string, ctx *RenderContext) string

{
	name := val
	isW := strings.HasSuffix(val, ":w")
	if isW {
		name = strings.TrimSuffix(val, ":w")
		return fmt.Sprintf("@signal:%s:w", name)
	}
	if prop, ok := ctx.Props[name]; ok {
		if sig, ok := prop.(interface{ Read() any }); ok {
			v := sig.Read()
			return fmt.Sprintf(`<span data-signal="%s">%s</span>`, name, escapeValue(v))
		}
	}
	return fmt.Sprintf(`@signal:%s`, name)
}
F
function

renderPropCmd

Parameters

val
string

Returns

string
rtmlast/renderer.go:192-197
func renderPropCmd(val string, ctx *RenderContext) string

{
	if v, ok := ctx.Props[val]; ok {
		return escapeValue(v)
	}
	return fmt.Sprintf(`@prop:%s`, val)
}
F
function

renderEventCmd

Parameters

val
string

Returns

string
rtmlast/renderer.go:199-207
func renderEventCmd(val string) string

{
	parts := strings.SplitN(val, ":", 2)
	if len(parts) == 2 {
		event := parts[0]
		handler := parts[1]
		return fmt.Sprintf(`data-on-%s="%s"`, event, handler)
	}
	return fmt.Sprintf(`data-on-%s`, val)
}
F
function

evalExpr

Parameters

Returns

any
rtmlast/renderer.go:209-235
func evalExpr(e Expr, ctx *RenderContext) any

{
	switch v := e.(type) {
	case IdentExpr:
		return lookupIdent(v.Name, ctx)
	case LiteralExpr:
		return v.Value
	case BinaryExpr:
		return evalBinary(v, ctx)
	case UnaryExpr:
		return evalUnary(v, ctx)
	case FieldExpr:
		obj := evalExpr(v.Obj, ctx)
		if m, ok := obj.(map[string]any); ok {
			return m[v.Field]
		}
		return nil
	case CallExpr:
		return nil
	case TernaryExpr:
		if evalBool(v.Cond, ctx) {
			return evalExpr(v.Then, ctx)
		}
		return evalExpr(v.Else, ctx)
	default:
		return nil
	}
}
F
function

evalBool

Parameters

Returns

bool
rtmlast/renderer.go:237-253
func evalBool(e Expr, ctx *RenderContext) bool

{
	val := evalExpr(e, ctx)
	switch v := val.(type) {
	case bool:
		return v
	case string:
		return v != ""
	case int:
		return v != 0
	case float64:
		return v != 0
	case nil:
		return false
	default:
		return true
	}
}
F
function

evalBinary

Parameters

Returns

any
rtmlast/renderer.go:255-284
func evalBinary(b BinaryExpr, ctx *RenderContext) any

{
	lhs := evalExpr(b.LHS, ctx)
	rhs := evalExpr(b.RHS, ctx)
	switch b.Op {
	case OpEq:
		return fmt.Sprintf("%v", lhs) == fmt.Sprintf("%v", rhs)
	case OpNeq:
		return fmt.Sprintf("%v", lhs) != fmt.Sprintf("%v", rhs)
	case OpAnd:
		return toBool(lhs) && toBool(rhs)
	case OpOr:
		return toBool(lhs) || toBool(rhs)
	case OpLt, OpGt, OpLte, OpGte:
		return compareValues(lhs, rhs, b.Op)
	case OpAdd:
		return toFloat(lhs) + toFloat(rhs)
	case OpSub:
		return toFloat(lhs) - toFloat(rhs)
	case OpMul:
		return toFloat(lhs) * toFloat(rhs)
	case OpDiv:
		r := toFloat(rhs)
		if r == 0 {
			return 0.0
		}
		return toFloat(lhs) / r
	default:
		return nil
	}
}
F
function

evalUnary

Parameters

Returns

any
rtmlast/renderer.go:286-296
func evalUnary(u UnaryExpr, ctx *RenderContext) any

{
	val := evalExpr(u.Expr, ctx)
	switch u.Op {
	case UnaryNot:
		return !toBool(val)
	case UnaryNeg:
		return -toFloat(val)
	default:
		return val
	}
}
F
function

lookupIdent

Parameters

name
string

Returns

any
rtmlast/renderer.go:298-326
func lookupIdent(name string, ctx *RenderContext) any

{
	if strings.HasPrefix(name, "store:") {
		parts := strings.Split(strings.TrimPrefix(name, "store:"), ".")
		if len(parts) == 3 && ctx.StoreMgr != nil {
			store := ctx.StoreMgr.GetStore(parts[0], parts[1])
			if store != nil {
				return store.Get(parts[2])
			}
		}
		return nil
	}
	if strings.HasPrefix(name, "signal:") {
		sigName := strings.TrimPrefix(name, "signal:")
		if prop, ok := ctx.Props[sigName]; ok {
			if sig, ok := prop.(interface{ Read() any }); ok {
				return sig.Read()
			}
			return prop
		}
		return nil
	}
	if v, ok := ctx.Props[name]; ok {
		if sig, ok := v.(interface{ Read() any }); ok {
			return sig.Read()
		}
		return v
	}
	return nil
}
F
function

toBool

Parameters

v
any

Returns

bool
rtmlast/renderer.go:328-341
func toBool(v any) bool

{
	switch val := v.(type) {
	case bool:
		return val
	case string:
		return val != ""
	case int:
		return val != 0
	case float64:
		return val != 0
	default:
		return v != nil
	}
}
F
function

toFloat

Parameters

v
any

Returns

float64
rtmlast/renderer.go:343-358
func toFloat(v any) float64

{
	switch val := v.(type) {
	case int:
		return float64(val)
	case float64:
		return val
	case string:
		f, err := strconv.ParseFloat(val, 64)
		if err != nil {
			return 0
		}
		return f
	default:
		return 0
	}
}
F
function

compareValues

Parameters

lhs
any
rhs
any
op

Returns

bool
rtmlast/renderer.go:360-374
func compareValues(lhs, rhs any, op BinOp) bool

{
	l, r := toFloat(lhs), toFloat(rhs)
	switch op {
	case OpLt:
		return l < r
	case OpGt:
		return l > r
	case OpLte:
		return l <= r
	case OpGte:
		return l >= r
	default:
		return false
	}
}