Whitespace, tabs and columns
Where a space is required, where a tab reaches, blank lines, line endings and trailing whitespace.
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.
Paragraph trailing whitespace
1 conformance fixture
Whitespace at the end of a paragraph's final line is stripped before rendering (CommonMark / Djot): abc renders without the trailing space. An interior two-space hard break is unaffected.
abc<p>abc</p>Trailing whitespace boundaries
4 conformance fixtures
The trailing-whitespace strip (paragraph, NORMATIVE) removes whitespace at the end of the paragraph's SOURCE line before rendering. It does not touch spaces a construct produces during rendering, so a paragraph whose entire content is an all-space verbatim span keeps those spaces.
!` `<p> </p>The same holds for a lone all-space code span, which keeps its <code> wrapper.
` `<p><code> </code></p>... and for lone all-space math.
$` `<p><span class="math inline">\( \)</span></p>A trailing NO-BREAK SPACE is content, not trailing whitespace: it is left in place and rendered as a character entity. Only ASCII whitespace is stripped.
A trailing no-break space<p>A trailing no-break space </p>A marker separator is a space, never a tab
2 conformance fixtures
Every marker that takes a separator takes the space character: a tab after the marker leaves ordinary paragraph text. The definition term is shown because it was the last construct to agree - carve-js and carve-php read a tab as a term until carve#532.
:: term
: d<p>:: term
: d</p>A space opens the same document as written.
:: term
: d<dl>
<dt>term</dt>
<dd>d</dd>
</dl>An invisible line does not cancel a blank-line separation
2 conformance fixtures
§17 L1 asks whether an item holds a blank-line-separated second paragraph. A line that renders nothing - a comment, a definition, an attribute line - is not a paragraph, which is why it cannot be the second one; the same fact means it cannot stand between the blank line and the paragraph that follows either. Delete the comment below and every implementation renders the item loose, so a construct that outputs nothing may not change that.
- a
%% n
text<ul>
<li><p>a</p>
<p>text</p>
</li>
</ul>An invisible line on its own is still not a second paragraph, so the item stays tight.
- a
%% n<ul>
<li>a</li>
</ul>A tab after a heading, quote or caption marker leaves the line as prose
2 conformance fixtures
The definition markers already pin this rule; the block markers are the same one and nothing covered them. A tab after #, > or ^ is not the marker's separator, so no block opens.
# Heading
> quoted<p># Heading</p>
<p>> quoted</p>The caption marker needs a block to attach to before the rule is observable at all - on its own line it is prose either way. Directly under an image, a space makes a <figure> and a tab does not.

^ Figure 1<p><img src="m.jpg" alt="Moon">
^ Figure 1</p>A tab indent is the column it reaches, whatever the line holds
2 conformance fixtures
§24 C1 makes indentation a column claim: a space advances one column, a tab advances to the next multiple of 4. 1. claims columns 0-2, so the item's content column is 3 and a tab reaches column 4 - one column PAST it, which is what four spaces reach too. A block opener at the content column nests; one column past it is text, and the tab spelling has to say the same thing as the space spelling of the same column.
The corpus pinned neither, so two engines read the tab as if it stopped at the content column and nested a block quote no space spelling of column 4 produces (carve-js#767, carve-php#890).
1. a
> quote<ol>
<li>a
> quote</li>
</ol>At the content column itself it nests, which is the boundary the rule above is drawn against.
1. a
> quote<ol>
<li>a
<blockquote><p>quote</p></blockquote>
</li>
</ol>The same column, written with four spaces
1 conformance fixture
The control for the rule above, and the half that was stated rather than checked. That pair pins the tab against the THREE-space spelling, which shows the two DIFFER. What makes the tab case decidable is the other comparison: a tab reaches column 4, four spaces reach column 4, so the two are the same claim and must get the same answer.
Without this document an engine can pass both halves of that pair while still answering column 4 differently depending on which whitespace arrived there - which is the exact shape of the defect the rule was written for (carve-js#767, carve-php#890).
1. a
> quote<ol>
<li>a
> quote</li>
</ol>Trailing whitespace after a block marker
6 conformance fixtures
A block marker is what it is regardless of whitespace after it. Every engine already reads it that way, and no corpus document carried any of these six shapes - so an engine that dropped one of those tolerances could not be caught here. carve-php shipped exactly that for the continuation marker (carve#871).
A thematic break:
a
---
b<p>a</p>
<hr>
<p>b</p>A code fence's closer:
```
x
```
y<pre><code>x
</code></pre>
<p>y</p>A colon fence's closer:
::: note
x
:::
y<aside class="admonition note">
<p>x</p>
</aside>
<p>y</p>A table's continuation row:
| a | b |
+ c | d |<table>
<tbody>
<tr><td>a c</td><td>b d</td></tr>
</tbody>
</table>A footnote definition separated by more than one space:
[^f]: note
see[^f]<p>see<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a></p>
<section role="doc-endnotes">
<hr>
<ol>
<li id="fn1">
<p>note<a href="#fnref1" role="doc-backlink">↩</a></p>
</li>
</ol>
</section>And the continuation marker, which §17 L3 spells "a line whose only content is +":
- a
+
b
x<ul>
<li>a
b
</li>
</ul>
<p>x</p>Line endings and a byte order mark
4 conformance fixtures
A line ends at \n, \r\n or a lone \r, and a byte order mark at the start of a document is not content. All four spellings below are the same document and produce the same output, including the same heading id - a carriage return that leaked into the text would show up there rather than only in whitespace nobody looks at.
The examples are written with ordinary newlines. The bytes are applied when the fixture is generated (::: compare crlf, cr, bom), because the example files are reviewable Markdown and are not protected from line-ending normalization the way tests/corpus/** is.
# Title
a
b<section id="Title">
<h1>Title</h1>
<p>a
b</p>
</section># Title
a
b<section id="Title">
<h1>Title</h1>
<p>a
b</p>
</section># Title
a
b<section id="Title">
<h1>Title</h1>
<p>a
b</p>
</section># Title
a
b<section id="Title">
<h1>Title</h1>
<p>a
b</p>
</section>A tab separates two attributes, and pads a block, as a space does
3 conformance fixtures
The heading is the name this category was given when it pinned the opposite answer, and it is kept deliberately. Corpus category names are an append-only contract - every engine allowlists them as NN-slug, so renaming one invalidates all of those lists at once. The documents below now pin the NARROWED answer (markup-carve/carve#906): a tab does not separate two attributes inside an INLINE block. The second half of the heading is still true, and is what the ruling turned on - a tab does pad a block-attribute LINE, which the category The inline attribute interior is space-only, the attribute line is not pins directly.
Every whitespace slot of the inline block takes space. All of them sit AFTER the first non-whitespace character of their line, which is where PART 7's rule says a tab is not syntax, so the block is unrecognized and its braces show:
*x*{.a .b}
*y*{ .c}
*z*{.d }<p><strong>x</strong>{.a .b}</p>
<p><strong>y</strong>{ .c}</p>
<p><strong>z</strong>{.d }</p>A tab after an UNQUOTED value ends the value - unquoted_value holds letters, digits, -, _, . and : and no whitespace at all - and then satisfies no separator either, so the whole block fails. Inside a QUOTED value it is content, as any other character is, and that half did not move:
*x*{k=a .b}
*y*{k="a b"}<p><strong>x</strong>{k=a .b}</p>
<p><strong k="a b">y</strong></p>The blessed EMPTY block is a separate position rather than a use of the separator, and it has to move with it: narrow the separator alone and [x]{ tab } is still a valid empty block, so the document below would keep passing and pin nothing.
[x]{ }<p>[x]{ }</p>Colon fence separator must be a space
10 conformance fixtures
254-colon-fence-separator-must-be-a-space254-colon-fence-separator-must-be-a-space-2254-colon-fence-separator-must-be-a-space-3254-colon-fence-separator-must-be-a-space-4254-colon-fence-separator-must-be-a-space-5254-colon-fence-separator-must-be-a-space-6254-colon-fence-separator-must-be-a-space-7254-colon-fence-separator-must-be-a-space-8254-colon-fence-separator-must-be-a-space-9254-colon-fence-separator-must-be-a-space-10
The colon fence has ONE separator slot -- the whitespace immediately after the fence run -- and all four openers share it. It is a MARKER SEPARATOR (PART 7, MARKER SEPARATORS AND PADDING SLOTS): the token after it selects an admonition, a div, a line block or a local hard-break block, so it is spelled space and a tab does not satisfy it. A tabbed opener is an ordinary paragraph, exactly as a tab after a heading, list or definition marker already is.
The four openers are pinned separately because implementations decide them in four separate places. In carve-rs the same rule lived in four branches, and fixing the first left the other three opening; carve-js and carve-php each had their own split. One representative shape would have covered a quarter of that.
An admonition opener:
::: note
x
:::<p>::: note
x
:::</p>The separator is a run, and the rule is about the whole run rather than its first character. Both mixed spellings are prose too -- the shape that survives a fix written as "the first character must be a space" is the one with the space first.
::: note
x
:::<p>::: note
x
:::</p>::: note
x
:::<p>::: note
x
:::</p>A bare [label] may sit flush against the fence (:::[First]), so this slot is OPTIONAL in the div opener. Optional is a different property from a different role: when the slot IS written, it answers the same way.
::: [First]
x
:::<p>::: [First]
x
:::</p>::: [First]
x
:::<p>::: [First]
x
:::</p>A line block:
::: |
x
:::<p>::: |
x
:::</p>::: |
x
:::<p>::: |
x
:::</p>A local hard-break block. The trailing backslash is no longer a block selector once the line is prose, so it is read as ordinary inline content and produces a hard break inside the paragraph -- which is itself the discriminator, since the recognized form would have produced a div.hardbreaks wrapper instead.
::: \
x
:::<p>::: <br>
x
:::</p>::: \
x
:::<p>::: <br>
x
:::</p>Cardinality is a separate question from the terminal, and this case is the guard against a fix that drifts the other way. space names the character, not the width: a run of more than one space still opens the block.
::: note
x
:::<aside class="admonition note">
<p>x</p>
</aside>Colon fence metadata slots must be a space too
5 conformance fixtures
Once admonition_type has been read the block is decided, so the opener's "title" and [label] slots carry no recognition -- they are PADDING. They are spelled space all the same, for the other reason PART 7 gives: a tab is syntax ONLY in a line's leading indentation run, and a padding slot sits after the first non-whitespace character of its line.
This half is pinned beside the separator half deliberately. A case that pinned only the separator invites a fix that narrows the whole line, and a case that pinned only the padding invites the reverse; carve-js carried both defects at once, in opposite directions.
::: note "Title"
x
:::<p>::: note “Title”
x
:::</p>::: note "Title"
x
:::<p>::: note “Title”
x
:::</p>The [label] slot reverts independently of the "title" slot, so it carries its own pair: a fixture with a tab at both cannot tell them apart, because narrowing either one already leaves the line as prose.
::: note "Title" [First]
x
:::<p>::: note “Title” [First]
x
:::</p>::: note "Title" [First]
x
:::<p>::: note “Title” [First]
x
:::</p>The spaced spellings are unchanged, and both slots still carry their metadata.
::: note "Title" [First]
x
:::<aside class="admonition note">
<p class="admonition-title">Title</p>
<p class="div-label">First</p>
<p>x</p>
</aside>Table cell padding must be a space
21 conformance fixtures
256-table-cell-padding-must-be-a-space256-table-cell-padding-must-be-a-space-2256-table-cell-padding-must-be-a-space-3256-table-cell-padding-must-be-a-space-4256-table-cell-padding-must-be-a-space-5256-table-cell-padding-must-be-a-space-6256-table-cell-padding-must-be-a-space-7256-table-cell-padding-must-be-a-space-8256-table-cell-padding-must-be-a-space-9256-table-cell-padding-must-be-a-space-10256-table-cell-padding-must-be-a-space-11256-table-cell-padding-must-be-a-space-12256-table-cell-padding-must-be-a-space-13256-table-cell-padding-must-be-a-space-14256-table-cell-padding-must-be-a-space-15256-table-cell-padding-must-be-a-space-16256-table-cell-padding-must-be-a-space-17256-table-cell-padding-must-be-a-space-18256-table-cell-padding-must-be-a-space-19256-table-cell-padding-must-be-a-space-20256-table-cell-padding-must-be-a-space-21
A table cell has a padding slot at each end -- the whitespace between the opening | and the cell content, and between the content and the closing |. Both are spelled space (grammar.ebnf delimiter_cell, header_cell, data_cell, rowspan_marker, colspan_marker). Every one of them sits after the row's opening pipe, so every one of them is INLINE, and a tab is syntax only in a line's leading indentation run (PART 7, MARKER SEPARATORS AND PADDING SLOTS).
A tab written in one of those slots is therefore not padding. It stays where it is and becomes ordinary cell content, which is a visible answer rather than a rejection: the cell keeps the tab, and a delimiter cell stops being one.
Each end is pinned separately. A padding rule is easy to implement at one end only, and a document carrying a tab at both ends cannot tell a half-fix from a whole one -- the shape carve#901 found at the admonition opener.
A data cell, tab-first at the leading slot:
| a | b |<table>
<tbody>
<tr><td> a</td><td> b</td></tr>
</tbody>
</table>The slot is a RUN, and the rule is about the whole run rather than its first character. Both mixed spellings keep the tab as content too -- the one that survives a fix written as "the first character must be a space" is the one with the space first.
| a | b |<table>
<tbody>
<tr><td> a</td><td> b</td></tr>
</tbody>
</table>| a | b |<table>
<tbody>
<tr><td> a</td><td> b</td></tr>
</tbody>
</table>The trailing slot answers the same way, and reverts independently of the leading one:
| a | b |<table>
<tbody>
<tr><td>a </td><td>b </td></tr>
</tbody>
</table>| a | b |<table>
<tbody>
<tr><td>a </td><td>b </td></tr>
</tbody>
</table>A header cell carries the same two slots, after its = marker:
|= h |= i |
| 1 | 2 |<table>
<thead><tr><th scope="col"> h</th><th scope="col"> i</th></tr></thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
</tbody>
</table>|= h |= i |
| 1 | 2 |<table>
<thead><tr><th scope="col"> h</th><th scope="col"> i</th></tr></thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
</tbody>
</table>|= h |= i |
| 1 | 2 |<table>
<thead><tr><th scope="col">h </th><th scope="col">i </th></tr></thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
</tbody>
</table>|= h |= i |
| 1 | 2 |<table>
<thead><tr><th scope="col">h </th><th scope="col">i </th></tr></thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
</tbody>
</table>A delimiter cell is the one slot whose failure is structural rather than textual. With a tab in the padding the cell is no longer a delimiter_cell, so the second line is not a delimiter row: no header is promoted, no alignment is assigned, and the --- run is ordinary inline content that smart typography renders as an em dash.
| a | b |
| --- | --- |
| 1 | 2 |<table>
<tbody>
<tr><td>a</td><td>b</td></tr>
<tr><td> —</td><td> —</td></tr>
<tr><td>1</td><td>2</td></tr>
</tbody>
</table>| a | b |
| --- | --- |
| 1 | 2 |<table>
<tbody>
<tr><td>a</td><td>b</td></tr>
<tr><td> —</td><td> —</td></tr>
<tr><td>1</td><td>2</td></tr>
</tbody>
</table>| a | b |
| --- | --- |
| 1 | 2 |<table>
<tbody>
<tr><td>a</td><td>b</td></tr>
<tr><td>— </td><td>— </td></tr>
<tr><td>1</td><td>2</td></tr>
</tbody>
</table>The two span markers are padding around a single token, so a tab beside one makes the cell ordinary content and the span does not happen:
| a | b |
| ^ | c |<table>
<tbody>
<tr><td>a</td><td>b</td></tr>
<tr><td> ^</td><td>c</td></tr>
</tbody>
</table>| a | b |
| c | < |<table>
<tbody>
<tr><td>a</td><td>b</td></tr>
<tr><td>c</td><td> <</td></tr>
</tbody>
</table>A continuation row's cells are data_cells too (grammar.ebnf continuation_row), so they carry the same two slots. This is the spelling an implementation is most likely to pad in a second place, and a fix applied only to the standard row leaves it joining the tab away.
| a | b |
+ x | y |<table>
<tbody>
<tr><td>a x</td><td>b y</td></tr>
</tbody>
</table>| a | b |
+ x | y |<table>
<tbody>
<tr><td>a x </td><td>b y </td></tr>
</tbody>
</table>| a | b |
+ x | y |<table>
<tbody>
<tr><td>a x</td><td>b y</td></tr>
</tbody>
</table>The spaced spellings are unchanged, and so is a cell with no padding at all or with more than one space -- cardinality is a separate question from the terminal.
|=h|= i |
|a| b |<table>
<thead><tr><th scope="col">h</th><th scope="col">i</th></tr></thead>
<tbody>
<tr><td>a</td><td>b</td></tr>
</tbody>
</table>| a | b |
| --- | ---: |
| 1 | 2 |<table>
<thead><tr><th scope="col">a</th><th scope="col" style="text-align: right;">b</th></tr></thead>
<tbody>
<tr><td>1</td><td style="text-align: right;">2</td></tr>
</tbody>
</table>| a | b |
| ^ | c |<table>
<tbody>
<tr><td rowspan="2">a</td><td>b</td></tr>
<tr><td>c</td></tr>
</tbody>
</table>| a | b |
| c | < |<table>
<tbody>
<tr><td>a</td><td>b</td></tr>
<tr><td colspan="2">c</td></tr>
</tbody>
</table>Link and image title slots must be a space
7 conformance fixtures
257-link-and-image-title-slots-must-be-a-space257-link-and-image-title-slots-must-be-a-space-2257-link-and-image-title-slots-must-be-a-space-3257-link-and-image-title-slots-must-be-a-space-4257-link-and-image-title-slots-must-be-a-space-5257-link-and-image-title-slots-must-be-a-space-6257-link-and-image-title-slots-must-be-a-space-7
The whitespace before a link or image title is a PADDING SLOT: the link is already a link once its destination is read, and the title sits inline after it. link_title spells that slot space, and image_title = link_title inherits it. A tab is syntax only in a line's leading indentation run (PART 7, MARKER SEPARATORS AND PADDING SLOTS), and this slot sits well past the first non-whitespace character of its line.
A tab written there is therefore not padding, and the inline is not a link at all: the bracket run stays literal text, with the tab where it was written.
A link title, tab-first:
[t](/u "T")<p>[t](/u “T”)</p>The slot is a RUN, and the rule is about the whole run rather than either of its ends. Both mixed spellings keep the text literal too -- the one that survives a fix written as "the first character must be a space" is the one with the space first, and the one that survives "the last character must be a space" is the other.
[t](/u "T")<p>[t](/u “T”)</p>[t](/u "T")<p>[t](/u “T”)</p>An image title answers the same way. image_title = link_title is one production defined by reference, and every engine serves the link tail and the image tail from one function -- so the two agree by construction today, and nothing would notice the day one of them splits:
<p></p><p></p><p></p>A single space is the titled form, unchanged:
[t](/u "T")<p><a href="/u" title="T">t</a></p>Code fence metadata slots must be a space too
10 conformance fixtures
258-code-fence-metadata-slots-must-be-a-space-too258-code-fence-metadata-slots-must-be-a-space-too-2258-code-fence-metadata-slots-must-be-a-space-too-3258-code-fence-metadata-slots-must-be-a-space-too-4258-code-fence-metadata-slots-must-be-a-space-too-5258-code-fence-metadata-slots-must-be-a-space-too-6258-code-fence-metadata-slots-must-be-a-space-too-7258-code-fence-metadata-slots-must-be-a-space-too-8258-code-fence-metadata-slots-must-be-a-space-too-9258-code-fence-metadata-slots-must-be-a-space-too-10
A fenced code block carries three of the same slots: the one before the info string (fenced_code_block, spelled [space]), and the "header" and [label] slots inside code_fence_info (spelled space+). All three sit after the fence run, which has already decided the block, so all three are padding and all three take a space.
The fallback here is the INVALID-FENCE FALLBACK the grammar already names: the opener is not a fence opener, so the run is read as an inline verbatim span in a paragraph and every character of it survives.
Each slot is pinned separately, because implementations decide them in three places and a document carrying a tab in all three cannot tell a partial fix from a whole one.
The slot before the info string, tab-first:
``` js
x
```<p><code> js
x
</code></p>Both mixed runs answer the same way, for the reason the title slots give:
``` js
x
```<p><code> js
x
</code></p>``` js
x
```<p><code> js
x
</code></p>The "header" slot, which sits after the language token and reverts independently of the slot before it:
```js "T"
x
```<p><code>js "T"
x
</code></p>```js "T"
x
```<p><code>js "T"
x
</code></p>```js "T"
x
```<p><code>js "T"
x
</code></p>And the [label] slot, which reverts independently of both:
```js "T" [L]
x
```<p><code>js "T" [L]
x
</code></p>```js "T" [L]
x
```<p><code>js "T" [L]
x
</code></p>```js "T" [L]
x
```<p><code>js "T" [L]
x
</code></p>A single space in each slot is the fenced form, unchanged:
```js "T" [L]
x
```<pre title="T"><code class="language-js">x
</code></pre>A tab continues a list item just as two spaces do
2 conformance fixtures
The tab-stop rule that lets a tab reach a marker column applies to a list item's CONTINUATION line as much as to its first. - item puts the content column at 2, and a following line indented with one tab reaches it exactly as two spaces do, so both spellings are one paragraph inside the item.
Pinned because the two spellings are decided in different places and one engine decides them differently. carve-js publishes no position for the paragraph or any of its three inlines when the continuation is a TAB, and places all four when it is two spaces or when the marker is 1.; carve-rs and carve-php place them either way and agree to the offset (carve-js#712). The HTML is identical in all three, which is why nothing in this corpus could see it - the divergence is entirely in PART 12 positions, which the pair below does not express and npm run ast:check now does.
The tab spelling:
- item
more
x<ul>
<li>item
more</li>
</ul>
<p>x</p>The two-space spelling, which is the control: it is the same document, and the engine that drops the positions above keeps them here.
- item
more
x<ul>
<li>item
more</li>
</ul>
<p>x</p>A blank line holds spaces and tabs and nothing else
3 conformance fixtures
blank_line = {whitespace}, newline (resources/grammar.ebnf, PART 1) over whitespace = ' ' | '\t' (PART 7). Two characters, and no third. Every other character that a host language's whitespace class might sweep up - a Unicode space separator, a C0 control, a zero-width character - is CONTENT, so a line holding one of them keeps the paragraph open and soft-breaks into it.
PART 0 states the U+FEFF row of that outright, under A LEADING BYTE ORDER MARK IS STRIPPED: a leading byte order mark is stripped, "ONE, and only there: a U+FEFF anywhere else is an ordinary zero-width character". The three documents below carry the characters raw; they are invisible in review, which is why tests/fixture-bytes.test.mjs pins each one by name.
The rule and its opposite, in one document: the line holding only a byte order mark is content and a continues into b, while a line holding only spaces and a line holding only a tab each end the paragraph. Covers U+FEFF, U+0020 and U+0009.
a
b
c
d<p>a
b</p>
<p>c</p>
<p>d</p>The Unicode space separators are not whitespace either, nor is the other zero-width character PART 9 names alongside the byte order mark. One paragraph, nine soft breaks. Covers U+00A0, U+1680, U+2000, U+2009, U+200A, U+202F, U+205F, U+3000 and U+200B.
a
b<p>a
b</p>The C0 controls and the Unicode line and paragraph separators are the rows a regular-expression whitespace class reaches without anyone deciding it should. They are content too. Covers U+000B, U+000C, U+0085, U+2028 and U+2029.
a
b<p>a
b</p>A link title takes exactly one space
4 conformance fixtures
link_title = space, ('"' ... ) spells its padding slot as exactly ONE character, and four artifacts read it as a run: carve-js, carve-php, carve-rs and the executable spec all took the title after two spaces. carve#912 settled which side gives. The production is right and the four are lax, so a second space is no longer padding.
This is deliberately the opposite call from the one carve#905 made for the same slots. That change settled WHICH character a slot admits (a space, never a tab) and left HOW MANY alone; this one settles the cardinality, and settles it tight.
With two spaces the quoted run is not a title, so the bracket run is not a link at all and every character of the line survives as text:
[t](/u "T")<p>[t](/u “T”)</p>image_title = link_title is one production defined by reference, so the image tail answers the same way:
<p></p>The CONTROL, and the point of the ruling: a single space is still the titled form. This pair passed before carve#912 and passes after it, and it is what distinguishes narrowing the slot from breaking it.
[t](/u "T")<p><a href="/u" title="T">t</a></p><img src="/p.png" alt="a" title="T">A code fence opener takes exactly one space
2 conformance fixtures
fenced_code_block = code_fence_open, [space], [code_fence_info] spells the opener slot as exactly one character. A second space reaches language_info, whose character class holds no space, so the opener matches no shape and the INVALID-FENCE FALLBACK applies: the run is an inline verbatim span in a paragraph.
The two metadata slots INSIDE code_fence_info are spelled space+ and are unaffected. The productions differ, so the cardinality differs, and carve#912 ruled only on the four slots spelled with a bare space.
``` php
x = 1
```<p><code> php
x = 1
</code></p>The CONTROL. One space is the lenient Djot spelling and stays a fenced block with its language:
``` php
x = 1
```<pre><code class="language-php">x = 1
</code></pre>The canonical writer glues a code fence to its info string
2 conformance fixtures
fenced_code_block calls the no-space opener canonical and the reader accepts either spelling, so nothing in the source says which one the WRITER emits. PART 11 §6d does: no padding space between the fence run and the info string, and exactly one space before each metadata token inside it. The .fmt sidecar beside this pair pins the whole opener line, which is what was missing - the fmt corpus held no document whose canonical form contains an info string at all, so two engines normalized to the glued form and one to the spaced one with nothing to adjudicate between them.
The author's spacing is normalized away in all three slots at once, and the rendered block is unchanged by any of it.
``` php "src/Auth.php" [Composer]
composer require x
```<pre title="src/Auth.php"><code class="language-php">composer require x
</code></pre>The canonical form is a fixed point. An opener already written the canonical way comes back byte for byte, which is the half a writer that merely reproduced the author's spelling would also pass - both examples are needed to tell the two apart.
```php "src/Auth.php" [Composer]
composer require x
```<pre title="src/Auth.php"><code class="language-php">composer require x
</code></pre>A frontmatter opener takes exactly one space
2 conformance fixtures
frontmatter_open = "---", [space], [frontmatter_format] spells its slot as exactly one character too. With two, the second space reaches frontmatter_format = (letter | digit)+, which cannot match it, so the line is not a typed opener.
What is left is not a thematic break either -- a break is a dash run and nothing else -- so the line is ordinary paragraph text, the metadata lines fold into it as lazy continuation, and the closing --- is the thematic break. The opening dashes are then subject to smart typography like any other text, which is why they render as an em dash.
--- yaml
title: T
---
body<p>— yaml
title: T</p>
<hr>
<p>body</p>The CONTROL. One space is the lenient spelling and still opens frontmatter, which renders nothing:
--- yaml
title: T
---
body<p>body</p>A reference definition's metadata slots take exactly one space
4 conformance fixtures
The definition line carries two of the four slots carve#912 narrowed: link_title before the quoted title, and [space, attributes] before the trailing attribute block. Both are padding -- the definition is already a definition at [a]: /url -- and both are spelled as exactly one space.
With two spaces the title is not a title, and the leftover quoted run is what the line then fails on: the definition is anchored at end of line (carve#911), so the whole line is an ordinary paragraph and the reference does not resolve.
[a]: /u "T"
[a][]<p>[a]: /u “T”</p>
<p>[a][]</p>The attribute block answers the same way, and note where it does NOT go: the zero-space case is a different shape, because [a]: /u{.c} glues the braces to the destination and gives href="/u{.c}". Two spaces end the destination instead, so the block is left over and the production fails on it.
When these two documents were written under carve#912 the leftover was silently dropped and the line stayed a definition, which is the outcome PART 7 names as the one to avoid. carve#911 anchored the line, and this is the fallback the clause promises.
[a]: /u {.c}
[a][]<p>[a]: /u {.c}</p>
<p>[a][]</p>The CONTROLS. One space carries the title, and one space carries the attributes:
[a]: /u "T"
[a][]<p><a href="/u" title="T">a</a></p>[a]: /u {.c}
[a][]<p><a href="/u" class="c">a</a></p>A definition marker's separator is a space, and it is a run
10 conformance fixtures
267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run-2267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run-3267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run-4267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run-5267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run-6267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run-7267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run-8267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run-9267-a-definition-marker-s-separator-is-a-space-and-it-is-a-run-10
The three definition markers share one separator rule: the marker-to-content separator is the space terminal, U+0020, and a tab never satisfies it. What the grammar did not say is how MANY. footnote_definition and abbreviation_definition were spelled with a single space while all three engines and the executable spec consumed a run, so the productions forbade a shape nothing rejected. carve#892 corrects them to space+.
Note that this is the OPPOSITE cardinality answer from carve#912's, which held four PADDING SLOTS to exactly one space. The two are not in conflict, because they govern different positions. A padding slot sits between two tokens on a line whose construct is already fixed, and its width means nothing. A marker separator is what stands between the marker and the content it introduces, and a writer aligning definitions in a column is writing separator, not content.
Two spaces, at both markers:
*[HTML]: Hyper Text
HTML<p><abbr title="Hyper Text">HTML</abbr></p>x[^f]
[^f]: note<p>x<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a></p>
<section role="doc-endnotes">
<hr>
<ol>
<li id="fn1">
<p>note<a href="#fnref1" role="doc-backlink">↩</a></p>
</li>
</ol>
</section>The run is ASCII spaces, so the first other character is content
This is where the three engines disagreed, each in a different place, and where the executable spec gave a fourth answer no engine gave: it refused a footnote marker followed by any non-ASCII whitespace as a definition at all.
A NO-BREAK SPACE after the separator is content. The abbreviation expands to a string that starts with it:
*[HTML]: Hyper Text
HTML<p><abbr title=" Hyper Text">HTML</abbr></p>and the footnote is defined, with the character opening its body:
x[^f]
[^f]: note<p>x<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a></p>
<section role="doc-endnotes">
<hr>
<ol>
<li id="fn1">
<p> note<a href="#fnref1" role="doc-backlink">↩</a></p>
</li>
</ol>
</section>A TAB after the run is content by the same rule, and the two markers then answer differently for a reason downstream of this clause rather than in it. An abbreviation expansion is a raw string, so the tab survives into the title:
*[HTML]: Hyper Text
HTML<p><abbr title=" Hyper Text">HTML</abbr></p>while a footnote body is parsed as blocks, where a leading tab is that body's own indentation run (PART 9 section 24 C1) rather than a character in it:
x[^f]
[^f]: note<p>x<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a></p>
<section role="doc-endnotes">
<hr>
<ol>
<li id="fn1">
<p>note<a href="#fnref1" role="doc-backlink">↩</a></p>
</li>
</ol>
</section>The CONTROLS
A tab as the SEPARATOR is still not a separator. Widening the run is not widening the terminal, and this is the pair that separates the two:
*[HTML]: Hyper Text
HTML<p>*[HTML]: Hyper Text</p>
<p>HTML</p>x[^f]
[^f]: note<p>x[^f]</p>
<p>[^f]: note</p>And MARKER REQUIRES CONTENT still applies after the run. A marker followed by spaces and nothing else is a paragraph, because a line of whitespace is blank (PART 1). The spaces-only forms are pinned in tests/separator-role-split.test.mjs rather than here -- a trailing whitespace run in a reviewable Markdown source file is one editor save from vanishing -- and this is the version the corpus can hold, a bare marker:
x[^f]
[^f]:<p>x[^f]</p>
<p>[^f]:</p>One space is unchanged, which is the form every document actually uses:
*[HTML]: Hyper Text
HTML<p><abbr title="Hyper Text">HTML</abbr></p>Trailing whitespace on a content line is dropped
14 conformance fixtures
268-trailing-whitespace-on-a-content-line-is-dropped268-trailing-whitespace-on-a-content-line-is-dropped-2268-trailing-whitespace-on-a-content-line-is-dropped-3268-trailing-whitespace-on-a-content-line-is-dropped-4268-trailing-whitespace-on-a-content-line-is-dropped-5268-trailing-whitespace-on-a-content-line-is-dropped-6268-trailing-whitespace-on-a-content-line-is-dropped-7268-trailing-whitespace-on-a-content-line-is-dropped-8268-trailing-whitespace-on-a-content-line-is-dropped-9268-trailing-whitespace-on-a-content-line-is-dropped-10268-trailing-whitespace-on-a-content-line-is-dropped-11268-trailing-whitespace-on-a-content-line-is-dropped-12268-trailing-whitespace-on-a-content-line-is-dropped-13268-trailing-whitespace-on-a-content-line-is-dropped-14
A whitespace run at the end of a content line does not reach the output. It is not content, and there is no shape of the language that gives it meaning: Carve's hard break is the backslash form, never two trailing spaces.
The rule is the project's rather than any one production's, and it decides more than the shape that raised it:
trailing (invisible and bad) whitespace is the one important rule we have: no such thing.
It was previously written down only for a paragraph's FINAL line, and PART 12 section 7 asserted the opposite for a line before a soft break. carve#926 settled it as general and corrected both.
A trailing space on the last line of a paragraph, which was already the stated rule:
abc<p>abc</p>And on a line before a SOFT BREAK, which is the half the specification had backwards. These two documents are the same document:
abc
def<p>abc
def</p>A tab answers the same way, at both positions:
abc
def<p>abc
def</p>Every other line that carries content answers the same way. A heading, a list item, a block quote line and a definition entry:
# Title
- item
> quoted<section id="Title">
<h1>Title</h1>
<ul>
<li>item</li>
</ul>
<blockquote><p>quoted</p></blockquote>
</section>:: term
: def<dl>
<dt>term</dt>
<dd>def</dd>
</dl>A table caption, which kept its run until carve#926 measured it:
| a |
^ Cap<table>
<caption>Cap</caption>
<tbody>
<tr><td>a</td></tr>
</tbody>
</table>The run is whitespace, and nothing else is whitespace
The dropped run is ' ' or a tab, the same two-character terminal blank_line = {whitespace} takes. Every other character is CONTENT and survives, however invisible it looks in an editor. This one document carries a no-break space, a zero-width space, a byte order mark, an en quad and a form feed, each at the end of its own line:
a
b
c
d
e<p>a
b
c
d
e</p>That is why U+FEFF was a red herring in the shape that raised this. In a line holding a space, a byte order mark and a space, the BOM is content and what is dropped is the trailing SPACE:
<p></p>Where the rule does not reach
Verbatim content keeps its bytes. A fenced code block's body is the block's payload, not a content line:
```
abc
```<pre><code>abc
</code></pre>And whitespace INSIDE a construct is not trailing: it ends at the construct's delimiter rather than at the line's end. A code span, a literal inline and a table cell all keep it, and so does the run before a hard-break backslash:
`x ` and !`y `<p><code>x </code> and y </p>a \
b<p>a <br>
b</p>A LINE BLOCK is not an exception either, in the order that matters. Its MEDIAL GAPS rule converts an inner or trailing run of two or more columns into NBSP CONTENT first, and content is not whitespace -- so this rule never reaches it, and only the one-column case is left for it to drop:
::: |
abc
def
:::<div class="line-block">
<p>abc <br>
def</p>
</div>A definition term's continuation line
A dt written across two physical lines is one logical line assembled from two, and the second line's trailing run is dropped exactly like the first line's would be (markup-carve/carve#1289). Nothing exempts a term: the run is whitespace at the end of a content line, and the verbatim run that spans the break carries a newline rather than the dropped space.
:: `a
b
: d<dl>
<dt><code>a
b</code></dt>
<dd>d</dd>
</dl>The control belongs to "where the rule does not reach" above rather than to this one: spaces INSIDE the run are the construct's content and end at its closing delimiter, so a term whose whole content is an all-space verbatim keeps them.
:: ` `
: d<dl>
<dt><code> </code></dt>
<dd>d</dd>
</dl>A tab after a fence or a frontmatter opener depends on where it sits
4 conformance fixtures
Two clauses meet on these two lines, and which one governs is decided by POSITION rather than by construct (markup-carve/carve#1295).
A tab BEFORE content on the opener is the marker-to-content separator, which is the space terminal and nothing else, so the construct does not open - the rule the definition, heading, list and task markers already carry. A tab at the END of the line, with nothing after it, is never that slot: it is trailing whitespace on a content line, PART 2 drops it, and what is left is the bare opener. Read that way the two clauses never overlap, so neither needs an exception written into it to protect the other.
A code fence whose info string is preceded by a tab is not a fence opener. The backtick run is then an ordinary inline verbatim run, and it reaches the end of the block:
``` php
x
```<p><code> php
x
</code></p>The same fence with the tab at the end of the line and no info string opens normally, because the tab never reaches the separator's question:
```
x
```<pre><code>x
</code></pre>The frontmatter delimiter is the same pair. Its opener may name a metadata format, so a tab before that token is the same separator and the same refusal - the line is not a delimiter, and with no frontmatter consumed the document starts with the text of those lines:
--- yaml
title: x
---
body<p>— yaml
title: x</p>
<hr>
<p>body</p>And a delimiter with a trailing tab and no format token opens frontmatter, which renders nothing:
---
title: x
---
body<p>body</p>Fenced code
3 conformance fixtures
Literal tabs in code content are preserved verbatim (a tab is not the same as spaces; display width is a CSS tab-size concern). Opt in to tab→space expansion with a tab-normalize extension.
```
indented with a tab
```<pre><code> indented with a tab
</code></pre>A fenced-code delimiter sits exactly at its container's content column — at document level, that is column 0. Carve has no indented-code-block construct, so leading spaces carry no meaning to disambiguate against, and the fence is strict like every other block opener (an indented heading, thematic break, or block quote is likewise plain text). A run of backticks indented at document level is therefore an ordinary paragraph, and its delimiters fall back to inline code spans.
```
code
```<p><code>
code
</code></p>The closer is column-exact too. A closing run indented past the opener is not a delimiter but code content, which is what lets an indented line appear as sample text inside a fence. Here the two-space `` is content and the flush ``` `` closes.
```
code
```
still code
```<pre><code>code
```
still code
</code></pre>Reference link
1 conformance fixture
Trailing whitespace after the colon does not create a destination either, so it is still literal.
[r]:<p>[r]:</p>Comments
2 conformance fixtures
Leading whitespace before %% does not matter: an indented line whose first non-whitespace content is %% is a line comment, exactly like one in the first column. It renders nothing and, like any block, interrupts an open paragraph.
x
%% indented comment
y<p>x</p>
<p>y</p>An indented comment-only line on its own renders nothing (it does not leave an empty paragraph).
before
%% indented comment
after<p>before</p>
<p>after</p>Line blocks
1 conformance fixture
A gap written with TABS measures in columns, not characters: each tab advances to the next four-column stop, counted from where the run starts. So a tab is a medial gap or a lone space depending on where it lands - below, tab sits at column 3 so its tab crosses one column and collapses, while the two after wide cross two full stops and are kept. A LEADING tab follows the same arithmetic.
::: |
tab gap
wide gap
lead
:::<div class="line-block">
<p>tab gap<br>
wide gap<br>
lead</p>
</div>