What problem does this address?
The diff library is declared at ^4.0.2 in packages/editor and ^8.0.3 in packages/sync. The Syncpack alignment work (#77950, #77954) needs to converge these onto a single version across the monorepo, which means lifting editor to v8 (a four-major jump).
Bumping diff to ^8.0.3 in editor causes 51 unit tests to fail across block-diff.js, revision-fields-diff/index.js, revision-diff-panel/index.js, and preserve-client-ids.js. The failures fall into two classes:
1. LCS tie-breaker change (v6+)
diff v6 added "prefer deletions before insertions" as a tie-breaker. For inputs where multiple equal-length LCSes exist, v8 picks a different one than v4. Concrete example with prev=[First, Second], curr=[Second-modified, First]:
- v4 picked
First as the matched element → output [add Smod, match F, remove S] → pairSimilarBlocks sees one removed/one added of core/paragraph and pairs them cleanly into [Smod-modified, F-unchanged].
- v8 picks the whitespace/
null block between paragraphs as the matched element → output [remove F, add Smod, match null, remove S, add F] → pairSimilarBlocks sees two removed and two added paragraphs and uses similarity matching, which incorrectly pairs F-removed with Smod-added and S-removed with F-added. The user-visible result is two confusing inline diffs (<del>First</del><ins>Second</ins> block content <ins>modified</ins> etc.) instead of a clean modified+unchanged pair.
This is a real UX regression in the revision-diff feature, not just a test snapshot drift.
2. diffWords semantics change (v6+)
diff v6 stopped treating whitespace as a token. Adjacent word changes now coalesce into a single removed/added pair instead of being reported per-word. Example:
- v4:
Visit <a><del>our</del><ins>the</ins> <del>site</del><ins>website</ins></a> today (two precise inline diffs).
- v8:
Visit <a><del>our site</del><ins>the website</ins></a> today (one coarser diff).
Less precise but arguably still acceptable. Affects the rich-text inline diff display in revision previews.
What is your proposed solution?
Fix the consumer code in block-diff.js so v8's output produces the same user-visible behaviour as v4. The current tests encode the correct UX (clean modified+unchanged pairs, per-word inline diffs) and should keep passing without modification.
Class 1: LCS tie-breaker
Replace the direct diffArrays(previousSigs, currentSigs) call with a matching pass that prefers content-bearing blocks (paragraphs, headings, etc.) over freeform/whitespace blocks. Two viable approaches:
- Filter the input. Strip null/whitespace
rawBlock entries from both arrays before passing to diffArrays, then re-interleave them in the output in current-revision order. The LCS is then computed only over content-bearing blocks, sidestepping v8's preference for the whitespace match.
- Custom LCS. Build the matching chain ourselves with a scoring function that weights content matches above whitespace matches. More invasive but gives full control over tie-breakers going forward.
The first approach is the smaller change and likely sufficient.
Class 2: diffWords granularity
For the rich-text inline diffs, switch from diffWords to diffWordsWithSpace (which still treats whitespace as tokens, preserving v4-style per-word output), or post-process diffWords output to split coalesced runs at whitespace boundaries.
Approach for the PR
- Bump
diff to ^8.0.3 in packages/editor/package.json.
- Implement the Class 1 and Class 2 fixes above.
- Verify all affected tests pass without modifying assertions.
- Manually verify the post revisions UI in the editor — open a post with several revisions and confirm the inline word-level diffs and block add/remove/modify markers render the same as before the bump.
Background
What problem does this address?
The
difflibrary is declared at^4.0.2inpackages/editorand^8.0.3inpackages/sync. The Syncpack alignment work (#77950, #77954) needs to converge these onto a single version across the monorepo, which means liftingeditorto v8 (a four-major jump).Bumping
diffto^8.0.3ineditorcauses 51 unit tests to fail acrossblock-diff.js,revision-fields-diff/index.js,revision-diff-panel/index.js, andpreserve-client-ids.js. The failures fall into two classes:1. LCS tie-breaker change (v6+)
diff v6added "prefer deletions before insertions" as a tie-breaker. For inputs where multiple equal-length LCSes exist, v8 picks a different one than v4. Concrete example withprev=[First, Second],curr=[Second-modified, First]:Firstas the matched element → output[add Smod, match F, remove S]→pairSimilarBlockssees one removed/one added ofcore/paragraphand pairs them cleanly into[Smod-modified, F-unchanged].nullblock between paragraphs as the matched element → output[remove F, add Smod, match null, remove S, add F]→pairSimilarBlockssees two removed and two added paragraphs and uses similarity matching, which incorrectly pairsF-removedwithSmod-addedandS-removedwithF-added. The user-visible result is two confusing inline diffs (<del>First</del><ins>Second</ins> block content <ins>modified</ins>etc.) instead of a clean modified+unchanged pair.This is a real UX regression in the revision-diff feature, not just a test snapshot drift.
2.
diffWordssemantics change (v6+)diff v6stopped treating whitespace as a token. Adjacent word changes now coalesce into a single removed/added pair instead of being reported per-word. Example:Visit <a><del>our</del><ins>the</ins> <del>site</del><ins>website</ins></a> today(two precise inline diffs).Visit <a><del>our site</del><ins>the website</ins></a> today(one coarser diff).Less precise but arguably still acceptable. Affects the rich-text inline diff display in revision previews.
What is your proposed solution?
Fix the consumer code in
block-diff.jsso v8's output produces the same user-visible behaviour as v4. The current tests encode the correct UX (clean modified+unchanged pairs, per-word inline diffs) and should keep passing without modification.Class 1: LCS tie-breaker
Replace the direct
diffArrays(previousSigs, currentSigs)call with a matching pass that prefers content-bearing blocks (paragraphs, headings, etc.) over freeform/whitespace blocks. Two viable approaches:rawBlockentries from both arrays before passing todiffArrays, then re-interleave them in the output in current-revision order. The LCS is then computed only over content-bearing blocks, sidestepping v8's preference for the whitespace match.The first approach is the smaller change and likely sufficient.
Class 2:
diffWordsgranularityFor the rich-text inline diffs, switch from
diffWordstodiffWordsWithSpace(which still treats whitespace as tokens, preserving v4-style per-word output), or post-processdiffWordsoutput to split coalesced runs at whitespace boundaries.Approach for the PR
diffto^8.0.3inpackages/editor/package.json.Background
diffv6 → v7 → v8 release notes: https://github.com/kpdecker/jsdiff/blob/master/release-notes.mdpackages/syncalready prepared for v8+ vianormalizeChangeCountsinpackages/sync/src/quill-delta/Delta.ts— that pattern may be relevant for consumers that depend oncountsemantics.