Markdown Processing in Docling#
Overview#
Docling handles Markdown as a declarative format: parsing happens entirely in MarkdownDocumentBackend, which produces a DoclingDocument directly — no layout analysis, no OCR. It is driven by SimplePipeline, which calls backend.convert() and does no further post-processing.
Supported input formats: .md, .txt, .text, .qmd, .rmd (all map to InputFormat.MD) .
Parsing Pipeline: Marko AST → DoclingDocument#
The entry point is MarkdownDocumentBackend.convert(), which:
- Parses the raw Markdown string using Marko (
marko.Markdown().parse()), producing an AST . - Walks the AST via
_iterate_elements(), dispatching on element type to build theDoclingDocument. - Closes any hanging table at the end of the walk .
- Delegates HTML blocks to
HTMLDocumentBackendwhen inline HTML is detected .
Dependency: marko is an optional extra#
The marko package is guarded by a lazy import so that import docling works on installs without it. If Markdown is actually parsed and marko is missing, an ImportError with the install hint is raised at backend init time . Install with:
pip install 'docling[format-markdown]'
Pre-processing#
Before the AST is built, the backend runs two sanitization passes on the raw text :
- Underscore shortening: sequences of
_longer than 10 characters are truncated (overly long sequences cause pathological parse times in Marko). - Leading-dash shortening: excessively long dash-list prefixes are collapsed to a single
-.
Element Mapping#
_iterate_elements() dispatches on Marko AST node types to build DoclingDocument items:
| Marko element | DoclingDocument item |
|---|---|
Heading / SetextHeading (level 1) | doc.add_title() |
Heading / SetextHeading (level 2+) | doc.add_heading(level=n-1) |
List | doc.add_list_group() |
ListItem | doc.add_list_item() |
Image | doc.add_picture() with optional caption |
Emphasis / StrongEmphasis | Formatting(italic=True/bold=True) propagated to children |
Link | hyperlink propagated to children |
RawText / Literal | doc.add_text(label=TEXT) or triggers table row buffering |
CodeSpan | doc.add_code() |
CodeBlock / FencedCode | doc.add_code() with language detection |
HTMLBlock | Buffered and re-processed by HTMLDocumentBackend |
Lazy creation for headings and list items#
Headings and list items are created lazily via a creation_stack. When a Heading or ListItem node is encountered, a _HeadingCreationPayload or _ListItemCreationPayload is pushed onto the stack. The actual DoclingDocument item is only created when the first inline content (e.g. RawText, CodeSpan) is flushed by _flush_creation_stack(). This handles cases where inline nodes other than RawText appear as the sole children of a heading or list item .
Table Parsing#
Markdown pipe-delimited tables are not parsed by Marko's AST. Instead, the backend detects table rows by looking for | characters in RawText nodes and buffers them in self.md_table_buffer .
The buffered rows are flushed by _close_table(), which:
- Treats row 0 as the header row (sets
column_header=True) and skips row 1 (the| --- |separator line). - Creates a
TableCellfor every cell withrow_span=1andcol_span=1. - Calls
doc.add_table(data=TableData(...)).
Key limitation: Row/column spans are hardcoded to 1 — pipe-delimited Markdown tables do not support merged cells .
Known Issues and Limitations#
No span support in parsed tables#
Markdown's pipe-table format has no syntax for rowspan/colspan. Span values are always 1 in _close_table() . If rich span information is needed, use HTML or DOCX input.
Headings in rich table cells (export-side)#
When a DoclingDocument containing headings inside table cells is exported back to Markdown, MarkdownTableSerializer emits #-prefixed heading syntax inside the cell. This is invalid per the Markdown spec and most parsers won't render it correctly . A fix requires overriding MarkdownTextSerializer in docling-core to emit plain text for headings when inside a table cell context.
MarkdownTableSerializer separator missing on chunked table continuations (export-side)#
When a large table is split across chunks by HybridChunker with repeat_table_header=True, subsequent chunks emit the header row but omit the | --- | separator, breaking Markdown table rendering . The root cause is "\n".join(header_lines) in HybridChunker.segment() producing a double newline between the header and separator when lines already carry a trailing \n (from splitlines(True)). The fix is a one-character change: "".join(header_lines) . Tracked in docling-core#672.
Non-repeating page headers classified as furniture (PDF-input only)#
When converting PDF → Markdown, non-repeating headings at the top of a page may be classified as PAGE_HEADER by the layout model and mapped to ContentLayer.FURNITURE, causing them to be omitted from default Markdown export . This is a PDF pipeline issue, not a Markdown backend issue. Workaround: include ContentLayer.FURNITURE in the export layers.
Markdown Export (DoclingDocument → Markdown)#
The reverse direction — serializing a DoclingDocument to Markdown — is handled by MarkdownDocSerializer in docling-core. See the Markdown Export Configuration knowledge base article for full details on MarkdownParams options .
Key export entry points:
Table export from TableItem uses export_to_markdown(doc=...), which delegates to MarkdownDocSerializer when a document context is provided .
Source References#
| File | Purpose |
|---|---|
docling/backend/md_backend.py | MarkdownDocumentBackend — full Markdown parsing pipeline |
docling/pipeline/simple_pipeline.py | SimplePipeline driving all declarative backends |
docling_core/transforms/serializer/markdown.py | MarkdownDocSerializer, MarkdownTableSerializer, MarkdownParams |
docling_core/types/doc/document.py | DoclingDocument, TableItem, TableCell, TableData |