Measured against pandoc 3.11
Every reader and writer pandoc ships was probed with 66 element and feature variants, one at a time. The question each probe answers: can this format tell the rich version apart from the stripped-down one at all - and if it can also be read back, does the AST come home unchanged?
Method
Expressiveness. For each feature a pair of documents is built: one with the feature (a table cell spanning two columns) and one without it (two plain cells). Both go through the writer. If the bytes come out identical, the format has no way to say it. This needs no reader, so it covers all 76 writers.
Round-trip identity. For the 34 formats pandoc can also read, the rich
document is written and read straight back. Exact means the returned AST equals
what went in, after normalizing Plain/Para and stripping the
document wrappers ipynb, epub and fb2 add. Canonical is the softer verdict
beside it: the content came back but the writer restyled it - a default-aligned
column reported as left-aligned, a measured column width where the source gave
none, raw tex for raw latex, a heading id invented by
--auto_identifiers. Counting those as loss overstates pandoc's markdown by eleven probes, so they get their own column instead of being folded either way.
Every writer runs with --wrap=preserve. Without it a wrapping
writer turns a SoftBreak into a space, and the probe reads as "cannot express" for
a distinction the format handles fine - that alone moved 45 writers.
These measurements use released pandoc 3.11 and were refreshed on
2026-10-04. The current comparison with the preceding measurement is in
results/delta.json; dated comparisons are in
results/history.jsonl. Carve source, exchange AST, and export-format
round trips have separate results below, with the bridge and renderer revisions
recorded alongside them.
Expressiveness is the ceiling; exact round-trip is the floor. Between them sits everything that renders correctly but cannot be parsed back - which is most of it.
Headline
Carve is not a pandoc format; it enters through the pandoc-carve bridge, put
through the identical 66 probes. Via the exchange AST it expresses
65 features and returns 65 unchanged;
via .crv source, 64 and
64. These scores describe the bridge boundary. Exporting
through another format adds that format's losses, measured separately below.
html, html4/5 and every HTML-backed slide writer
(revealjs, s5, slidy, slideous, dzslides) hit 63. The epub writers and
chunkedhtml land one behind for different reasons: epub still folds a SoftBreak
into a space, chunkedhtml spends a heading level splitting files. The three
every one of them misses are latex raw inline and raw block (dropped on purpose)
and the 1) vs 1. list delimiter. Nothing else in
pandoc's AST is beyond them.
Ids, classes and key-value pairs are where formats fall apart. HTML and EPUB express 16 and 16 of 16 attribute probes, JATS 14, djot 13, pandoc markdown 12, latex 10, docx 10, typst 9, ODT 7. The attribute grid shows each format and probe.
Pandoc's table type has row groups, a row-head-column count, a foot, and attributes on the table, each row and each cell. Only the HTML family and JATS express all eleven table probes in their own markup - commonmark and markdown_strict also reach eleven, by falling back to a raw HTML block. LaTeX, typst, docx, ODT and asciidoc manage nine; djot and markua six; org five.
It keeps both html and latex raw passthrough - 4/4 raw, matched only by commonmark_x, markdown_mmd and muse. It returns 46 probes exactly; html returns 44, commonmark_x 39, epub 36, and djot 35.
markdown 10, gfm 10, and commonmark_x 10. These probes cover nested maps, booleans, custom keys, and the usual document metadata. The metadata grid gives each key's result.
The pptx writer expresses 40 features; its reader recovers none of them exactly - it is a text extractor with slide headers. rtf writes 40 and round-trips 26, dropping code, notes, blockquotes and every ordered-list style; bullets and nesting do come back.
Carve
Carve has no pandoc reader or writer - it reaches pandoc through
pandoc-carve, which maps
the Pandoc AST to Carve's own AST and back. That is a different code path from a
built-in reader/writer pair, but it answers the same user-facing question, so the identical 66 probes were run through it. Two lanes were measured: through
.crv source (what pandoc-carve report.docx -o report.crv
produces) and through the Carve exchange AST, which skips the source layer.
| format | expressed/66 | exact/66 | meta/10 | inline/16 | block/9 | list/10 | table/11 | attrs/16 | raw/4 |
|---|---|---|---|---|---|---|---|---|---|
| carve-ast | |||||||||
| carve | |||||||||
| html | |||||||||
| markdown | |||||||||
| commonmark_x | |||||||||
| djot | |||||||||
| gfm |
Bridge revision c3d85783623c.
Renderer @markup-carve/carve 0.1.9 (revision 04845b7fc316); measured 2026-09-30T22:21:03Z.
Carve source returns 16 of 16 attribute probes, 4 of 4 raw-content probes, and 16 of 16 inline probes exactly. The grids below show which individual features survive each conversion path.
The same probes give these expressiveness and exactness scores: markdown 59 expressed / 46 exact, commonmark_x 57 / 39, djot 56 / 35, gfm 50 / 24. The Carve source lane expresses 64 features, and the AST lane 65.
42 Carve fixtures · renderer @markup-carve/carve 0.1.9 (revision 04845b7fc316) · pandoc 3.11 · bridge revision c3d85783623c
| conversion path | exact | equivalent | respelled | lossy | unparsable | err | came back readable/42 |
|---|---|---|---|---|---|---|---|
| bridge | - | - | |||||
| bridge-preserve | - | - | |||||
| markdown | - | - | - | ||||
| gfm | - | - | - | ||||
| commonmark_x | - | - | - | ||||
| djot | - | - | - | ||||
| html | - | - | - | ||||
| latex | - | - | |||||
| typst | - | - | |||||
| docx | - | - | |||||
| odt | - | - | - | ||||
| rst | - | - | - | ||||
| org | - | - | - | ||||
| jats | - | - | |||||
| epub | - | - | - |
These verdicts compare rendered content. A dropped comment can count as respelled because it renders to nothing; warnings record that source loss. Each format row covers writing, reading, bridge conversion, and Carve source serialization. An error may occur at any of those steps; the failure details identify the step.
bridge-preserve enables roundtrip: true metadata on the direct bridge path. Export-format rows use the default bridge options.
Table-span and table-rowspan test continuation cells; table-ragged tests a short row that Pandoc pads with an empty cell.
Preservation-mode warnings: table-ragged.
Carve round-trip measurement: 2026-09-30T22:12:39Z; bridge revision c3d85783623c2498370941e09d757b68b34fb839.
The default bridge lane changes 7 of them: comment (respelled), footnote (respelled), frontmatter (equivalent), ordered-list (respelled), quote-container (respelled), table-align (respelled), table-ragged (lossy).
jats/raw-html: pandoc export/import: Command failed: pandoc -f jats -t json
Invalid XML:
Missing root element
latex/table-span: pandocToCarve: renderCarve cannot spell table_row: a table row whose every cell is blank has no Carve source spellingtypst/heading: pandoc export/import: Command failed: pandoc -f typst -t json
(line 1, column 1):
unexpected Elt {eltName = Identifier "heading", eltPos = Just (line 1, column 1), eltFields = fromList [(Identifier "body",VContent (fromList [Elt {eltName = Identifier "document", eltPos = Just (line 1, column 1), eltFields = fromList [(Identifier "body",VContent (fromList [Elt {eltName = Identifier "text", eltPos = Just (line 1, column 1), eltFields = fromList [(Identifier "body",VContent (fromList [Txt "A second-level heading"]))]}])),(Identifier "level",VInteger 2)]}]))]}
typst/table: pandocToCarve: Cannot read properties of undefined (reading 'flatMap')typst/table-align: pandocToCarve: Cannot read properties of undefined (reading 'flatMap')typst/table-ragged: pandocToCarve: Cannot read properties of undefined (reading 'flatMap')typst/table-rowspan: pandocToCarve: Cannot read properties of undefined (reading 'flatMap')typst/table-span: pandocToCarve: Cannot read properties of undefined (reading 'flatMap')The bridge says out loud what it dropped on comment, table-ragged.
widths metadata maps to Pandoc ColWidth, in both
source and AST lanes.(@)) has no Carve form. The list
is emitted as a decimal list, so the resolved numbers survive but the
document-wide counter that makes separate lists renumber each other does not.
Neither lane returns its document-wide counter.pandocToCarveAst, while source merges body boundaries and
reports that flattening. A table foot now crosses through ListTable's
footer-rows.aligns metadata preserves it even without a header row.[@key]: definitions and an undefined key renders verbatim. The probe
carries no bibliography, so that part is the bridge's account, not a measurement
here.Table 1
How many of the 66 features the writer can represent at all, broken out by group. Groups: 16 inline, 9 block, 10 list, 11 table, 16 attribute, 4 raw-passthrough probes. A raw score of 2/4 is normal and correct - a writer should emit raw content aimed at itself and drop raw content aimed at another target. The two highlighted rows are Carve through pandoc-carve, not pandoc built-ins.
| writer | total/66 | inline/16 | block/9 | list/10 | table/11 | attrs/16 | raw/4 | reader? |
|---|---|---|---|---|---|---|---|---|
| json | - | |||||||
| native | yes | |||||||
| xml | - | |||||||
| carve-ast | yes | |||||||
| carve | yes | |||||||
| dzslides | - | |||||||
| html | yes | |||||||
| html4 | - | |||||||
| html5 | - | |||||||
| revealjs | - | |||||||
| s5 | - | |||||||
| slideous | - | |||||||
| slidy | - | |||||||
| chunkedhtml | - | |||||||
| epub | yes | |||||||
| epub2 | - | |||||||
| epub3 | - | |||||||
| markdown | yes | |||||||
| commonmark_x | yes | |||||||
| commonmark | yes | |||||||
| djot | yes | |||||||
| beamer | - | |||||||
| jats | yes | |||||||
| jats_archiving | - | |||||||
| jats_publishing | - | |||||||
| latex | yes | |||||||
| docx | yes | |||||||
| typst | yes | |||||||
| jats_articleauthoring | - | |||||||
| markdown_strict | yes | |||||||
| context | - | |||||||
| mediawiki | yes | |||||||
| gfm | yes | |||||||
| markdown_mmd | yes | |||||||
| odt | yes | |||||||
| opendocument | - | |||||||
| asciidoc | yes | |||||||
| asciidoc_legacy | - | |||||||
| asciidoctor | - | |||||||
| markdown_phpextra | yes | |||||||
| markua | - | |||||||
| ipynb | yes | |||||||
| rst | yes | |||||||
| markdown_github | yes | |||||||
| textile | yes | |||||||
| muse | yes | |||||||
| docbook4 | - | |||||||
| org | yes | |||||||
| docbook | yes | |||||||
| docbook5 | - | |||||||
| icml | - | |||||||
| ms | - | |||||||
| pptx | yes | |||||||
| rtf | yes | |||||||
| tei | - | |||||||
| fb2 | yes | |||||||
| ansi | - | |||||||
| dokuwiki | yes | |||||||
| texinfo | - | |||||||
| xwiki | - | |||||||
| jira | yes | |||||||
| t2t | yes | |||||||
| zimwiki | - | |||||||
| man | yes | |||||||
| haddock | yes | |||||||
| plain | - | |||||||
| bbcode_hubzilla | - | |||||||
| bbcode | - | |||||||
| vimdoc | - | |||||||
| bbcode_xenforo | - | |||||||
| bbcode_steam | - | |||||||
| bbcode_phpbb | - | |||||||
| bbcode_fluxbb | - | |||||||
| opml | yes | |||||||
| biblatex | - | |||||||
| bibtex | - | |||||||
| csljson | - | |||||||
| - |
Table 2
The gap between the writer column and the exact column is the cost of using a format as storage rather than as output. The last column lists features the writer emits correctly but the matching reader cannot recover.
| format | writer/66 | round‑trip/66 | exact/66 | canon./66 | metadata/10 | reader drops |
|---|---|---|---|---|---|---|
| native | - | - | ||||
| carve-ast | - | list_loose | ||||
| carve | - | list_loose | ||||
| markdown | - | |||||
| html | ol_example | |||||
| commonmark_x | - | |||||
| epub | ol_example | |||||
| djot | - | - | ||||
| rst | header_level, list_loose | |||||
| muse | table_colspan, table_rowspan | |||||
| markdown_mmd | - | |||||
| markdown_phpextra | - | |||||
| mediawiki | math_display, ol_style_roman, ol_style_alpha, ol_example, table_attrs, table_attrs_nocaption, table_row_attrs | |||||
| typst | - | - | code_inline_attrs, list_loose, table_colwidth | |||
| gfm | - | |||||
| latex | rawblock_tex, ol_start, ol_style_roman, ol_style_alpha, ol_delim_paren, list_loose, table_colwidth | |||||
| docx | code_inline_attrs, softbreak, span_attrs, codeblock_lang, codeblock_attrs, div_attrs, ol_example, figure_attrs, table_attrs, table_attrs_nocaption | |||||
| odt | smallcaps, code_inline_attrs, math_display, link_title, image_size, ol_start, ol_example, list_loose, table_align, table_cell_align, table_rowheadcols | |||||
| docbook | math_display, div_attrs | |||||
| markdown_github | - | |||||
| asciidoc | - | header_level | ||||
| commonmark | - | - | ||||
| dokuwiki | rawblock_html, rawblock_tex, div_attrs | |||||
| ipynb | - | |||||
| jats | - | span_attrs, link_title, div_attrs, ol_start, ol_delim_paren, table_colwidth, table_attrs, table_attrs_nocaption, table_cell_attrs, table_row_attrs, table_rowheadcols | ||||
| org | ol_delim_paren | |||||
| textile | image_size, rawblock_tex | |||||
| markdown_strict | - | - | ||||
| jira | math_display | |||||
| man | table_colwidth | |||||
| rtf | code_inline, note, blockquote, codeblock, orderedlist, ol_start, ol_style_roman, ol_style_alpha, ol_delim_paren, list_loose, table_colspan, table_rowspan, table_align, table_colwidth | |||||
| fb2 | - | table_head, table_foot, table_colspan, table_rowspan, table_align, table_block_cell | ||||
| haddock | list_loose, table_colspan, table_colwidth | |||||
| t2t | - | rawblock_tex, div_attrs | ||||
| opml | - | - | ||||
| pptx | emph, strong, underline, strikeout, superscript, subscript, smallcaps, code_inline, math_display, linebreak, link, blockquote, codeblock, lineblock, bulletlist, orderedlist, ol_start, ol_style_roman, ol_style_alpha, ol_delim_paren, deflist, table_colspan, table_rowspan, table_align, table_colwidth, table_block_cell |
ipynb's round-trip column is not meaningful: its writer stamps a fresh UUID on every cell and the reader hands it back as a Div id, so two conversions of the same document differ. Its exact column is measured after that wrapper is stripped and is trustworthy.
Table 3
Sorted by how rarely a feature makes it through a round-trip intact. Nothing is universal - opml's reader rejects almost every probe - but emphasis returns exactly in 32 of the 34 readers and bullet lists in 31, where example lists and cell alignment manage one each and citations two.
| element / feature | group | writers/73 | exact round‑trip/33 | carve | carve‑ast |
|---|---|---|---|---|---|
| ol_example | list | ||||
| table_cell_align | attrs | ||||
| cite | block | ||||
| figure_attrs | attrs | ||||
| table_multibody | table | ||||
| table_row_attrs | attrs | ||||
| table_rowheadcols | table | ||||
| codeblock_attrs | attrs | ||||
| table_attrs_nocaption | attrs | ||||
| table_attrs | attrs | ||||
| table_foot | table | ||||
| rawblock_tex | raw | ||||
| table_cell_attrs | attrs | ||||
| code_inline_attrs | attrs | ||||
| table_colwidth | table | ||||
| span_attrs | attrs | ||||
| div_attrs | attrs | ||||
| table_rowspan | table | ||||
| table_block_cell | table | ||||
| header_attrs | attrs | ||||
| rawinline_html | raw | ||||
| link_attrs | attrs | ||||
| image_size | attrs | ||||
| table_colspan | table | ||||
| table_caption | table | ||||
| rawinline_tex | raw | ||||
| quoted_single | inline | ||||
| ol_style_roman | list | ||||
| lineblock | block | ||||
| quoted_double | inline | ||||
| rawblock_html | raw | ||||
| smallcaps | inline | ||||
| figure | block | ||||
| ol_style_alpha | list | ||||
| header_level | block | ||||
| ol_delim_paren | list | ||||
| ol_start | list | ||||
| table_align | table | ||||
| header | block | ||||
| orderedlist | list | ||||
| link_title | attrs | ||||
| image_title | attrs | ||||
| math_display | inline | ||||
| codeblock_lang | attrs | ||||
| underline | inline | ||||
| math_inline | inline | ||||
| table | table | ||||
| image | inline | ||||
| deflist | list | ||||
| table_head | table | ||||
| note | block | ||||
| subscript | inline | ||||
| superscript | inline | ||||
| strikeout | inline | ||||
| hrule | block | ||||
| softbreak | inline | ||||
| codeblock | block | ||||
| linebreak | inline | ||||
| code_inline | inline | ||||
| blockquote | block | ||||
| link | inline | ||||
| list_nested | list | ||||
| list_loose | list | ||||
| bulletlist | list | ||||
| emph | inline | ||||
| strong | inline |
Table 4
A standalone document carrying ten metadata values, written and read back. Filled dot = identical, half dot = present but reshaped, hollow = gone.
| meta key | commonmark_x | gfm | markdown | native | carve | carve-ast | markdown_mmd | muse | docbook | docx | fb2 | jats | latex | html | org | epub | asciidoc | man | opml | commonmark | djot | dokuwiki | haddock | ipynb | jira | markdown_github | markdown_phpextra | markdown_strict | mediawiki | odt | pptx | rst | rtf | t2t | textile |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| title | |||||||||||||||||||||||||||||||||||
| subtitle | |||||||||||||||||||||||||||||||||||
| author | |||||||||||||||||||||||||||||||||||
| date | |||||||||||||||||||||||||||||||||||
| abstract | |||||||||||||||||||||||||||||||||||
| keywords | |||||||||||||||||||||||||||||||||||
| lang | |||||||||||||||||||||||||||||||||||
| custom | |||||||||||||||||||||||||||||||||||
| nested | |||||||||||||||||||||||||||||||||||
| flag |
Reading the result
Need everything preserved. json, native and
xml are the AST itself and score 66/66 by construction. For anything
that has to render, html5 and epub3 are the practical
maximum.
Need it back later. carve through pandoc-carve returns 64 probes
exactly (65 via its AST); markdown returns
46 and commonmark_x 39.
For metadata, scores out of ten are: markdown 10 and
Carve source 10.
Print and typesetting. latex 54 and beamer 55 lead,
typst 53 is close and loses mostly attributes, context 50
has the better table story but no raw passthrough at all.
Word processing. docx expresses 53 probes and
odt 49. They express 10 and
7 attribute probes respectively. icml keeps layout but
almost no attributes.
Structured publishing. jats 54 is the strongest XML target and
the only non-HTML writer with the complete table model; docbook and tei trail at
42 and 40.
Importing. The best sources are html, docx, latex, jats, org, rst, mediawiki and markdown. Avoid pptx and rtf as inputs when structure matters. Twelve input formats - bits, creole, csv, endnotexml, mdoc, pod, ris, tikiwiki, tsv, twiki, vimwiki, xlsx - have no matching writer and are not measured here.
Not document writers at all. csljson, bibtex and
biblatex serialize the references metadata and emit nothing
for body content; opml keeps only the heading outline. Their low scores
describe their purpose, not a defect.
Limits
--wrap=auto
almost every text writer folds SoftBreak into a space. With
--wrap=preserve html, markdown, latex, djot and typst all distinguish
it. Treat that one row as noise.RawBlock "latex" is target routing working as designed, not a coverage
gap. Only formats that host both (markdown, commonmark_x, markdown_mmd, muse)
reach 4/4.--columns, no custom
templates. Turning on markdown_strict+raw_html or similar moves these
numbers.carve and
carve-ast rows come from pandoc-carve's own converters, not from a
pandoc reader/writer pair. Same probes, same two questions - but a library
written against the Pandoc AST has an easier job than a writer constrained by an
existing file format's syntax. Read the comparison as "what a user gets", not as
a like-for-like implementation contest.core.xml, app.xml, meta.xml. On the path the
writer lane actually uses - pandoc's server with standalone off - all
75 checkable writers re-emit byte-identical output for identical input, pdf excepted
because it errors; make controls reruns that check. Standalone output is
not reproducible in the same way: a CLI epub embeds a
random urn:uuid in content.opf and toc.ncx,
and ipynb a random cell id.Reproduce
git clone https://github.com/markup-carve/pandoc-format-fidelity cd pandoc-format-fidelity make # fetch pinned pandoc, run every lane, build the report make carve CARVE_BRIDGE=../pandoc-carve/dist/index.js # optional Carve lanes
The probe set lives in src/probes.py: each entry is a pair
of pandoc AST block lists differing in exactly one feature. Writer conversions go
through pandoc's own HTTP server for speed; the server has no filesystem access, so any media a probe references is handed over in the request; the CLI lanes get the same file through --resource-path. Both had to be fixed - an image pandoc cannot resolve makes epub leave the src alone and round-trip to a false exact.