Markup is a document authoring language designed to replace writing HTML by hand. It provides the expressiveness of HTML with a single, unified syntax. A Markup document is converted to HTML by a conforming implementation.
- One syntax, one rule. Every construct — formatting, structure, and
computation — follows the same
{tag ...}pattern. There are no mode switches, no ambiguous parsing contexts, no special cases. - HTML-level expressiveness. If you can build it in HTML, you can express it in Markup without writing HTML.
- Turing-complete by design. Variables, conditionals, iteration, and component definitions are first-class constructs using the same tag syntax. A single Markup document can be a static page, a data-driven template, or a reusable component library.
- Unambiguous by design. The grammar is strict and context-free. There is exactly one way to parse any valid Markup document.
- HTML passthrough. When raw HTML is genuinely needed, a clean escape hatch exists. The goal is that you rarely need it.
Goals:
- Cover 95%+ of common HTML authoring needs without HTML
- Unify document formatting and template logic in one syntax
- Be trivially parseable with a single-pass recursive descent parser
- Produce clean, semantic HTML output
- Support external data binding for template use cases
Non-Goals:
- Replacing Markdown for ultra-simple plain-text notes
- Pixel-perfect layout control (that is CSS's job)
- Being a general-purpose programming language
Every Markup construct follows one pattern:
{tag "content"}
Where:
{opens a constructtagis a tag name (lowercase alphanumeric plus hyphen:[a-z][a-z0-9-]*)"content"is a quoted string containing the tag's text content}closes the construct- Content may contain nested
{tag "..."}constructs - Whitespace outside of quoted strings is formatting — the lexer discards it
A construct with no content is valid: {line}, {br}.
Content quoting rules:
- Content strings are delimited by
"..." - Nested tags inside content are recognized:
{p "Hello, {b "world"}!"} \"produces a literal quote inside content- Tags with only nested tags and no text need no quotes:
{list {item "One"} {item "Two"}} - Whitespace between tag name, attributes, and content is just token separation — the lexer discards it entirely
Tags may have key-value attributes between the tag name and the content.
{tag attr1=value attr2="value with spaces" "content goes here"}
Attribute rules:
- Attribute names:
[a-z][a-z0-9-]* - Unquoted values: no spaces, no
}, no" - Quoted values: enclosed in
"...", supports\"for literal quote - A bare
=with no value is invalid - Boolean attributes (no value):
{audio controls "song.mp3"} - Attributes end and content begins at the first
"that is not part of akey=valuepair
A tag is inline if its content string contains no newlines:
{p "This has {b "bold"} text."}
A tag is block if its content spans multiple lines:
{note "This is a block-level callout.
It spans multiple lines."}
The content determines the rendering context. However, certain tags
(table, section, grid, list, olist, dlist, form) are
always block-level regardless of content.
\{produces a literal{\}produces a literal}\\produces a literal\- Inside
{code ...}and{html ...}blocks, no escaping is performed. Content is consumed verbatim until the matching closing}. The parser tracks brace depth to find the true closing brace.
- Content inside quoted strings is preserved exactly as written
- A
{br}tag forces a hard line break (<br>) - Newlines within a content string are preserved in the output
- Whitespace outside of quoted strings and tag constructs is formatting and is discarded by the lexer
Content inside quoted strings is HTML-escaped in output (<, >,
&, " are converted to entities). The \" escape produces a
literal quote within a content string.
Inside content strings, {=expr} evaluates an expression and inserts
the result as text.
{let name "Alice"}
{p "Hello, {=name}! You have {=count({get items})} items."}
Interpolation supports:
- Simple variable references:
{=name} - Path expressions:
{=data.user.email} - Function calls:
{=upper(name)} - Expressions:
{=price * qty} - Pipes (see Section 4.3):
{=name | upper}
{=expr} is syntactic sugar. The parser expands it to {eval expr}
before evaluation. Inside {code} and {html} blocks, interpolation
is NOT processed (content is verbatim).
Whitespace outside of quoted strings is formatting — the lexer discards it. This means indentation, blank lines between tags, and newlines after closing braces produce no output.
Only text inside "..." content strings is preserved in the output.
This eliminates the need for trim markers or whitespace hacks.
{-- This is a comment and produces no output --}
Comments may span multiple lines. They do not nest.
Markup uses short, purpose-driven tag names. The compiler maps them to semantic HTML elements during rendering.
| Markup Tag | HTML Output | Description |
|---|---|---|
b |
<strong>text</strong> |
Bold |
i |
<em>text</em> |
Italic |
u |
<u>text</u> |
Underline |
s |
<s>text</s> |
Strikethrough |
sub |
<sub>text</sub> |
Subscript |
sup |
<sup>text</sup> |
Superscript |
mark |
<mark>text</mark> |
Highlight |
small |
<small>text</small> |
Small text |
abbr |
<abbr title="...">text</abbr> |
Abbreviation |
code |
<code>text</code> |
Inline code |
kbd |
<kbd>text</kbd> |
Keyboard input |
q |
<q>text</q> |
Inline quote |
cite |
<cite>text</cite> |
Citation |
time |
<time datetime="...">text</time> |
Date/time |
{h1 "Main Title"}
{h2 "Section Title"}
{h3 "Subsection"}
{h4 "Sub-subsection"}
{h5 "Minor heading"}
{h6 "Smallest heading"}
Headings support id and class attributes:
{h2 id=features "Features"}
Links:
{link "https://example.com" "Link text here"}
{link title="About us" "/about" "About"}
{link "#section" "Jump to section"}
Images (void tag — no content):
{img "photo.jpg" "A description"}
{img width=800 height=600 "photo.jpg" "Description"}
Figure with caption:
{fig
{img "diagram.png" "Architecture"}
{caption "System architecture as of v2.3"}
}
Audio and video:
{audio controls "song.mp3"}
{video controls width=640 "demo.mp4"}
Unordered list:
{list
{item "First item"}
{item "Second item"}
{item "Third with {b "bold"} text"}
}
Ordered list:
{olist
{item "Step one"}
{item "Step two"}
}
Nested lists:
{list
{item "Item one"}
{item "Item two"
{list
{item "Nested A"}
{item "Nested B"}
}
}
}
Description list:
{dlist
{term "Term"}
{desc "Definition of the term."}
}
Full structured syntax:
{table
{thead
{row
{hcol "Name"}
{hcol "Role"}
}
}
{tbody
{row
{col "Alice"}
{col "Engineer"}
}
}
}
Shorthand pipe syntax:
{table caption="Team Roster"
| Name | Role |
| Alice | Engineer |
| Bob | Designer |
}
In shorthand mode, the first row becomes <thead>, remaining rows become
<tbody>. The parser activates shorthand mode when the tag content begins
with |.
Table-level attributes: caption, id, class.
Inline code:
{p "Use the {code print()} function."}
Block code with language hint:
{code lang=delphi
procedure TFoo.Bar();
begin
WriteLn('hello');
end;
}
Output: <pre><code class="language-delphi">...</code></pre>
Inside {code ...}, content is verbatim — no tag parsing, no escaping.
The parser tracks brace nesting depth to find the true closing }.
| Markup Tag | HTML Output |
|---|---|
section |
<section>...</section> |
article |
<article>...</article> |
aside |
<aside>...</aside> |
header |
<header>...</header> |
footer |
<footer>...</footer> |
nav |
<nav>...</nav> |
main |
<main>...</main> |
box |
<div>...</div> |
span |
<span>...</span> |
All support id and class attributes.
{quote
"To be or not to be, that is the question."
{footer {cite "William Shakespeare"}}
}
{details summary="Click to expand"
"Hidden content here. Supports {b "any"} Markup constructs."
}
{note "This is informational."}
{tip "A helpful suggestion."}
{warning "Be careful with this."}
{danger "This is critical."}
| Markup Tag | HTML Output |
|---|---|
line |
<hr /> |
br |
<br /> |
img |
<img ... /> |
input |
<input .../> |
{form method=post "/submit"
{label for=username "Username:"}
{input type=text id=username placeholder="Enter name"}
{label for=bio "Bio:"}
{textarea id=bio "Default text here"}
{select id=role
{option value=dev "Developer"}
{option value=mgr "Manager"}
{option value=qa selected "QA"}
}
{button type=submit "Submit"}
}
These produce <div>-based structures with semantic classes:
Grid:
{grid cols=3 gap=16
{cell "Content A"}
{cell "Content B"}
{cell "Content C"}
}
Card:
{card
{h3 "Title"}
"Body text here."
}
Columns:
{columns
{column "Left content"}
{column "Right content"}
}
Layout helpers are syntactic sugar. Implementations MUST document the exact HTML and CSS class names they produce.
Computation tags use the same {tag ...} syntax as formatting tags. They
evaluate expressions, bind variables, control flow, and define reusable
components. A conforming implementation MUST support all computation tags
defined in this section.
Binding:
{let name "value"}
{let version "2.1.0"}
{let greeting "Hello, world"}
{let count {eval 5 + 3}}
{let} binds a name to a value in the current scope. If the value
is a quoted string, it is stored as-is. If the value is a single
{eval ...} expression, it is evaluated.
Reference:
{get name}
{get data.user.email}
{get items[0].title}
{get} resolves a name from the current scope, walking up parent scopes
if not found locally. Dot notation accesses map fields. Bracket notation
accesses list indices. If the path cannot be resolved, the result is an
empty string (no error).
Assignment:
{set name "newvalue"}
{set count {eval {get count} + 1}}
{set} updates an existing variable in the nearest enclosing scope where
it is defined. If the variable does not exist, behavior is implementation-
defined (implementations SHOULD create it in the current scope).
The {eval ...} tag evaluates an expression and produces its result as
a string. Expressions support:
Arithmetic: +, -, *, /, %
Comparison: ==, !=, <, >, <=, >=
Logical: and, or, not
Grouping: (, )
String concatenation: + when either operand is a string
{eval 5 + 3} → 8
{eval {get price} * {get qty}} → computed value
{eval {get name} + " Jr."} → string concatenation
Values referenced inside {eval} via {get} resolve normally through
the scope chain.
Filters transform values using a pipe (|) operator. Pipes chain
left-to-right, passing the result of each step as the first argument
to the next function.
{=name | upper}
{=name | lower | trim}
{=price | round | toStr}
{=items | sort | reverse}
{=data.bio | escape | nl2br}
Pipe with arguments:
{=name | substr(0, 10)}
{=items | join(", ")}
{=price | toStr | replace(".", ",")}
When a piped function takes additional arguments, the piped value is
always the first argument. {=x | substr(0, 10)} is equivalent to
{eval substr({get x}, 0, 10)}.
Pipes in {eval}:
{eval {get name} | upper}
{eval {get price} * {get qty} | round}
Pipes bind with lowest precedence. {eval a * b | round} means
round(a * b), not a * round(b).
Pipes in {if} and {each}:
{if {get name} | len | gt(0)
"Name is not empty."
}
{each {get items} | sort item
{p "{=item}"}
}
Filters are syntactic sugar for function calls. Any built-in function
or registered custom function can be used as a filter. Implementations
MUST support pipes in {eval}, {=}, {if}, and {each} contexts.
{if {expr}
"Content when true."
}
{if {expr}
"Content when true."
{else}
"Content when false."
}
{if {expr1}
"First branch."
{elseif {expr2}}
"Second branch."
{else}
"Default branch."
}
The condition expression is any value. The following are falsy: empty
string, integer 0, float 0.0, boolean false, nil, empty list, empty
map. Everything else is truthy.
Parsing rule for {else} and {elseif}: These are branch
delimiters recognized by the parser ONLY inside an {if} body. They
are not independent tags. When the parser is collecting content inside
an {if} block, encountering {else} or {elseif at the current
brace depth signals a branch boundary. The parser splits the {if}
node's children into branch groups accordingly. Outside of an {if}
body, {else} and {elseif} are parse errors.
Iterating over a list:
{each {get items} item
{p "{get item.name} — {get item.role}"}
}
{each} takes a list expression, a binding name, and a body. For each
element in the list, it binds the element to the given name in a new child
scope, then evaluates the body.
Loop variables: Inside an {each} body, the following implicit
variables are available:
| Variable | Description |
|---|---|
loop.index |
Zero-based index of current item |
loop.count |
One-based index of current item |
loop.first |
true if this is the first item |
loop.last |
true if this is the last item |
loop.length |
Total number of items in the list |
Iterating over a map:
{each {get headers} key value
{p "{=key}: {=value}"}
}
When two binding names are provided and the source is a map, the first binds to the key and the second to the value.
Components allow defining reusable document fragments.
Defining a component:
{def card icon title body
{box class=card
{box class=card-header
{span class=icon "{=icon}"} {h3 "{=title}"}
}
{box class=card-body "{=body}"}
}
}
{def} takes a name, a parameter list, and a body. Parameters become
local variables within the component body when it is invoked.
Parameter defaults:
{def card icon="star" title="Untitled" body
...
}
Parameters with =value are optional and use the specified default when
not provided by the caller. Parameters without defaults are required.
Required parameters MUST appear before optional parameters in the
definition (the last parameter may be required as it receives block content).
Invoking a component:
{call card icon=rocket title="Fast" body="Built for speed."}
{call card title="Simple"}
{call} looks up the named component, creates a new scope, binds
the provided attributes to the parameter names (using defaults for
any omitted optional parameters), and evaluates the body.
Block content passing:
{call card icon=star title="Fast"
"This entire block is passed as the {b "body"} parameter."
}
When a {call} has content after the attributes, that content is
bound to the last declared parameter of the component.
Nested component definitions: Components may define local sub-components:
{def page title
{def sidebar
{nav "{=links}"}
}
{h1 "{=title}"}
{call sidebar}
}
Sub-components are scoped to their parent — they are not visible outside.
The {include} tag inserts the contents of another Markup file at the
point of inclusion. This enables composing documents from reusable
partials.
{include "header.mu"}
{h1 "Page Title"}
{p "Page content here."}
{include "footer.mu"}
Include with data:
{include "user-card.mu" user={get data.currentUser}}
Attributes on {include} are bound as local variables in the included
file's scope. This allows partials to receive parameters without relying
on global state.
Include resolution: File paths are relative to the including document's location. Implementations MUST support relative paths and SHOULD support configurable search paths. Implementations MUST detect and report circular includes as errors.
Include vs component: {include} loads an external file.
{def}/{call} defines and invokes inline components within the same
document. Both create isolated scopes. Use {include} for shared
partials across documents; use {def}/{call} for reuse within a
single document.
Built-in functions are invoked within {eval} expressions or as
filters in pipe chains:
{eval upper({get name})}
{eval len({get items})}
String functions:
| Function | Description |
|---|---|
upper(s) |
Uppercase |
lower(s) |
Lowercase |
trim(s) |
Strip leading/trailing whitespace |
len(s) |
String length (or list length) |
substr(s, start, length) |
Substring (zero-based) |
replace(s, find, repl) |
Replace all occurrences |
split(s, delimiter) |
Split string into list |
join(list, delimiter) |
Join list into string |
startsWith(s, prefix) |
Test prefix |
endsWith(s, suffix) |
Test suffix |
contains(s, sub) |
Test containment |
Math functions:
| Function | Description |
|---|---|
round(n) |
Round to nearest integer |
floor(n) |
Round down |
ceil(n) |
Round up |
abs(n) |
Absolute value |
min(a, b) |
Minimum |
max(a, b) |
Maximum |
Collection functions:
| Function | Description |
|---|---|
count(list) |
Number of items (alias for len) |
first(list) |
First item |
last(list) |
Last item |
index(list, n) |
Item at index n |
range(start, end) |
Generate list of integers |
sort(list) |
Sort (natural order) |
reverse(list) |
Reverse a list |
keys(map) |
List of map keys |
values(map) |
List of map values |
Type functions:
| Function | Description |
|---|---|
typeof(v) |
Returns type name as string |
toStr(v) |
Convert to string |
toInt(s) |
Parse integer from string |
toFloat(s) |
Parse float from string |
HTML functions:
| Function | Description |
|---|---|
escape(s) |
HTML-escape a string |
raw(s) |
Mark string as safe (no escaping) |
nl2br(s) |
Convert newlines to <br> tags |
Comparison functions (for use in pipes):
| Function | Description |
|---|---|
eq(a, b) |
Equal |
neq(a, b) |
Not equal |
gt(a, b) |
Greater than |
lt(a, b) |
Less than |
gte(a, b) |
Greater than or equal |
lte(a, b) |
Less than or equal |
When used as a pipe filter with one argument, the piped value becomes
the first argument: {=age | gt(18)} → gt(age, 18).
Date/time functions:
| Function | Description |
|---|---|
now() |
Current date/time as string |
formatDate(s, fmt) |
Format a date string |
Implementations MAY provide additional built-in functions but MUST support all functions listed above.
A conforming implementation MUST support rendering a parsed document against an external data source. The data source is a tree of maps, lists, and scalar values (strings, integers, floats, booleans, nil).
External data is accessed through {get} with the data root:
{get data.title}
{get data.user.name}
{get data.items[0].price}
The data prefix is a reserved root name. When a document is rendered
with external data, data is bound in the root scope before evaluation
begins.
Dot notation traverses map fields: data.user.name resolves field user
on the data root, then field name on the result.
Bracket notation accesses list indices: data.items[0] resolves field
items on the data root, then index 0 on the resulting list.
If any step in the path fails (missing key, out-of-bounds index, wrong type), the result is an empty string. No error is raised. This allows templates to be defensive without explicit conditionals.
| Type | Description |
|---|---|
| String | UTF-8 text |
| Integer | 64-bit signed integer |
| Float | 64-bit IEEE 754 double |
| Boolean | true or false |
| Nil | Absence of value |
| List | Ordered sequence of values |
| Map | String-keyed collection of values |
Implementations that accept external data as JSON MUST map JSON types to Markup types as follows: JSON string → String, JSON number (integer) → Integer, JSON number (fractional) → Float, JSON boolean → Boolean, JSON null → Nil, JSON array → List, JSON object → Map.
{meta title="My Document" author="Alice" lang=en}
{meta} tags do not produce visible HTML output. They declare document-
level metadata that conforming implementations MUST make available through
their API. Multiple {meta} tags are merged; later declarations override
earlier ones for the same key.
A {meta} tag MUST appear before any content-producing tags. Implementations
MAY ignore {meta} tags that appear after content.
{html
<canvas id="myCanvas" width="400" height="300"></canvas>
<script>
var ctx = document.getElementById('myCanvas').getContext('2d');
</script>
}
Content inside {html ...} is passed through verbatim to the output.
No tag parsing or escaping is performed. The parser tracks brace depth
to find the true closing brace.
Implementations MAY provide an option to disable HTML passthrough for security-sensitive contexts (e.g., processing untrusted input).
Any inline tag may nest inside any other inline or block tag:
{b {i "bold and italic"}}
{item "Item with {link to="/page" "a link"} in it"}
Block tags may nest inside other block tags:
{section
{h2 "Title"}
{note "This is inside a section."}
}
Computation tags may nest inside any context and may contain any tags:
{if {get show_header}
{h1 "{=data.title}"}
}
- Headings (
h1–h6) MUST NOT contain block-level tags {code ...}and{html ...}MUST NOT contain Markup tags (verbatim)- Void tags (
{img},{line},{br},{input}) have no content body {meta}MUST NOT contain nested tags
The parser uses a depth counter for brace matching. Every { increments
depth; every } decrements it. A construct closes when depth returns to
zero. Inside {code ...} and {html ...}, braces are counted but not
parsed, ensuring } characters in code do not prematurely close the block.
Because Markup is Turing-complete, conforming implementations MUST enforce execution limits to prevent runaway computation:
| Limit | Minimum Default | Description |
|---|---|---|
| Max iterations | 10,000 | Per {each} loop |
| Max recursion depth | 100 | Nested {call} invocations |
| Max output size | 10 MB | Total HTML output |
When a limit is exceeded, the implementation MUST stop execution and report an error. Implementations SHOULD allow these limits to be configured.
Implementations MUST terminate computation cleanly on limit violation — partial output up to the violation point is acceptable, but the implementation MUST NOT hang or crash.
- Fail gracefully, never silently. Malformed input should produce reasonable output and reportable errors, not garbage.
- Unrecognized tags produce implementation-defined output.
Implementations SHOULD either pass them through as escaped text or
wrap them in a neutral container (e.g.,
<span class="mu-unknown">). - Unclosed braces at end of input: close all open constructs implicitly and report a warning.
- Empty tags like
{b}produce an empty element:<strong></strong>. - Type errors in expressions (e.g., adding a string to a list) produce an empty value and a reported error.
- Undefined variables in
{get}produce an empty string.
Errors MUST include:
- Source position (line and column)
- Error severity (error, warning)
- Human-readable message
- Error code for programmatic handling
document ::= (element | WHITESPACE)*
element ::= tag | comment | interpolation
tag ::= '{' TAGNAME attributes? content? '}'
attributes ::= attribute+
attribute ::= ATTRNAME '=' ATTRVALUE
| ATTRNAME
content ::= STRING
| element+
| STRING (element STRING?)*
STRING ::= '"' string_body '"'
string_body ::= (string_char | element)*
string_char ::= [^"\\{] | '\\' .
-- any char except ", \, { OR an escaped char
interpolation ::= '{=' expression pipe_chain? '}'
pipe_chain ::= ('|' IDENT ('(' arg_list? ')')? )+
arg_list ::= expression (',' expression)*
comment ::= '{--' comment_body '--}'
comment_body ::= (. - '--}')*
escape ::= '\{' | '\}' | '\\'
WHITESPACE ::= [ \t\r\n]+ -- discarded by lexer
TAGNAME ::= [a-z][a-z0-9-]*
ATTRNAME ::= [a-z][a-z0-9-]*
ATTRVALUE ::= QUOTED_STRING | BARE_WORD
BARE_WORD ::= [^\s}"]+
QUOTED_STRING ::= '"' ([^"\\] | '\\' .)* '"'
let_tag ::= '{let' IDENT (STRING | tag) '}'
get_tag ::= '{get' PATH pipe_chain? '}'
set_tag ::= '{set' IDENT (STRING | tag) '}'
eval_tag ::= '{eval' expression pipe_chain? '}'
if_tag ::= '{if' tag_expr pipe_chain? content
('{elseif' tag_expr '}' content)*
('{else}' content)?
'}'
each_tag ::= '{each' tag_expr pipe_chain?
IDENT IDENT? content '}'
def_tag ::= '{def' IDENT param_list content '}'
param_list ::= param+
param ::= IDENT ('=' ATTRVALUE)?
call_tag ::= '{call' IDENT attributes? content? '}'
include_tag ::= '{include' STRING attributes? '}'
tag_expr ::= tag
expression ::= expr_pipe
expr_pipe ::= expr_or pipe_chain?
expr_or ::= expr_and ('or' expr_and)*
expr_and ::= expr_cmp ('and' expr_cmp)*
expr_cmp ::= expr_add (('==' | '!=' | '<' | '>' | '<=' | '>=')
expr_add)*
expr_add ::= expr_mul (('+' | '-') expr_mul)*
expr_mul ::= expr_unary (('*' | '/' | '%') expr_unary)*
expr_unary ::= 'not' expr_unary | expr_primary
expr_primary ::= NUMBER | STRING | 'true' | 'false' | 'nil'
| IDENT '(' (expression (',' expression)*)? ')'
| get_tag
| '(' expression ')'
PATH ::= IDENT ('.' IDENT | '[' NUMBER ']')*
IDENT ::= [a-zA-Z_][a-zA-Z0-9_]*
NUMBER ::= [0-9]+ ('.' [0-9]+)?
STRING ::= '"' ([^"\\] | '\\' .)* '"'
| Precedence | Operators | Associativity |
|---|---|---|
| 1 | | (pipe) |
Left |
| 2 | or |
Left |
| 3 | and |
Left |
| 4 | == != |
Left |
| 5 | < > <= >= |
Left |
| 6 | + - |
Left |
| 7 | * / % |
Left |
| 8 | not (unary) |
Right |
| 9 | () (call) |
Left |
A conforming implementation:
- MUST parse all constructs defined in Sections 2–4
- MUST produce semantically correct HTML for all formatting tags (Section 3)
- MUST evaluate all computation tags (Section 4)
- MUST support string interpolation
{=expr}(Section 2.7) - MUST support filter/pipe syntax (Section 4.3)
- MUST support component definitions with defaults (Section 4.6)
- MUST support file includes (Section 4.7)
- MUST support external data binding (Section 5)
- MUST enforce execution limits (Section 9)
- MUST report errors with position information (Section 10)
- MUST HTML-escape plain text in output
- MUST support the
{html}passthrough mechanism (Section 7) - SHOULD support whitespace trim markers (Section 2.8)
- MAY provide additional built-in functions beyond those listed
- MAY provide additional tags beyond those listed
- MUST document any extensions or deviations from this specification
- Ambiguous parsing (emphasis, lists, nesting edge cases)
- No standard spec (CommonMark is a retrofit, many flavors remain)
- Limited expressiveness — HTML required for anything non-trivial
- Extensions are incompatible across implementations
- No computation model — separate template engine always needed
- Closing tags add noise:
[b]text[/b]vs{b text} - Square brackets conflict with other syntaxes in mixed contexts
- No standardized attribute syntax
- No computation model
- Too verbose for human authoring
- Easy to produce invalid HTML (unclosed tags, bad nesting)
- Not designed for humans to write at scale
- Template engines (Jinja, Handlebars, Liquid) bolt computation on top with a second, incompatible syntax
- Rare in natural language text (unlike
<,>,[,],*,#) - Visually distinct — easy to spot construct boundaries
- Supported in all text encodings
- No closing tag needed — the brace itself is the delimiter
- Eliminates the need for a separate template engine
- A single document can be static, a template, or a component library
- Components (
{def}/{call}) enable reuse without external tooling - Data binding is a natural extension of the same syntax
- Execution limits prevent the theoretical risks of unbounded computation
The closest prior art is LaTeX (Turing-complete but hostile syntax, targets PDF), XSLT (Turing-complete but XML verbosity), and Typst (scripting + typesetting, targets PDF). No existing system unifies HTML document authoring and computation in a single, clean syntax.
