Attributes
Attribute lines and inline blocks, names and values, classes, booleans and the semantic/language sugar.
Generated from resources/examples/edge-cases.md and resources/examples/core.md - edit the cases there, not here. Each case links the conformance fixture it produces.
Attribute edge cases
18 conformance fixtures
71-attribute-edge-cases71-attribute-edge-cases-271-attribute-edge-cases-371-attribute-edge-cases-471-attribute-edge-cases-571-attribute-edge-cases-671-attribute-edge-cases-771-attribute-edge-cases-871-attribute-edge-cases-971-attribute-edge-cases-1071-attribute-edge-cases-1171-attribute-edge-cases-1271-attribute-edge-cases-1371-attribute-edge-cases-1471-attribute-edge-cases-1571-attribute-edge-cases-1671-attribute-edge-cases-1771-attribute-edge-cases-18
Classes accumulate; #id and key=value (bare or quoted) attach in source order on the <span>.
[note]{.a .b #n key=val}<p><span class="a b" id="n" key="val">note</span></p>A quoted value keeps its spaces.
[x]{title="a b"}<p><span title="a b">x</span></p>A } inside a quoted value is part of the value — the closing } is the first one outside quotes.
[x]{data-x="{y}"}<p><span data-x="{y}">x</span></p>The same quoted-} rule holds for every attribute-bearing construct, not just spans. On an inline link:
[t](u){k="{y}"}<p><a href="u" k="{y}">t</a></p>On an image:
{k="{y}"}<img src="u" alt="a" k="{y}">On a heading (via a preceding block-attribute line; the attributes attach to the <h1>):
{k="{y}"}
# H<section id="H">
<h1 k="{y}">H</h1>
</section>On a generic div (via a preceding block-attribute line; the ::: fence itself takes no inline attributes):
{k="{y}"}
:::
body
:::<div k="{y}">
<p>body</p>
</div>On an inline extension (the attributes attach to its output element):
:widget[x]{k="{y}"}<p><span class="ext-widget" k="{y}">x</span></p>A value may be single-quoted as well as double-quoted; either form strips its delimiters (grammar quoted_value).
[x]{k='{y}'}<p><span k="{y}">x</span></p>Author attributes on an inline extension attach to its rendered element — a class lands on the fallback span, and on the semantic element where an extension supplies one.
:widget[x]{.foo}<p><span class="ext-widget foo">x</span></p>A semantic span carries its attributes on the element it names, because a consumed name RENAMES the span rather than wrapping it (PART 9 §9).
[x]{#k .key kbd}<p><kbd id="k" class="key">x</kbd></p>A backslash escapes ASCII punctuation inside a quoted value, so the value can contain a literal quote.
[x]{title="a\"b"}<p><span title="a"b">x</span></p>The same escape applies on a heading's attribute block (a preceding block-attribute line, §15).
{title="a\"b"}
# H<section id="H">
<h1 title="a"b">H</h1>
</section>A trailing brace block that yields no attribute is not an attribute block — on a heading it stays part of the heading text rather than being dropped.
# H {???}<section id="H">
<h1>H {???}</h1>
</section>An attribute name (id, class, or key) is a grammar identifier, so it may not start with a digit. A name that violates this makes the whole {…} not an attribute block, so it stays literal. (A deliberate divergence from djot, which accepts digit-first identifiers and class="123"; see jgm/djot issue 399.)
[x]{.123} and [y]{12=v}<p>[x]{.123} and [y]{12=v}</p>A non-identifier character anywhere in the name is just as invalid, and one bad name leaves the whole block literal even alongside a valid class.
[x]{.a!b}<p>[x]{.a!b}</p>[x]{.ok .1}<p>[x]{.ok .1}</p>A digit, hyphen, or underscore after the first identifier character is fine.
[x]{.a1 #b2 k3=v}<p><span class="a1" id="b2" k3="v">x</span></p>Trailing attribute block edge cases
3 conformance fixtures
A trailing attribute block applies to an emphasis span, like any other inline node.
*x*{.real}<p><strong class="real">x</strong></p>A line-leading image is a standalone block image only when a trailing {…} yields real attributes. An empty/whitespace or invalid block falls through to a paragraph and stays literal.
{???}<p><img src="/i" alt="a">{???}</p>{ }<p><img src="/i" alt="a">{ }</p>Block attribute lines
7 conformance fixtures
A {...} attribute block on its own line attaches to the next block element and floats forward across intervening blank lines (§15 — reach).
{#id}
Text<p id="id">Text</p>Consecutive attribute blocks targeting the same element accumulate in source order: the last id wins, the last value for a given key wins, and classes accumulate with no de-duplication (§15 — accumulation; the djot canonical case).
{#id}
{key=val}
{.foo .bar}
{key=val2}
{.baz}
{#id2}
Okay<p id="id2" key="val2" class="foo bar baz">Okay</p>A single attribute block may wrap across lines — the closing } need not sit on the opening line (§15 — multi-line block).
{#id
.foo}
Text<p id="id" class="foo">Text</p>The next block can be any container, not just a paragraph. A block-attribute line before a table attaches to the <table>:
{.data}
|= A |= B |
| 1 | 2 |<table class="data">
<thead><tr><th scope="col">A</th><th scope="col">B</th></tr></thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
</tbody>
</table>…and before a blockquote it attaches to the <blockquote>:
{.epigraph}
> To be or not to be.<blockquote class="epigraph"><p>To be or not to be.</p></blockquote>A {...} line that directly trails a paragraph (no blank line) is still a leading block-attribute line: it interrupts the paragraph and floats forward. With no following block it is dropped:
Para
{.class}<p>Para</p>…and it floats across the blank line to the next block, never attaching backward to the paragraph it follows:
Para
{.class}
Next<p>Para</p>
<p class="class">Next</p>Boolean attributes
2 conformance fixtures
A bare word in a {…} block (no # / . / =) is a value-less (boolean) attribute, normally rendered name="". It works in any attribute position and mixes with id / class / key=value. The nine semantic span names are the core exception: on [content]{attrs} they select their semantic wrapper.
Press [Tab]{kbd} to indent.<p>Press <kbd>Tab</kbd> to indent.</p>A leading block-attribute line carries booleans too (here onto a paragraph), alongside a class:
{.callout open}
Details here.<p class="callout" open="">Details here.</p>Unquoted attribute values may contain dots and colons
1 conformance fixture
An unquoted attribute value admits . and : (besides letters, digits, -, _) so version strings, paths, and namespaced tokens need no quoting.
[a]{k=v.w}<p><span k="v.w">a</span></p>Adjacent attribute blocks on one line merge
1 conformance fixture
Two (or more) {...} blocks written back-to-back on a block-attribute line combine into one attribute set, exactly like a single space-separated block.
{.c}{#i}
# H<section id="i">
<h1 class="c">H</h1>
</section>Classes are deduplicated
1 conformance fixture
Repeated class values are merged into a single class attribute and deduplicated, keeping first-occurrence order (PART 9 §15). class="a a" and class="a" are equivalent in HTML, so the shorter form is emitted.
[x]{.a .a .b}<p><span class="a b">x</span></p>Code span and image trailing attributes are strict
1 conformance fixture
A trailing {...} on a code span or an image obeys the same strict attribute rule as any other inline attribute (PART 9 §14): a digit-first or otherwise invalid payload makes the whole block literal, not a bogus attribute.
`x`{2=v}<p><code>x</code>{2=v}</p>A bare attribute block on its own line is literal
1 conformance fixture
A block_attributes line requires at least one attribute (PART 9 §15); there is no block-level blessed-empty form (only the inline [text]{} span is blessed). So a bare {} line stays a literal paragraph.
{}<p>{}</p>Leading attribute brace before an inline span stays literal
1 conformance fixture
An unattached {…} attribute block that opens a line has nothing to its left to attach to, so it stays literal text; a following inline span still parses normally. The line is not consumed or dropped.
{k=v}{+i+}<p>{k=v}<ins>i</ins></p>Attribute block after a mention stays literal
1 conformance fixture
Mentions and tags are inert stable spans that do not take attributes (they share the soft-break / hard-break / plain-text class in this respect). A {…} glued after one stays literal text rather than attaching or vanishing.
@u{k=v.w}<p><span class="mention"><strong>@u</strong></span>{k=v.w}</p>Image trailing attribute is strict about the glue
2 conformance fixtures
A trailing {…} attaches to a sole image only when glued directly to the closing paren. A space between the image and the block breaks the glue, so the {…} stays literal text alongside the image.
Glued: the attributes attach to the image.
{.x}<img src="img.png" alt="alt" class="x">Spaced: the block stays literal.
 {.x}<p><img src="img.png" alt="alt"> {.x}</p>Indented attribute line stays literal
3 conformance fixtures
A top-level block opener only fires at column 0. An attribute brace indented by even a single space is not a floating attribute block, so it does not attach to what follows: the brace and the block below it fold together as one literal paragraph (the newline shows as a space when the two lines join).
An indented {…} above a paragraph stays literal.
{.note}
This paragraph.<p>{.note}
This paragraph.</p>An indented {…} above a list does not attach to the list either; the whole run is one literal paragraph and the bullet lines never open a list.
{.todo}
- one
- two<p>{.todo}
- one
- two</p>Control - flush left at column 0 the same brace is a floating attribute block and attaches to the paragraph below it.
{.note}
Para<p class="note">Para</p>Attribute braces on a list-item marker line
1 conformance fixture
Three shapes that look alike and mean different things (PART 9 §15 A8). What decides is whether content follows the brace run on that line, not the column the braces sit in.
-{…} text with no space after the marker attributes the item. With a space and text after the braces, the braces are part of that text. With a space and nothing after them, it is an ordinary attribute line that floats to the next block - a container does not get its own attribute rules.
Worth pinning because the two halves were each pinned already and their boundary was not: carve-rs read the third shape as literal text while the other engines read it as an attribute line, and neither could be shown wrong (carve#454).
-{.item} An attributed item.
- {.c} literal text
- {a=b .c}
# Attributed heading<ul>
<li class="item"><p>An attributed item.</p></li>
<li><p>{.c} literal text</p></li>
<li>
<h1 a="b" class="c" id="Attributed-heading">Attributed heading</h1>
</li>
</ul>A floating attribute stops at the item boundary
1 conformance fixture
§15 A2a floats a pending attribute past what renders nothing and attaches it to the next VISIBLE block. An item boundary ends that scope: the attribute does not carry into the next item's paragraph, so neither a nor b takes the class. All four implementations agree, and agreement is not a check - without a case, a future regression has nothing to fail against.
- a
{.c}
- b<ul>
<li><p>a</p></li>
<li><p>b</p></li>
</ul>A marker attribute may hold a quoted brace
1 conformance fixture
A list marker's attribute block is glued to the marker (1.{…}), and a quoted value inside it may contain } - the quote ends the value, not the first brace that comes along.
1.{title='a}b'} item<ol>
<li title="a}b">item</li>
</ol>An attribute name admits no colon
3 conformance fixtures
identifier is the production behind every attribute name - #id, .class, key=value and a bare boolean key all build on it - and it admits letters, digits, _ and - only. A colon-bearing name is therefore not recognized, and §14's rule that ONE unrecognized name makes the whole {...} not an attribute block leaves the run literal.
Nothing pinned this. No corpus document carried a colon in an attribute name, so compare:impls had no input that could show carve-php building xlink:href, class="a:b" and id="a:b" where carve-js and carve-rs left the same source literal (carve#797). The id row is the one with teeth: an anchor target exists in one engine and not in the other, so a link to #a:b resolves or dangles depending on which engine rendered the page.
The #a:b row does not render as inert text a reader sees verbatim. The attribute block is rejected and the leftover source is inline-parsed, so the #a inside it is an ordinary hashtag - which is why pinning the rendering is worth more here than describing it.
[a]{xlink:href=u}
[b]{k:v="q"}
[c]{.sm:hover}
[d]{#a:b}
[e]{.ok xml:lang=en}<p>[a]{xlink:href=u}</p>
<p>[b]{k:v=“q”}</p>
<p>[c]{.sm:hover}</p>
<p>[d]{<span class="tag"><strong>#a</strong></span>:b}</p>
<p>[e]{.ok xml:lang=en}</p>The colon is legal one position over, inside an unquoted VALUE, which unquoted_value admits so that xml:lang and sm:hover need no quoting when they are what an attribute HOLDS rather than what it is called. This pair is the control: it fails if a fix reaches past the name into the value.
[a]{k=x:y}
[b]{#i .c k=v}
| a | b |{.x}<p><span k="x:y">a</span></p>
<p><span id="i" class="c" k="v">b</span></p>
<table>
<tbody>
<tr class="x"><td>a</td><td>b</td></tr>
</tbody>
</table>A carrier other than an inline span reaches the same answer, so a table row whose trailing block carries a colon is not a table row at all, and a bullet whose glued block carries one does not open a list.
| a | b |{.a:b}
-{.a:b} item<p>| a | b |{.a:b}</p>
<p>-{.a:b} item</p>An inline attribute block does not span lines, but an attribute line does
3 conformance fixtures
attributes pads and separates with opt_ws, which grammar.ebnf annotates "spaces/tabs only, no line breaks". The line-spanning form is a different production: block_attributes separates with attr_separator = (whitespace | continuation), opt_ws, and continuation is where a newline is admitted.
So a brace run broken across two lines directly after an inline construct is literal text.
*x*{.a
.b}<p><strong>x</strong>{.a
.b}</p>A standalone attribute line may be written the same way, and it attaches to the block below it. The line break is admitted in the padding as well as between two attributes, so all three placements are one block.
{.a
.b}
paragraph<p class="a b">paragraph</p>{
.a}
first
{.b
}
second<p class="a">first</p>
<p class="b">second</p>The inline attribute interior is space-only, the attribute line is not
3 conformance fixtures
markup-carve/carve#906 is a POSITION distinction rather than a per-construct exception, and this category is the half that does not move. The three documents above pin the inline block narrowing; these pin what it narrowed against.
CONTROL. The SPACE forms of all four inline positions - the run after {, the run between two attributes, the run before }, and the blessed empty block - are untouched, and a reader that narrowed too far breaks here rather than silently accepting less:
*x*{.a .b}
*y*{ .c}
*z*{.d }
[w]{ }<p><strong class="a b">x</strong></p>
<p><strong class="c">y</strong></p>
<p><strong class="d">z</strong></p>
<p><span>w</span></p>The block-attribute LINE keeps whitespace at all three of its slots. It is the one construct in this grammar whose interior can hold a leading indentation run: after a continuation, the next line's leading whitespace IS indentation, and the rule that narrows the inline block is the same rule that protects this one.
{ .a .b }
paragraph<p class="a b">paragraph</p>And the continuation line's own indentation, which is the position the whole distinction is about:
{.a
.b}
paragraph<p class="a b">paragraph</p>A quoted attribute value stops at the newline
5 conformance fixtures
quoted_value is ONE production, read by the inline attribute block and by the block-attribute line alike, and the two normative files answered it differently: resources/grammar.ebnf built the value out of character, which is any Unicode character, and resources/carve-core.ohm excluded a newline at the same slot. Nothing pinned either answer (markup-carve/carve#888).
It is settled the ohm file's way, because the alternative falsifies a sentence the grammar already states. An inline attribute block cannot span lines (markup-carve/carve#897), and since markup-carve/carve#906 its padding takes space and its separator space+ - neither admits a line break. The quoted value was the last way through:
*x*{k="a
b"}<p><strong>x</strong>{k=“a
b”}</p>CONTROL. The same value on one line is an ordinary attribute, so the rule is about the line break and not about the quotes:
*x*{k="a b"}<p><strong k="a b">x</strong></p>The BLOCK-attribute line reads the same production, so a line break inside a quoted value ends that block too. This is the half with a cost: all three engines accept it today, and they do not agree on what it means - one keeps the newline in the value, two collapse it to a space, which no production describes.
{k="a
b"}
paragraph<p>{k=“a
b”}</p>
<p>paragraph</p>CONTROL, and the reason the rule is about the value rather than about the block: a block attribute may still span lines. continuation is where a newline is admitted, and it sits BETWEEN two tokens, never inside one.
{.a
.b}
paragraph<p class="a b">paragraph</p>CONTROL. A BLANK line is not a continuation - it ends the block, and the braces stay literal. The ohm grammar accepted one at every slot of blockAttrs until this landed, which no document could show because the layout automaton stops at a blank line before the rule is reached; it is pinned directly in tests/block-attribute-line-breaks.test.mjs and pinned here as behavior.
{.a
.b}
paragraph<p>{.a</p>
<p>.b}</p>
<p>paragraph</p>A structural attribute leads the author's own
2 conformance fixtures
type and start are fixed by the first item's marker, so they belong to the element's shape rather than to what the author wrote in an attribute block. They are emitted first, and the author's attributes keep their source order after them (PART 11 §5.1). Nothing pinned this before, and the two readings of it split the engines (markup-carve/carve#1090).
{k=v .attr}
a. alpha<ol type="a" k="v" class="attr">
<li>alpha</li>
</ol>A decimal marker emits no type, so there is nothing to lead and the author's attributes stand alone. This is the control: it agreed in every engine already, and it fails only if a change moves authored attributes rather than ordering the structural one.
{.attr}
1. one<ol class="attr">
<li>one</li>
</ol>A boolean and a key/value of the same name are one attribute
3 conformance fixtures
A boolean and a key=value of the SAME name are one attribute, not two. A boolean is a key/value whose value is empty (PART 4), so it takes that name's slot and the repeated key keeps the LAST value at the FIRST position - the same rule any repeated key follows. Emitting both would produce two HTML attributes with one name, which is not valid HTML.
[x]{a=1 a}<p><span a="">x</span></p>Order decides which value survives, not which spelling:
[x]{a a=2}<p><span a="2">x</span></p>The slot is the FIRST appearance of the name, so an unrelated attribute written between them keeps its own place:
[x]{a .c a=2}<p><span a="2" class="c">x</span></p>A semantic name renames the span, and the leftovers ride the element
4 conformance fixtures
An authored [content]{attrs} span renders its <span> element whether or not any attribute reaches the output - hardening removes attributes, never the element the author wrote. A semantic name is not an attribute that was removed: it never reaches the output as one. It renames the element (PART 9 §9), so the span does not survive as a wrapper and every remaining attribute lands on the outermost semantic element.
[x]{}<p><span>x</span></p>[x]{onclick="steal()"}<p><span>x</span></p>[x]{kbd onclick="steal()"}<p><kbd>x</kbd></p>[x]{kbd}<p><kbd>x</kbd></p>A language attribute is exact sugar for lang
5 conformance fixtures
{:TAG} sets the natural language of the content it attaches to. It is sugar for lang=TAG and nothing else: the attribute reaching the AST, the merge, and the HTML are the ones the long form already produced (language_attribute in resources/grammar.ebnf, markup-carve/carve#1114).
The title is [Le Bon Usage]{:fr}.<p>The title is <span lang="fr">Le Bon Usage</span>.</p>A tag is structure, not a registry lookup: any hyphen-separated run of ASCII alphanumeric subtags of one to eight characters parses, so script and region subtags, private use and grandfathered tags all reach lang unchanged and with their case intact.
[a]{:de-CH} [b]{:sr-Latn-RS} [c]{:x-acme} [d]{:i-klingon}<p><span lang="de-CH">a</span> <span lang="sr-Latn-RS">b</span> <span lang="x-acme">c</span> <span lang="i-klingon">d</span></p>The empty form declares the language explicitly unknown. That is not the same as leaving the attribute off: lang="" stops the content inheriting the language of whatever surrounds it.
{:de}
> Der Titel ist [unbekannt]{:}.<blockquote lang="de"><p>Der Titel ist <span lang="">unbekannt</span>.</p></blockquote>It takes its place in source order among the other attribute kinds.
[x]{#quote :fr .formal title=bonjour}<p><span id="quote" lang="fr" class="formal" title="bonjour">x</span></p>A block attribute line carries it as well.
{:grc}
Μῆνιν ἄειδε θεά<p lang="grc">Μῆνιν ἄειδε θεά</p>A malformed language tag leaves the whole block literal
4 conformance fixtures
The envelope is checked while parsing, so a candidate that misses it is not a half-consumed attribute: the block fails and the braces stay in the text, which is what a malformed attribute block does everywhere else (PART 9 §14). An underscore, an empty subtag, a leading or trailing hyphen and a non-ASCII letter each fail it.
[a]{:en_US} [b]{:-en} [c]{:en-} [d]{:français}<p>[a]{:en_US} [b]{:-en} [c]{:en-} [d]{:français}</p>A subtag runs to eight characters. The ninth has nothing to match, so the block fails there rather than truncating the tag.
[a]{:abcdefgh} [b]{:abcdefghi}<p><span lang="abcdefgh">a</span> [b]{:abcdefghi}</p>The deferred braced-symbol spelling keeps its slot: a trailing : is not a subtag character, so {:name:} stays literal under this production (docs/dismissed-syntax.md).
[x]{:tada:}<p>[x]{:tada:}</p>An attribute NAME still admits no colon, so a namespaced spelling stays literal too - the language sigil leads its attribute, it does not appear inside one.
[x]{xml:lang=en}<p>[x]{xml:lang=en}</p>A language attribute and lang are one key
5 conformance fixtures
{:TAG} desugars before any merge runs, so writing it beside lang=TAG is a repeated key and follows the rule every repeated key follows: the last value wins and the slot stays where the key first appeared (§15 - accumulation). There is no precedence between the two spellings; only their order matters.
[a]{:fr lang=de} [b]{lang=de :fr}<p><span lang="de">a</span> <span lang="fr">b</span></p>The surviving value lands in the slot the FIRST of the two opened, which is what makes the shorthand invisible to the serializer.
[x]{k=1 :fr lang=de title=t}<p><span k="1" lang="de" title="t">x</span></p>Two shorthands collide the same way.
[x]{:fr :de}<p><span lang="de">x</span></p>Across accumulated block-attribute lines the answer is the same, because the desugaring happens before that merge too.
{:fr}
{lang=de}
Text<p lang="de">Text</p>An UPPERCASE key is a different key. Attribute names are case-sensitive (PART 11 §1), so LANG is an ordinary attribute that happens to spell a reserved name differently - the same way {KBD} is not the semantic span {kbd} (PART 9 §10). Only the exact lowercase lang is the language attribute's other spelling.
[a]{LANG=fr} [b]{lang=fr}<p><span LANG="fr">a</span> <span lang="fr">b</span></p>This pair is what the round-trip check needs to be able to fail. It compares toHtml(fmt(x)) against toHtml(x) over every document, and a writer that folded the key case would rewrite [a]{LANG=fr} to [a]{:fr} and render lang="fr" where the source asked for LANG="fr". Every other corpus document writes its attribute names in lower case, so the check could not see it (carve#1137).
The language sigil takes no padding
2 conformance fixtures
A space after : does not belong to the language attribute. The TAG is optional, the separator is not, so {: fr} is the empty language attribute followed by a SEPARATE boolean fr - not a language attribute reading fr, and not a failed block. It follows from ':', [ language_tag ] with no special case, and the cost falls on a typo rather than on anything written deliberately.
[x]{: fr}<p><span lang="" fr="">x</span></p>The same source without the space is the ordinary form, which is the control: the two differ by one character and by one attribute.
[x]{:fr}<p><span lang="fr">x</span></p>A boolean lang is the third spelling of the same key
2 conformance fixtures
{lang} means lang="" under PART 4's boolean rule, so the key has three spellings - :TAG, lang=TAG and the bare name - and all three land in one slot with the last value winning. This was stated in the grammar and pinned nowhere until markup-carve/carve#1125 made the executable spec merge a boolean with a key/value of the same name.
[a]{:fr lang} [b]{lang :fr}<p><span lang="">a</span> <span lang="fr">b</span></p>The bare name on its own is the empty language attribute written the long way, so it declares the language unknown exactly as {:} does.
[x]{lang}<p><span lang="">x</span></p>The semantic registry holds no element Carve already spells
4 conformance fixtures
abbr, time, samp, var, kbd, cite and dfn render as their same-named HTML element, in the :name[…] form and as a compact span attribute. A name is admitted only where the language has no other spelling for that element, so code and mark are NOT in the registry (PART 9 §9): a code span already writes <code> and the highlight syntax already writes <mark>. The inline literal beside them writes neither - it drops the wrapper, which is what it is for.
`x` !`x` =x=<p><code>x</code> x <mark>x</mark></p>Both are ordinary extension names instead, and take the generic fallback.
:code[*b*] :mark[*b*]<p><span class="ext-code"><strong>b</strong></span> <span class="ext-mark"><strong>b</strong></span></p>As compact span attributes they are ordinary booleans, and land on the outer span beside whatever else the author wrote.
[*b*]{code} [*b*]{mark}<p><span code=""><strong>b</strong></span> <span mark=""><strong>b</strong></span></p>code is the name that showed why one spelling per element is a rule and not a preference: a code span is verbatim while an extension body is parsed, so the registry entry gave one tag two content models, chosen by which spelling the author reached for.
`*b*`<p><code>*b*</code></p>Two attributes need a separator between them
4 conformance fixtures
attribute_list is attribute, {space+, attribute} (PART 7), so two attributes may not touch. {.a.b} is not two classes, it is one malformed class name, and one invalid item makes the whole block literal (§14).
[x]{.a.b}<p>[x]{.a.b}</p>The same holds when the two kinds differ, which is the shape a strip-based validator gets wrong: it removes .a, leaves a separator behind where the source had none, and then accepts #i as though it had been separated.
[x]{.a#i}<p>[x]{.a#i}</p>An id abutting a class is literal for the same reason, and the #i.c left inside the braces is then ordinary content - where a # opens a tag.
[x]{#i.c}<p>[x]{<span class="tag"><strong>#i.c</strong></span>}</p>A colon inside an UNQUOTED VALUE is not a separator question: the value runs to the next whitespace, so it is one attribute and the block is valid.
[x]{k=a:b}<p><span k="a:b">x</span></p>A derived title yields to an authored one
2 conformance fixtures
abbr and time values become title and datetime, which are ordinary attribute names an author may also write. Where both are present the authored one wins - one element never carries the same attribute twice (PART 9 §9).
[x]{abbr="derived" title="authored"} [y]{time="2026" datetime="custom"}<p><abbr title="authored">x</abbr> <time datetime="custom">y</time></p>Without an authored one the derived attribute is what the element carries.
[HTML]{abbr="HyperText Markup Language"}<p><abbr title="HyperText Markup Language">HTML</abbr></p>An attribute block reaches the nested list it precedes
10 conformance fixtures
322-an-attribute-block-reaches-the-nested-list-it-precedes322-an-attribute-block-reaches-the-nested-list-it-precedes-2322-an-attribute-block-reaches-the-nested-list-it-precedes-3322-an-attribute-block-reaches-the-nested-list-it-precedes-4322-an-attribute-block-reaches-the-nested-list-it-precedes-5322-an-attribute-block-reaches-the-nested-list-it-precedes-6322-an-attribute-block-reaches-the-nested-list-it-precedes-7322-an-attribute-block-reaches-the-nested-list-it-precedes-8322-an-attribute-block-reaches-the-nested-list-it-precedes-9322-an-attribute-block-reaches-the-nested-list-it-precedes-10
An attribute block attaches to the block that FOLLOWS it, and a nested list is a block. Inside a list item that is easy to get wrong, because the item's continuation collector stops at a marker sitting at the item's content column so the list parser can own the sub-list: an implementation that splits there leaves the attribute line at the end of one run and the nested list at the start of the next, and the attributes are silently discarded. Three engines disagreed about this for a long time with nothing in the corpus to say who was right (carve#1238).
The target is the nested <ul>/<ol> - not the item, and not the outer list. With a blank line before the attribute block:
- a
{.x}
- b<ul>
<li>a
<ul class="x">
<li>b</li>
</ul>
</li>
</ul>The blank line decides nothing. The same three lines with no blank between them mean the same document, and the item stays tight either way (PART 9 §17 L2: a sub-block attached after a blank leaves the item tight):
- a
{.x}
- b<ul>
<li>a
<ul class="x">
<li>b</li>
</ul>
</li>
</ul>That the blank is irrelevant is not a claim about lists in particular. The same unseparated attribute line in front of a PARAGRAPH has always attached, in the same position with the same spacing:
- a
{.x}
para<ul>
<li>a
<p class="x">para</p>
</li>
</ul>One nesting level up the three lines read identically. This is the control that makes the rule above uniform rather than a special case for nested lists - the top-level pair {.x} before a list is already pinned by 13-attributes-5:
para
{.x}
- b<p>para</p>
<ul class="x">
<li>b</li>
</ul>An ordered nested list is the same block in the same position:
- a
{.x}
1. b<ul>
<li>a
<ol class="x">
<li>b</li>
</ol>
</li>
</ul>Stacked attribute blocks MERGE into one set, the way they do at top level and in front of a paragraph. An implementation that keeps a single pending slot and overwrites it drops everything but the last block, and only a two-block document says so:
- a
{.x}
{#i}
- b<ul>
<li>a
<ul class="x" id="i">
<li>b</li>
</ul>
</li>
</ul>The attribute line does not have to be alone in the run it ends. A fix keyed on "the whole continuation run is an attribute block" passes the cases above and fails this one:
- a
para
{.x}
- b<ul>
<li><p>a</p>
<p>para</p>
<ul class="x">
<li>b</li>
</ul>
</li>
</ul>A line that merely ENDS in a brace is a paragraph, not a second attribute block: the first block attaches to that paragraph, the text survives, and the nested list below it is left plain:
- a
{.x}
more text}
- b<ul>
<li><p>a</p>
<p class="x">more text}</p>
<ul>
<li>b</li>
</ul>
</li>
</ul>The braces have to be FLUSH in the item's body. One space past the content column is a paragraph, exactly as 87-compact-list-blocks-10 pins for the form with nothing after it - and the nested list that follows is then plain. An implementation that trims the indentation before looking for the brace deletes this paragraph and re-tightens the item:
- a
{.c}
- b<ul>
<li><p>a</p>
<p>{.c}</p>
<ul>
<li>b</li>
</ul>
</li>
</ul>None of this touches the abutting form, which is a different mechanism reaching a different element: a block glued to the marker attributes the <li> (PART 9 §15), at any depth. 90-list-item-attributes pins it at top level; nested, the class lands on the item and the nested <ul> stays plain:
- a
-{.x} b<ul>
<li>a
<ul>
<li class="x">b</li>
</ul>
</li>
</ul>An attribute line after a continuation marker attributes the attached block
4 conformance fixtures
325-an-attribute-line-after-a-continuation-marker-attributes-the-attached-block325-an-attribute-line-after-a-continuation-marker-attributes-the-attached-block-2325-an-attribute-line-after-a-continuation-marker-attributes-the-attached-block-3325-an-attribute-line-after-a-continuation-marker-attributes-the-attached-block-4
An attribute block attaches to the block that FOLLOWS it, and the target is that block (carve#1238). Nothing in that rule exempts a + continuation marker, and a continuation is exactly the case where the following block is inside the item: PART 2 has block = … | block_attributes | … and PART 11's grammar has continuation_marker_block = continuation_marker, block, so an attribute line is itself a block the marker can attach, and PART 9 §15 gives it its float to the next one.
An implementation that reads the line as ordinary text loses both halves at once - the attributes AND the containment - because the run the marker opened then ends at the text and the quote below it starts a new top-level block (carve-rs#1020):
- a
+
{.x}
> q<ul>
<li>a
<blockquote class="x"><p>q</p></blockquote>
</li>
</ul>The control is the same document with the attribute line removed. The marker's own job is unchanged by this rule, so the quote lands in the item either way and only the class moves:
- a
+
> q<ul>
<li>a
<blockquote><p>q</p></blockquote>
</li>
</ul>A PARAGRAPH after the attribute line is the second half, and it fails differently: an implementation that keeps the line as text has nowhere to put it but the item's open lead paragraph, so the whole run folds into a and the attributes vanish with the block boundary.
- a
+
{.x}
para<ul>
<li>a
<p class="x">para</p>
</li>
</ul>A second item pins the boundary the mis-parse moves. The attached quote belongs to the first item, so - c is still a sibling of - a and not of anything the attribute line produced:
- a
+
{.x}
> q
- c<ul>
<li>a
<blockquote class="x"><p>q</p></blockquote>
</li>
<li>c</li>
</ul>Attributes
1 conformance fixture
Consecutive attribute lines merge, and classes accumulate in source order.
{.a}
{.b}
Merged.<p class="a b">Merged.</p>Inline span
4 conformance fixtures
A valid attribute block forms a span even when it is empty — an empty {} is the explicit "make this a span" hook (it can be decorated by a processor).
[x]{}<p><span>x</span></p>A whitespace-only block ({ }) is also a valid empty block and forms the same bare span.
[x]{ }<p><span>x</span></p>A block whose content is not a recognized attribute (e.g. {???}) is not an attribute block at all: the brackets and the block render literally.
[x]{???}<p>[x]{???}</p>The bracket content is still inline-parsed even when the trailing block is invalid, so emphasis inside the brackets is rendered.
[*x*]{???}<p>[<strong>x</strong>]{???}</p>