Skip to content

fix(tables): read tables ruled with hairline fills, with row-group labels and merged cell runs - #512

Open
yilmazhasan wants to merge 3 commits into
firecrawl:mainfrom
yilmazhasan:bug-fix/table-conversion
Open

yilmazhasan wants to merge 3 commits into
firecrawl:mainfrom
yilmazhasan:bug-fix/table-conversion

Conversation

@yilmazhasan

@yilmazhasan yilmazhasan commented Sep 8, 2026 •

Copy link
Copy Markdown

Fixes #353

Some generators draw every table rule as a thin filled rectangle instead of a stroke. Those slivers are too small for rect clustering and never reach line-based detection, so the table falls to the heuristic pass and its rows and columns come out broken.

Changes

  • Hairline fills → rules (synthesize_lines_from_thin_rects): thin fills become line segments for the line-based pass, with per-cell slivers welded into rules. It only returns lines when they form a grid (≥3 horizontal and ≥2 vertical rules of table length). Stray underlines can't fabricate a table, and horizontal-only layouts keep their per-cell segment endpoints for the existing thin-rect fallback.
  • Page rules aren't row edges: horizontals more than one row pitch outside the vertical rules' span (running-header and footer rules, note boxes) are ignored. Rules just past the span, like a header rule above partial-height verticals, still count.
  • Runs crossing a column rule are cut: when a drawn vertical rule crosses a run's baseline, the run is cut at the nearest word space. This handles two cells painted so close that extraction merges them ("…corpuscular MCHC"). The cut is limited to the line-based path and to drawn rules. Fragments map back to their item index once.
  • Rotated row-group labels: a rotated label that sits wholly inside the grid goes into the merged cell its column's rules describe. Labels that cross a ruled interior row edge are rejected.
  • Row-group entries stay separate rows: in a table whose first column is mostly empty, a row that fills the same columns as the entry above is not merged on either continuation path. Wrapped tails still merge.

Review feedback addressed

  • The old double-space split in assign_items_to_grid was removed. It depended on an extraction quirk that fix(extractor): keep word spaces painted as squeezed whitespace runs #517 removed, and it also applied to inferred column edges. It's replaced by the rule-crossing cut above, so detect_rects.rs is unchanged.
  • Duplicate item_indices from split runs are fixed: fragments are deduplicated back to one original index.
  • Weld clustering: segments are grouped by coordinate first, then welded in axis order, so near-collinear runs can't interleave.
  • Row-group heuristic: the empty-first-cell count skips blank and footnote rows, the lowercase check looks only at the entry label (lowercase values like yes are fine), and the guard covers is_wrapped_continuation too.
  • Tests: fixtures renamed to describe what they cover. There is one test_snapshot_* per fixture with accurate doc comments, the order assertion compares positions, and the redundant prefix check is gone. Unit tests were added for the row-group heuristic (positive and wrapped-tail negative), the run split, synthesis welding, the grid gate, and the weld.

Second round:

  • Ruled-span slack uses the median rule gap, not the largest, so one tall merged row can't re-admit page furniture.
  • The rule-crossing test uses the run's baseline instead of its box midpoint, and only applies to level runs. Oblique runs are left whole.
  • Row-group detection also covers a three-row group with two blank first cells. Larger tables keep the 3/4 ratio, because a 2/3 ratio also matched nexo-price-en, whose blank-first-cell rows are wrapped tails.
  • Two-column row-group entries stay separate rows: one filled entry cell is enough, and lowercase wrapped tails still merge.

Not changed: the trailing 1-3 chapter-page footer in the second snapshot. Stripping N-N lines is a separate postprocess change, and a standalone range like that is ambiguous, so I'd rather not fold it into this PR.

Verification

  • cargo fmt, cargo clippy -- -D warnings and cargo test pass.
  • Output on all 46 fixture PDFs was compared against main. Besides the two new fixtures, only td9264 changes: in its three-column cross-reference table (section / remove / add), four references that were merged into one cell now get one row each, as they appear in the PDF. That table's other two columns are still lost, as on main.
  • pdf-evals (bench.py test) has not been run yet; it isn't available in my environment.

🤖 Generated with Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files

Confidence score: 3/5

  • In src/tables/detect_rects.rs, a split run can add the same original index more than once to a cell, so callers may see duplicate items. Keep item_indices unique per cell.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/tables/detect_rects.rs">

<violation number="1" location="src/tables/detect_rects.rs:1862">
P2: A split run now contributes its original index `idx` once per fragment that lands in a cell, so `item_indices` can hold the same index multiple times. `assign_items_to_grid`'s contract says it returns "the original item indices that were consumed", and the rect-path callers (e.g. lines 1679, 2321, 3316, 3629) build `Table::new(..., item_indices)` straight from this list without dedup, unlike the detect_lines callers which dedup at lines 1037-1038. Duplicate entries inflate `item_indices.len()`, which is used as positive evidence in `table_evidence_score` (detect_lines.rs:1086, `item_indices.len() * 100`) and as a tie-breaker (line 1097), so one split run can over-count a table's claimed items. Push `idx` into `indices` only once per split run (e.g. track whether any fragment of the run was placed, then push once).</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.
Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread src/tables/detect_lines.rs
Comment thread src/tables/detect_lines.rs
Comment thread src/tables/detect_lines.rs
Comment thread src/tables/format.rs Outdated
Comment thread src/tables/format.rs
Comment thread src/tables/detect_rects.rs Outdated
Comment thread src/tables/detect_rects.rs Outdated
Comment thread src/tables/detect_rects.rs Outdated
Comment thread src/tables/format.rs Outdated
Comment thread src/tables/format.rs
@yilmazhasan yilmazhasan mentioned this pull request Sep 10, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Confidence score: 5/5

  • The comments in tests/integration_tests.rs say these tests check table headers, but they assert data rows instead. Update the comments to match what the tests cover.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/integration_tests.rs">

<violation number="1" location="tests/integration_tests.rs:1360">
P3: The doc comment on `test_custom_table_first_row` and `test_body_before_table` says "Whether the table header is rendered correctly", but neither tests a table header: the first asserts a data row and the second asserts a body paragraph before the table. This was copy-pasted from `test_custom_table_header` and misleads readers about what each test guards. Give each test a doc comment that matches what it asserts.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.

Fix all with cubic | Re-trigger cubic

Comment thread tests/snapshots/hairline_fill_table_page_rules.md
Comment thread tests/integration_tests.rs Outdated
Comment thread tests/integration_tests.rs Outdated
@yilmazhasan
yilmazhasan marked this pull request as draft September 12, 2026 10:31
@yilmazhasan
yilmazhasan marked this pull request as ready for review October 15, 2026 13:40

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment thread src/tables/detect_lines.rs
Comment thread src/tables/detect_lines.rs
Comment thread src/tables/format.rs
Comment thread src/tables/detect_rects.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files

Shadow auto-approve: would not auto-approve because issues were found.
Reply to a comment to ask cubic a question or push back. It learns from your replies.
Click Fix with cubic on a comment, or reply @cubic-dev-ai fix this, and cubic writes the fix.

View guided diff | Re-trigger cubic

Comment thread src/tables/detect_rects.rs Outdated
Comment thread src/tables/detect_lines.rs Outdated
Comment thread src/tables/detect_lines.rs
Comment thread src/tables/detect_lines.rs
Comment thread src/tables/detect_rects.rs Outdated
Comment thread tests/integration_tests.rs Outdated
…bels and merged cell runs

Some generators draw every table rule as a thin filled rectangle instead
of a stroke. Those slivers are too small for rect clustering and never
reach line-based detection, so the grid is left to the heuristic pass and
rows and columns come out broken (firecrawl#353).

- Synthesize line segments from hairline fills for the line-based pass,
  welding per-cell slivers into rules. Only a real grid (>=3 horizontal
  and >=2 vertical rules of table length) is returned, so stray
  underlines cannot fabricate a table and horizontal-only layouts keep
  their per-cell segments for the later thin-rect fallback.
- Ignore horizontal rules more than one row pitch outside the vertical
  rules' span: running-header and footer rules no longer stretch the
  table over the prose above it.
- Cut a text run where a drawn column rule crosses its baseline, at the
  nearest word space, so two cells painted close enough to merge into one
  run land in their own columns. Fragments map back to their original
  item index once.
- Place a rotated row-group label wholly inside the grid into the merged
  cell its column's rules describe, rejecting labels that cross a ruled
  interior row edge.
- Keep row-group entries (empty first column, same filled columns as the
  entry above) from merging as continuations on both continuation paths,
  while wrapped tails still merge.

Fixes firecrawl#353

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yilmazhasan
yilmazhasan force-pushed the bug-fix/table-conversion branch from 0b3b90e to dde9f03 Compare October 16, 2026 08:49
@yilmazhasan yilmazhasan changed the title Fix markdown table issue fix(tables): read tables ruled with hairline fills, with row-group labels and merged cell runs Oct 16, 2026
@yilmazhasan
yilmazhasan marked this pull request as draft October 21, 2026 11:48
@yilmazhasan
yilmazhasan marked this pull request as ready for review October 21, 2026 11:48

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files

Shadow auto-approve: would not auto-approve because issues were found.

View guided diff | Re-trigger cubic

Comment thread src/tables/detect_lines.rs Outdated
Comment thread src/tables/detect_lines.rs Outdated
Comment thread src/tables/format.rs Outdated
Comment thread src/tables/format.rs Outdated
- Widen the ruled span by the median rule gap rather than the largest, so
  one tall merged row cannot re-admit page furniture rules.
- Test a column rule against the run's baseline instead of its box
  midpoint, so rules ending at the baseline still cut merged cell runs.
- Recognize a three-row group (two blank first cells of three) as a
  row-group label column; larger tables keep the 3/4 ratio, since a
  label column broken up by wrapped tails sits near 2/3.
- Keep two-column row-group entries apart: a single filled entry cell is
  enough, lowercase wrapped tails still merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.

View guided diff | Re-trigger cubic

Comment thread tests/snapshots/td9264.md
Comment thread src/tables/detect_lines.rs
`is_upright()` admits runs up to 45° off horizontal, but an oblique run's
box edge only approximates its baseline and its characters do not advance
along +x, so a vertical rule could cut it at an unrelated word space. Skip
any run that is not level before comparing the rule with the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would require human review. Hairline-fill rule synthesis, row-group label placement, run splitting, and row-merge heuristics with snapshots/tests. Unresolved dismissed findings on rotated-label capture, endpoint preservation, duplicate indices, and a stray footer require human sign-off.

View guided diff | Re-trigger cubic

This branch has not been deployed

No deployments
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.

Table conversion

1 participant