Skip to content

fix: preserve native VLM inline math through exports - #4908

Open
PierreMesure wants to merge 4 commits into
docling-project:mainfrom
PierreMesure:fix/vlm-inline-math
Open

PierreMesure wants to merge 4 commits into
docling-project:mainfrom
PierreMesure:fix/vlm-inline-math

Conversation

@PierreMesure

@PierreMesure PierreMesure commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Nemotron Parse 2.0 and DOTS stored inline LaTeX as ordinary text, so Markdown export escaped subscripts or interpreted math asterisks as emphasis. Both adapters now use a shared math-aware renderer and the rich HTML mapper to produce formula items while preserving native block types, coordinates, surrounding text, code, and escaped currency.

Native inline blocks keep literal tildes, pipe-table text, alert markers, and bare URLs. GFM loads only for full Markdown pages. Non-math Nemotron regions keep their existing conversion path. Plain code-only DOTS blocks keep their literal-text behavior. The branch retains the upstream heading normalization and formula helpers from #4964 and the DOTS fixes for escaping, line breaks, and setext headings.

Currency ranges such as Price $5-$10 and $x$. remain text and create only the x formula. Numeric math such as $5$ and $2+2$ still parses as math. DOTS literal protection now shares the same math delimiter rules. Parser instances are reused per thread and mode to avoid concurrent document state leaks. The math renderer falls back to escaped literal text when a match is unavailable.

Remaining heading dependency: docling-core#924 preserves rich title and section-heading context in hierarchical chunks and prevents heading children from becoming body chunks. The heading blocker remains open with locked docling-core 2.103.1. After that fix is released, update the minimum core dependency and lock, and add the four prepared DOTS/Nemotron Title/Section-header chunk metadata regressions. Those tests pass with the companion fix and fail with locked core; they are not included in this branch yet.

Land this PR before LightOnOCR #4789. Rebase #4789 to use the shared renderer and tests from this PR; the shared files currently differ because of these review fixes.

Resolves #4904. Related to #2352; this addresses VLM response conversion, not formula recognition in the standard PDF pipeline.

Validation on Python 3.12.10:

  • Focused Markdown, DOTS, Nemotron, heading, and formula suites with locked docling-core 2.103.1: 118 passed.
  • The same suites plus the four prepared chunk metadata regressions with the companion core fix: 122 passed.
  • Companion core hierarchical chunker and serialization suites: 244 passed, 1 existing expected failure.
  • Validation hooks pass on the changed files in both repositories. No reference fixtures were regenerated. Full repository suites and model inference were not run locally. GitHub CI runs separately on the pushed commits.

Checklist:

  • Documentation assessed: output correction; no public option change.
  • Examples included in regression tests.
  • Parser and export tests cover both adapters across text, titles, sections, lists, and captions.
  • Core heading fix released, minimum dependency and lock updated, and downstream chunk metadata regressions included.

@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @PierreMesure, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@PierreMesure

Copy link
Copy Markdown
Contributor Author

Combined the changes from #4906, #4907, #4908, and #4909 in an isolated worktree and ran the focused parser suites together: 119 passed, 1 skipped on Python 3.12. The skipped case requires an external Ollama server.

The Nemotron changes overlap in the imports and section-header emission block. When rebasing after the other PRs land, retain all shared-helper imports, the corrected heading-level conversion from #4906, and the returned heading item used for inline math here. Those combined paths passed the test run. Each PR currently targets main and has separate issue and regression coverage.

@github-actions github-actions Bot added ai:ci-needs-care AI triage: check CI-safety findings before you approve CI bug Something isn't working vlm-pipeline labels Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

AI triage (advisory)

Checked commit e219b3a32b. The analysis read the diff only. It did not run code from this pull request.

The PR adds a shared math-aware Markdown renderer in markdown_math.py. The DOTS and Nemotron Parse adapters use it to turn inline LaTeX into formula items. This keeps subscripts, math asterisks, code, and currency text intact in exports.

CI safety: no concern found. A maintainer must still approve CI.

2 related pull request(s) in the same code
PR State Signal Assessment
#4789 open referenced as #4789; also references issue #4904; changes the same code related: Both PRs add docling/utils/markdown_math.py and tests/test_markdown_math.py, so they will conflict. But #4789 adds LightOnOCR presets and output parsing, and this PR fixes inline math in the DOTS and Nemotron adapters.
#4964 merged referenced as #4964 related: #4964 is merged. It changed heading depth normalization and formula delimiter handling in the same two adapters. This PR builds on those changes and adds inline math handling, which is a different change.

The ai:* labels are advisory. They do not approve CI and they do not add tests:full. A new push runs this check again.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI first review (advisory): blocker found

This review covers the full diff at 1d0bce45a7. It read the code and did not run it.

The shared renderer adds the marko gfm extension. gfm changes plain text that has no math, and the PR does not say so. Check that tildes and GFM block elements cannot corrupt text that now takes the math or emphasis path.

A maintainer makes the merge decision. This review never approves the pull request.

Comment thread docling/utils/markdown_math.py Outdated
return marko.Markdown(
parser=InlineParser if inline_only else marko.Parser,
extensions=[
"gfm",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

blocker: The gfm extension strips ~ characters and turns the text between them into strikethrough

The gfm extension of marko adds Strikethrough with the pattern (?<!~)(~|~~)([^~]+)\1(?!~). A single ~ is enough to trigger it. The renderer outputs <del>…</del>, and docling/utils/chandra_utils.py maps del to strikethrough formatting.

Trigger: a DOTS or Nemotron text block with two ~ and any math or emphasis, for example about ~5 m and ~10 m, where $x$ is the length. The output has no ~, and the text 5 m and is struck through. The block text changes, so content is lost. The base code did not load gfm, so this is a regression. Strikethrough is also a new formatting claim that the model did not make.

The extension also adds Table, Alert, and Url (autolink). InlineParser.__init__ filters the block elements, but marko calls add_element for extension elements after __init__. So Table and Alert are active again with inline_only=True. Native list and heading blocks can then be reshaped by pipe-table or > [!NOTE] text. A bare www. or http:// string becomes an <a> link.

Fix: remove "gfm" from extensions. Add Strikethrough explicitly only if DOTS needs ~~text~~. Add a regression test with two tildes and a math span, and assert that the ~ characters stay.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed. In both DOTS and Nemotron, about ~5 m and ~10 m, where $x$ is the length exported with unintended strikethrough. GFM also registered Table and Alert after the inline parser had filtered block elements.

Fixed in 6372eeb. The shared renderer now loads GFM only for full Markdown pages. Native inline blocks keep literal tildes, pipe-table text, alert markers, and bare URLs. Full-page table parsing still works.

Added seven test cases. Six reproduce the regressions and failed before the fix; the seventh checks full-page tables with math. The targeted DOTS/Nemotron suite passes (67 tests). The combined four-PR suite passes (129 passed, 1 skipped). UV_NO_SYNC=1 make validate passes. The shared module and its tests are identical in #4789 (ab79d5b).

@github-actions github-actions Bot added ai:review-changes AI first review suggests changes and removed ai:ci-needs-care AI triage: check CI-safety findings before you approve CI ai:review-changes AI first review suggests changes labels Oct 19, 2026
@github-actions github-actions Bot added ai:ci-needs-care AI triage: check CI-safety findings before you approve CI ai:possible-duplicate AI triage: another PR may fix the same problem labels Oct 19, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI first review (advisory): no blocker found

This review covers the changes since 1d0bce45a7 at 6372eebe65. It read the code and did not run it.

The earlier GFM finding is fixed. gfm now loads only for full-page parsing, so native inline blocks keep literal tildes, pipe tables, alert markers and bare URLs. The inline-only path is the same as the base DOTS parser setup, plus the math element. I found no new defect in the new changes.

Earlier findings

  • fixed (blocker): The gfm extension strips ~ characters and turns the text between them into strikethrough

A maintainer makes the merge decision. This review never approves the pull request.

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docling/utils/markdown_math.py 96.49% 1 Missing and 1 partial ⚠️
docling/utils/nemotron_parse_utils.py 89.47% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@geoHeil geoHeil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed 6372eebe65217caef506ab480040c70434defc3b. One blocker remains in markdown_math.py.

A native block with Price $5-$10 and $x$. creates formulas 5- and x in both DOTS and Nemotron. Export changes the price to Price $5-$ 10 and $x$ .. Base preserves the price text. Keep currency ranges as text. Add a cross-adapter regression that checks the formula list and the exported price.

The prior GFM fix is correct. Local parser tests: 58 passed. Current CI is green. Codecov patch coverage is 97.01493%.

Local make check stops on file-length violations that already exist on base. The other read-only checks pass. I made no source or reference-data changes.

Share a math-aware Markdown renderer between DOTS and Nemotron. Keep code and currency literal and retain each native block as its coordinate owner. Fixes docling-project#4904.

Signed-off-by: Pierre Mesure <pierre@mesu.re>
Signed-off-by: Pierre Mesure <pierre@mesu.re>
Signed-off-by: Pierre Mesure <pierre@mesu.re>
@PierreMesure
PierreMesure force-pushed the fix/vlm-inline-math branch 2 times, most recently from 6372eeb to 6992439 Compare October 22, 2026 17:40
@github-actions github-actions Bot removed ai:ci-needs-care AI triage: check CI-safety findings before you approve CI ai:possible-duplicate AI triage: another PR may fix the same problem ai:review-lgtm AI first review found no blocker or major issue (advisory) labels Oct 22, 2026
@PierreMesure

Copy link
Copy Markdown
Contributor Author

Confirmed the currency-range blocker and fixed it in 6992439. Price $5-$10 and $x$. now creates only the x formula and exports the original price. The shared parser rejects a closing single dollar followed by a digit; numeric math such as $5$ still works.

Added eight cross-adapter cases for hyphen/en-dash ranges, decimals, and thousands separators, plus a shared-renderer case that includes numeric math. Six cross-adapter cases failed before the fix. All now pass.

Rebased onto current main (1ee1db0f) and retained the upstream heading normalization, formula normalization, escaped-text, line-break, and setext fixes. The shared renderer and its tests are identical in #4789. Validation: 124 passed for the focused suites, 267 passed, 1 skipped when combined with #4789, and all applicable validation hooks pass. The PR description now reflects the final changes and current evidence.

@github-actions github-actions Bot added the ai:ci-safe AI triage found no CI-safety concern (advisory) label Oct 22, 2026
@dolfim-ibm

Copy link
Copy Markdown
Member

#4789 adds the same docling/utils/markdown_math.py and tests/test_markdown_math.py. Which of the two should land first? The other one then rebases and drops its copy.

Small points:

  • create_markdown_parser builds new marko classes and a Markdown instance for every text block. Cache it, for example with functools.cache on inline_only.
  • dots_utils.py _LITERAL_INLINE_PATTERN keeps the old single-dollar regex, so the currency rule now lives in two regexes. Reuse _MATH, or document why they differ.
  • MathRenderer uses assert match is not None for control flow. Raise, or fall back to the literal text.
  • Titles, headings and list items with math get item.text = "". The chunker reads item.text for heading metadata, so those headings lose their text there.

@dolfim-ibm
dolfim-ibm requested a review from geoHeil October 25, 2026 08:33

@geoHeil geoHeil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed 699243912782f59fe273ebe16d40573095fa3e46. One blocker remains: titles and section headings with math lose their text in chunk heading metadata. See the inline finding.

The earlier currency and GFM findings are fixed. Local focused parser, export, and heading suites: 115 passed. CI passes at this head. The new export tests do not check chunk metadata, so they do not catch the remaining defect.

Local make check stops on file-length failures already present on base. The other read-only checks complete; ty reports warnings. Model inference and the full repository test suite were not run locally. No source or reference data changed.

Comment thread docling/utils/nemotron_parse_utils.py
@PierreMesure

Copy link
Copy Markdown
Contributor Author

Thanks both for the review! Let's merge this one first, I'll address your comments now.

Signed-off-by: Pierre Mesure <pierre@mesu.re>
@PierreMesure

Copy link
Copy Markdown
Contributor Author

@dolfim-ibm Following up on your review: #4908 should land before #4789, once the core heading dependency below is resolved. Then #4789 can rebase and drop its copies of the shared renderer and tests. The shared files currently differ because of the review fixes in this PR.

The parser changes are in e219b3a:

  • Reuse one Marko parser per thread and inline_only mode. Marko instances hold mutable parsing state, so a process-wide cached instance would be unsafe for concurrent conversions. Tests cover sequential reuse and concurrent reference/math isolation.
  • Reuse _MATH in DOTS literal protection so currency and math delimiters follow the same rules. Named capture groups keep the Markdown escape handling independent of the math regex's groups.
  • Replace the renderer assertion with an escaped literal fallback.

The heading finding is tracked in docling-core#925, with the fix in docling-core#924. It preserves serialized heading metadata and consumes the inline children so they do not become body chunks. This PR still needs a core release containing that fix, a dependency/lock update, and the four prepared cross-adapter chunk metadata regressions.

Local focused validation: 118 tests pass with locked core 2.103.1; 122 pass with the companion core fix and the four prepared regressions. Changed-file validation hooks pass. The PR description now records the dependency and merge order.

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

ai:ci-safe AI triage found no CI-safety concern (advisory) bug Something isn't working vlm-pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nemotron and DOTS exports alter inline LaTeX from model responses

3 participants