Expand description
The block model as markdown: headings, lists, tables and a marker where the page changed (#572). The block model as markdown (#568, #569, #570, #571).
The heading classifier next door is only worth what a consumer can read off
it, and what a RAG pipeline reads is markdown: Docling, Marker and every
tool this milestone is measured against publish in it, and the benchmark
harness grades every column through one markdown reader. So a heading that
never reaches a # is a heading nobody downstream has.
DELIBERATELY THE SMALLEST THING THAT IS TRUE. Headings become #..###,
list items become - and 1., tables become a table, paragraphs become
paragraphs, and nothing else claims a syntax it has not earned. The
serializers ticket (#572) owns what grows here – page-break markers, the
other formats, and the golden tests over all of it. What this file owes that
ticket is the mapping and a deterministic byte for byte output, which it can
then extend rather than replace.
WHY A TABLE IS SOMETIMES HTML. A markdown table is a rectangle: every row
the same number of cells, none reaching across two. A table that is not one
has to be written in something that can say colspan, and that is HTML –
which every markdown reader this milestone is measured through passes along
untouched, and which is the rule Docling follows for the same reason.
AND WHY THE FIRST ROW IS THE HEADER ROW EVEN WHERE THE DOCUMENT DID NOT SAY
SO: a markdown table has no shape without a delimiter row, and the row above
it is the header whatever it holds. BlockKind::TableCell carries header
for the tables that DID say so, and the HTML form writes it as th against
td. The markdown form cannot, and says the only thing the format lets it.
WHY A NESTED ITEM IS INDENTED BY ITS PARENT’S MARKER AND NOT BY A CONSTANT.
CommonMark reads a list inside an item only when the nested line starts at
or past the parent item’s own CONTENT column, and that column is as wide as
the parent’s marker: two for - , three for 1. , five for 100. . A
constant wide enough for the widest is a constant that opens an indented
code block under the narrowest, and a constant narrow enough for - drops
out of 100. – which the detector produces, because it accepts three
digits (codex, round 2). So the items still open are kept on a stack with
the column each of them opened, and a child is written at its parent’s.
AND WHY THE LEVEL IS NOT TAKEN AT FACE VALUE. A level is a rank among the indent steps of a REGION, so a page whose outer list the detector missed can open with an item at level 1 and no level 0 above it. Written out indented with nothing to nest into, that line is an indented code block, and every word of it leaves the text layer. So the depth actually written is never more than one past the deepest item still open – which is what the stack answers anyway.
WHY THE TEXT IS ESCAPED AT ALL. A paragraph that opens # , - or <
is prose the document set, and emitting it raw would hand the reader a
heading, a list item or a block of HTML the document does not have – the
same mistake in the other direction from missing one. Only the markers that
start a line are escaped, because those are the only ones the benchmark
reader (and CommonMark) reads at the front of a line.
A FENCE IS THE WORST OF THEM, and it is why this is not a nicety. A
paragraph opening ``` – a code listing, a manual, anything that quotes
markdown – opens a fenced block that nothing here ever closes, and every
heading and paragraph after it on the page is swallowed into it (codex,
round 1). One backslash ends that.
WHAT DOES NOT REACH THE MARKDOWN AT ALL is the furniture: the running head
and the page number a document repeats are marked as such (#571) and are
not written, because standing between two paragraphs is exactly what makes
them a defect for anything that chunks the text. They are still in the
block model, which is where a caller that wants them reads them – or the
caller asks MarkdownOptions::include_headers and
MarkdownOptions::include_footers, and they are written as plain
paragraphs at their reading-order position.
AND WHAT REACHES IT AS ONE PARAGRAPH is the sentence a page break cut in
half: Block::continues_previous says so, and the second half is written
without a break, without an escape – it is not at the start of a line any
more – and with the word the break was made inside of put back together.
WHERE THE PAGE DID CHANGE, an HTML COMMENT says so (#572). A chunker that
cites the document needs the page a chunk came from, and markdown has no
syntax for one. A comment is the only thing CommonMark carries through
untouched and shows to nobody: every reader this milestone is measured
through passes it along, no renderer prints it, and the benchmark’s own
reader strips it before a single axis sees it – so the marker cannot buy
or cost a point on any of them. A caller that wants no marker asks
PageSeparators::None, and the paragraphs stand separated by a blank
line.
THE NUMBER IN IT IS Block::page, which is 0-BASED, and it is the same
number the JSON form carries under page. A reader’s chrome counts pages
from one and this does not: a caller joining the two serialisations of one
document must find the same page in both, and a marker that agreed with
Acrobat’s status bar instead would disagree with our own model.
AND IT NEVER STANDS INSIDE A PARAGRAPH. A sentence the break cut in two is one paragraph (above), and a marker between the halves would cut it again – which is the defect #571 removed, rebuilt out of comments. The marker is therefore DEFERRED and not dropped: the new page is owed one, and the next block on it opens with it. A paragraph whose second half ends the document is the one case that leaves no marker at all, because there is nothing after it to attribute to the new page.
THE SYNTAX IS RESERVED, which is what makes the marker readable at all. A
document that QUOTES an HTML comment – a manual, a style guide, a page of
markdown about markdown – would otherwise put <!-- page 7 --> at the
start of a line as body text, byte for byte what this writes, and a
chunker following the convention would file the rest of the document under
page 7 (codex, round 1). So a < that opens a line is escaped like every
other marker below, and a marker at the start of a line is always ours.
MONOSPACE READS AS CODE (#636). A run of spans the document set in a
monospace face is written `like this` inside a paragraph or a list
item, and a paragraph set wholly in one over two lines or more as a fenced
block with its line breaks kept – the way pymupdf4llm renders what its
ignore_code switch reads. Tables, headings and blocks of tabular figures
never are, and ExtractOptions::code switches the whole of it off.
Structs§
- Markdown
Options - Which blocks reach the Markdown, and how a change of page is marked.
Functions§
- to_
markdown - Every block as markdown, blocks separated by a blank line.
- to_
markdown_ with_ code to_markdown, with the monospace runs as code or as plain text (#636).- to_
markdown_ with_ options to_markdown, with the furniture and the page marker asoptionssays.