ADR 146: the :name sugar
Status: accepted 2026-10-04 (decision 146 and its addendum in the decisions log). Depends on: ADR 145 (defaultTag). Implementation: three PRs (parser patch, core, tooling and docs).
Context
Marko has two tag-adjacent shorthands borrowed from CSS selectors: <div#main> sets id, <div.card> sets class. A great many tags, in HTML (<input>, <select>, <button>, <slot>, <meta>) and in data vocabularies (attributes, fields, resources, actions), carry a name attribute, and there is no shorthand for it. The data target made the gap visible: a vocabulary that declares attributes writes <attribute name="title" type="string"/> for every entry.
MX is a language first (its targets exist to test it), so the question was treated as one of language design: which spelling, in which positions, at what cost in parser work and divergence from Marko.
Decision
:val sets name="val", the way #val sets id and .val sets class. All three sugars work in three positions:
| position | example | meaning |
|---|---|---|
| tag-adjacent | <input:email>, <:email> |
name="email"; a bare <:email> is the unnamed tag of ADR 145 with a name |
| first attribute | <input :email> |
same |
| after any attribute | <input type="email" :email>, <input x="1" #main .big> |
same, in HTML and concise mode |
Tag-adjacent sugars compose in any order (<a.c:b>, <a#d:b.c>, <:b.c>). The value is the same identifier-like token Marko’s shorthand accepts; :x=1 is a positioned error. After the sugar is resolved, name is an ordinary attribute: contracts, duplicate rules and attribute typing apply unchanged.
What was measured
All claims below were measured on @marko/compiler 5.42.5 (htmljs-parser 5.15) with a capture translator, 2026-10-04.
- Marko’s parser is permissive about tag names: only whitespace,
/,>,.,#,,,=,(and|end or change a name.<a:b>is the taga:b,<:b>is the tag:b. So the tag-adjacent split is a post-parse rewrite; no parser work. #bin attribute position already parses as an attribute named#b, first, after a quoted value, after an unquoted value, in concise mode.:bfirst or after,parses as the default attribute with a modifier,{ name: "value", modifier: "b" }, i.e.value:b. After any attribute value it is the parse error “Expected a single expression, but found:after it”: the parser reads:as a ternary continuation of the previous value..bafter any value is swallowed as member access (x="1" .bis"1".b).- Bare
:modmeaningvalue:modis undocumented. It falls out of two generic parser rules (split an attribute name at its last:; fill an empty head withvalue), and the only use found anywhere was MX’s own oracle fixture. - The expression-continuation rule is general: after a value, any token that can continue a JavaScript expression continues it (
- + * / % & | ^ < .,?/:,in,instanceof,=>, a(call, across newlines). It never bites today because every real attribute name starts with a letter,_,$,@or#, none of which continue an expression.
Alternatives considered
| option | works in every position without parser change | reads as “name” | rejected because |
|---|---|---|---|
: |
no (: after a value is an error) |
yes | chosen; the parser rule it needs is non-breaking |
$ |
yes | no: reads as “variable” in JS, shell and PHP | sugar that is misread is sugar people avoid |
@ |
yes | partly | <@x> is an attribute tag; one sigil, two positions, two meanings |
& |
no (x=a &b is a valid bitwise expression today) |
YAML anchor reading is nice | needs a breaking parser change; real ambiguity, not just an error |
:: |
no (same : error) |
yes | same parser problem; two spellings for one thing once <input:email> exists |
wildcard attribute tags (<@title type="string"/>) |
yes | different thing | forces a tag’s whole contract into attribute-tag shape; attribute tags do not have every tag capability |
replace the modifier operator : with | |
n/a | n/a | changes a documented Marko feature (class:x, style:x, value:fn:=x) and buys nothing the chosen rule does not |
Consequences
Four divergences from Marko, each recorded in divergences.md:
- A tag name may not contain
:;tag:restis tag plus name. No HTML element has a:in its name, so nothing real is lost. - Bare
:xin attribute position isname="x", notvalue:x. The named modifier forms (class:x,style:x,value:fn:=x) are untouched. - A shorthand class or id cannot contain
:(<div.hover:x>is classhover, namex). Tailwind-style variants useclass="hover:x", which is unchanged. - Parser: after whitespace inside an attribute value, a
:with no open?in that value, and a.followed by an identifier start, begin a new attribute instead of continuing the previous value. The:half breaks nothing (every input it affects is a parse error today) and is offered upstream to htmljs-parser. The.half changesx=a.b .c(member access across whitespace), which must now be written without the space or in parentheses.
MX carries rule 4 as a bun patchedDependencies hunk on htmljs-parser until upstream takes the : half. Marko’s own language tools will flag :x after a value in .marko files; .mx files use MX’s tools.
What stays true: # and . keep their Marko meaning and merge rules; every existing golden is byte-identical; the sugar never produces anything a user could not have written as name="…".