Skip to content

Markdown mode: header row emitted as prose when free text is interleaved, shifting the separator #395

Description

@knealfortegra

Summary

With output_format="markdown", a free-text line interleaved near the top of a table causes the header row to be emitted as prose instead of as a table row. The resulting Markdown table has its first data row sitting above the |---| separator, so most Markdown parsers read that data row as the header and drop it as data.

The same rows are correctly column-aligned in the default text output, so the geometry is clearly available — it's the Markdown structuring step that loses it.

Reproduction

Fully self-contained (pip install liteparse reportlab):

from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas

W, H = letter
COLS = [48, 108, 190, 300, 360, 424, 486, 545]
HEADERS = ["Order Date", "Item Code", "Description", "Status", "Unit Cost", "Qty", "Tax", "Ref"]
ROWS = [
    ["03/14/2024", "IC-1048", "WIDGET ASSEMBLY", "Shipped", "$482,110.40", "$0.00", "$9,215.75", "N/A"],
    ["05/02/2024", "IC-1049", "BRACKET SET",     "Shipped", "$0.00",       "$0.00", "$0.00",     "N/A"],
    ["07/19/2024", "IC-1050", "CONTROL MODULE",  "Open",    "$31,905.22",  "$4,100.00", "$0.00", "N/A"],
]

c = canvas.Canvas("description_before_row.pdf", pagesize=letter)
c.setFont("Helvetica-Bold", 12)
c.drawString(48, H - 56, "Order Detail Report by Account")
c.setFont("Helvetica", 8)
y = H - 96
for x, h in zip(COLS, HEADERS):
    c.drawString(x, y, h)
y -= 6
c.line(48, y, 580, y)
y -= 16
for i, row in enumerate(ROWS):
    if i == 0:   # a free-text note between the header and the first data row
        c.drawString(108, y, "Shipment was delayed in transit and re-routed through the hub.")
        y -= 14
    for x, cell in zip(COLS, row):
        c.drawString(x, y, cell)
    y -= 16
c.showPage()
c.save()

from liteparse import LiteParse
r = LiteParse(ocr_enabled=False, quiet=True, output_format="markdown").parse(
    open("description_before_row.pdf", "rb").read()
)
print(r.pages[0].markdown)

Actual output

# Order Detail Report by Account
Order Date Item Code Description Status Unit Cost Qty Tax Ref
Shipment was delayed in transit and re-routed through the hub.
| 03/14/2024 | IC-1048 | WIDGET ASSEMBLY | Shipped | $482,110.40 | $0.00 | $9,215.75 | N/A |
|---|---|---|---|---|---|---|---|
| 05/02/2024 | IC-1049 | BRACKET SET | Shipped | $0.00 | $0.00 | $0.00 | N/A |
| 07/19/2024 | IC-1050 | CONTROL MODULE | Open | $31,905.22 | $4,100.00 | $0.00 | N/A |
---

Two problems:

  1. The header row (Order Date | Item Code | …) is emitted as prose, not as a table row — even though it is perfectly column-aligned in the source and appears correctly aligned in text mode.
  2. The separator sits after the first data row. Rendered as Markdown, 03/14/2024 | IC-1048 | … becomes the header, and that order silently disappears from the data.

There's also a stray trailing --- after the table, which some parsers read as a thematic break and others as an empty separator.

Expected

The header row should be a table row, the separator should follow it, and the interleaved note should be its own paragraph before the table.

On real documents

We've seen the same mechanism take a data row instead of the header: on a report where a description line sat between a group row and its detail rows, the single largest line item was emitted as a prose line while the remaining rows stayed correctly structured — producing a table that looked complete but was missing its biggest entry.

Why it matters

The consumer receives a table that is missing a row while appearing complete. There's no warning and no partial-parse signal. Because the absorbed row's text is still present elsewhere on the page, character-count or checksum-style validation won't necessarily catch it either — the content is all there, just not where the structure claims.

Suggested fix

When a line's text items align to the column grid already established for a table, emit it as a table row even when it's adjacent to a full-width prose line. Interleaved free text would be better emitted as a separate paragraph between table rows than by absorbing a neighbouring row into it.

The stray trailing --- looks like a separate, smaller bug in the same code path.

Environment

  • liteparse==2.10.1 (PyPI)
  • Python 3.13
  • macOS 26.6, arm64

Section-header detection in this mode works well on real documents — ### … headings come out correctly — which is exactly why we'd like to adopt Markdown output.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions