Repository navigation
fix(diff): keep paragraph-spanning fields balanced and entry tabs in place (#1156) - #1173
Merged
Merged
Conversation
…place (#1156) A complex field that opens in one paragraph and closes in another (a Word table of contents at body level) came out of a comparison unbalanced when the entry holding its end character was deleted or a new last entry was appended: field characters are zero-width to the token stream and the reader dropped an end with no begin in its paragraph, so the paragraph the end moved into paired Equal and its copy went into the redline unmarked. The reader now records each paragraph's field characters that belong to a field opened or closed elsewhere in the paragraph's field envelope, so such a pair takes the reversible whole-paragraph path. Editing a hyperlinked entry's text put its tab after the PAGEREF field's begin/instruction/separate characters: the edit's slice took the always-kept field plumbing at its end even though the zero-width tab before it belonged to the next op. A marker behind a trailing zero-width token the next op of the same side owns now goes with that op. The semantic change set's field-envelope digest tag advances to v3.
…des a tab (#1156) The tab-ordering fix let a deleted span leave every always-kept marker behind a trailing tab to the op owning the tab. On the left that op is usually an Equal span, which emits only the revised side's runs, so a bookmark or comment range only the original carried was dropped from the redline, and a deferred marker could instead be re-emitted by a later deletion after inserted text. A left marker is now deferred only when the owning op slices the left side too, or when that op's right span holds the same marker (field plumbing, a bookmark by name, a comment by id). After emitting its right runs, an Equal span claims the left copies of exactly the markers it emitted, so no later deletion emits them again. Deferral also requires the owner to be rendered in the same call (Consolidate renders reviewer ops apart from the base spans). Claiming the represented field plumbing also stops a field whose result changes right after unchanged text from gaining a second begin, instruction and separate on reject (the boundary case of #1165).
This was referenced Oct 16, 2026
…ken unchanged (#1156) Markers behind a trailing zero-width token were left to the op owning that token even when it inserts or deletes the token. Behind an edited inline w:customXml (a zero-width opaque token) that pulled the shared comment reference into both the insertion and the deletion; comment normalization then renumbered the deleted copy while the range inside the element (left alone by #1157) kept its id, so the rejected view's range and reference no longer matched. A marker is now deferred only to an Equal or FormatChanged span with identical raw text, which emits the token unwrapped; behind an inserted or deleted token the marker stays with the earlier op, as before #1156.
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.
Closes #1156
Second of a three-PR stack for the comparison engine: #1169 (#1157), then this one, then the #1149 PR. It is based on #1169's branch and will be retargeted to
mainwhen #1169 merges.Why
Word writes a table of contents as one complex field spread over several paragraphs: the first entry paragraph holds the field's begin, its
TOCinstruction and its separator, and the last entry paragraph holds the field's end. Each entry is a hyperlink with a nestedPAGEREFfield after a tab. At body level, comparing two such documents broke the field in three ways:PAGEREFfield.How it works
There were two separate defects.
(a) A field end that moved between paragraphs carried no revision mark.
IrReader.UnmatchedFieldCharactersnow records each paragraph's unmatched field characters, in document order: depth-0 ends and separators, plus one begin per field left open. That list is folded into the paragraph's field-envelope digest.(b) The tab moved inside the page-number field.
PAGEREFbegin/instruction/separator all sit. The tab belongs to the next, unchanged span, but the insertion grabbed the field characters behind it.SourceRunModel.Slicenow leaves markers at a span's end to the next operation, but only when that operation:Validation
BodyFieldSpanDiffTests(44 cases). For every case, every paragraph's sequence of field characters, instructions, tabs and text must match in both accept and reject, and the redline runs through the OOXML validator. The cases are:HC022-Table-Of-Contents.docxwith its content control stripped;Also fixed in passing (#1165)
The same boundary rule fixes part of #1165. A field whose cached result changes directly after unchanged text no longer gains a second begin, instruction and separator on reject. That covers the common case of a table-of-contents page-number update. Two shapes of #1165 remain open, and that issue stays open for them:
Adversarial review
One round of hostile review ran on the first commit. It confirmed that part (a) doesn't over-trigger: self-contained fields, editing the first or last entry, and split/merge all stay word-level. It found one real regression in part (b): markers left for the next operation could be lost or misplaced on the original's side.
The second commit fixes that with the original-side narrowing and the claim rule, and adds tests for both shapes. A probe set of 54 body and 6 Consolidate cases is now no worse than
mainin any case and fixes 49 ofmain's failing checks. The CHANGELOG was corrected to claim only what holds.Stacking on #1157 then exposed one interaction: an edited inline
w:customXmlmade a shared comment reference a revised copy. The third commit fixes it by deferring only to an operation that keeps the token unchanged; #1157's test now passes unchanged.Pre-existing gaps the review found are filed as #1165 and #1166.