Skip to main content

Module markdown

Module markdown 

Source
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§

MarkdownOptions
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 as options says.