Skip to content

Commit 33e2ce6

Browse files
feat: diffing extension for comparing documents (SD-1324 and SD-89) (#2306)
* feat: add function for mapping doc paragraphs by id * feat: add function for flattening paragraph text but keep track of positions * feat: add function for calculating text diffs using LCS * feat: add function for calculating paragraph-level diffing * feat: add diffing extension * test: add diffing tests * refactor: switch LCS algorith to Myers for performance * refactor: code structure * feat: compute text similarity using Levenshtein distance * feat: identify contiguous text changes as single operation * feat: implement logic for diffing paragraph attributes * refactor: extract generic sequence diffing helper * refactor: modify paragraph diffing to reuse generic helper * refactor: extract operation reordering function This function can then be reused when diffing paragraphs and runs. It helps identifying modifications instead of delete/insert pairs * fix: standardize positions for text diffing Always maps starting/ending positions to the old document instead of the new one. * refactor: change text diffing logic to account for formatting * refactor: change from "type" to "action" * feat: support diffing non-textual inline nodes * feat: include previous element when building add diff * docs: add JSDoc to inline diffing * fix: handle arrays in attributes diff * feat: implement generic diffing for all nodes * refactor: move paragraph utility functions * feat: use generic diffing for diff computation command * refactor: simplify grouping logic during inline diffing * refactor: convert modules to typescript and improve documentation * fix: diffing of inline node attributes * fix: diff positions for modified non-paragraph nodes * feat: improve diff comparison for table rows * fix: emit single diff when container node is deleted * fix: compute correct insertion position for first child node * fix: remove unused position resolver for inline diffing * refactor: remove serialization/deserialization of run attrs during diff * fix: include node type when extracting content from paragraph * refactor: include JSON nodes instead of PM nodes in diffs * refactor: simplify typescript interfaces * refactor: extract insertion position calculation logic into helper * refactor: simplify resolution of inline node positions * refactor: remove unecessary exports * refactor: standardize diff attribute names for modifications * docs: add JSDoc to compareDocuments command * feat: add function for diffing marks * feat: take marks into account during diff computation * refactor: modify computeDiff signature to account for comments diffing * feat: add generic inline content tokenization function * refactor: use inline content tokenization function for paragraphs * feat: implement tokenization of comment data * refactor: change diffNodes signature to facilitate reuse * feat: allow specifying keys to be ignored when diffing attributes * feat: implement comments diffing hooks * feat: update compareDocuments command to support diffing comments * test: move test files to separate directory * docs: improve TSDoc comments for interfaces * feat: initialize directory structure for diff replays * feat: implement replay of non-paragraph nodes * feat: implement deriving new marks from diff * docs: improve TSDoc comments * docs: improve typing * feat: implement replay of inline nodes and text * feat: implement replay of paragraph nodes * feat: implement diff replay entrypoint * feat: add command for replaying diffs onto current document * feat: add button to dev playground to allow comparing documents * feat: include diffing extension * feat: improve replay accuracy for inline/run attrs and tracked changes * fix: replay of formatting changes * fix: tracked change comment update * fix: set selection during track changes * fix: adjust track changes during diff replay * fix: linting errors * feat: add logic for replaying comment differences * fix: adjust computation of comment position key * fix: adjust comment diffing logic * fix: diffing of track change marks * test: diff with track changes * fix: duplicate function defs * fix: replaying diffs with track changes * chore: add param type * fix: invalid test * test: adjust existing tests * fix: replaying tracked changes * fix: syncing of tracked changes after diff replay * fix: correctly apply run attribute changes * fix(diffing): preserve duplicate same-type marks in mark diff and replay * fix(diffing): preserve multi-block comment body edits in modified comment diffs * fix(diffing): allow replayDifferences to be called without options * fix(diffing): make replayDifferences side-effect free in can() checks * fix(superdoc): clear active reply when replay deletion removes parent thread * fix(diffing): match inline node types by name across editor schemas * fix(superdoc): always flush replay tracked-change sync even with no comment positions * fix(superdoc): delete full comment subtree on replay thread removal * fix(diffing): preserve existing comments when compareDocuments omits updatedComments * fix(comments-store): scope tracked-change stale pruning to active document * fix(diffing): reuse transaction * fix(superdoc): match replay comment updates by importedId before commentId - prefer importedId when resolving existing comments for COMMENT_EVENTS.UPDATE - fall back to commentId only if imported-id lookup misses - prevent duplicate comment creation when replay updates carry a new runtime commentId for an existing imported thread - add regression test covering update reconciliation with stable importedId and changed commentId * fix(diffing): honor applyTrackedChanges=false by setting skipTrackChanges on replay - set skipTrackChanges meta in replayDifferences when tracked replay is disabled - keep forceTrackChanges behavior when tracked replay is enabled - prevent replay from generating tracked marks when track changes mode is already active - add regression test covering replay with active track changes and applyTrackedChanges: false * fix(superdoc): handle replay delete events with importedId-first identity fallback - resolve delete targets from both importedId and commentId (importedId prioritized) - seed subtree removal with all resolved ids to match re-keyed replay comments - keep active comment clearing aligned with the expanded removal set - add regression test for stale runtime commentId + stable importedId delete payloads to prevent orphaned sidebar threads * fix(diffing): prevent compareDocuments from dispatching read-only transactions - set preventDispatch meta in compareDocuments so CommandService skips dispatch - keep compare behavior read-only with no transaction/update side effects - add regression assertions that compare calls do not emit transaction events (with omitted and explicit comments inputs) * fix(diffing): derive insertion anchors from old sequence context - refactor getInsertionPos to use only oldNodes + oldIdx and derive the previous node internally - remove redundant previousOldNodeInfo plumbing from generic and paragraph add-diff builders - keep depth-aware insertion behavior for deeper/same/shallower transitions - update diff utility and paragraph diff tests to match the new API and validate anchor resolution * fix(superdoc): scope replay comment updates to active document - reuse getCommentDocumentId / belongsToDocument from comments store in replay update reconciliation - expose those helpers from comments-store to avoid duplicating document-scoping logic in SuperDoc.vue - constrain replay UPDATE comment matching to the active editor document when IDs collide - add regression coverage for same importedId across multiple documents so only the active document comment is updated * fix(superdoc): scope replay thread deletions to active document - restrict replay DELETED handling to comments that belong to the active editor document - apply document scoping consistently across seed matching, subtree expansion, and final filtering - prevent cross-document comment/thread removal when IDs overlap across open docs - keep active-comment clearing document-aware using pre-delete comment snapshot - add regression test for overlapping IDs across documents during replay deletion * fix(superdoc): preserve comment identity on replay updates - replace raw replay Object.assign with a safe field-level updater for existing comment models - update only an explicit allowlist of mutable replay fields and normalize commentText from payload text/commentText - preserve useComment identity invariants (commentId/importedId and constructor-captured metadata) - strengthen replay update test to assert model identity remains stable via getValues() after updates * fix(diff-replay): include document identity in replayed comment events - add replay comment payload normalization to always emit commentId and preserve/add ownership metadata (documentId, fileId) - use editor.options.documentId as fallback when replay comment payload lacks document scope - keep existing payload documentId/fileId unchanged when already present - update replay editor typing in replayComments/replayDiffs to include optional options.documentId - add tests covering both fallback ownership injection and preservation of existing ownership fields * fix(comments-store): treat importedId as live in tracked-change prune - update tracked-change stale-prune liveness check to consider both commentId and importedId - prevent pruning live tracked-change threads when runtime commentId diverges but mark IDs still match importedId - keep existing descendant removal behavior unchanged - add regression test for replay/import flows where importedId remains stable while commentId changes * fix(comments-store): dedupe tracked-change sync by commentId and importedId - seed tracked-change existingIds with both runtime commentId and stable importedId - prevent duplicate tracked-change thread creation when grouped mark id matches an existing imported id - add created sync IDs (id, params.changeId, params.importedId) back into dedupe set during sync pass - add regression test for mixed-ID replay/sync scenario where commentId diverges but importedId remains live * fix(comments): keep replayed docxCommentJSON in export payloads - make useComment store docxCommentJSON as reactive state instead of a construction-time constant - update getValues() to return the current docxCommentJSON value - ensure replay-updated imported comment structure is reflected in translateCommentsForExport output - add unit test verifying getValues() returns updated docxCommentJSON after mutation * fix(superdoc): preserve DOCX comment structure on replay add - add replay payload normalization for comment model creation (text -> commentText, elements -> docxCommentJSON) - apply normalization in replay ADD path before useComment(...) - reuse the same normalization in replay UPDATE fallback when creating missing comments - ensure replay-added imported comments keep DOCX-native body structure for export/ round-trip - add regression test verifying replay ADD maps elements into docxCommentJSON * fix(superdoc): replayed comment resolution persistence for export - map replay isDone updates to resolvedTime/resolvedBy* when payload resolved fields are null/missing - apply the same fallback during replay payload normalization and model updates - refactor shared isDone resolution fallback logic to avoid duplicated code - add regression test covering replay update payloads with isDone: true and null resolved fields, ensuring resolved state is persisted and can be exported * fix(superdoc): scope replay update active comment id to matched document thread - return the matched comment’s concrete id (prefer commentId) from replay update matching - avoid cross-document active-thread misselection when importedId overlaps across open documents - update replay regression coverage to assert setActiveComment receives the active document’s thread id * fix(superdoc): stop replay comment updates from forcing active-thread reselection - only sync active comment when activeCommentId is explicitly present in the event payload - avoid inferring active selection from replay add/update events to prevent repeated focus/unfocus churn - preserve explicit active clear behavior on replay deletions - update replay update test expectation to reflect non-selecting replay events * test: add missing test documents * test: adjust diff replay test * fix(superdoc): update replay comment parent linkage fields on thread remap * fix(superdoc): scope replay add deduplication to active document context * fix(track-changes): preserve property attrs for ReplaceAroundStep updates * feat: implement diffing for styles * feat: implement replay for style differences * feat: implement diffing for numbering * feat: implement replay for numbering differences * fix(comments): scope tracked-change dedupe to active document * refactor(diffing): share deepEquals helper across replay modules * chore: add diffing example * fix(numbering): missing imports * chore: update lock file * test: add missing test documents * chore: fix diff test
1 parent 2db8f16 commit 33e2ce6

99 files changed

Lines changed: 30260 additions & 7919 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/features/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Focused examples that each demonstrate a single SuperDoc feature.
55
| Example | Description | Docs |
66
|---------|-------------|------|
77
| [track-changes](./track-changes) | Accept/reject workflow with suggesting mode | [Track Changes](https://docs.superdoc.dev/extensions/track-changes) |
8+
| [diffing](./diffing) | Compare two DOCX files and replay directional diffs as tracked changes | - |
89
| [ai-redlining](./ai-redlining) | LLM-powered document review with tracked changes | [AI Agents](https://docs.superdoc.dev/getting-started/ai-agents) |
910
| [comments](./comments) | Threaded comments with resolve workflow and event log | [Comments](https://docs.superdoc.dev/modules/comments) |
1011
| [custom-toolbar](./custom-toolbar) | Custom button groups, excluded items, and custom buttons | [Toolbar](https://docs.superdoc.dev/modules/toolbar) |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>SuperDoc Diffing Example</title>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
<script type="module" src="/src/main.ts"></script>
11+
</body>
12+
</html>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "superdoc-diffing-example",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build"
8+
},
9+
"dependencies": {
10+
"superdoc": "workspace:*",
11+
"vue": "^3.5.0"
12+
},
13+
"devDependencies": {
14+
"@vitejs/plugin-vue": "^5.2.0",
15+
"typescript": "^5.7.0",
16+
"vite": "^6.2.0"
17+
}
18+
}

0 commit comments

Comments
 (0)