Markdown line breaks in tables: keep rows intact

Use an unquoted <br> inside a single source row to add a Markdown line break in a table, provided the destination supports pipe tables and inline HTML. Diagnose three failures separately: a table that prints as pipes, a cell split into another data row, and a break tag that appears as text. 19

Match the fix to the failed output

Choose the repair by what happened to the table structure before changing the break syntax.

What you see How to identify the case Repair
Pipes and dashes appear as prose Generated HTML contains a paragraph around the pipe text, with no table element Enable table support and retain a valid delimiter row
Part of a cell moves into another row Source contains a physical newline inside the intended data row Join that source row and insert an inline break
A valid table prints the break tag Source has backticks around the tag, or generated cell HTML contains escaped angle brackets Move the tag outside code spans, then distinguish HTML policy from syntax
Short phrases need a deliberate break The row already parses correctly and the destination accepts inline HTML Put <br> between the phrases on the same source line

A table extension and an HTML option solve different problems. The delimiter row gives a table parser its column structure. Inline parsing then handles the content inside each cell. GFM defines this table extension separately from ordinary paragraphs. A CommonMark parser can accept a raw HTML break while leaving the surrounding pipes as paragraph text. 13

In our strict CommonMark configuration, the break survived, but the pipe text never became a table. Adding more break tags at this stage only changes a paragraph containing table-like text. 10

Output from our pipe-table sample, unedited:

<p>| Service | Contact |
        | --- | --- |
        | Support | Email<br>Phone |</p>
        

If a long source row wraps on screen without inserting a newline character, its table structure has not changed. A deliberate cell break becomes necessary when a boundary carries meaning, such as separating contact methods or commands. Short prose can stay on a single source line and wrap with the destination's layout.

Keep Markdown table line breaks on one source line

Keep the complete data row on one physical source line and place <br> between the inline fragments that need separation.

The header and delimiter remain on their own source lines. The data row holds the label and the complete cell value, including the break tag. GitLab's documentation uses this inline-HTML approach for cells with multiple lines. It does not require an extra empty row or a closing break tag. 9

Input from our contact sample, unedited:

| Service | Contact |
        | --- | --- |
        | Support | Email<br>Phone |
        

The table-enabled markdown-it-py path kept Email and Phone in the same data cell. Its raw HTML contains the break between them, with no new row between the words. The fragment is from the table body, so the header is omitted here. 10

Output from our contact sample, with the table rule enabled and soft breaks off or on, unedited:

<tr>
        <td>Support</td>
        <td>Email<br>Phone</td>
        </tr>
        

Python-Markdown with its tables extension produced the same cell structure for this input. 610

Output from our contact sample, unedited:

<tbody>
        <tr>
        <td>Support</td>
        <td>Email<br>Phone</td>
        </tr>
        

Pandoc's GFM reader also retained the inline break in its HTML output. Choosing a GFM reader is an input-format decision. It does not mean the output has passed through GitHub's website or its post-processing. 810

Output from our contact sample, unedited:

<td>Support</td>
        <td>Email<br>Phone</td>
        </tr>
        </tbody>
        

The self-closing form <br /> produced the same cell-break structure in these local paths. Use the spelling your destination accepts. Both forms require an intact source row, and a code span still prints the tag as text. 10

For commands, close the code span before the break and reopen it around the next command. The command strings below are formatting examples, not executed for this guide. Keeping the tag outside the code spans lets the renderer retain code styling without printing the tag as part of the command. 10

Input from our command-formatting sample, unedited:

| Step | Commands |
        | --- | --- |
        | Install | `npm install`<br>`npm run build` |
        

Repair split rows before changing break settings

A physical newline inside a pipe-table row can create another data row before paragraph line-break rules have any chance to act.

In our split contact sample, Email ended the intended source row and Phone began the following line. The table-enabled paths moved Phone into the label column and left its contact cell empty. 10

Output from our split-row sample, unedited:

<td>Email</td>
        </tr>
        <tr>
        <td>Phone</td>
        <td></td>
        </tr>
        </tbody>
        </table>
        

In markdown-it-py, the table rule reads a physical line, splits its columns and supplies each trimmed cell to inline parsing. Python-Markdown likewise splits its table block into source lines before building the cells. Neither operation leaves the original row's newline inside the contact cell for an inline break rule to consume. 47

Trailing spaces and a backslash belong to a different situation: a line ending within an inline block. CommonMark defines those hard breaks for that context. In our table samples, adding either form before the physical newline still produced another data row. 210

The soft-break option did not reconnect the split cell either. With table support disabled, it inserted breaks into the paragraph containing the pipes. The result still lacked a table. With table support enabled, changing the option left the split-row failure in place. 10

Output from our break-enabled paragraph sample, unedited:

<p>| Service | Contact |<br />
        | --- | --- |<br />
        | Support | Email<br>Phone |</p>
        

Join an accidentally split row, then decide whether the original text needs a deliberate break or can remain ordinary prose. Adding empty lines within a pipe table is a different structural change: GFM ends the table at a blank line. It cannot supply an extra paragraph inside the existing cell. 1

Separate literal tags from missing HTML support

A printed <br> inside a valid table calls for a code-span or HTML-policy diagnosis, not another source newline.

Backticks tell the parser to treat their contents as code. When our contact text and tag shared a code span, the resulting cell contained escaped angle brackets inside a code element. The table itself remained intact. A monospace appearance is a clue, but the code element in the generated HTML identifies this case more precisely. 210

Output from our code-span sample, unedited:

<tr>
        <td>Support</td>
        <td><code>Email&lt;br&gt;Phone</code></td>
        </tr>
        

Removing those backticks cannot override an HTML policy. In the table-enabled markdown-it-py path with HTML disabled, the unquoted tag also became escaped text. This time no code element surrounded it. The HTML inline rule checks the HTML option before recognizing the tag, while the table rule continues to build the grid. 510

Output from our HTML-disabled sample, unedited:

<tr>
        <td>Support</td>
        <td>Email&lt;br&gt;Phone</td>
        </tr>
        
Evidence in the cell What it tells you Action
Escaped tag inside a code element The source code span made the tag literal Place the break outside the code spans
Escaped unquoted tag without a code element The tested HTML-disabled path produced literal text Use the destination's supported content structure, or its documented inline-HTML setting if you control it
Real break in parser output, missing break after delivery A later stage changed the result Compare the delivered cell HTML with the parser fragment to locate that change

If its content policy excludes inline HTML, keep related phrases in a cell with ordinary punctuation or move detailed content below the table. Use separate rows when the content represents separate records. A break tag only separates inline text, so it cannot give a cell independently structured paragraphs or a Markdown list. Pandoc documents grid tables as a different syntax for block content, outside the pipe-table path tested here. 8

If the table came from PDF to Markdown conversion, apply the repair to the extracted source without treating every visual line in the PDF as a new record. Compare the field with the source: a wrapped sentence can stay together, an address may need a meaningful break, and separate records need separate rows.

How we tested

The experiments isolate source-line boundaries, table support, and inline HTML handling in local parser output. They do not test GitHub.com, VS Code, browser layout, accessibility, sanitizers, or PDF/Word exports. “Pandoc GFM” names the local reader, not a live GitHub rendering result. 1011

Corpus, versions and commands

The focused experiment ran on 2026-11-04 with Python 3.11.14, markdown-it-py 4.0.0, Python-Markdown 3.10.2, and Pandoc 3.11. The original upstream renderer matrix remains unchanged. The supplementary corpus and outputs are under lab/renderers/. 10

Input file What it represents
cell-br.md A contact field with an inline <br> on one source row
cell-br-selfclosing.md The same field with <br />
cell-newline.md A physical newline before Phone
cell-two-spaces.md Two trailing spaces before that newline
cell-backslash.md A backslash before that newline
cell-code-literal.md The field and tag enclosed in one code span
cell-code-separated.md Code spans separated by an unquoted break tag

From the repository root, the complete executed command was:

PYTHONDONTWRITEBYTECODE=1 .venv/bin/python runs/2026-11-04-markovo-markdown-line-break-in-table/lab/renderers/table_breaks.py
        

The script writes every input and raw HTML output, records versions and hashes, and counts structure with Python's HTMLParser. It uses these configurations: 11

Report label Exact options or invocation
markdown-it-commonmark MarkdownIt("commonmark", {"html": True, "breaks": False})
markdown-it-breaks Same preset, html=True, breaks=True; no table rule enabled
markdown-it-table Same preset, html=True, breaks=False, then .enable("table")
markdown-it-table-breaks Same table rule, html=True, breaks=True
markdown-it-table-nohtml Same table rule, html=False, breaks=False
python-markdown markdown.markdown(source, extensions=["tables", "fenced_code"])
pandoc-gfm pandoc -f gfm -t html, source supplied on standard input

The renderer dependencies were already available in .venv; no packages or environments were changed. The separate PDF lab environment did not contain these Markdown parsers. No performance timings were recorded.

Structure counts and raw evidence

These are structure counts, not accuracy scores. table_count counts table elements. body_rows counts rows inside the table body, excluding the header. data_cells counts completed data cells. br_in_cells counts actual break elements inside those cells. literal_br_text_cells counts cells whose decoded text contains the literal substring <br. A break outside a table does not count as a cell break. 1011

The tables below are generated from table-break-results.json. T/R/C/B/L means table count / body rows / data cells / breaks in cells / cells containing literal break-tag text.

Renderer, inline-br input T R C B L
markdown-it-commonmark 0 0 0 0 0
markdown-it-breaks 0 0 0 0 0
markdown-it-table 1 1 2 1 0
markdown-it-table-breaks 1 1 2 1 0
markdown-it-table-nohtml 1 1 2 0 1
python-markdown 1 1 2 1 0
pandoc-gfm 1 1 2 1 0
Input markdown-it table+HTML: T/R/C/B/L Python-Markdown tables: T/R/C/B/L Pandoc GFM: T/R/C/B/L
cell-br-selfclosing 1/1/2/1/0 1/1/2/1/0 1/1/2/1/0
cell-newline 1/2/4/0/0 1/2/4/0/0 1/2/4/0/0
cell-two-spaces 1/2/4/0/0 1/2/4/0/0 1/2/4/0/0
cell-backslash 1/2/4/0/0 1/2/4/0/0 1/2/4/0/0
cell-code-literal 1/1/2/0/1 1/1/2/0/1 1/1/2/0/1
cell-code-separated 1/1/2/1/0 1/1/2/1/0 1/1/2/1/0

For the focused inputs, enabling breaks=True alongside the table rule did not change the row or cell-break counts. The strict CommonMark configurations generated paragraphs, not tables. The self-closing tag and separated code spans retained a cell break in the HTML-enabled table paths. 10

Raw output filenames follow <sample>--<renderer>.txt, using the exact input stems and report labels above. For example, cell-newline--markdown-it-table.txt contains the split-row fragment, while cell-br--markdown-it-table-nohtml.txt contains the escaped unquoted tag. HTML is stored as text evidence. The report links each source and output and includes its SHA-256. 10

Source inspection covered markdown_it/rules_block/table.py (getLine, table), markdown_it/rules_inline/html_inline.py (html_inline), and markdown/extensions/tables.py (TableProcessor.run, _build_row). Installed-file hashes and function locations are recorded in source-notes.md; public version-tagged sources support the mechanism explanation. The installed files are identified independently, without asserting byte-for-byte identity with the public tags. 45712

Sources

  1. GitHub Flavored Markdown specification: tables
  2. CommonMark specification: hard line breaks and code spans
  3. markdown-it-py: configuration and rules
  4. markdown-it-py v4.0.0 table source
  5. markdown-it-py v4.0.0 inline HTML source
  6. Python-Markdown tables extension
  7. Python-Markdown 3.10.2 table source
  8. Pandoc manual: pipe and grid tables
  9. GitLab documentation: cells with multiple lines
  10. Cell-break experiment: raw result manifest
  11. Executed experiment and structure counter
  12. Installed-source inspection and hashes