Skip to content

Tables ​

Alignment, spans, multi-line cells, ragged rows and the pipe rules that decide whether a table exists at all.

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.

Table without alignment ​

1 conformance fixture
carve
|= Name     |= Age |
| Alice     |   28 |
| Bob       |   34 |
html
<table>
  <thead>
    <tr><th scope="col">Name</th><th scope="col">Age</th></tr>
  </thead>
  <tbody>
    <tr><td>Alice</td><td>28</td></tr>
    <tr><td>Bob</td><td>34</td></tr>
  </tbody>
</table>

Table alignment with colspan ​

1 conformance fixture
carve
|=> Category |= Item   |= Price |
| Fruit       | Apple    | $1      |
| Total       | <        | $1.50   |
html
<table>
  <thead>
    <tr><th scope="col" style="text-align: right;">Category</th><th scope="col">Item</th><th scope="col">Price</th></tr>
  </thead>
  <tbody>
    <tr><td style="text-align: right;">Fruit</td><td>Apple</td><td>$1</td></tr>
    <tr><td colspan="2" style="text-align: right;">Total</td><td>$1.50</td></tr>
  </tbody>
</table>

Table doubled alignment marker ​

1 conformance fixture

An alignment run is accepted as a unit. A duplicate horizontal axis makes the whole run invalid, so |=<< keeps both < characters as visible content rather than consuming a valid prefix. The kind marker falls with it: T11's run is atomic, and with no space to end it the = is content too, so the cell is an ordinary data cell whose text begins with =<<. The second cell, spelled with its space, is still a header -- and one header cell among data cells is a row header rather than a thead.

carve
|=<< Note |= Plain |
| a         | b       |
html
<table>
  <tbody>
    <tr><td>=&lt;&lt; Note</td><th scope="row">Plain</th></tr>
    <tr><td>a</td><td>b</td></tr>
  </tbody>
</table>

Table columns carry alignment, vertical alignment and widths ​

1 conformance fixture

A preceding table attribute line supplies positional column defaults even when the table has no header row. Empty entries are unset. Source widths are percentages; the exchange AST stores the corresponding fractions. Cell-local markers remain more specific than these defaults.

carve
{aligns="right,,center" valigns="top,middle,bottom" widths="25,50,25"}
| A | B | C |
| D | E | F |
html
<table>
  <colgroup>
    <col style="width: 25%;">
    <col style="width: 50%;">
    <col style="width: 25%;">
  </colgroup>
  <tbody>
    <tr><td style="text-align: right; vertical-align: top;">A</td><td style="vertical-align: middle;">B</td><td style="text-align: center; vertical-align: bottom;">C</td></tr>
    <tr><td style="text-align: right; vertical-align: top;">D</td><td style="vertical-align: middle;">E</td><td style="text-align: center; vertical-align: bottom;">F</td></tr>
  </tbody>
</table>

A table alignment run carries two independent axes ​

1 conformance fixture

The horizontal marker comes first and the optional vertical marker follows it. Rendered CSS likewise writes text-align before vertical-align. A header-cell run supplies column defaults; a body-cell run overrides only that cell.

carve
|=~ Item |=>^ Qty |
| Apple | 12 |
| Subtotal |<v 12 |
html
<table>
  <thead>
    <tr><th scope="col" style="text-align: center;">Item</th><th scope="col" style="text-align: right; vertical-align: top;">Qty</th></tr>
  </thead>
  <tbody>
    <tr><td style="text-align: center;">Apple</td><td style="text-align: right; vertical-align: top;">12</td></tr>
    <tr><td style="text-align: center;">Subtotal</td><td style="text-align: left; vertical-align: bottom;">12</td></tr>
  </tbody>
</table>

A vertical table marker needs a horizontal partner ​

1 conformance fixture

Horizontal alignment may stand alone. Vertical alignment is meaningful only as the second axis of a paired run, so a lone ^ or v stays visible instead of silently changing layout. Axes are always written horizontal then vertical; vertical-first runs stay visible rather than switching the order. A rejected run takes the kind marker with it (T11): only the paired cell keeps its =, and the rest read as data cells whose text begins with =.

carve
|=^ Top |=v Bottom |=<^ Paired |=v> Reverse |=~> Middle |
| a | b | c | d | e |
html
<table>
  <tbody>
    <tr><td>=^ Top</td><td>=v Bottom</td><th scope="row" style="text-align: left; vertical-align: top;">Paired</th><td>=v&gt; Reverse</td><td>=~&gt; Middle</td></tr>
    <tr><td>a</td><td>b</td><td>c</td><td>d</td><td>e</td></tr>
  </tbody>
</table>

A table cell can inherit horizontal alignment ​

1 conformance fixture

? in the horizontal position preserves the column's horizontal alignment while supplying a cell-level vertical alignment. It is valid only as the first marker of ?^, ?~, or ?v; every other use stays visible content.

carve
|=>^ Name |= Value |
|?v Bottom |?~ Middle |
|?^ Top | plain |
|? lone |v? reversed |
|?< wrong |^< axes |
html
<table>
  <thead>
    <tr><th scope="col" style="text-align: right; vertical-align: top;">Name</th><th scope="col">Value</th></tr>
  </thead>
  <tbody>
    <tr><td style="text-align: right; vertical-align: bottom;">Bottom</td><td style="vertical-align: middle;">Middle</td></tr>
    <tr><td style="text-align: right; vertical-align: top;">Top</td><td>plain</td></tr>
    <tr><td style="text-align: right; vertical-align: top;">? lone</td><td>v? reversed</td></tr>
    <tr><td style="text-align: right; vertical-align: top;">?&lt; wrong</td><td>^&lt; axes</td></tr>
  </tbody>
</table>

Pipe tables can state head and foot row counts ​

1 conformance fixture

The explicit counts partition the rows. Rows in the explicit head are promoted to column headers; a native |= cell in the body remains a row header.

carve
{header-rows=2 footer-rows=1}
| Region | Q1 |
| Detail | EUR |
|= North | 11 |
| All | 33 |
html
<table>
  <thead>
    <tr><th scope="col">Region</th><th scope="col">Q1</th></tr>
    <tr><th scope="col">Detail</th><th scope="col">EUR</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">North</th><td>11</td></tr>
  </tbody>
  <tfoot>
    <tr><td>All</td><td>33</td></tr>
  </tfoot>
</table>

Table cell escaped pipe ​

1 conformance fixture

A backslash-escaped pipe is literal content and does not split the cell.

carve
|= A |= B |
| x \| y | z |
html
<table>
  <thead>
    <tr><th scope="col">A</th><th scope="col">B</th></tr>
  </thead>
  <tbody>
    <tr><td>x | y</td><td>z</td></tr>
  </tbody>
</table>

Table cell pipe inside code span ​

1 conformance fixture

A pipe inside a code span is protected and does not split the cell.

carve
|= A |= B |
| `a|b` | z |
html
<table>
  <thead>
    <tr><th scope="col">A</th><th scope="col">B</th></tr>
  </thead>
  <tbody>
    <tr><td><code>a|b</code></td><td>z</td></tr>
  </tbody>
</table>

An unclosed verbatim run in a row stops at the closing pipe ​

6 conformance fixtures

A | inside an open code span is cell content, not a separator — that is what lets | a `x|` | b | be two cells. An unclosed run has no such reach: the row's own closing pipe still closes the row, and the run stops there (markup-carve/carve#1284).

carve
| a `b | c d |
html
<table>
  <tbody>
    <tr><td>a <code>b | c d</code></td></tr>
  </tbody>
</table>

It is a real row, so the table continues below it:

carve
| a `b | c d |
| e | f |
html
<table>
  <tbody>
    <tr><td>a <code>b | c d</code></td></tr>
    <tr><td>e</td><td>f</td></tr>
  </tbody>
</table>

The same under a header row, since nothing here depends on the row's position:

carve
|= h |
| a `b | c |
html
<table>
  <thead>
    <tr><th scope="col">h</th></tr>
  </thead>
  <tbody>
    <tr><td>a <code>b | c</code></td></tr>
  </tbody>
</table>

The run's delimiter length does not matter — an unclosed double-backtick run behaves the same:

carve
| a ``b | c |
html
<table>
  <tbody>
    <tr><td>a <code>b | c</code></td></tr>
  </tbody>
</table>

The control, and the reason this is a narrow rule rather than "pipes always split": a run that DOES close still hides the pipes inside it, so this row has two cells and not three.

carve
| a `x|` | b |
html
<table>
  <tbody>
    <tr><td>a <code>x|</code></td><td>b</td></tr>
  </tbody>
</table>

And a line with content dangling after its last pipe is still prose, open run or not — the closing-pipe requirement is unchanged.

carve
| a | b
html
<p>| a | b</p>

A continuation row's open run, and an escaped closing pipe ​

5 conformance fixtures

Two shapes sit either side of the row terminator, and neither was settled by the rule that an unclosed run stops at the row's closing pipe (markup-carve/carve#1293).

A + continuation extends the cell, so the block an unclosed run reaches the end of is that whole cell, continuation included. The run therefore spans the row boundary and closes on the continuation row:

carve
| a `b |
+ c` |
html
<table>
  <tbody>
    <tr><td>a <code>b c</code></td></tr>
  </tbody>
</table>

An escaped closing pipe is still an escape. The row closes there, because the line ends in a pipe; what the escape decides is what the CELL holds, which is a literal pipe and not an orphaned backslash:

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

The control that makes the asymmetry visible, and the argument that settled it: every reader already honors \| mid-cell. Reading the escape at every position except the last one is a position exception with nothing behind it, and \| is the only way to put a literal pipe in a cell:

carve
| a \| b | c |
html
<table>
  <tbody>
    <tr><td>a | b</td><td>c</td></tr>
  </tbody>
</table>

The continuation is cut into cells while that run is still open, which is what keeps its own pipes content. Splitting it with a fresh scanner cuts inside the run and leaves a segment with no column to join, and a dropped segment is content loss rather than a second answer:

carve
| a `b |
+ c | d` |
html
<table>
  <tbody>
    <tr><td>a <code>b c | d</code></td></tr>
  </tbody>
</table>

The open run belongs to ONE column, and a continuation joins per column, so the columns before it are still cut at their own pipes. Carrying the run across the whole continuation line instead swallows those separators and pushes the text into the wrong cell, which leaves the run's own cell holding an empty <code></code> - the artifact this ruling rejects, produced from the other direction:

carve
| x | a `b |
+ y | c` |
html
<table>
  <tbody>
    <tr><td>x y</td><td>a <code>b c</code></td></tr>
  </tbody>
</table>

An attributed cell keeps its attributes and its literal marker ​

2 conformance fixtures

PART 9 §5 T4 says a cell carrying attributes is never a bare span marker, and that its content is literal even when the content is just ^ or <. It says nothing about the attributes, which stay on the cell that carries them: the block is what makes the cell ordinary content in the first place, so reading it back out as text would leave the sentence with nothing to act on (markup-carve/carve#1463).

carve
| a | b |
|{.x} < |{.y} ^ |
html
<table>
  <tbody>
    <tr><td>a</td><td>b</td></tr>
    <tr><td class="x">&lt;</td><td class="y">^</td></tr>
  </tbody>
</table>

The contrast, and the reason the rule is worth pinning: WITHOUT the attributes the same character is a span marker and the cell beside it grows.

carve
| a | b |
| c | < |
html
<table>
  <tbody>
    <tr><td>a</td><td>b</td></tr>
    <tr><td colspan="2">c</td></tr>
  </tbody>
</table>

Table stacked rowspan ​

1 conformance fixture

Consecutive ^ cells extend the same origin cell; two stacked ^ markers produce rowspan="3".

carve
|= Tier |= User |
| Gold   | Ann  |
| ^      | Bo   |
| ^      | Cy   |
html
<table>
  <thead>
    <tr><th scope="col">Tier</th><th scope="col">User</th></tr>
  </thead>
  <tbody>
    <tr><td rowspan="3">Gold</td><td>Ann</td></tr>
    <tr><td>Bo</td></tr>
    <tr><td>Cy</td></tr>
  </tbody>
</table>

Table rowspan with multi-line content ​

1 conformance fixture

A + continuation before a ^ rowspan extends the spanned cell.

carve
|= Category       |= Item   |
| Fresh Fruits    | Apple   |
+ from local      |         |
+ farms           |         |
| ^               | Banana  |
html
<table>
  <thead>
    <tr><th scope="col">Category</th><th scope="col">Item</th></tr>
  </thead>
  <tbody>
    <tr><td rowspan="2">Fresh Fruits from local farms</td><td>Apple</td></tr>
    <tr><td>Banana</td></tr>
  </tbody>
</table>

Superscript in a table cell ​

2 conformance fixtures

Superscript in a cell uses the braced form {^…^}. A lone ^ as the sole cell content is a rowspan marker; any other bare ^ in a cell is literal text.

carve
| Value |
| {^2^} |
html
<table>
  <tbody>
    <tr><td>Value</td></tr>
    <tr><td><sup>2</sup></td></tr>
  </tbody>
</table>
carve
| Value |
| ^2^   |
html
<table>
  <tbody>
    <tr><td>Value</td></tr>
    <tr><td>^2^</td></tr>
  </tbody>
</table>

Table span marker in first column ​

1 conformance fixture

A span marker (^ rowspan / < colspan) must be the whole cell. In the first column a < (or in the first row a ^) has nothing to merge into, so it renders as an empty cell rather than being dropped.

carve
| < | b |
|---|---|
| c | d |
html
<table>
  <thead>
    <tr><th scope="col"></th><th scope="col">b</th></tr>
  </thead>
  <tbody>
    <tr><td>c</td><td>d</td></tr>
  </tbody>
</table>

Table cell attributes ​

1 conformance fixture

A {…} attribute block glued to a cell (no space) sets that cell's attributes; the rest, after optional whitespace, is the cell content. A space before the brace keeps it literal, and a cell carrying attributes is never a bare span marker. The block binds after the cell's kind and alignment markers, so on a cell with no marker it sits directly against the opening |.

carve
|{.highlight} Total | 99 |
|---|---|
| a | b |
html
<table>
  <thead>
    <tr><th scope="col" class="highlight">Total</th><th scope="col">99</th></tr>
  </thead>
  <tbody>
    <tr><td>a</td><td>b</td></tr>
  </tbody>
</table>

Cell attributes bind after the kind and alignment markers ​

6 conformance fixtures

A cell's {…} attribute block attaches AFTER the kind marker = and after the alignment marker, in every cell. The block is glued to the marker run where the cell has one and to the opening | where it has none; a space in front of it still keeps it literal. This is what makes an attributed HEADER cell expressible: with the block bound ahead of the =, the only available shape is |{#x}=R|, which reads as a data cell whose content starts with =.

carve
|={.total} Total |= 99 |
| a | b |
html
<table>
  <thead>
    <tr><th scope="col" class="total">Total</th><th scope="col">99</th></tr>
  </thead>
  <tbody>
    <tr><td>a</td><td>b</td></tr>
  </tbody>
</table>

Both markers may precede the block. The kind marker comes first, then the alignment marker, then the attributes; a native alignment marker on a header cell still sets the whole column's alignment.

carve
|=~{#score} Score |
| 9 |
html
<table>
  <thead>
    <tr><th scope="col" id="score" style="text-align: center;">Score</th></tr>
  </thead>
  <tbody>
    <tr><td style="text-align: center;">9</td></tr>
  </tbody>
</table>

On a data cell the alignment marker likewise comes first, and the computed alignment composes with the authored block rather than replacing it.

carve
|= Item |= Cost |
| Pen |>{.num} 9 |
html
<table>
  <thead>
    <tr><th scope="col">Item</th><th scope="col">Cost</th></tr>
  </thead>
  <tbody>
    <tr><td>Pen</td><td class="num" style="text-align: right;">9</td></tr>
  </tbody>
</table>

The other order is no longer a marker position. A < written AFTER the attribute block is ordinary content, so the cell carries the attributes and is not aligned. This is the released spelling that changes meaning, and it reinterprets rather than erroring. The block is followed by the space that ends the marker run (T11); without it the braces would be content too.

carve
|{#x} < content |
html
<table>
  <tbody>
    <tr><td id="x">&lt; content</td></tr>
  </tbody>
</table>

The shape that used to be the only candidate for an attributed header cell is still a data cell, which is the ambiguity the rule removes: the = is content because the block ahead of it already committed the cell.

carve
|{#x} =R |
html
<table>
  <tbody>
    <tr><td id="x">=R</td></tr>
  </tbody>
</table>

Row attributes do not move. They stay glued to the row's closing |, and they compose with cell attributes written in the new position.

carve
|=<{.h} Name |=>{.c} Score |{.head}
| Ann |>{.num} 9 |{.win}
html
<table>
  <thead>
    <tr class="head"><th scope="col" class="h" style="text-align: left;">Name</th><th scope="col" class="c" style="text-align: right;">Score</th></tr>
  </thead>
  <tbody>
    <tr class="win"><td style="text-align: left;">Ann</td><td class="num" style="text-align: right;">9</td></tr>
  </tbody>
</table>

A table cell's marker run ends at a space ​

5 conformance fixtures

A cell's marker run is the kind marker =, the alignment run and the attribute block, in T10's order, and PART 9 §5 T11 gives it the terminator it never had: a run must be followed by one space. Without that space there is no run, and every character of it is content. The alignment run already worked this way; the kind marker and the attribute block did not, so a cell whose content began with = lost it to a marker nobody wrote.

carve
|=hot= is the reading |
html
<table>
  <tbody>
    <tr><td><mark>hot</mark> is the reading</td></tr>
  </tbody>
</table>

The header cell is the padded spelling, and it is what a canonical writer already emits (PART 11 §6e), so a formatted document needs no migration:

carve
|= Item |= Qty |
| Pen | 9 |
html
<table>
  <thead>
    <tr><th scope="col">Item</th><th scope="col">Qty</th></tr>
  </thead>
  <tbody>
    <tr><td>Pen</td><td>9</td></tr>
  </tbody>
</table>

An attribute block is part of the run and takes the same space. Glued to the content it is not a block at all, which is why the braces reach the output:

carve
|{.total} 99 |
|{.total}99 |
html
<table>
  <tbody>
    <tr><td class="total">99</td></tr>
    <tr><td>{.total}99</td></tr>
  </tbody>
</table>

THE CLOSING PIPE IS NOT A TERMINATOR, so an empty marked cell takes its space too. A cell with NO run is the one shape this clause leaves alone: |a| still means what it meant, because there is no marker for the space to end.

carve
|= |a|
|=|a|
html
<table>
  <tbody>
    <tr><th scope="row"></th><td>a</td></tr>
    <tr><td>=</td><td>a</td></tr>
  </tbody>
</table>

An escape reaches the reading a space cannot spell, unchanged by this clause:

carve
|\= a |
html
<table>
  <tbody>
    <tr><td>= a</td></tr>
  </tbody>
</table>

Table row attributes ​

1 conformance fixture

An attribute block glued to a row's closing | sets that row's <tr> attributes - the row-level twin of a cell's attribute block, in a different position: a row's follows the row's last |, a cell's follows the cell's markers. It applies to a header or a body row and composes with the GFM delimiter row.

carve
| Name | Score |{.head}
|------|-------|
| Ann  | 9     |{.win}
html
<table>
  <thead>
    <tr class="head"><th scope="col">Name</th><th scope="col">Score</th></tr>
  </thead>
  <tbody>
    <tr class="win"><td>Ann</td><td>9</td></tr>
  </tbody>
</table>

Table header cell rowspan ​

1 conformance fixture

A ^ rowspan marker extends the cell above it even across the header/body boundary: a header cell can span into the body rows below, rendering as <th scope="col" rowspan="N">.

carve
|= H |= G |
| ^ | b |
| ^ | c |
html
<table>
  <tbody>
    <tr><th scope="col" rowspan="3">H</th><th scope="col">G</th></tr>
    <tr><td>b</td></tr>
    <tr><td>c</td></tr>
  </tbody>
</table>

An explicit table head span keeps one row group ​

1 conformance fixture

When a header cell spans into a body row, the table uses one <tbody> and keeps the cell's header scope.

carve
{header-rows=1}
| H | G |
| ^ | b |
html
<table>
  <tbody>
    <tr><th scope="col" rowspan="2">H</th><th scope="col">G</th></tr>
    <tr><td>b</td></tr>
  </tbody>
</table>

A table foot span keeps one row group ​

1 conformance fixture

A body cell spanning into a footer row uses the same single-body layout.

carve
{footer-rows=1}
| a | b |
| ^ | c |
html
<table>
  <tbody>
    <tr><td rowspan="2">a</td><td>b</td></tr>
    <tr><td>c</td></tr>
  </tbody>
</table>

Blocked span marker renders as empty cell ​

1 conformance fixture

A span marker merges into the nearest still-available origin: a ^ walks up its column, a < walks left along its row, skipping cells already consumed by another span. When the walk reaches no available cell at all - it runs off the edge of the table - the marker is neither dropped nor left literal: it renders as an EMPTY cell (<td></td>) carrying no content and no span. The first-row ^ / first-column < orphan is one instance (see "Table span marker in first column"); the same rule covers a marker BLOCKED when every cell back to the edge is already consumed.

Here the second body row leads with ^, so the x above it gains rowspan="2". The next cell is <; its only left neighbor (the first column) is now occupied by that rowspan, so the leftward walk runs off the edge with nothing to merge and the < becomes an empty cell. The trailing d follows as usual.

carve
| A | B | C |
|---|---|---|
| x | y | z |
| ^ | < | d |
html
<table>
  <thead>
    <tr><th scope="col">A</th><th scope="col">B</th><th scope="col">C</th></tr>
  </thead>
  <tbody>
    <tr><td rowspan="2">x</td><td>y</td><td>z</td></tr>
    <tr><td></td><td>d</td></tr>
  </tbody>
</table>

Colspan marker scans left past a consumed cell ​

1 conformance fixture

The same leftward walk SUCCEEDS when an available cell sits beyond the consumed columns: a < skips every column already taken by another span and merges into the nearest cell that is still free, only falling back to an empty cell when the walk reaches the table edge with nothing to merge.

Here the second body row is | p | ^ | < | e |. The ^ (column 2) continues the rowspan of b directly above it, so column 2 is consumed and b gains rowspan="2". The < (column 3) then walks left, skips that consumed column, and merges into p (column 1), so p gains colspan="2". The trailing e follows as a plain cell.

The walk counts the consumed column toward the span it grows, so the resulting colspan can visually overlap the cell occupying that column (here p's colspan="2" covers the column b's rowspan still holds). That overlap is the defined result of the walk-and-merge model, not an error: span markers only ever grow an existing cell or, when blocked at the edge, become an empty cell - the author chooses the layout by where they place the markers.

carve
| p | q | r | s |
|---|---|---|---|
| a | b | c | d |
| p | ^ | < | e |
html
<table>
  <thead>
    <tr><th scope="col">p</th><th scope="col">q</th><th scope="col">r</th><th scope="col">s</th></tr>
  </thead>
  <tbody>
    <tr><td>a</td><td rowspan="2">b</td><td>c</td><td>d</td></tr>
    <tr><td colspan="2">p</td><td>e</td></tr>
  </tbody>
</table>

A pipe pair with no cell is not a table ​

1 conformance fixture

|| has no cell between the pipes, so it is ordinary paragraph text, not a one-cell table.

carve
||
html
<p>||</p>

A row whose every cell is blank is not a table ​

7 conformance fixtures

The table detector counts delimiter slots and never asks whether a slot holds anything. A single empty PLAIN cell was already rejected - || is a paragraph - but the rejection reached neither a MULTI-cell all-empty row nor the HEADER path, so ||| opened a two-column table of empty cells and |= |, an empty header cell, opened a <thead>-only table where the identical | | was rejected. A table whose every cell is empty carries no data, and degrading it to text is what the one- and two-pipe cases already do (markup-carve/carve#1950).

carve
|||
html
<p>|||</p>

Whitespace-only cells are empty too - the content is trimmed before it is weighed.

carve
| | |
html
<p>| | |</p>

The header path, which was the clearest gap: an empty header cell had no rejection at all, though the identical plain cell did. A table that is one empty header and no body is as empty as output gets.

carve
|= |
html
<p>|= |</p>

A row with any content is a table, unchanged. This is the control the rule must not move.

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

ONE filled cell is enough - the rule is EVERY cell blank, not any cell blank. A row of one value and one empty cell is a real table with an empty cell in it.

carve
| a | |
html
<table>
  <tbody>
    <tr><td>a</td><td></td></tr>
  </tbody>
</table>

A CELL MARKER IS SOMETHING THE AUTHOR WROTE. An attribute block is a deliberate construct, so a cell that carries one and no text is not blank and the row stays a table - the boundary is content a reader wrote, and an attribute is that.

carve
|{.x} |
html
<table>
  <tbody>
    <tr><td class="x"></td></tr>
  </tbody>
</table>

An alignment run is deliberate the same way, and holds its row a table too.

carve
|> |
html
<table>
  <tbody>
    <tr><td style="text-align: right;"></td></tr>
  </tbody>
</table>

A continuation row needs a body row ​

1 conformance fixture

A + continuation row joins the row ABOVE IT IN THE SOURCE, and a delimiter row is consumed: it produces no <tr> and is not a row of the table. So a + line directly under one has no row to join and stays an ordinary paragraph.

The reason is the delimiter row and not the missing body row. A header row on its own IS joinable - see A continuation row joins the row above it, whatever its cells hold below - so "no body row yet" would answer that document wrongly (markup-carve/carve#1354).

carve
| a | b |
| - | - |
+ cont |
html
<table>
  <thead>
    <tr><th scope="col">a</th><th scope="col">b</th></tr>
  </thead>
</table>
<p>+ cont |</p>

Table row closing pipe ​

3 conformance fixtures

A table row must close with a pipe. A line that starts with | but has content dangling after its last pipe is prose, wherever it appears - it neither opens a table at a block start nor interrupts an open paragraph.

carve
| a | b
html
<p>| a | b</p>

The rule applies to every row, not only the first: once a line fails to close, the table ends and that line is a paragraph.

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

And an unclosed line does not swallow a following well-formed row: the row still opens its own table.

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

A table delimiter cell needs at least one dash ​

1 conformance fixture

A lone : does not make a delimiter row, so the row is an ordinary body row and the table gets no header.

carve
| a | b |
|:|:|
| 1 | 2 |
html
<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 continuation row carries no trailing text ​

2 conformance fixtures

The + continuation marker replaces the leading pipe, and the row must close with one. A proper continuation merges into the row above; text after the closing pipe leaves the line outside the table entirely.

carve
| a | b |
+ c | d |
html
<table>
  <tbody>
    <tr><td>a c</td><td>b d</td></tr>
  </tbody>
</table>
carve
| a | b |
+ c | d | junk
html
<table>
  <tbody>
    <tr><td>a</td><td>b</td></tr>
  </tbody>
</table>
<p>+ c | d | junk</p>

A ragged table keeps each row's cell count ​

3 conformance fixtures

Cell count is content, not source padding. The canonical writer can align the cells a row carries, but it does not append empty cells to make the table rectangular. A missing trailing cell and an authored empty cell render as different table structures.

carve
| ~x~ |
| a | b |
html
<table>
  <tbody>
    <tr><td><s>x</s></td></tr>
    <tr><td>a</td><td>b</td></tr>
  </tbody>
</table>

The same rule applies when the first row is a header. A short body row stays short, and the canonical native = spelling does not need a delimiter row.

carve
| |x |
|---|
| y |
html
<table>
  <thead>
    <tr><th scope="col"></th><th scope="col">x</th></tr>
  </thead>
  <tbody>
    <tr><td>y</td></tr>
  </tbody>
</table>

A header row may itself be shorter than a later body row. Its native markers still preserve it as a header without manufacturing another header cell.

carve
| h |
|---|
| |x |
html
<table>
  <thead>
    <tr><th scope="col">h</th></tr>
  </thead>
  <tbody>
    <tr><td></td><td>x</td></tr>
  </tbody>
</table>

Tables ​

2 conformance fixtures

An escaped pipe inside cell content (\|) renders as a literal | and does not split the cell.

carve
|= Symbol |= Meaning  |
| \|      | pipe char |
html
<table>
  <thead>
    <tr><th scope="col">Symbol</th><th scope="col">Meaning</th></tr>
  </thead>
  <tbody>
    <tr><td>|</td><td>pipe char</td></tr>
  </tbody>
</table>

Empty cells produce empty <td> elements — placement is preserved, not collapsed.

carve
|= A |= B |= C |
| 1  |    | 3  |
html
<table>
  <thead>
    <tr><th scope="col">A</th><th scope="col">B</th><th scope="col">C</th></tr>
  </thead>
  <tbody>
    <tr><td>1</td><td></td><td>3</td></tr>
  </tbody>
</table>

Released under the MIT License.