Skip to content

Paragraphs, breaks and captions

Paragraph folding, thematic breaks, caption attachment and adjacent block openers.

Generated from resources/examples/edge-cases.md - edit the cases there, not here. Each case links the conformance fixture it produces.

Blocks that render to nothing

3 conformance fixtures

A comment, a comment block, an abbreviation definition and a non-HTML raw block produce no output. Inside a container they contribute no line either - the container's body is what remains.

carve
> q
> %%%
> x
> %%%
> body
html
<blockquote>
  <p>q</p>
  <p>body</p>
</blockquote>

A definition body that renders to nothing closes on its own line, like the single-paragraph form.

carve
:: t
:  %%%
   x
   %%%
html
<dl>
  <dt>t</dt>
  <dd></dd>
</dl>

An abbreviation definition is collected for the document's abbreviation table and leaves nothing behind. It renders to nothing only where it is a definition, which is at document level: inside a container the same line is ordinary text.

carve
*[HTML]: HyperText Markup Language

:::
body
:::
html
<div>
  <p>body</p>
</div>

Paragraph interruption

20 conformance fixtures

A paragraph ends at a blank line — or at a line that begins an interrupting block. Under the Markdown-like rule (§10) a visible block interrupts an open paragraph with no blank line before it, at the top level and inside nested content. Three carve-outs keep common prose safe: list markers never interrupt — neither a bullet (- /* ) nor an ordered marker, in any dialect or value, so a list always needs a blank line before it (symmetric, Djot-like); a fence or ::: interrupts only when it has a matching closer ahead; and a bare image is never a block. Invisible constructs (reference definitions, comments, block-attribute lines) interrupt as they always have.

A heading marker after a prose line interrupts.

carve
text
# H
html
<p>text</p>
<section id="H">
  <h1>H</h1>
</section>

A fenced code block with a closer interrupts (an inline span no longer).

carve
text
```
code
```
html
<p>text</p>
<pre><code>code
</code></pre>

A thematic break interrupts; the line after it parses fresh (not a smart em-dash any more).

carve
text
---
more
html
<p>text</p>
<hr>
<p>more</p>

A block quote marker followed by a space interrupts.

carve
text
> q
html
<p>text</p>
<blockquote><p>q</p></blockquote>

Without the space, > is ordinary paragraph text. This keeps operators and technical prose from opening accidental quotes.

carve
text
>>= operator
>=3 items
>_< face
html
<p>text
&gt;≥ operator
≥3 items
&gt;_&lt; face</p>

An unordered list does not interrupt — like an ordered marker it needs a blank line, so the bullet lines fold into the paragraph.

carve
text
- a
- b
html
<p>text
- a
- b</p>

An ordered-list marker does not interrupt either — the bullet and the ordered marker behave identically at the paragraph boundary.

carve
text
1. x
2. y
html
<p>text
1. x
2. y</p>

A valid table row interrupts.

carve
text
| a | b |
html
<p>text</p>
<table>
  <tbody>
    <tr><td>a</td><td>b</td></tr>
  </tbody>
</table>

An admonition (or generic div) with a closer interrupts.

carve
text
:::note
body
:::
html
<p>text
:::note
body
:::</p>

Carve-out — list markers never interrupt. Neither a bullet nor an ordered marker interrupts a paragraph; both need a blank line. An ordered marker is too common in prose ("see step 2.", "version 1985.", "upgrade to 1. today") to interrupt, and making the bullet match removes the asymmetry (and the residual false positive where a hard-wrapped prose line beginning with a bullet became a list). So no ordered value — 1., 2., a year — and no bullet interrupts; all stay paragraph text.

carve
text
2. y
3. z
html
<p>text
2. y
3. z</p>
carve
text
1985. was the year
html
<p>text
1985. was the year</p>

Carve-out — closer lookahead. A ::: block (or a fence) with no matching closer ahead does not interrupt; it stays paragraph text, so a stray marker never swallows the rest of the block.

carve
text
:::note
body
html
<p>text
:::note
body</p>

Carve-out — image excluded. A bare image is inline content, so it renders in the same paragraph, never as its own block.

carve
text
![a](u)
html
<p>text
<img src="u" alt="a"></p>

Nested content. The rule applies inside a block quote too: a list marker after a prose line does not interrupt within the quote — it folds into the quoted paragraph (a blank line is needed to start the list).

carve
> p one
> - item
html
<blockquote><p>p one
- item</p></blockquote>

An indented sublist still nests with no blank line (unchanged).

carve
- a
   - b
html
<ul>
  <li>a
    <ul>
      <li>b</li>
    </ul>
  </li>
</ul>

Invisible constructs still interrupt with no blank line: a comment line is consumed,

carve
para
%% c
html
<p>para</p>

and a reference definition is collected, leaving only the paragraph.

carve
a[r]
[r]: http://x
html
<p>a[r]</p>

A blank line still ends the paragraph and the block parses fresh, exactly as before.

carve
text

# H
html
<p>text</p>
<section id="H">
  <h1>H</h1>
</section>

An unterminated code fence opener does not interrupt a paragraph (§10 closer lookahead): with no matching closer ahead, the ``` line stays paragraph text. It is then an unclosed inline verbatim run, which renders as a <code> span to the end of the block (matching the code_span maximal-run rule).

carve
Text
```
code
html
<p>Text
<code>
code</code></p>

A ::: opener goes the other way: its closer is optional (PART 9 §12), so there is nothing to look ahead for. The opener interrupts, and the container it opens closes at the end of the input. That is the counterweight to the exact closer - a mistyped closer costs the container's extent, not the rest of the document.

carve
Text
:::
stuff
html
<p>Text</p>
<div>
  <p>stuff</p>
</div>

Thematic break requires contiguous markers

4 conformance fixtures

A thematic break is three or more of the same marker (-, *, _) contiguous at column zero. Spacing the markers apart, or indenting the run, disqualifies it: the line is parsed as ordinary block content instead.

Spaced * markers are a bullet list, not a break.

carve
* * *
html
<ul>
  <li>
    <ul>
      <li>*</li>
    </ul>
  </li>
</ul>

Spaced _ markers are a plain paragraph.

carve
_ _ _
html
<p>_ _ _</p>

An indented *** run is a paragraph, not a break.

carve
 ***
html
<p>***</p>

A contiguous run at column zero is still a thematic break.

carve
***
html
<hr>

A flush-left line needs an open paragraph to fold into

7 conformance fixtures

A lazy continuation folds into the innermost OPEN paragraph (PART 1 S4). Where nothing is open, the unmatched containers close and the line is re-classified at the top level - and an item whose last block is an EMPTY container has nothing open, whatever column the next line starts at.

Pinned because no case had a container as an item's last block followed by a flush-left line, so three engines gave three answers with every suite green (carve#561, carve#572, carve#582).

An empty quote on the marker line opens no paragraph, so the item closes:

carve
. >
X
html
<ol>
  <li>
    <blockquote>

    </blockquote>
  </li>
</ol>
<p>X</p>

The same with a bullet marker:

carve
- >
lazy
html
<ul>
  <li>
    <blockquote>

    </blockquote>
  </li>
</ul>
<p>lazy</p>

CONTRAST: give the quote content and a paragraph IS open, so the line folds into it and the item stays:

carve
- > q
lazy
html
<ul>
  <li>
    <blockquote><p>q
lazy</p></blockquote>
  </li>
</ul>

A sub-list is not special either. The item's last block is a list whose own last item holds an open paragraph, so the flush-left line folds into THAT paragraph - the same answer as when the sub-list is opened on its own line.

carve
- - a
b
html
<ul>
  <li>
    <ul>
      <li>a
b</li>
    </ul>
  </li>
</ul>

The line does not have to be flush left, and it does not have to look like prose. One column in it reaches no content column - not the sub-list's, not the outer item's - so it opens nothing and folds as text, marker and all.

carve
- - a
 - b
html
<ul>
  <li>
    <ul>
      <li>a
- b</li>
    </ul>
  </li>
</ul>

A heading in that position folds the same way. Flush left it would be a heading, and at the sub-list's own column the marker above would be a sibling item: the fold is about reaching no column at all.

carve
- x
  - a
 # H
html
<ul>
  <li>x
    <ul>
      <li>a
# H</li>
    </ul>
  </li>
</ul>

How FAR below the column it sits changes nothing. §24 C3 asks one question - does the line reach the content column - and Rule B's "any indent" is scoped to where a TOP-LEVEL list may open (C4), not to nesting. Here the sub-list's content column is 6 and the outer item's is 4, so a marker at 2 reaches neither and folds, exactly as it does one column in.

carve
-   x
    - a
  - b
html
<ul>
  <li>x
    <ul>
      <li>a
- b</li>
    </ul>
  </li>
</ul>

Two dashes are not a thematic break

1 conformance fixture

A thematic break needs three or more markers. Two dashes are ordinary text - and smart typography renders them as an en dash, which is what the reader sees.

carve
a

--

b
html
<p>a</p>
<p></p>
<p>b</p>

A caption attaches across one blank line

8 conformance fixtures

PART 9 §4 gives one rule for all five captionable hosts: adjacent OR exactly one blank line attaches, two blank lines detach and leave the ^ line an ordinary paragraph. WHERE that rule is written down differs, and that is the whole reason this category exists. For the fenced code block, the block quote and the table it is STRUCTURAL - each production ends in [caption_slot], and that slot's single optional blank_line IS the allowance. For the IMAGE PARAGRAPH and the STANDALONE DISPLAY-MATH BLOCK it is PROSE, because neither has a production to hang the slot on: both ARE a paragraph, and what distinguishes them is a condition on that paragraph's inline content (carve#991, carve#992).

One rule, two spellings, and the corpus could tell them apart only for one host. Of the twenty documents carrying a ^ caption line, exactly ONE separated the host from its caption with a blank line - 55-blockquote-caption-after-a-blank-line

  • and blockquote is one of the three hosts that has the slot. The allowance was unpinned for the other four, and for the two prose hosts it was unpinned structurally as well. A reader could have dropped the blank-line form on four of five hosts and stayed green.

Four rows below pin it, one per remaining host. Each is preceded by the SAME document with the blank line taken out, which must render identically: that is the actual claim - not that a caption attaches, which is already pinned all over the corpus, but that these two spellings are ONE rule and produce one answer. The adjacent members are controls. They are unaffected by any mutation of the allowance, and they are here so a row cannot be satisfied by a reader that has stopped attaching captions altogether.

A table caption, adjacent

The control for the row below. 09-tables already pins this shape; it is repeated here so the pair reads as a pair and the two documents differ by exactly one line.

carve
|= Fruit |= Price |
| Apple  | $1     |
^ Fruit prices
html
<table>
  <caption>Fruit prices</caption>
  <thead><tr><th scope="col">Fruit</th><th scope="col">Price</th></tr></thead>
  <tbody>
    <tr><td>Apple</td><td>$1</td></tr>
  </tbody>
</table>

A table caption, after one blank line

table ends in [caption_slot], so this is the structural spelling. Byte for byte the same output as the row above.

carve
|= Fruit |= Price |
| Apple  | $1     |

^ Fruit prices
html
<table>
  <caption>Fruit prices</caption>
  <thead><tr><th scope="col">Fruit</th><th scope="col">Price</th></tr></thead>
  <tbody>
    <tr><td>Apple</td><td>$1</td></tr>
  </tbody>
</table>

A code block caption, adjacent

The control. A captioned code block is a numbered LISTING (§4), and the <figure> wrapper is what carries the caption.

carve
```python
def greet():
    return 1
```
^ Listing: a greeting
html
<figure>
  <pre><code class="language-python">def greet():
    return 1
</code></pre>
  <figcaption>Listing: a greeting</figcaption>
</figure>

A code block caption, after one blank line

The second structural host. The blank line sits between the fence's CLOSER and the caption, which is the position caption_slot's optional blank_line describes: fenced_code_block ends at a newline, so there is no competing optional for the blank to be consumed by.

carve
```python
def greet():
    return 1
```

^ Listing: a greeting
html
<figure>
  <pre><code class="language-python">def greet():
    return 1
</code></pre>
  <figcaption>Listing: a greeting</figcaption>
</figure>

An image caption, adjacent

The control. 08-image-with-caption pins this shape too; the pair is repeated here for the same reason the table pair is.

carve
![Apollo 11](apollo.jpg)
^ Figure 1: First moon landing
html
<figure>
  <img src="apollo.jpg" alt="Apollo 11">
  <figcaption>Figure 1: First moon landing</figcaption>
</figure>

An image caption, after one blank line

The first of the two PROSE hosts, and the one an author reaches for most often. There is no image_paragraph production and no slot: §4 is the rule, and PART 3 says beside image that "there is no separate grammar production for the pair".

So this row is the only thing that holds the allowance for this host. Nothing structural does.

carve
![Apollo 11](apollo.jpg)

^ Figure 1: First moon landing
html
<figure>
  <img src="apollo.jpg" alt="Apollo 11">
  <figcaption>Figure 1: First moon landing</figcaption>
</figure>

A display-math caption, adjacent

The control. A captioned standalone display-math block is a numbered EQUATION (§4); the block must be solely the $$…`` span.

carve
$$`E = mc^2`
^ Equation: mass-energy
html
<figure>
  <p><span class="math display">\[E = mc^2\]</span></p>
  <figcaption>Equation: mass-energy</figcaption>
</figure>

A display-math caption, after one blank line

The second PROSE host, and the same argument as the image paragraph: this block IS a paragraph, distinguished by a condition on its inline content, so no slot can be hung on it without making every paragraph captionable. §4 is the rule and this row is the pin.

carve
$$`E = mc^2`

^ Equation: mass-energy
html
<figure>
  <p><span class="math display">\[E = mc^2\]</span></p>
  <figcaption>Equation: mass-energy</figcaption>
</figure>

Nothing here is new behavior. Every one of these eight documents is what carve-js already produced when the drift audit measured the five hosts across three separations, which is what makes them committable as a pin rather than a proposal.

Two blank lines detach a caption

10 conformance fixtures

PART 9 §4 states the caption allowance in two halves: adjacent or exactly ONE blank line attaches, and anything wider does not. caption_slot's single optional blank_line IS that allowance, and the word that carries the second half is [...] rather than {...} - one blank line at most, not a run.

The first half is pinned. 281-a-caption-attaches-across-one-blank-line put a one-blank-line document on every host, and removing the optional blank_line from caption_slot now breaks five documents where it broke one before.

The second half was pinned nowhere, for any host. Widening caption_slot to

caption_slot = {blank_line}, caption ;

so that ANY number of blank lines attaches broke NOTHING in 856 documents. Every captioned document in the corpus had zero or one blank line between the host and the ^ line, so not one of them could tell "at most one" apart from "any number". A reader that attached a caption across three blank lines, or ten, satisfied every document and every gate in this repository (carve#997).

That is the same shape as the control recorded at 279-a-boundary-line-inside-an-open-fence-does-not-end-the-container-6: a document that passes for a reason unrelated to the rule it looks like it covers.

Five rows below pin the second half, one per captionable host. Each is preceded by the SAME document with one blank line instead of two, which must attach. That pairing is the point rather than decoration: a row that only proves detachment at two blank lines is equally satisfied by a reader that stopped attaching captions across a blank line at all, which is the opposite defect. The one-blank-line members are CONTROLS - the widening mutation does not touch them, and the complementary mutation touches only them.

What detachment looks like is worth stating once, because it is the same on all five hosts: the host renders UNCAPTIONED - a bare <table>, <pre>, <blockquote>, <img> or math paragraph with no <figure> around it - and the ^ line becomes an ordinary paragraph whose text begins with a literal caret. Nothing is dropped and nothing is an error; the two blocks simply stop being one.

A table caption, after one blank line

The control for the row below. table ends in [caption_slot], so the attachment here is structural.

carve
|= City |= People |
| Oslo  | 700k   |

^ Table: city sizes
html
<table>
  <caption>Table: city sizes</caption>
  <thead><tr><th scope="col">City</th><th scope="col">People</th></tr></thead>
  <tbody>
    <tr><td>Oslo</td><td>700k</td></tr>
  </tbody>
</table>

A table caption, after two blank lines

One line more than the control. The table keeps its rows and loses its <caption>; the ^ line is a paragraph.

carve
|= City |= People |
| Oslo  | 700k   |


^ Table: city sizes
html
<table>
  <thead><tr><th scope="col">City</th><th scope="col">People</th></tr></thead>
  <tbody>
    <tr><td>Oslo</td><td>700k</td></tr>
  </tbody>
</table>
<p>^ Table: city sizes</p>

A code block caption, after one blank line

The control. A captioned code block is a numbered LISTING (§4) and the <figure> wrapper is what carries the caption.

carve
```lua
local n = 1
```

^ Listing: a local
html
<figure>
  <pre><code class="language-lua">local n = 1
</code></pre>
  <figcaption>Listing: a local</figcaption>
</figure>

A code block caption, after two blank lines

The <figure> goes with the attachment. What is left is the plain <pre> a fenced block renders on its own, and a paragraph.

carve
```lua
local n = 1
```


^ Listing: a local
html
<pre><code class="language-lua">local n = 1
</code></pre>
<p>^ Listing: a local</p>

A blockquote caption, after one blank line

The control. This is the host the corpus could already see: blockquote ends in [caption_slot] and 55-blockquote-caption-after-a-blank-line has pinned the one-blank-line form since long before this category existed.

carve
> the cited line

^ Source: the cited work
html
<figure>
  <blockquote><p>the cited line</p></blockquote>
  <figcaption>Source: the cited work</figcaption>
</figure>

A blockquote caption, after two blank lines

Detached, the quote is an ordinary <blockquote> with no <figure> and no <figcaption>.

carve
> the cited line


^ Source: the cited work
html
<blockquote><p>the cited line</p></blockquote>
<p>^ Source: the cited work</p>

An image caption, after one blank line

The control, and the first of the two PROSE hosts. There is no image_paragraph production and no slot to widen or narrow: the block IS a paragraph, and what makes it captionable is a condition on its inline content (carve#992). §4 is the whole rule, so a corpus row is the only thing that can hold either half of it.

carve
![Ganymede](ganymede.jpg)

^ Figure: the largest moon
html
<figure>
  <img src="ganymede.jpg" alt="Ganymede">
  <figcaption>Figure: the largest moon</figcaption>
</figure>

An image caption, after two blank lines

Without the caption there is no <figure>, and an image-only paragraph renders as the bare <img> at block level.

carve
![Ganymede](ganymede.jpg)


^ Figure: the largest moon
html
<img src="ganymede.jpg" alt="Ganymede">
<p>^ Figure: the largest moon</p>

A display-math caption, after one blank line

The control, and the second PROSE host. A captioned standalone display-math block is a numbered EQUATION (§4); the block must be solely the $$-prefixed span.

carve
$$`a + b = c`

^ Equation: the sum
html
<figure>
  <p><span class="math display">\[a + b = c\]</span></p>
  <figcaption>Equation: the sum</figcaption>
</figure>

A display-math caption, after two blank lines

Detached, the math block is the ordinary paragraph it always was, and the caption is a second one.

carve
$$`a + b = c`


^ Equation: the sum
html
<p><span class="math display">\[a + b = c\]</span></p>
<p>^ Equation: the sum</p>

None of this is new behavior. All ten documents were measured against carve-js, carve-php and carve-rs before they were written down, and all three produce these bytes on all ten, so the category pins existing behavior rather than proposing any.

One thing the rows do not prove, and should not be read as proving: the five hosts are five SPELLINGS of the rule, not five independent implementations of it. The image paragraph and the display-math block share one decision site in the executable spec, because both are the same paragraph with a different condition on their content. Both rows still belong - a change to either condition would move one and not the other - but a mutation of the shared site kills them together, and the five-host count is a count of hosts, not of code paths.

Adjacent block openers in an attached run stay separate

2 conformance fixtures

Two adjacent blocks can each be valid at a list item's content column and still be invalid as a sequence there. Adjacent quote lines become one quote, so the canonical writer keeps the continuation marker on both attached quotes.

carve
- x
+
> q
+
> q
html
<ul>
  <li>x
    <blockquote><p>q</p></blockquote>
    <blockquote><p>q</p></blockquote>
  </li>
</ul>

Tables have the same sequence rule. Without the second boundary, the next header row becomes part of the first table's body.

carve
- x
+
| a |
|---|
| b |
+
| a |
|---|
| b |
html
<ul>
  <li>x
    <table>
      <thead><tr><th scope="col">a</th></tr></thead>
      <tbody>
        <tr><td>b</td></tr>
      </tbody>
    </table>
    <table>
      <thead><tr><th scope="col">a</th></tr></thead>
      <tbody>
        <tr><td>b</td></tr>
      </tbody>
    </table>
  </li>
</ul>

A caret line does not end a paragraph it cannot caption

4 conformance fixtures

PART 9 §10 I1 enumerates the lines that interrupt an open paragraph: a heading, a thematic break, a block quote, a valid table row, a guarded fence opener and a ::: opener. I5 adds the invisible ones - a reference definition, a comment, a block-attribute line. A ^ caption line is in neither list, so it does not interrupt. What ends a paragraph at a caret is §4, and §4 reaches exactly five captionable hosts; for the two it spells in prose that means a paragraph whose WHOLE content is one image or one display-math span. Everywhere else the ^ line is ordinary paragraph text and folds in, caret and all.

158-indented-image-and-caption-stay-literal pins the INDENTED spelling, where both readings agree because an indented line opens no top-level block at all. The flush-left spelling was pinned nowhere, and the two readers in this repository answered it differently: every engine folded the line in, the executable spec ended the paragraph and opened a second one. Nothing failed while the canonical writer force-escaped a line-initial caret. When that escape came off, the writer was right and oracle(fmt(x)) parted from oracle(x) on a document all three engines agreed about (carve#1046).

A caret line after ordinary prose.

carve
Text
^ Figure 1: moon
html
<p>Text
^ Figure 1: moon</p>

A caret line after a paragraph that merely CONTAINS an image. The image is not the whole paragraph, so no §4 host is present and the caret folds in behind it.

carve
Text
![Apollo](a.jpg)
^ Figure 1: moon
html
<p>Text
<img src="a.jpg" alt="Apollo">
^ Figure 1: moon</p>

§10 I6 applies the relation to every open paragraph, including one inside a container, so a quoted caret line folds the same way.

carve
> Text
> ^ Figure 1: moon
html
<blockquote><p>Text
^ Figure 1: moon</p></blockquote>

Control - when the paragraph IS the image, §4 attaches and the pair is a figure. 158-indented-image-and-caption-stay-literal-3 already pins this shape; it is repeated here because the rule above is only half a claim without it. The mutation that folds every caret line in leaves this row untouched, and a reader that stopped attaching captions altogether - the opposite defect - fails here and passes everything else in the section.

carve
![Apollo](a.jpg)
^ Figure 1: moon
html
<figure>
  <img src="a.jpg" alt="Apollo">
  <figcaption>Figure 1: moon</figcaption>
</figure>

Released under the MIT License.