Skip to content

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

Classes accumulate; #id and key=value (bare or quoted) attach in source order on the <span>.

carve
[note]{.a .b #n key=val}
html
<p><span class="a b" id="n" key="val">note</span></p>

A quoted value keeps its spaces.

carve
[x]{title="a b"}
html
<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.

carve
[x]{data-x="{y}"}
html
<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:

carve
[t](u){k="{y}"}
html
<p><a href="u" k="{y}">t</a></p>

On an image:

carve
![a](u){k="{y}"}
html
<img src="u" alt="a" k="{y}">

On a heading (via a preceding block-attribute line; the attributes attach to the <h1>):

carve
{k="{y}"}
# H
html
<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):

carve
{k="{y}"}
:::
body
:::
html
<div k="{y}">
  <p>body</p>
</div>

On an inline extension (the attributes attach to its output element):

carve
:widget[x]{k="{y}"}
html
<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).

carve
[x]{k='{y}'}
html
<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.

carve
:widget[x]{.foo}
html
<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).

carve
[x]{#k .key kbd}
html
<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.

carve
[x]{title="a\"b"}
html
<p><span title="a&quot;b">x</span></p>

The same escape applies on a heading's attribute block (a preceding block-attribute line, §15).

carve
{title="a\"b"}
# H
html
<section id="H">
  <h1 title="a&quot;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.

carve
# H {???}
html
<section id="H">
  <h1>H {???}</h1>
</section>

An explicit id or class may start with an ASCII digit, matching valid imported HTML. Attribute keys still use the narrower grammar identifier, so a digit-leading key makes the whole {…} stay literal. No other leading character is newly admitted.

carve
[x]{.123} [y]{#7-x} and [z]{12=v}
html
<p><span class="123">x</span> <span id="7-x">y</span> and [z]{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.

carve
[x]{.a!b}
html
<p>[x]{.a!b}</p>
carve
[x]{.ok .1}
html
<p><span class="ok 1">x</span></p>

A digit, hyphen, or underscore after the first identifier character is fine.

carve
[x]{.a1 #b2 k3=v}
html
<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.

carve
*x*{.real}
html
<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.

carve
![a](/i){???}
html
<p><img src="/i" alt="a">{???}</p>
carve
![a](/i){ }
html
<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).

carve
{#id}

Text
html
<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).

carve
{#id}
{key=val}
{.foo .bar}
{key=val2}
{.baz}
{#id2}
Okay
html
<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).

carve
{#id
 .foo}
Text
html
<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>:

carve
{.data}
|= A |= B |
| 1  | 2  |
html
<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>:

carve
{.epigraph}
> To be or not to be.
html
<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:

carve
Para
{.class}
html
<p>Para</p>

…and it floats across the blank line to the next block, never attaching backward to the paragraph it follows:

carve
Para
{.class}

Next
html
<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. In the AST it has the same empty-string value as name="", and it normally renders name="". It works in any attribute position and mixes with id / class / key=value. The three core semantic span names are the exception: on [content]{attrs} they select their semantic wrapper. Four more names do so when the SemanticSpan extension is enabled.

carve
Press [Tab]{kbd} to indent.
html
<p>Press <kbd>Tab</kbd> to indent.</p>

A leading block-attribute line carries booleans too (here onto a paragraph), alongside a class:

carve
{.callout open}
Bare spelling.

{.callout open=""}
Explicit empty string.
html
<p class="callout" open="">Bare spelling.</p>
<p class="callout" open="">Explicit empty string.</p>

The bare and explicit empty-string spellings have the same meaning. The canonical writer uses the bare one for both.

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.

carve
[a]{k=v.w}
html
<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.

carve
{.c}{#i}
# H
html
<section id="i">
  <h1 class="c">H</h1>
</section>

Glued attribute blocks on an inline element merge ​

3 conformance fixtures

An inline element takes a run of attribute blocks glued one after another, and the run attaches as one list. The blocks merge the way stacked block-attribute lines do (§15 A3), so classes accumulate.

carve
*x*{.k}{.j}
html
<p><strong class="k j">x</strong></p>

The same merge on any inline element: an id or a key keeps its last value, a repeated class appears once, and each attribute stays where it first appeared.

carve
`c`{#a .k}{#b k=1}{.k k=2}
html
<p><code id="b" class="k" k="2">c</code></p>

A run ends at the first thing that is not an attribute block: a space, a brace group that is not a valid block, or a glued construct, which stays content.

carve
*a*{.k} {.j}

*b*{.k}{???}

*c*{.k}{*d*}
html
<p><strong class="k">a</strong> {.j}</p>
<p><strong class="k">b</strong>{???}</p>
<p><strong class="k">c</strong><strong>d</strong></p>

Footnote references take an attribute run, editorial substitution and comment take none ​

3 conformance fixtures

A footnote reference and an inline note are inline elements with an attribute slot, so a glued run of blocks merges onto the note reference like any other (§15 A3).

carve
a[^n]{.k}{.j} b

[^n]: x
html
<p>a<a id="fnref1" href="#fn1" role="doc-noteref" class="k j"><sup>1</sup></a> b</p>
<section role="doc-endnotes" aria-label="Footnotes">
  <hr>
  <ol>
    <li id="fn1">
      <p>x<a href="#fnref1" role="doc-backlink" aria-label="Back to reference">↩</a></p>
    </li>
  </ol>
</section>
carve
a^[x]{.k}{.j} b
html
<p>a<a id="fnref1" href="#fn1" role="doc-noteref" class="k j"><sup>1</sup></a> b</p>
<section role="doc-endnotes" aria-label="Footnotes">
  <hr>
  <ol>
    <li id="fn1">
      <p>x<a href="#fnref1" role="doc-backlink" aria-label="Back to reference">↩</a></p>
    </li>
  </ol>
</section>

Editorial substitution and editorial comment have no attribute slot, so a block after either one is text.

carve
{~a~>b~}{.k} c

{#note#}{.k} c
html
<p><del>a</del><ins>b</ins>{.k} c</p>
<p><span class="critic-comment">note</span>{.k} c</p>

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.

carve
[x]{.a .a .b}
html
<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 attribute rule as any other inline attribute (PART 9 §14): digit-leading ids/classes are valid, while a digit-leading key or otherwise invalid payload stays literal.

carve
`x`{2=v}
html
<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.

carve
{}
html
<p>{}</p>

Leading attribute brace before an inline span stays literal ​

3 conformance fixtures

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.

carve
{k=v}{+i+}
html
<p>{k=v}<ins>i</ins></p>

The braces of an unattached block are text like any other, so a code span or link that starts inside them runs past the }, and a lone brace inside a forced span is text too.

carve
x{.k title="`"} y

x{title="[a"}](u)

x{*a { b*} c
html
<p>x{.k title=“<code>"} y</code></p>
<p>x{title=“<a href="u">a”}</a></p>
<p>x<strong>a { b</strong> c</p>

A bare delimiter that opens nothing takes no block either: the characters stay content, so the run inside them reaches the end of the block. A delimiter that does close a span still takes the block.

carve
x*{title="`"} y

*x*{title="`"} y
html
<p>x*{title=“<code>"} y</code></p>
<p><strong title="`">x</strong> y</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.

carve
@u{k=v.w}
html
<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.

carve
![alt](img.png){.x}
html
<img src="img.png" alt="alt" class="x">

Spaced: the block stays literal.

carve
![alt](img.png) {.x}
html
<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.

carve
 {.note}
 This paragraph.
html
<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.

carve
 {.todo}
 - one
 - two
html
<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.

carve
{.note}
Para
html
<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).

carve
-{.item} An attributed item.
- {.c} literal text

- {a=b .c}
  # Attributed heading
html
<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.

carve
- a

  {.c}
- b
html
<ul>
  <li><p>a</p></li>
  <li><p>b</p></li>
</ul>

An attribute line under an attributed sub-item stays in that item ​

3 conformance fixtures

A marker-attached attribute block contributes zero to the content column (§24 C3), so a line below it reaches the sub-item at the column it would reach with no such block. A sibling marker closes that item and §15 A4 drops the pending attribute, which may no more open a second sublist than escape to a document paragraph (CARVE-P0-008).

carve
- a
  -{#p} b
    {#x}
  - c
html
<ul>
  <li>a
    <ul>
      <li id="p">b</li>
      <li>c</li>
    </ul>
  </li>
</ul>

Dropping {#p} is the control: the reading is the same one, so the attribute block is what an engine splitting the sublist has read as width.

carve
- a
  - b
    {#x}
  - c
html
<ul>
  <li>a
    <ul>
      <li>b</li>
      <li>c</li>
    </ul>
  </li>
</ul>

An ordered sub-item puts the same line at column 5, since 1. is three columns of marker and the attribute block is still none.

carve
- a
  1.{#p} b
     {#x}
  2. c
html
<ul>
  <li>a
    <ol>
      <li id="p">b</li>
      <li>c</li>
    </ol>
  </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.

carve
1.{title='a}b'} item
html
<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.

carve
[a]{xlink:href=u}

[b]{k:v="q"}

[c]{.sm:hover}

[d]{#a:b}

[e]{.ok xml:lang=en}
html
<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.

carve
[a]{k=x:y}

[b]{#i .c k=v}

| a | b |{.x}
html
<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.

carve
| a | b |{.a:b}

-{.a:b} item
html
<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.

carve
*x*{.a
.b}
html
<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.

carve
{.a
.b}

paragraph
html
<p class="a b">paragraph</p>
carve
{
.a}

first

{.b
}

second
html
<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:

carve
*x*{.a .b}

*y*{ .c}

*z*{.d }

[w]{ }
html
<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.

carve
{	.a	.b	}

paragraph
html
<p class="a b">paragraph</p>

And the continuation line's own indentation, which is the position the whole distinction is about:

carve
{.a
	.b}

paragraph
html
<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:

carve
*x*{k="a
b"}
html
<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:

carve
*x*{k="a b"}
html
<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.

carve
{k="a
b"}

paragraph
html
<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.

carve
{.a
.b}

paragraph
html
<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.

carve
{.a

.b}

paragraph
html
<p>{.a</p>
<p>.b}</p>
<p>paragraph</p>

A quoted value and a quoted title escape different sets ​

10 conformance fixtures

quoted_value reads escaped_char, a backslash plus ASCII punctuation, so the set is not the closing quote alone: an escaped brace, an escaped backslash, an escaped pipe and the other quote glyph all resolve to that character. Both quote spellings read the same set, and a block-attribute line reads the same quoted_value an inline block does.

link_title escapes its own closing quote and nothing else, so the two slots do not share one helper. What they do agree on is the quote itself, in both spellings, and image_title = link_title reads it the same way.

carve
[x]{k="a\}b"}
html
<p><span k="a}b">x</span></p>
carve
[x]{k='a\}b'}
html
<p><span k="a}b">x</span></p>
carve
[x]{k="a\\b"}
html
<p><span k="a\b">x</span></p>
carve
[x]{k='a\\b'}
html
<p><span k="a\b">x</span></p>
carve
[x]{k="a\'b"}
html
<p><span k="a&apos;b">x</span></p>
carve
[x]{title="a\|b"}
html
<p><span title="a|b">x</span></p>
carve
{k="a\}b"}
p
html
<p k="a}b">p</p>
carve
[a](/u 't\'u')
html
<p><a href="/u" title="t&apos;u">a</a></p>
carve
[a](/u "t\"u")
html
<p><a href="/u" title="t&quot;u">a</a></p>
carve
![a](/i 't\'u')
html
<img src="/i" alt="a" title="t&apos;u">

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).

carve
{k=v .attr}
a. alpha
html
<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.

carve
{.attr}
1. one
html
<ol class="attr">
  <li>one</li>
</ol>

A core directive-kind class leads authored attributes ​

1 conformance fixture

The kind identifies the core div. Its class leads both authored attributes (CARVE-P10-011).

carve
{#d k=v}
::: foo
a
:::
html
<div class="foo" id="d" k="v">
  <p>a</p>
</div>

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.

carve
[x]{a=1 a}
html
<p><span a="">x</span></p>

Order decides which value survives, not which spelling:

carve
[x]{a a=2}
html
<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:

carve
[x]{a .c a=2}
html
<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.

carve
[x]{}
html
<p><span>x</span></p>
carve
[x]{onclick="steal()"}
html
<p><span>x</span></p>
carve
[x]{kbd onclick="steal()"}
html
<p><kbd>x</kbd></p>
carve
[x]{kbd}
html
<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).

carve
The title is [Le Bon Usage]{:fr}.
html
<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.

carve
[a]{:de-CH} [b]{:sr-Latn-RS} [c]{:x-acme} [d]{:i-klingon}
html
<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.

carve
{:de}
> Der Titel ist [unbekannt]{:}.
html
<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.

carve
[x]{#quote :fr .formal title=bonjour}
html
<p><span id="quote" lang="fr" class="formal" title="bonjour">x</span></p>

A block attribute line carries it as well.

carve
{:grc}
Μῆνιν ἄειδε θεά
html
<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.

carve
[a]{:en_US} [b]{:-en} [c]{:en-} [d]{:français}
html
<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.

carve
[a]{:abcdefgh} [b]{:abcdefghi}
html
<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).

carve
[x]{:tada:}
html
<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.

carve
[x]{xml:lang=en}
html
<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.

carve
[a]{:fr lang=de} [b]{lang=de :fr}
html
<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.

carve
[x]{k=1 :fr lang=de title=t}
html
<p><span k="1" lang="de" title="t">x</span></p>

Two shorthands collide the same way.

carve
[x]{:fr :de}
html
<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.

carve
{:fr}
{lang=de}
Text
html
<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.

carve
[a]{LANG=fr} [b]{lang=fr}
html
<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.

carve
[x]{: fr}
html
<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.

carve
[x]{:fr}
html
<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.

carve
[a]{:fr lang} [b]{lang :fr}
html
<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.

carve
[x]{lang}
html
<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.

carve
`x` !`x` =x=
html
<p><code>x</code> x <mark>x</mark></p>

Both are ordinary extension names instead, and take the generic fallback.

carve
:code[*b*] :mark[*b*]
html
<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.

carve
[*b*]{code} [*b*]{mark}
html
<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.

carve
`*b*`
html
<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).

carve
[x]{.a.b}
html
<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.

carve
[x]{.a#i}
html
<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.

carve
[x]{#i.c}
html
<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.

carve
[x]{k=a:b}
html
<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).

carve
[x]{abbr="derived" title="authored"} [y]{time="2026" datetime="custom"}
html
<p><abbr title="authored">x</abbr> <time datetime="custom">y</time></p>

Without an authored one the derived attribute is what the element carries.

carve
[HTML]{abbr="HyperText Markup Language"}
html
<p><abbr title="HyperText Markup Language">HTML</abbr></p>

An attribute block reaches the nested list it precedes ​

10 conformance fixtures

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:

carve
- a

  {.x}
  - b
html
<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):

carve
- a
  {.x}
  - b
html
<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:

carve
- a
  {.x}
  para
html
<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:

carve
para
{.x}
- b
html
<p>para</p>
<ul class="x">
  <li>b</li>
</ul>

An ordered nested list is the same block in the same position:

carve
- a

  {.x}
  1. b
html
<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:

carve
- a

  {.x}
  {#i}
  - b
html
<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:

carve
- a

  para
  {.x}
  - b
html
<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:

carve
- a

  {.x}
  more text}
  - b
html
<ul>
  <li><p>a</p>
    <p class="x">more text}</p>
    <ul>
      <li>b</li>
    </ul>
  </li>
</ul>

The braces may establish an authored block base past the canonical column. The attribute remains structural and attaches to the nested list that follows at the same authored base:

carve
- a

   {.c}
   - b
html
<ul>
  <li>a
    <ul class="c">
      <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:

carve
- a

  -{.x} b
html
<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

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):

carve
- a
+
{.x}
> q
html
<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:

carve
- a
+
> q
html
<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.

carve
- a
+
{.x}
para
html
<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:

carve
- a
+
{.x}
> q
- c
html
<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.

carve
{.a}
{.b}
Merged.
html
<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).

carve
[x]{}
html
<p><span>x</span></p>

A whitespace-only block ({ }) is also a valid empty block and forms the same bare span.

carve
[x]{ }
html
<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.

carve
[x]{???}
html
<p>[x]{???}</p>

The bracket content is still inline-parsed even when the trailing block is invalid, so emphasis inside the brackets is rendered.

carve
[*x*]{???}
html
<p>[<strong>x</strong>]{???}</p>

An attribute line below a list item interrupts it ​

2 conformance fixtures

An attribute line is an INVISIBLE construct, and the three of them - reference definitions, comments and {...} attribute lines - interrupt a paragraph with no blank line and produce no block of their own. A list item's paragraph is a paragraph, so a column-0 attribute line below one ends the item and floats forward to whatever block comes next, exactly as a comment or a definition written in the same place does.

The executable spec folded it instead, so the line came back as literal text inside the item while the block below took no attributes. It read the same line correctly one construct over: a %% comment and a [r]: /u definition at that column already interrupted (markup-carve/carve-rs#1167).

carve
- b
{.x}

> q
html
<ul>
  <li>b</li>
</ul>
<blockquote class="x"><p>q</p></blockquote>

With no block beneath it the attributes are simply consumed, which is what "produces no block of its own" means - not kept as text.

carve
- b
{.x}
html
<ul>
  <li>b</li>
</ul>

A wrapped attribute line leaves no paragraph open ​

3 conformance fixtures

A block-attribute line is one interrupter even when its braces span physical lines. Its continuation is not paragraph text and cannot keep a list item open for a later below-column lazy line. The result is identical to the one-line attribute spelling.

carve
- {.a
  .b}
tail
html
<ul>
  <li></li>
</ul>
<p>tail</p>

The same rule applies after visible item prose. The floating attributes remain scoped to the item and become dangling when the item closes; they do not escape onto the top-level paragraph.

carve
- prose
  {.a
  .b}
tail
html
<ul>
  <li>prose</li>
</ul>
<p>tail</p>

The one-line spelling is the control.

carve
- {.a}
tail
html
<ul>
  <li></li>
</ul>
<p>tail</p>

An engine-written shape says what it is called ​

7 conformance fixtures

Four places carried a ROLE, or a state, and no accessible NAME (carve#1468). An untitled admonition is named by its type word, drawn from the labels map so it is not fixed English; a TITLED one points at the title the author already wrote, so the visible name and the spoken one are one string.

carve
::: note
Untitled.
:::

::: warning "Careful"
Titled.
:::
html
<aside class="admonition note" aria-label="Note">
  <p>Untitled.</p>
</aside>
<aside class="admonition warning" aria-labelledby="adm-1">
  <p class="admonition-title" id="adm-1">Careful</p>
  <p>Titled.</p>
</aside>

The author's own name WINS, and no id is minted: a second naming attribute beside the author's would leave the name undefined.

carve
{aria-label="Mine"}
::: note "Careful"
The author named it.
:::
html
<aside class="admonition note" aria-label="Mine">
  <p class="admonition-title">Careful</p>
  <p>The author named it.</p>
</aside>

A task box takes the item's own visible text, DERIVED rather than invented, so nothing is written in English and a translated document translates it once.

carve
- [ ] read the /docs/
- [x] done
html
<ul>
  <li><input type="checkbox" disabled aria-label="read the docs"> read the <em>docs</em></li>
  <li><input type="checkbox" checked disabled aria-label="done"> done</li>
</ul>

Only the item's FIRST block sits beside the box, and only a paragraph carries inline text, so a non-paragraph lead takes NO name - an empty one would be worse than none. The checkbox and its state are still written.

carve
- [ ] > quoted lead
html
<ul>
  <li><input type="checkbox" disabled> 
    <blockquote><p>quoted lead</p></blockquote>
  </li>
</ul>

A math span says it is mathematics whether or not a typesetter ever runs, and the NAME stays the author's on the same carrier. role is written last, so it never moves an attribute the author placed, and an authored role is kept.

carve
An inline $`x = 1` and a named $`y`{aria-label="why"} one.
html
<p>An inline <span class="math inline" role="math">\(x = 1\)</span> and a named <span class="math inline" aria-label="why" role="math">\(y\)</span> one.</p>

The endnotes section is named for the reason its backlinks are: the role says what the region IS and nothing said what it is CALLED.

carve
Text[^a]

[^a]: A note.
html
<p>Text<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a></p>
<section role="doc-endnotes" aria-label="Footnotes">
  <hr>
  <ol>
    <li id="fn1">
      <p>A note.<a href="#fnref1" role="doc-backlink" aria-label="Back to reference">↩</a></p>
    </li>
  </ol>
</section>

An author's attribute NAME is emitted verbatim and HTML attribute names are case-insensitive, so the author-wins test is too: ARIA-LABEL and ROLE are the same attributes as the lower-case spellings, and matching only the exact case wrote a DUPLICATE of the attribute being checked for.

carve
{ARIA-LABEL="Mine"}
::: note
body
:::

Math $`x`{ROLE="img"} here.
html
<aside class="admonition note" ARIA-LABEL="Mine">
  <p>body</p>
</aside>
<p>Math <span class="math inline" ROLE="img">\(x\)</span> here.</p>

Text-block alignment renders the CSS declaration ​

5 conformance fixtures

On a paragraph, div, or heading, the three text-alignment values render the modern CSS declaration. An existing style is kept in the same attribute.

carve
{align=right}
Aligned text.
html
<p style="text-align: right;">Aligned text.</p>
carve
{align=center}
::: box
Aligned text.
:::
html
<div class="box" style="text-align: center;">
  <p>Aligned text.</p>
</div>
carve
{align=right style="color: red"}
Aligned text.
html
<p style="color: red; text-align: right;">Aligned text.</p>

The rewrite is deliberately narrow. A table's align controls placement, not cell text, and an unrecognized value remains an ordinary authored attribute.

carve
{align=right}
| a |
html
<table align="right">
  <tbody>
    <tr><td>a</td></tr>
  </tbody>
</table>
carve
{align=justify}
Aligned text.
html
<p align="justify">Aligned text.</p>

A sigil fence takes its attribute line ​

3 conformance fixtures

::: | and ::: \ take an attribute line like every other block. The class list merges with the block's own class, which goes last, and the id and the rest follow in source order.

carve
{#poem .verse}
::: |
Roses are red,
  Violets are blue.
:::
html
<div id="poem" class="verse line-block">
  <p>Roses are red,<br>
&nbsp;&nbsp;Violets are blue.</p>
</div>

With no class of its own to merge, the block's class stands alone after the id.

carve
{#poem}
::: |
one
two
:::
html
<div id="poem" class="line-block">
  <p>one<br>
two</p>
</div>

The hard-break fence reads the line the same way.

carve
{#addr .contact}
::: \
one
two
:::
html
<div id="addr" class="contact hardbreaks">
  <p>one<br>
two</p>
</div>

Released under the MIT License.