Skip to content

fix(serializer): derive markdown table header rows from column_header flags - #723

Open
acharyaanusha wants to merge 3 commits into
docling-project:mainfrom
acharyaanusha:fix/markdown-table-column-header
Open

fix(serializer): derive markdown table header rows from column_header flags#723
acharyaanusha wants to merge 3 commits into
docling-project:mainfrom
acharyaanusha:fix/markdown-table-column-header

Conversation

@acharyaanusha

Copy link
Copy Markdown

Fixes #722.

MarkdownTableSerializer hardcoded grid row 0 as the only header row, while TableItem._export_to_dataframe_with_options, the HTML serializer and export_to_otsl all derive the header rows from TableCell.column_header. A table with a stacked header serialized to Markdown with its lower header rows demoted to data rows, and HybridChunker repeated only the first of them atop each chunk of an oversized table.

This takes the header row count from the flags and flattens those rows into the single header row GFM allows, dropping the repeated text a cell spanning several header rows produces (so human / human stays human, not human human).

When no cell in the table carries column_header, the previous behavior is kept. Backends that never set the flag cannot be told apart from genuinely headerless tables, and several *.dt.json fixtures round-trip with zero flags. Making it unconditional instead changed output for those too, so I left it — see the first open question in #722 if you would rather it be strict.

Ground truth

Five ground-truth files are regenerated. Every diff recovers real column names from the body:

  • 2206.01062.yaml.md / .paged.md (DocLayNet) — class label | Count | % of Total Train | ... was a body row under a header of % of Total repeats; likewise MRCNN R50 / MRCNN R101, Class-count Split, Testing on PLN
  • 2408.09869v3_enriched_p1_mark_meta_{true,false}.gt.mdnative backend TTS | native backend Pages/s | native backend Mem
  • 0b_out_chunks.json / 0d_out_chunks.json — the same headers as repeated in chunks

Numeric columns shift to right-alignment in a few places, because _column_type no longer sees the former header row when detecting the column type.

Verification

  • Full docling-core suite: no new failures, +3 from the tests added here. My environment has pre-existing unrelated failures (missing cv2 stubs, doclang schema resources, and a Pillow that re-encodes the fixture PNGs), so I diffed the failure sets before and after rather than trusting the totals; the sets are identical.
  • ruff format, ruff check, and mypy clean on the changed files.
  • End-to-end on a live conversion: converted arXiv 2408.09869 with docling 2.120.2 and diffed the full 248-line Markdown export before and after. Exactly one table changes; the two single-header tables in the same document are byte-identical.

Tests

Three cases in test/test_serialization.py: a stacked header flattened, repeated span text deduplicated, and a table with no header flags keeping its first row as the header.

@github-actions

Copy link
Copy Markdown
Contributor

DCO Check Passed

Thanks @acharyaanusha, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 2 protections blocking · waiting on 👀 reviews

Protection Waiting on
🔴 Require two reviewer for test updates 👀 reviews
🟢 Enforce conventional commit

🔴 Require two reviewer for test updates

Waiting for

  • #approved-reviews-by >= 2
This rule is failing.

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docling_core/transforms/serializer/markdown.py 96.15% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

… flags

MarkdownTableSerializer hardcoded grid row 0 as the only header row, while
TableItem._export_to_dataframe_with_options, the HTML serializer and the OTSL
export all derive the header rows from TableCell.column_header. A table with a
stacked header therefore serialized to markdown with its lower header rows
demoted to data rows, and HybridChunker repeated only the first of them at the
top of each chunk of an oversized table.

Take the header row count from the flags and flatten those rows into the single
header row GFM allows, dropping the repeated text a cell spanning several header
rows produces. When no cell in the table carries column_header the previous
behavior is kept, since backends that never set the flag cannot be told apart
from genuinely headerless tables.

Signed-off-by: Anusha Acharya <acharyaanusha123@gmail.com>
The empty-header-block branch of _flatten_header_rows was the one line of the
patch left uncovered. Pin the behavior it produces: with no leading run of
flagged rows there is no header to promote, so the header is empty and every
row stays in the body rather than one being consumed.

Signed-off-by: Anusha Acharya <acharyaanusha123@gmail.com>
Review feedback: a blank space loses the boundary between header levels, so
"native backend" over "TTS" is indistinguishable from a single cell reading
"native backend TTS". Join with " - " instead, kept as a module constant, and
document the header-resolution contract on MarkdownTableSerializer.

Signed-off-by: Anusha Acharya <acharyaanusha123@gmail.com>
@acharyaanusha
acharyaanusha force-pushed the fix/markdown-table-column-header branch from 4327e8a to 0158bf1 Compare August 19, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Markdown table serializer ignores TableCell.column_header, demoting stacked header rows to data

1 participant