docling-project / docling-project/docling-core
Markdown serializer: header block from column_header flags swallows the first data row of pivot tables (HTML <th rowspan> row headers)
- Dominant language
- HTML
- Stars
- 282
- Forks
- 214
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 21
Description
Summary
Since [docling-project/docling-core#723](https://github.com/docling-project/docling-core/pull/723) (v2.96.0) MarkdownTableSerializer derives the header block from column_header flags: every leading row on which a flagged cell starts is a header row, and stacked header rows are flattened per column with " - ".
Documents from the docling HTML backend flag every `` as column_header, including row headers (` `in body rows; row_header stays False). For a pivot table the first body row therefore has a flagged cell starting on it, the header block grows to two rows, and the first data row is folded into the header.
Reproduction
tests/data/html/groundtruth/example_08.html.json from the docling repo, "Pivot table with 1 row header"):
```
…
YearMonthRevenueCost
2025January$134$162
February$150$155
```
Grid flags (row 1 holds 2025 with column_header=True, start_row_offset_idx=1, row_span=5):
```
0 [('Year', True), ('Month', True), ('Revenue', True), ('Cost', True)]
1 [('2025', True), ('January', False), ('$134', False), ('$162', False)]
2 [('2025', True), ('February', False), ...]
```
`DoclingDocument.load_from_json(...).export_to_markdown()` with docling-core 2.96.0:
```
| Year - 2025 | Month - January | Revenue - $134 | Cost - $162 |
|---------------|-------------------|------------------|---------------|
| 2025 | February | $150 | $155 |
| 2025 | March | $160 | $143 |
```
Expected (2.95 output, and what the table means):
```
| Year | Month | Revenue | Cost |
|--------|----------|-----------|--------|
| 2025 | January | $134 | $162 |
| 2025 | February | $150 | $155 |
```
TableItem.export_to_dataframe() ([docling-project/docling-core#756](https://github.com/docling-project/docling-core/pull/756)) uses the same header count and yields the column name Year.2025.
**Possible fixes**
Serializer (_count_header_rows): stop the header block at a row on which a non-header cell with text also starts. A row mixing header and data cells is a data row with a row header. Rows made only of header cells (and empty corner cells) keep the current behaviour, so genuine stacked headers such as % of Total - Train are unaffected.
HTML backend (docling): flag cells in body rows or with scope="row" as row_header instead of column_header.
Contributor guide
Assessment
This issue has not been assessed yet.