feat: make inline serializers concatenate runs faithfully - #693
Draft
cau-git wants to merge 3 commits into
Draft
Conversation
`_get_children_simple_text_block` guarded against a second *text* fragment but the Element branch just overwrote `result`, so `<text>2<superscript>nd</superscript></text>` returned "nd" and the leading `2` was silently discarded. Guard both branches so the element is routed to `_parse_inline_group`, which already handles mixed children.
… faithfully The Markdown, HTML, LaTeX and DocLang inline serializers joined runs with a hard `" "`, inventing spaces the model never asked for: `2nd` -> `2 nd`, `O2.` -> `O 2 .`, `italic text.` -> `italic text .`. Those artifacts can never be removed while the separator lives in the serializer. Serializers: - join inline parts with `""`; DocLang keeps its delimiter between the element head and the body only, where `<content>` is the whitespace channel - hoist edge whitespace around the *complete* decoration stack (formatting and hyperlink together) for Markdown, HTML and LaTeX. Decorating it verbatim emits `**bold ** tail`, which CommonMark renders as literal asterisks; hoisting inside one formatter would let the hyperlink recapture it. DocLang opts out: it keeps whitespace inside the markup, in `<content>`. DocLang deserializer: - `_get_text` stripped every fragment and joined with `""`, collapsing `Advanced <bold>Topics</bold>` into `AdvancedTopics`. It now trims the outer block boundary only, and never trims whitespace delivered by `<content>`. - bare text nodes use the standard XML heuristic: a whitespace run containing a newline is pretty-print indentation, one without it is content. This is what hand-written and VLM-emitted DocLang needs, since it will not use `<content>`. - element-head children (`<caption>`, `<description>`, `<summary>`, `<custom>`) no longer bleed into body text, where they were also double-modelled in `item.meta`. Data at rest: schema minor bumped to 1.11.0 and a `mode="before"` validator re-injects the legacy separator for documents stamped below it, preserving their previous *visible* output including its extra-space bugs (`H 2 O` stays `H 2 O`). It must run before `check_version_is_compatible`, which rewrites `version` and destroys the evidence. Versionless input is left alone. The validator works on a copy, so the caller's dict is untouched and re-validating the same raw value cannot compound separators. A separator can never land inside a delimiter. Code and formula runs are wrapped in `` ` ``/`$` by the serializers, so the space goes on the plain neighbour; when both neighbours are delimited it becomes a plain run of its own, because `` `a``b` `` and `$a$$b$` are both corrupt. Chunk text changes too, since ChunkingDocSerializer inherits the Markdown join -- anyone with a persisted vector index gets silent retrieval drift, so that needs its own line in the release notes. DocTags code is untouched. Its golden gains one empty `<text></text>` for the legacy fixture because that document now carries an extra whitespace run, which DocTags renders empty by its own pre-existing strip-per-run behaviour. Ships as a minor, not a major: `feat!:` would make python-semantic-release (7.34.6, both repos) bump to 3.0, and a 3.0 is not affordable on either package.
Contributor
|
✅ DCO Check Passed Thanks @cau-git, all your commits are properly signed off. 🎉 |
Contributor
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/
|
3 tasks
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
InlineGroupruns now carry their own significant whitespace, and the serializers concatenate them faithfully instead of joining with a hard" ".The join was the defect. It invented spaces the model never asked for, and while the separator lived in the serializer those artifacts could never be removed by fixing a producer:
2<sup>nd</sup>2 nd2ndO<sub>2</sub>.O 2 .O2.**Advanced Topics** in *Machine Learning***Advanced Topics** in *Machine Learning***Advanced Topics** in *Machine Learning*This is *italic text*.This is *italic text* .This is *italic text*.The ODF backend already emitted contract-honouring runs, so its committed Markdown goldens carried the mirror-image artifact —
X 2 + Y 2 = Z— which is the shortest proof that the serializer, not the producer, was wrong.What changed
Serializers. Markdown, HTML, LaTeX and DocLang join inline parts with
"". DocLang keeps its delimiter between the element head and the body only;<content>remains its whitespace channel.Whitespace hoisting. In DOCX and HTML the boundary space frequently lives inside a formatted run. Emitting it verbatim produces
**bold ** tail, which CommonMark renders as literal asterisks. Markdown, HTML and LaTeX now split leading/core/trailing whitespace, apply the complete decoration stack to the core, and restore the edges. Doing this inside an individual formatter would not work — the hyperlink would immediately recapture the space ([**bold** ](url)). DocLang deliberately opts out and keeps the whitespace inside the markup, in<content>.DocLang deserializer.
_get_textstripped every fragment before joining, collapsingAdvanced <bold>Topics</bold>intoAdvancedTopics. It now trims the outer block boundary only, and never trims whitespace delivered by<content>.<content>for whitespace-bearing runs, so this only governs hand-written and VLM-emitted input.<caption>,<description>,<summary>,<custom>) no longer bleed into visible body text, where they were also being double-modelled initem.meta.Also fixed as a prerequisite (first commit, independently releasable):
<text>2<superscript>nd</superscript></text>deserialized to a single item"nd"— the leading2was silently discarded. That is data loss, not a whitespace artifact.Compatibility
Space-less runs are in users' saved
.jsonfiles, and release coordination cannot fix a file on disk. The schema minor is bumped to 1.11.0 and amodel_validator(mode="before")re-injects the legacy separator for documents stamped below it.The goal is narrow and exact: preserve the old serializer's visible output, including its existing extra-space bugs. The old document does not contain enough information to recover semantic whitespace —
["H", "2", "O"]and["Normal", "italic"]have the same shape but need different boundaries.mode="before", becausecheck_version_is_compatiblerewritesversiontoCURRENT_VERSIONand destroys the evidence.`/$, so the space goes to the plain neighbour, and when both neighbours are delimited it becomes a plain run of its own —`ab`` andThis does not cover live conversion against an old docling backend, which builds the document in memory at the current version with no old stamp to key on. That remains an accepted, documented risk, mitigated by the paired docling change.
Behavioural changes to expect
ChunkingDocSerializerdefines no inline serializer, so it inherits the Markdown join. This is not golden-file cosmetics: it changes embeddings, so anyone with a persisted vector index gets silent retrieval drift on upgrade. It needs its own line in the release notes.<text></text>for the legacy fixture, because that document now carries an extra whitespace run and DocTags renders every run stripped — pre-existing behaviour, reproducible without any migration.The failure mode inverts
Worth flagging for reviewers: today a producer that omits boundary whitespace yields extra spaces — ugly, harmless, unnoticed for as long as it has existed. After this change the same omission yields merged words. The cost of getting it wrong goes from cosmetic to severe, and it lands on third-party backends and on anything constructing
DoclingDocuments by hand.add_inline_group's docstring now states the contract so the next backend author sees it.Testing
602 passed, 6 skipped.New
test/test_inline_whitespace_contract.pyholds the contract as a table — one set of inline-run sequences checked againstmd/itxt/html/latex/doclang, including whitespace inside a formatted run and inside a formatted run with a hyperlink, plus a whitespace-only run. It also covers:Pre-contract fixtures deliberately keep their old version stamp, so their goldens are byte-identical to before this PR and act as the regression test for the migration.
Release notes
feat!:/BREAKING CHANGE:— python-semantic-release 7.34.6 would otherwise bump to 3.0.docling-coreindependently (docling-serve, docling-eval, docling-mcp, docling-jobkit) need a floor bump in the same wave.