Skip to content

feat(extractor): stamp items with the font family name, not the resource tag - #415

Merged
abimaelmartell merged 5 commits into
mainfrom
feat/base-font-names
Aug 18, 2026
Merged

feat(extractor): stamp items with the font family name, not the resource tag#415
abimaelmartell merged 5 commits into
mainfrom
feat/base-font-names

Conversation

@abimaelmartell

@abimaelmartell abimaelmartell commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

TextItem::font carried the page's font resource name ("F2", "T22") — an arbitrary per-page tag — even though both content-stream parsers already resolve the /BaseFont family name for bold/italic detection at every item-creation site. This stamps that resolved family name instead ("ABCDEF+CMMI10", "Courier"), through a single item_font_name helper so the two parsers cannot drift.

One deliberate, documented carve-out: resource names using Distiller's CID convention (C2_0, C0_1) are kept as-is, because text_utils::is_cid_font keys on that prefix for micro-gap joining and the family name carries no CID marker to replace it. The clean fix is an explicit CID flag on TextItem (touches its ~29 construction sites) — queued for the next TextItem shape change.

Consumer effects

Name-matching consumers start working against real face names:

  • Code detection (is_monospace_font) previously never fired against opaque tags. It now does — so line classification also moves from any-item matching to an essentially-all rule (line_is_monospace, ≥90% of characters, hyperlink/underline-styled items excluded): code lines are wholly monospace, while a URL sidebar or a sentence quoting an inline code literal must not fence the surrounding prose. Code blocks now buffer and emit their fence only when content exists, so an empty pair can never appear.
  • Heading/body font grouping merges resource aliases of the same family instead of treating them as distinct fonts.
  • Positioned-item output (--items-json, bindings) reports real face names.
  • Unlocks font-evidence math detection for feat(markdown): opt-in inline-formula recovery as LaTeX from native math glyphs #414 (LMMathItalic/MSBM anchoring now fires on arXiv PDFs).

Test plan

  • Unit tests: family-vs-resource selection incl. the CID carve-out; monospace line rule
  • cargo fmt / cargo clippy -- -D warnings / full suites (947 unit + 165 integration, zero fixture-snapshot changes)
  • Regression corpus: all pass; code-heavy manuals improve substantially (assembly/C snippets previously emitted as prose now fence with line structure preserved); every diff reviewed — snapshot update PR to follow in the evals repo

🤖 Generated with Claude Code


Summary by cubic

Stamps TextItem::font with the resolved /BaseFont family name instead of the per-page resource tag. Previously items carried tags like "F2"; now they carry face names like "Courier" or "ABCDEF+CMMI10". CID-style resource names (C2_0, C0_1) are kept to preserve micro-gap joining.

  • Fonts: Use fonts::item_font_name from both parsers to select the family name; CID tags remain a carve-out until an explicit CID flag exists.

  • Markdown/code detection: Add line_is_monospace (≥90% by characters; ignores links/underline). Font-based code blocks open only at paragraph boundaries or continue an open block. Treat "Monotype" foundry names as non-monospace. Buffer code and emit fences only when content exists; flush before positioned tables/images; emit sub-3-character mono fragments as plain text.

  • Outputs: --items-json and bindings now report face names. Font grouping merges aliases of the same family. Enables font-evidence math detection on families like LMMathItalic/MSBM.

  • Refactor: Drop an unreachable paragraph-flush branch made obsolete by the boundary gate (no behavior change).

  • Migration: Consumers matching on TextItem::font must use family names instead of resource tags; update any tag-based logic.

Written for commit bb66d71. Summary will update on new commits.

Review in cubic

…rce tag

TextItem::font carried the page's font resource name ("F2", "T22") —
an arbitrary per-page tag — even though both content-stream parsers
already resolve the /BaseFont family name for bold/italic detection at
every item-creation site. Stamp that resolved family name instead
("ABCDEF+CMMI10", "Courier"), from a single item_font_name helper so
the two parsers cannot drift.

One deliberate carve-out, documented on the helper: resource names
using Distiller's CID convention (C2_0, C0_1) are kept as-is, because
text_utils::is_cid_font keys on that prefix for micro-gap joining and
the family name carries no CID marker to replace it.

Consumers that match on font names start working against real names:

- Code detection (is_monospace_font) previously never fired against
  opaque resource tags. It now does — so line classification also moves
  from any-item matching to a majority-by-characters rule
  (line_is_monospace): code lines are wholly monospace, while a lone
  URL or identifier styled in a mono face inside a prose line must not
  fence the surrounding sentence.
- Heading/body font grouping now merges resource aliases of the same
  family instead of treating them as distinct fonts.
- Positioned-item output (--items-json and the bindings) reports real
  face names.

Regression corpus: code-heavy manuals improve substantially (assembly
and C snippets previously emitted as prose now fence with line
structure preserved); remaining churn reviewed as improvements.

@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 5 files

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

Fix all with cubic | Re-trigger cubic

Comment thread src/markdown/classify.rs
Comment thread src/markdown/convert.rs
- Monotype is a foundry prefix on proportional faces (Monotype Corsiva,
  Monotype Garamond); it must not satisfy is_monospace_font's generic
  "mono" token. Regression tests pin both directions.
- Flush the pending code block before inserting a positioned table or
  image, so a block that falls between two code lines cannot be emitted
  ahead of code that precedes it in reading order; a code line after
  the block reopens a new fence naturally.
@abimaelmartell

Copy link
Copy Markdown
Member Author

Both review findings addressed in 893c8fd:

  1. Monotype foundry facesis_monospace_font now rejects any name containing monotype before the generic mono token runs; regression tests pin MonotypeCorsiva/Monotype-Garamond as proportional and RobotoMono/PTMono/Courier as monospace.
  2. Positioned blocks vs buffered code — the pending code block flushes before a table/image inserts, so a block falling between two code lines can no longer be emitted ahead of code that precedes it in reading order. Verified on the regression corpus: one document's interleaved code/table section now emits in true reading order (evals snapshot companion updated in firecrawl/pdf-evals#73), everything else byte-identical, 192/192 pass.

@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 2 files (changes from recent commits).

Shadow auto-approve: would require human review. Changes the public contract of TextItem::font (resource tag to family name) and introduces new monospace detection heuristics with broad output impact; consumers must migrate and behavior change warrants human sign-off.

Re-trigger cubic

A lone registered-trademark glyph or stray bullet set in a mono face is
not code; a fenced block containing one character reads as noise.

@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 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/markdown/convert.rs">

<violation number="1" location="src/markdown/convert.rs:780">
P2: Short mono fragments (trimmed length < 3) are emitted as raw plain text, so any Markdown-significant characters in a 1-2 char code line are written unescaped into the output. For example a mono line `**`, `__`, `~~`, `<<`, or `>>` is now pushed verbatim as `**\n\n`, which an unbalanced emphasis/format marker can corrupt the surrounding Markdown (markdown renderers and downstream AI readers will see stray formatting toggles). Previously these fragments were wrapped in a ``` fence and were therefore inert. Wrapping the short fragment in an inline code span (or escaping it) preserves the intended "reads as plain text" behavior without leaking formatting syntax into the document.</violation>
</file>

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

Fix all with cubic | Re-trigger cubic

Comment thread src/markdown/convert.rs
// a mono face — reads better as plain text than as a fenced block.
if trimmed.chars().count() < 3 {
if !trimmed.is_empty() {
output.push_str(trimmed);

@cubic-dev-ai cubic-dev-ai Bot Aug 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Short mono fragments (trimmed length < 3) are emitted as raw plain text, so any Markdown-significant characters in a 1-2 char code line are written unescaped into the output. For example a mono line **, __, ~~, <<, or >> is now pushed verbatim as **\n\n, which an unbalanced emphasis/format marker can corrupt the surrounding Markdown (markdown renderers and downstream AI readers will see stray formatting toggles). Previously these fragments were wrapped in a ``` fence and were therefore inert. Wrapping the short fragment in an inline code span (or escaping it) preserves the intended "reads as plain text" behavior without leaking formatting syntax into the document.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/markdown/convert.rs, line 780:

<comment>Short mono fragments (trimmed length < 3) are emitted as raw plain text, so any Markdown-significant characters in a 1-2 char code line are written unescaped into the output. For example a mono line `**`, `__`, `~~`, `<<`, or `>>` is now pushed verbatim as `**\n\n`, which an unbalanced emphasis/format marker can corrupt the surrounding Markdown (markdown renderers and downstream AI readers will see stray formatting toggles). Previously these fragments were wrapped in a ``` fence and were therefore inert. Wrapping the short fragment in an inline code span (or escaping it) preserves the intended "reads as plain text" behavior without leaking formatting syntax into the document.</comment>

<file context>
@@ -772,7 +772,15 @@ pub(super) fn to_markdown_from_lines_with_tables_and_images(
+        // a mono face — reads better as plain text than as a fenced block.
+        if trimmed.chars().count() < 3 {
+            if !trimmed.is_empty() {
+                output.push_str(trimmed);
+                output.push_str("\n\n");
+            }
</file context>
Fix with cubic

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Addressed: recovered historical ship dates from discussion timestamps/slugs so the index reflects actual historical ship dates rather than stamping today's backfill date.

HTML-to-PDF producers smear an inline code literal's mono style across
whole wrapped lines, so a prose paragraph can alternate body and mono
fonts line by line. Fencing those lines cut sentences in three: prose
head, fenced middle, prose tail. A mono-set line that continues an open
prose paragraph now stays prose; font-based blocks open at paragraph
boundaries (or continue an open block), and struct-tree Code roles are
honored unconditionally.
@abimaelmartell

Copy link
Copy Markdown
Member Author

One more refinement from re-reviewing the evals diff: HTML→PDF producers smear inline-code styling across whole wrapped lines, so a prose paragraph can alternate body/mono fonts line by line — and fencing those lines cut sentences into prose-head / fenced-middle / prose-tail (visible in the system card snapshot). Font-based code blocks now open only at paragraph boundaries (5bfc4c4); struct-tree Code roles remain unconditional. Corpus: chopped sentences rejoin, assembly listings and the line-numbered bill unaffected, 192/192 pass. Evals companion updated (firecrawl/pdf-evals#73, b7f4328).

@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 1 file (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.

Fix all with cubic | Re-trigger cubic

Comment thread src/markdown/convert.rs Outdated
…he boundary gate

The enclosing guard proves in_paragraph is false, so the nested flush
could never run; the guard and mono check collapse into one condition.

@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).

Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

@abimaelmartell
abimaelmartell merged commit 8478945 into main Aug 18, 2026
11 checks passed
abimaelmartell added a commit that referenced this pull request Aug 21, 2026
1.16.0 replaced TextItem::font's resource tag with the /BaseFont family
name (#415). That is the right default, but it erased the only signal
that distinguishes two font programs sharing a family: consumers that
partition items by font program (style grouping, training-time feature
parity) lost their key when same-family subsets merged.

Add font_tag, carrying exactly what font carried before 1.16.0: the raw
per-page resource tag, first-run's tag on merged items, empty for items
that don't come from a content-stream show operator (images, links,
form fields, OCR). Both content-stream parsers stamp it from the same
resource name, derived items (numeric splits, merges) propagate it, and
it is exposed through --items-json and the Node/Python bindings.
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.

2 participants