Repository navigation
Conversation
|
❌ DCO Check Failed Hi @cau-git, your pull request has failed the Developer Certificate of Origin (DCO) check. This repository supports remediation commits, so you can fix this without rewriting history — but you must follow the required message format. 🛠 Quick Fix: Add a remediation commitRun this command: git commit --allow-empty -s -m "DCO Remediation Commit for Christoph Auer <cau@zurich.ibm.com>
I, Christoph Auer <cau@zurich.ibm.com>, hereby add my Signed-off-by to this commit: 9e48e5d27b769c1b7aeed11c00ed2c81d98546c5
I, Christoph Auer <cau@zurich.ibm.com>, hereby add my Signed-off-by to this commit: 8a44fd21e9390cd600c19563d52e3f79b6c030a6"
git push🔧 Advanced: Sign off each commit directlyFor the latest commit: git commit --amend --signoff
git push --force-with-leaseFor multiple commits: git rebase --signoff origin/main
git push --force-with-leaseMore info: DCO check report |
Merge Protections🔴 1 of 2 protections blocking · waiting on 👀 reviews
🔴 Require two reviewer for test updatesWaiting for
This rule is failing.When test data is updated, we require two reviewers
Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Inline runs carry their own significant whitespace and the docling-core
serializers now concatenate them faithfully, so producers must stop stripping run
boundaries. Previously each backend relied on the serializer's hard `" "` join,
which invented spaces (`H 2 O`, `x 2 + y 3`, `(strong) .`) and made the real
boundary unrecoverable.
HTML: collapse whitespace without trimming at the extraction origin
(`" ".join(s.split())` did both), then normalize once per block across the whole
run list -- per-node collapsing cannot see that `["a ", " ", " b"]` is one
boundary. `simplify_text_elements` merges same-format neighbours without a
separator, `to_single_text_element` concatenates verbatim, and the emitters no
longer strip individual runs. The single-char compactor skips candidates carrying
edge whitespace, or it would glue words in rendered span soup.
`_is_rich_table_cell` consumes the normalized list too: inter-tag whitespace in
`<td>\n<p>x</p>\n</td>` otherwise counts as two extra runs and misclassifies the
cell.
A boundary never lands inside a `` ` `` delimiter, because the serializers hoist
whitespace out of emphasis but not out of code. It goes on whichever neighbour is
plain; when both sides are code it becomes an unformatted run of its own, since
`` `a`` b` `` is corrupt either way. The emitters therefore keep whitespace-only
runs: normalization has already trimmed the block edges, so one that survives is
significant.
`<pre>` is preformatted, so it is taken verbatim and only its block edges are
trimmed. Stripping each fragment merged `<pre>See <a>x</a> first</pre>` into one
word once the join stopped compensating. The one-code-item-per-fragment shape of
`<pre>` with inline markup is a separate pre-existing issue and is left alone.
MS Word: group runs by format without stripping, then trim only the paragraph
edges. `_add_inline_equations_to_parent` builds its own InlineGroup outside that
path and stripped the fragment after the last equation, merging the formula into
the next word (`$A= \pi r^{2}$is the area formula`); it now trims group edges
only. JATS: `_strip_segments` becomes `_trim_segment_edges`. Markdown: keep the
raw Marko `RawText` fragments and emit a space for a soft line break, which is
how a soft-wrapped paragraph renders and which the join used to supply.
Goldens are re-baselined. `textbox.docx` is left untouched: its output varies
between identical runs, and its test is already `xfail(strict=False)`, so
regenerating it would only bless nondeterministic data.
Ships as a minor, not a major: `feat!:` would make python-semantic-release
(7.34.6) bump to 3.0, and a 3.0 is not affordable here. Needs a docling-core
floor bump to the release that carries the faithful join, once that number exists
-- an older docling-core with these backends produces double spaces (cosmetic),
but a newer docling-core under older backends merges words.
These backends emit inline runs that own their boundary whitespace, which only a faithful-join docling-core renders correctly. Point `tool.uv.sources` at the branch carrying that change so the lock-step pair is installable and CI resolves the same core the goldens were baselined against. `uv.lock` pins the exact commit (66040cb7). Temporary: drop the source and raise the `docling-core` floor in `dependencies` once the release carrying the faithful join exists on PyPI. The lock diff is larger than the one entry because moving docling-core from a registry wheel to a git checkout re-narrows environment markers across the graph. The resolved package set is unchanged at 382, and no dependency is added or removed.
I, Christoph Auer <cau@zurich.ibm.com>, hereby add my Signed-off-by to this commit: ec6163c I, Christoph Auer <cau@zurich.ibm.com>, hereby add my Signed-off-by to this commit: 8a7b4fe I, Christoph Auer <cau@zurich.ibm.com>, hereby add my Signed-off-by to this commit: 31a4a73 Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Rebase integration. Upstream #3949 (ODF anchor hyperlinks) stripped whitespace at hyperlink boundaries in _normalize_odf_text_runs, correct under the old '' join which re-inserted a separator, but a word-merge under the faithful join (Watch[the talk]for context). The boundary space now stays in the plain run and the serializer places it outside the link markup, per the contract. - drop the hyperlink-boundary strip; update #3949's two tests to the contract-correct whitespace-bearing item texts (markdown was already right) - regenerate ODF goldens (double-space / space-before-punct artifacts gone, upstream #3850 inter-span text renders tight) - bump three upstream-added docx goldens to schema 1.11.0 Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Re-resolve the docling-core git source to the rebased branch tip (e4e745a, v2.91.0) after both PRs were rebased onto main; the lock previously pinned the pre-rebase commit 66040cb7. Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
33c43dc to
93e3049
Compare
|
Hi, Thanks for this work. One extra case that may be worth a test: a word split mid-word by a style change in DOCX, with no space at the run boundary. Source A control paragraph The test descriptions mention DOCX runs owning their whitespace and unspaced HTML code spans, but I could not tell from the PR page whether an unspaced bold run inside a word is asserted for DOCX. Expected with the faithful join: |
Conflicts resolved to the versions reviewed on the rebased branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
…aragraphs Under the faithful InlineGroup join, whitespace-only runs between other runs are text. `_add_body_text_items` dropped them when `skip_empty_text` was set, and `_clean_checkbox_symbols` stripped every run of a checkbox paragraph, which merged words (`Buy**milk**now`). Clean the checkbox symbol only from the leading edge of the first run and re-trim the block edges. With `skip_empty_text`, skip only a block that is empty as a whole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
The OMML paragraph path dropped every whitespace-only text fragment, so `Let $a$ $b$ hold.` became `Let $a$$b$ hold.` under the faithful InlineGroup join, and the list path kept untrimmed block edges. Trim only the outer edges of the fragment sequence in both paths and keep interior whitespace fragments. Without an inline group the formulas stay separate blocks, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Under the faithful InlineGroup join, a list item or <pre> run that held only <br> tags was dropped, so `<li><b>Bold</b><br><i>italic</i></li>` became `**Bold***italic*`. Keep it as a plain "\n" run between its neighbours (never inside a code run); the serializers render it as a hard line break (`**Bold** \n*italic*`, `<br>`). A <br> at an edge of a list item is a block edge and is trimmed, also when it ends the last run. `_emit_caption` joined runs without collapsing their boundary whitespace, which gave double spaces; normalize the runs first, as the figure caption path does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
The shared Chandra HTML mapper (also used by dots.ocr and Nemotron Parse)
stripped every inline run and relied on docling-core's " " join, so a
faithful-join core merges words ("**Rent**and terms"). Keep each run's
boundary whitespace, collapse a boundary to one space carried by the left
run, and trim only the block's outer edges. Edge whitespace of a code or
formula element moves out into a plain " " run, so a boundary space never
lands inside them. A <br> at a run edge now stays where it is, since core
hoists edge whitespace outside the emphasis markers; multi-run headings
collapse whitespace without trimming.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
_iterate_cell_inline stripped every RawText run of a GFM rich cell, so a
faithful-join docling-core merges words ("**C**Cadre", "run`build`now").
Keep the raw fragment like the paragraph path does: marko already trims the
cell edges, and whitespace-only runs between formatted runs are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
_flush_text_data stripped every inline run and relied on docling-core's
" " join, so a faithful-join core merges words ("Some**bold**,"). Keep each
run's boundary whitespace and trim only the block's outer edges. A code
run's edge whitespace (possible in ``unconstrained`` spans) moves out into
a plain " " run, so it never lands inside the code delimiters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
A list item's text is extracted from all its non-list descendants. Under the faithful run join, the text before a block child and the block's own text were concatenated, so `<ul><li>a<p>para<li>b</ul>` exported `- apara`. A block child (`_PARA_BREAKERS`) is now a word boundary: a space is added at its edges unless the source already has whitespace there, and normalize_whitespace() collapses it and moves it out of code runs. Preformatted extraction is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
These tests still expected stripped runs or the double spaces of the old
" " join. Each new value was checked against the contract: boundary
whitespace sits on the plain neighbour (never inside a code or formula run),
a soft break before a code span is a plain " " run, and the exports read
`*one* two`, `Value $x=1$`, `Already in `Reference/``.
- html: test_text_directly_inside_figure_is_kept (2 cases),
test_orig_keeps_source_text, test_link_and_image_outside_base_directory_keep_conversion
- msword: test_docx_hidden_text_next_to_equations (no double space before $x=1$)
- md: test_convert_line_breaks ("Author "), test_convert_line_break_next_to_code_span,
test_line_break_does_not_cross_block_boundary (" " run before the code span)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
A footnote's runs are collected from the whole <fn>. In compact XML (`<fn><label>1</label><p>See ...`) nothing separated the label from the paragraph, so under the faithful run join it exported `1See`. A <p> is now a block boundary: a plain " " run is added unless whitespace is already there, so compact and indented XML give the same runs. The JATS styling, footnote and ext-link run-list tests still expected stripped runs; they now expect the boundary whitespace on each run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Regenerated with DOCLING_GEN_TEST_DATA=1 for docx, html, md, jats, odf, epub, xbrl, latex, pages/keynote/numbers, asciidoc, pptx, chandra, boxnote, vtt, mhtml and email, then audited mechanically (whitespace removed, 1.10.0 mapped to 1.11.0 on both sides). Only spacing changes remain, e.g. latex `states $h_t$,` (was `states $h_t$ ,`), plus these explained differences: - inline-group goldens carry the 1.11.0 stamp; version-only changes to goldens without inline groups were reverted - DocLang `.dclg` header 0.7 -> 0.8 (docling-core #880) and link destinations `(`/`)` -> `%28`/`%29` in wiki_duck.html.md (docling-core #920) - fields that verify_docitems does not compare and that main left stale: epub `orig` keeps the source typography (#4434), latex 1706.03762 multirow span clamped to the table (#4974), escaped_characters list marker `1.` and the binary hash of the internal HTML round trip - md goldens were generated with docling-core #894's CodeItem escaping disabled, so they do not bake in that regression of the HTML-block round trip - embedded image bytes (machine dependent, partly LibreOffice) restored from HEAD Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Relock docling-core to the docling-core#693 head (92a694dd). Conflicting goldens were regenerated against it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
Merge Protections🔴 1 of 2 protections blocking · waiting on 👀 reviews
🔴 Require two reviewer for test updatesWaiting for
This rule is failing.When test data is updated, we require two reviewers
Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
AI triage (advisory)Checked commit This PR updates document backends to preserve boundary whitespace in inline runs. It aligns backend output with the inline group whitespace contract in Topic labels added: CI safety: needs maintainer care before you approve CI.
2 related pull request(s) in the same code
Reference data changes
Only the first 80 of 167 reference files were compared. Assessment: expected. The reference data changes are expected because the backends now preserve boundary whitespace in inline runs. The |
There was a problem hiding this comment.
AI first review (advisory): blocker found
This review covers the full diff at e021aa9063. It read the code and did not run it.
This PR changes every backend so that inline runs carry their own boundary whitespace, requiring the new docling-core faithful-join semantics. pyproject.toml pins docling-core to a git branch only in [tool.uv.sources], which is a workspace-local dev override never shipped in the wheel. The published package still declares docling-core>=2.103.1, so any released docling-core would apply the old join that invents separators and renders the new runs incorrectly. The reviewer must confirm that docling-core#693 has been released and that the dependency floor in the wheel metadata is raised to that version before merge.
Findings outside the diff lines
-
blocker
pyproject.toml:51: Dependency floor not raised: any released docling-core breaks the new whitespace contract
Every backend in this PR emits inline runs that carry their own boundary whitespace and relies on docling-core joining them with""(faithful join fromdocling-core#693). The[tool.uv.sources]pin tobranch = "cau/serializer-whitespace-fixes"is a workspace dev override; it is not packaged into the wheel. The published wheel's metadata still saysdocling-core>=2.103.1,<3.0.0. A user who installs from PyPI gets any released docling-core, which uses the old serialiser that invents a" "separator between runs. The backends that previously stripped their runs will now emit un-stripped runs that the old serialiser joins with an extra space, producing doubled spaces throughout:"bold " + " tail"→"bold tail".Fix: wait until docling-core#693 is released, then raise the floor in
dependenciesand in the slim-package and lock-file entries (lines 51, 291, 343) to that new release. The PR description says this but the code does not yet contain it.
A maintainer makes the merge decision. This review never approves the pull request.
Important
Paired with docling-project/docling-core#693, and it must merge after it. These backends emit inline runs that carry their own boundary whitespace, and only a docling-core that joins runs faithfully renders them correctly.
Update: merged
main, new producers fixedmain(90234a71) is merged into this branch. The conflicts were resolved to the versions reviewed on a rebased copy, and the newer commits were replayed on top, so no force push was needed.uv.lockpins docling-core#693 at92a694dd.Producers that landed on
mainin the meantime and broke the contract are fixed here. Each would merge words as soon as the new core is released:$a$$b$→$a$ $b$).<br>between inline runs becomes a"\n"run;_strip_segmentsis gone entirely, including the call that fix(jats): preserve styling in abstracts and footnotes #4361 added.mainstill strips whitespace at hyperlink boundaries.Groundtruth was regenerated rather than merged by hand. The net test-data diff against
mainis 167 files.Still to do before merge:
test_emf_docx.docxand others) on a machine with LibreOffice.textbox.docxstays as it is: it isxfail(strict=False)and its output differs between runs.docling-coreentry from[tool.uv.sources], raise the floor in the root, slim and meta-packagepyproject.toml, and re-lock.Summary
docling-core no longer joins
InlineGroupruns with a hard" "; it concatenates them faithfully. Producers must therefore stop stripping run boundaries, because the serializer no longer adds a separator back.Until now every backend relied on that join, which is why the committed groundtruth contained
H 2 O,x 2 + y 3and(strong) .. Once a backend strips the boundary, the real spacing can't be recovered downstream, so the fix has to happen where the runs are made.What changed
HTML. The whitespace was lost at extraction.
" ".join(text.split())collapses runs of whitespace, which is correct per the HTML spec, but it also strips the edges, which is the defect. Everything downstream therefore received runs that were already stripped. Extraction now collapses without trimming, and normalization happens once per block across the whole run list. Collapsing per node can't see that["a ", " ", " b"]is a single boundary.simplify_text_elementsmerges neighbours with the same format without a separator,to_single_text_elementconcatenates as-is, and the emitters no longer strip individual runs.Two consequences worth calling out:
`delimiter. The serializers hoist whitespace out of emphasis but not out of code, so the space goes to whichever neighbour is plain. When both sides are code, the space becomes an unformatted run of its own, because`ab` `` is corrupt either way. For the same reason the emitters keep whitespace-only runs: normalization has already trimmed the block edges, so any whitespace run that survives is significant._is_rich_table_celluses the normalized list too. Otherwise the whitespace between tags in<td>\n<p>x</p>\n</td>counts as two extra runs and the cell is misclassified.MS Word.
_get_paragraph_elementsgrouped runs by format and usedstrip()both to decide whether to emit a run and as the emitted value. It now groups without stripping and trims only the paragraph edges._add_inline_equations_to_parentbuilds its ownInlineGroupoutside that path. It stripped the text after the last equation, which merged the formula into the next word ($A= \pi r^{2}$is the area formula). It now trims only the group edges.Markdown. The backend keeps the raw Marko
RawTextfragments instead of stripping them. It emits a space for a soft line break, which is how a soft-wrapped paragraph renders and what the join used to supply. A hard break before a code span emits a"\n"run.JATS.
_strip_segmentsbecame_trim_segment_edges. It drops empty segments, merges compatible neighbours, and trims only the first and last segment.ODF was already the reference producer. Its tail-text recovery came from #3850. This PR only removes the hyperlink-boundary strip that #3949 added.
Temporary dependency source: remove before release
tool.uv.sourcespoints at the docling-core branch, so the paired change can be installed and CI resolves the same core the goldens were generated against.uv.lockpins the exact commit.Release checklist:
docling-coreentry from[tool.uv.sources].docling-corefloor to the release that carries the faithful join, and re-lock.Version skew
Both packages are on 2.x and a major bump isn't affordable, so the dangerous combination can be installed and is worth understanding:
" "join)""join)Hence the merge order: docling-core first, then this.
Testing
Full suite against the docling-core#693 head (
92a694dd):4070 passed, 62 skipped, 2 xfailed, 4 failed. The 4 failures depend on the test machine: PDF end-to-end, OCR end-to-end and webp without tessdata, and ocrmac language selection. They also fail withmain's code.tests/test_inline_whitespace_contract.pycovers the cases that need a source document built on the fly:<pre>interior whitespace;Known limitations (all already on
main, unchanged)- intro\n\n more **x** yexports as- intromore **x** y. This comes from the md backend's nesting.<br>inside<p>or<td>becomes a space. Words don't merge, but the line break is lost.Out of scope
<pre>with inline markup still produces one code item per fragment. This PR only stops it from merging words (<pre>See <a>x</a> first</pre>) and keeps the interior whitespace. The fragment structure is a separate issue that already exists." "thatget_textadds forp/li/th/td. It is a fake block separator at the character-extraction layer: a different concern, with other consumers.white-spaceproperty, computeddisplayper element, and bidi. Collapsing at extraction plus normalizing across runs covers the normal-flow content tested here.Checklist:
🤖 Generated with Claude Code