Sashmark / Blog

Pandoc grid tables in Markdown: syntax and alignment

Bobby Huang ·

In the pandoc 3.8.3 runs here, a grid table used + corners, - row lines and | cell walls, with a line of = under the header. Colons on that = line set alignment: on the right for right, on the left for left, on both for center. With no = line the table had no thead, and colons on the top border set alignment. One cell held an ordered list and a fenced code block, and another a block quote. Misdrawn lines still exited 0 with no warning, while the HTML merged cells, added columns or lost a row. The commonmark reader built no table.

Key takeaways

  • The rows above the = line became thead rows, and an = line on each side of the last row wrote a tfoot.
  • Colons on the = line gave 3 lines each of text-align right, left and center in the test file.
  • With no = line, colons on the top border set alignment. With an = line, top-border colons wrote no text-align at all.
  • Cells held an ordered list, a fenced code block and a block quote, each as its own HTML element.
  • All 9 broken test files exited 0 under --fail-if-warnings, and a control file proved that flag can fail.

A pandoc grid table is drawn with + corners, - row lines and | cell walls, and a line of = marks where the header ends. Colons on that = line set each column's alignment. Get a line one column off and pandoc still builds something, with no warning, as the tests below show.

Every command here ran on a Mac with pandoc --version reporting pandoc 3.8.3, on small files read with -f markdown, and -f commonmark as a control. The checks stopped at the HTML pandoc wrote, so how any app displays it is out of scope, and so is any other Markdown tool. Cells that span columns or rows have their own post, merged cells in grid tables, and text wrapped across lines has multiline cells. Neither is retested here.

The border rules

Here is cat gt-basic.md, the working file:

+----------+---------+
| Name     | Role    |
+==========+=========+
| Jane Doe | Owner   |
+----------+---------+
| John Doe | Editor  |
+----------+---------+

Every line is 22 characters wide, and the + and | marks sit at characters 1, 12 and 22 on all seven lines. The command pandoc -f markdown -t html gt-basic.md wrote:

<table style="width:29%;">
<colgroup>
<col style="width: 15%" />
<col style="width: 13%" />
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jane Doe</td>
<td>Owner</td>
</tr>
<tr>
<td>John Doe</td>
<td>Editor</td>
</tr>
</tbody>
</table>

On the saved HTML, grep -c '<table' printed 1, grep -c '<tr' printed 3 and grep -c '<td' printed 4. The style and colgroup lines are looked at in column width in Markdown tables.

The columns come from the + marks, and a | that lines up with no + is plain text. In gt-bare-dash.md the borders have no inner +:

+--------------------+
| Name     | Role    |
+====================+
| Jane Doe | Owner   |
+--------------------+

That file got one column: grep -c '<col ' printed 1, against 2 for gt-basic.html. The cells came out as <th>Name | Role</th> and <td>Jane Doe | Owner</td>.

The commonmark reader did not build a table from gt-basic.md. Its extension list has no grid entry (pandoc --list-extensions=commonmark | grep -c grid_tables printed 0; the control, grep -c pipe_tables, printed 1), while the markdown list shows +grid_tables on line 24. The command pandoc -f commonmark -t html --wrap=none gt-basic.md wrote one paragraph:

<p>+----------+---------+ | Name | Role | +==========+=========+ | Jane Doe | Owner | +----------+---------+ | John Doe | Editor | +----------+---------+</p>

Here grep -c '<table' printed 0, against 1 for the markdown run.

The = line marks the header

The rows above the = line went into thead. In gt-two-headers.md two rows sit above it:

+----------+---------+
| Team     | Docs    |
+----------+---------+
| Name     | Role    |
+==========+=========+
| Jane Doe | Owner   |
+----------+---------+

Pandoc wrote both of them as header rows:

<thead>
<tr>
<th>Team</th>
<th>Docs</th>
</tr>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
</thead>

A second use of = wrote a footer. In gt-foot.md the last row has an = line on each side:

+----------+---------+
| Name     | Hours   |
+==========+=========+
| Jane Doe | 3       |
+----------+---------+
| John Doe | 4       |
+==========+=========+
| Total    | 7       |
+==========+=========+

The Total row came out inside a tfoot, and grep -c '<tfoot' printed 1:

<tfoot>
<tr>
<td>Total</td>
<td>7</td>
</tr>
</tfoot>

Align columns with colons on the = line

Colons go at the ends of each = run. Here is cat gt-align.md:

+-------+----------+---------+
| Right | Left     | Center  |
+======:+:=========+:=======:+
| 12    | Jane Doe | x       |
+-------+----------+---------+
| 123   | John Doe | yy      |
+-------+----------+---------+

The cells pandoc wrote:

<thead>
<tr>
<th style="text-align: right;">Right</th>
<th style="text-align: left;">Left</th>
<th style="text-align: center;">Center</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">12</td>
<td style="text-align: left;">Jane Doe</td>
<td style="text-align: center;">x</td>
</tr>
<tr>
<td style="text-align: right;">123</td>
<td style="text-align: left;">John Doe</td>
<td style="text-align: center;">yy</td>
</tr>
</tbody>

A colon on the right end gave right, on the left end gave left, and on both gave center. Three greps on the saved file, for text-align: right, text-align: left and text-align: center, each printed 3: one header cell and two body cells. A file with no colons, gt-basic.md, shows the other case: its cells carry no style at all. The same colon idea in a pipe table is covered in Markdown tables.

Colons elsewhere did not do the same job. With the colons moved to the top border of a table that also has an = line (gt-top-colons.md), grep -c 'text-align' printed 0, and the table was otherwise normal.

On a row line between two body rows, they broke the table. Here is gt-row-colons.md:

+-------+----------+---------+
| Right | Left     | Center  |
+=======+==========+=========+
| 12    | Jane Doe | x       |
+------:+:---------+:-------:+
| 123   | John Doe | yy      |
+-------+----------+---------+

Pandoc did not treat that line as a border. Its dashes landed inside the cells, which is easier to read with smart punctuation off, so this is the tbody from pandoc -f markdown-smart -t html gt-row-colons.md:

<tbody>
<tr>
<td rowspan="2">12 ------: 123</td>
<td rowspan="2">Jane Doe :--------- John Doe</td>
<td rowspan="2">x :-------: yy</td>
</tr>
<tr>
</tr>
</tbody>

Two rows became one row of three rowspan cells and an empty tr. A count on the plain markdown run, grep -c 'rowspan', printed 3.

A grid table with no header

Leave out the = line and the header goes with it. In gt-noheader.md every border uses dashes, and grep -c '<thead' printed 0, against 1 for gt-basic.html. The two rows became four td cells. More on tables with no header in other forms is in Markdown tables without a header.

With no = line, the colons move up to the top border. Here is cat gt-noheader-align.md:

+---------:+:--------+
| Jane Doe | Owner   |
+----------+---------+
| John Doe | Editor  |
+----------+---------+

Pandoc wrote:

<tbody>
<tr>
<td style="text-align: right;">Jane Doe</td>
<td style="text-align: left;">Owner</td>
</tr>
<tr>
<td style="text-align: right;">John Doe</td>
<td style="text-align: left;">Editor</td>
</tr>
</tbody>

All 4 cells took a text-align, and there is still no thead. So the same top-border colons wrote nothing in gt-top-colons.md, which has an = line, and set alignment here, where there is none.

Block content in a cell

A cell can hold block elements, not just a line of text. Multiline cells tests a bulleted list with a paragraph. This file tries a paragraph, an ordered list and a fenced code block in one cell, and a block quote in another:

+--------+------------------------+
| Step   | Detail                 |
+========+========================+
| Build  | Run these in order:    |
|        |                        |
|        | 1. Install             |
|        | 2. Test                |
|        |                        |
|        | ```sh                  |
|        | pnpm   test            |
|        | ```                    |
+--------+------------------------+
| Review | > Read the diff first. |
+--------+------------------------+

The body from pandoc -f markdown -t html gt-blocks.md:

<tbody>
<tr>
<td>Build</td>
<td><p>Run these in order:</p>
<ol type="1">
<li>Install</li>
<li>Test</li>
</ol>
<div class="sourceCode" id="cb1"><pre
class="sourceCode sh"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">pnpm</span>   test</span></code></pre></div></td>
</tr>
<tr>
<td>Review</td>
<td><blockquote>
<p>Read the diff first.</p>
</blockquote></td>
</tr>
</tbody>

Greps for <ol, <pre and <blockquote each printed 1. The three spaces in pnpm test survived inside the code block.

When a line is off by one column

Each file below is gt-basic.md cut down to one body row, with one thing wrong. None of them failed. All came back with exit code 0.

A | one column to the right, in gt-off-pipe.md:

| Jane Doe  | Owner  |

That | no longer lines up with a +, so it became text, and the two cells merged into one:

<td colspan="2">Jane Doe | Owner</td>

A + one column to the right in the bottom border, in gt-off-plus.md:

+-----------+--------+

This one changed the shape of the whole table. The grep -c '<col ' count went to 3 for a table drawn with two columns, and the body row and header both picked up spans:

<colgroup>
<col style="width: 15%" />
<col style="width: 1%" />
<col style="width: 12%" />
</colgroup>
<thead>
<tr>
<th>Name</th>
<th colspan="2">Role</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="3">Jane Doe | Owner</td>
</tr>
</tbody>

A row one character short, in gt-short-row.md, where the last | sits at character 21 instead of 22:

| Jane Doe | Owner  |

The closing wall turned into cell text:

<td>Owner |</td>

A row one character long, in gt-long-row.md, with | Jane Doe | Owner |, added a third column. Its <col count was 3, with an empty header cell:

<th>Name</th>
<th>Role</th>
<th></th>

An = line one character short, in gt-short-sep.md, ending +========+ where the second column needs nine. With smart punctuation off so the dashes stay as typed, the table had two columns and this header cell:

<th>Role --------+ Owner</th>

The word Owner and dashes from the bottom border ended up in the header cell, and Owner still appeared in its body cell too.

Two missing lines did more damage. With the top border gone (gt-no-top.md), grep -c '<table' printed 0:

<div class="line-block">Name | Role |</div>
<p>+==========+=========+ | Jane Doe | Owner |
+----------+---------+</p>

That output is from the smart-off run. With the bottom border gone (gt-no-bottom.md), the table was built and the row was lost. The tbody came out empty, grep -c '<td' printed 0, and grep -c 'Jane Doe' printed 0, against 1 on gt-basic.html:

<tbody>
</tbody>

No run warned about any of this. The flag --fail-if-warnings left all 9 broken files at exit code 0: the seven in this section, plus gt-row-colons.md and gt-bare-dash.md. To show the flag can fire, a control file with a duplicate link reference went through the same command. It printed Failing because there were warnings. and returned exit code 3. So the exit code told nothing about these tables.

Steps to check a grid table

Each command ran on the files above. Put your own file's name in place of them.

  1. Print where each border character sits, one line per pattern:

    awk '{ s=""; for (i=1; i<=length; i++) { c=substr($0,i,1); if (c=="+" || c=="|") s=s" "i }; print s }' gt-basic.md | sort | uniq -c
    

    On gt-basic.md it printed one pattern, 7 1 12 22.

  2. Run the same command on the suspect file. gt-off-pipe.md printed a second pattern, 1 1 13 22, and gt-short-row.md printed 1 1 12 21. A pattern with a count of 1 is the line to look at.

  3. Allow for spans you meant to draw. A deliberate wide cell in gt-span.md also printed a second pattern, 1 1 22.

  4. Save the HTML with pandoc -f markdown -t html gt-basic.md -o gt-basic.html.

  5. Count columns with grep -c '<col ' gt-basic.html. It printed 2, the number drawn. The gt-long-row.md file printed 3.

  6. Count spans with grep -c 'colspan' gt-basic.html. It printed 0. The gt-off-pipe.md file, which draws none on purpose, printed 1.

  7. Check for a lost row by grepping for text you know is in it. grep -c 'Jane Doe' gt-no-bottom.html printed 0.

Where Sashmark fits

Sashmark is a Markdown reader and editor for people whose AI agents write Markdown. It is in development; nothing has been released. There are no AI features in the app.

Questions

How do I align a column in a pandoc grid table?
Put colons on the = line under the header. In the test, +======:+ gave right, +:=========+ gave left and +:=======:+ gave center, on the header and body cells alike.
Can a pandoc grid table have no header?
Yes. Leave out the = line. The test table came out with no thead element, and colons on its top border set its alignment.
Why did my grid table come out wrong with no error?
Pandoc exited 0 with no warning on all 9 broken test files. A | one column off became cell text, and a missing bottom border lost the last row. The awk command under Steps to check a grid table prints where each border character sits.

What Sashmark plans for v1: Features · Roadmap

View as Markdown