Measured against pandoc 3.11

Which pandoc format actually keeps your document

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?

76output formats
51input formats
66element probes
34round‑trippable
3lossless writers

Method

Two questions, two different answers

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

Seven things the matrix says

65 / 64

Carve at the bridge boundary

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.

63/66

HTML is the ceiling for real formats

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.

16 / 16

Attributes are the real divider

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.

2

Only HTML and JATS do the whole table model

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.

46/66

Pandoc Markdown as interchange text

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.

10

Metadata scores out of ten

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.

0

pptx and rtf are one-way doors

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

Where Carve lands

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.

formatexpressed/66exact/66meta/10inline/16block/9list/10table/11attrs/16raw/4
carve-ast656510169911164
carve646410169910164
html63443.5169911162
markdown59461016999124
commonmark_x57391016898124
djot5635016896134
gfm5024101687892

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.

Carve source round trips

42 Carve fixtures · renderer @markup-carve/carve 0.1.9 (revision 04845b7fc316) · pandoc 3.11 · bridge revision c3d85783623c

conversion pathexactequivalentrespelledlossyunparsableerrcame back readable/42
bridge35151--41
bridge-preserve36141--41
markdown29-67--35
gfm16-1016--26
commonmark_x26-79--33
djot23-712--30
html31-65--37
latex14-621-120
typst11-619-617
docx131523--19
odt12-525--17
rst18-618--24
org16-719--23
jats10-328-113
epub27-510--32

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).

Failures by step (including bridge failures)
  • 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 spelling
  • typst/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.

What Carve does not carry, and why

  • Column widths now cross exactly. Carve's positional widths metadata maps to Pandoc ColWidth, in both source and AST lanes.
  • Example-list numbering ((@)) 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.
  • Multiple row groups remain source-only loss. They survive whole through pandocToCarveAst, while source merges body boundaries and reports that flattening. A table foot now crosses through ListTable's footer-rows.
  • Column alignment now has a column-level spelling. Positional aligns metadata preserves it even without a header row.
  • Citations come back as citations, without their definitions. Quoted text, citations and block cells used to change AST shape on the way back and now return unchanged. What a citation still cannot carry is the bibliography - the bridge's own warning says entries live in pandoc metadata, so it emits no [@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

Writer expressiveness, all 76 output formats

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.

writertotal/66inline/16block/9list/10table/11attrs/16raw/4reader?
json661691011164-
native661691011164yes
xml661691011164-
carve-ast65169911164yes
carve64169910164yes
dzslides63169911162-
html63169911162yes
html463169911162-
html563169911162-
revealjs63169911162-
s563169911162-
slideous63169911162-
slidy63169911162-
chunkedhtml62168911162-
epub62159911162yes
epub262159911162-
epub362159911162-
markdown5916999124yes
commonmark_x5716898124yes
commonmark56158711132yes
djot5616896134yes
beamer55168109102-
jats54165811140yes
jats_archiving54165811140-
jats_publishing54165811140-
latex5416899102yes
docx53168109100yes
typst5316910990yes
jats_articleauthoring52165611140-
markdown_strict52158511112yes
context5116791090-
mediawiki511588992yes
gfm501687892yes
markdown_mmd501685894yes
odt4915810970yes
opendocument4915810970-
asciidoc481688970yes
asciidoc_legacy481688970-
asciidoctor481688970-
markdown_phpextra4815858102yes
markua4816796100-
ipynb471685882yes
rst471589843yes
markdown_github461585882yes
textile461388773yes
muse451587834yes
docbook4441568762-
org441597553yes
docbook431568761yes
docbook5431568761-
icml421588920-
ms411588730-
pptx401568920yes
rtf401489900yes
tei401577650-
fb2391669800yes
ansi381388720-
dokuwiki381584632yes
texinfo381587602-
xwiki381384472-
jira371664470yes
t2t371574623yes
zimwiki371574632-
man361478700yes
haddock351367900yes
plain351059920-
bbcode_hubzilla341287520-
bbcode331187520-
vimdoc33869910-
bbcode_xenforo321284530-
bbcode_steam301284510-
bbcode_phpbb291187210-
bbcode_fluxbb261185200-
opml1010000yes
biblatex0000000-
bibtex0000000-
csljson0000000-
pdf0000000-

Table 2

What comes back, for the 34 formats pandoc also reads

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.

formatwriter/66round‑trip/66exact/66canon./66metadata/10reader drops
native666666-10-
carve-ast656465-10list_loose
carve646364-10list_loose
markdown5959461110-
html63624433.5ol_example
commonmark_x575739210-
epub62613622.5ol_example
djot565635-0-
rst47453310header_level, list_loose
muse45433234.5table_colspan, table_rowspan
markdown_mmd50502836.5-
markdown_phpextra48482610-
mediawiki51442530math_display, ol_style_roman, ol_style_alpha, ol_example, table_attrs, table_attrs_nocaption, table_row_attrs
typst534825--code_inline_attrs, list_loose, table_colwidth
gfm505024210-
latex54472474rawblock_tex, ol_start, ol_style_roman, ol_style_alpha, ol_delim_paren, list_loose, table_colwidth
docx53432394code_inline_attrs, softbreak, span_attrs, codeblock_lang, codeblock_attrs, div_attrs, ol_example, figure_attrs, table_attrs, table_attrs_nocaption
odt49382330smallcaps, code_inline_attrs, math_display, link_title, image_size, ol_start, ol_example, list_loose, table_align, table_cell_align, table_rowheadcols
docbook43412254math_display, div_attrs
markdown_github46462230-
asciidoc484721-2header_level
commonmark565621-0-
dokuwiki38352110rawblock_html, rawblock_tex, div_attrs
ipynb47662130-
jats544321-4span_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
org44432133.5ol_delim_paren
textile46442120image_size, rawblock_tex
markdown_strict525218-0-
jira37361710math_display
man36351552table_colwidth
rtf40261510code_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
fb2393313-4table_head, table_foot, table_colspan, table_rowspan, table_align, table_block_cell
haddock35321310list_loose, table_colspan, table_colwidth
t2t373513-0rawblock_tex, div_attrs
opml100-2-
pptx4014010emph, 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

Which elements are hard

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 / featuregroupwriters/73exact round‑trip/33carvecarve‑ast
ol_examplelist250
table_cell_alignattrs280
citeblock161
figure_attrsattrs411
table_multibodytable192
table_row_attrsattrs192
table_rowheadcolstable212
codeblock_attrsattrs252
table_attrs_nocaptionattrs252
table_attrsattrs342
table_foottable662
rawblock_texraw153
table_cell_attrsattrs193
code_inline_attrsattrs293
table_colwidthtable433
span_attrsattrs454
div_attrsattrs464
table_rowspantable464
table_block_celltable664
header_attrsattrs195
rawinline_htmlraw295
link_attrsattrs355
image_sizeattrs425
table_colspantable475
table_captiontable655
rawinline_texraw106
quoted_singleinline686
ol_style_romanlist497
lineblockblock587
quoted_doubleinline687
rawblock_htmlraw328
smallcapsinline508
figureblock648
ol_style_alphalist519
header_levelblock549
ol_delim_parenlist2810
ol_startlist5210
table_aligntable5410
headerblock6810
orderedlistlist6810
link_titleattrs3011
image_titleattrs4611
math_displayinline5413
codeblock_langattrs5714
underlineinline6614
math_inlineinline6815
tabletable6816
imageinline6817
deflistlist6818
table_headtable6619
noteblock6820
subscriptinline6021
superscriptinline6021
strikeoutinline6721
hruleblock6022
softbreakinline4823
codeblockblock6824
linebreakinline6824
code_inlineinline6428
blockquoteblock6628
linkinline6628
list_nestedlist6829
list_looselist4230
bulletlistlist6830
emphinline6631
stronginline6631

Table 4

Document metadata

A standalone document carrying ten metadata values, written and read back. Filled dot = identical, half dot = present but reshaped, hollow = gone.

exact partial lost
meta keycommonmark_xgfmmarkdownnativecarvecarve-astmarkdown_mmdmusedocbookdocxfb2jatslatexhtmlorgepubasciidocmanopmlcommonmarkdjotdokuwikihaddockipynbjiramarkdown_githubmarkdown_phpextramarkdown_strictmediawikiodtpptxrstrtft2ttextile
title
subtitle
author
date
abstract
keywords
lang
custom
nested
flag

Reading the result

Picking a target

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

What these numbers do not say

Reproduce

How it was run

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.