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:
- 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.
- 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.
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
textoutput, so the geometry is clearly available — it's the Markdown structuring step that loses it.Reproduction
Fully self-contained (
pip install liteparse reportlab):Actual output
Two problems:
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 intextmode.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)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.