Commit 4b6f30e
feat: add header and footer diffing support (SD-2238) (#2575)
* feat(diff): add header/footer diff capture and replay
* feat(editor): refresh presentation after header/footer diff replay
* fix(diff): restore v1 compatibility and header/footer replay state
* fix(diff): gate header/footer capture on target coverage in compareToSnapshot
* fix(diff): reuse normalizePartPath in replay to fix denormalized relationship targets
findPartPathByRefId only prepended "word/" without stripping relative
prefixes (./、../、/), producing keys like "word/./header1.xml" that
don't match the canonical keys used during capture. Import and call the
shared normalizePartPath instead.
* fix(diff): only sync title page cache when slot changes were actually applied
syncTitlePageCache was called whenever the diff contained slot changes,
even if all of them were skipped (e.g. section projection not found).
Track the count of successfully applied slot changes and gate the cache
sync on that instead.
* refactor(diff): deduplicate SLOT_VARIANTS constant across diffing modules
Export SLOT_VARIANTS from header-footer-diffing.ts and import it in
replay-header-footers.ts instead of defining it independently in both.
* refactor(diff): replace inline margin parsing with readSectPrMargins
Remove buildSectionMarginsForAttrs and toInches from replay-header-footers.ts
in favor of the canonical readSectPrMargins from sections-xml.ts.
* refactor(diff): emit partChanged instead of custom headerFooterPartsChanged event
Replace the bespoke headerFooterPartsChanged event with a standard
partChanged emission so diff replay reuses the same handler that
document-api part mutations already use in PresentationEditor.
Add partPath to ModifiedHeaderFooterPart so the replay can build
accurate PartChangedEvent entries for every changed part.
* feat(diff): thread partsDiff through diff/replay pipeline
Add a new `parts` component to the diffing system that will support
OOXML part-level and media asset diffs. This commit wires the plumbing
end-to-end (compute → replay → summary → service → schema) with
placeholder no-op implementations, so subsequent changes can populate
the actual diff logic without reshaping the service contract.
* feat(diff): implement parts closure capture, diffing, and replay
Implement the actual logic for the partsDiff pipeline:
- capturePartsState walks header/footer parts and collects their full
OPC closure (XML parts, .rels files, and referenced media binaries)
- diffParts compares closures between base and target to produce
upserts and deletes scoped to header/footer changes
- replayPartsDiff applies upserts into convertedXml and media stores,
and removes deleted parts
- Wire capturePartsState into the diffing extension and diff-service
so snapshots include partsState
Includes a test verifying header part dependencies (images) round-trip
through the diff/replay pipeline.
* fix(editor): isolate extension storage across editors sharing the same extension list
Clone extension instances during editor creation so each editor gets
its own storage objects. Previously, editors constructed from the same
extensions array shared mutable storage references, causing one editor's
state (e.g. media files) to leak into or be destroyed alongside another.
* feat(diff): capture and diff body document.xml.rels closure for media replay
Extend parts diffing to cover the document body's relationship closure
alongside header/footer closures. When docDiffs are present, the body's
document.xml.rels is walked to capture referenced media and their
dependencies, excluding parts already handled by dedicated diff
channels (styles, numbering, comments, headers/footers, etc.).
Includes integration tests verifying body media round-trips through
both the direct compare/replay and snapshot-based diff-service paths.
* feat(diff): add partsFingerprint to snapshot and diff payload for integrity checks
Introduce a separate partsFingerprint (computed over the canonical state
including partsState) alongside the existing semantic fingerprint. This
lets the diff-service detect when a document's part/media state has
drifted even if the semantic content (body, comments, styles) hasn't
changed.
- captureSnapshot now emits both fingerprint and partsFingerprint
- compareToSnapshot re-derives and validates both fingerprints
- applyDiffPayload rejects payloads when partsFingerprint mismatches
- Schemas and types updated for v2 snapshots/payloads/apply results
* fix(diff): prevent deletion of parts still reachable by other closures
When removing a header/footer part, check whether its dependencies
(media, .rels files) are still referenced by another closure (body or
remaining header/footer) before marking them for deletion. This avoids
deleting shared assets like images used by multiple headers.
* fix(diff): resolve .rels paths relative to the part's own directory
toRelsPathForPart previously hardcoded the `word/_rels/` prefix, which
broke resolution for nested parts like `word/charts/chart1.xml`. Now
it derives the rels path from the part's actual directory. Also skips
.rels files themselves to avoid infinite recursion.
Adds a unit test verifying nested chart → embedded workbook closure
capture through relative relationship targets.
* fix(diff): skip partsDiff when partsState is unavailable (legacy callers)
Guard diffParts so it returns null when either old or new partsState is
missing, which happens when compareDocuments is called without a compare
editor. This preserves backward compatibility for legacy callers that
don't provide part closure state.
* feat(diff): detect and replay header/footer part path renames
Track the old part path on modified header/footer parts so the replay
can relocate XML and .rels entries when a part's filename changes (e.g.
header1.xml → header2.xml) even if the content is identical. The
diffing algorithm now treats a part path change as a modification, and
replay moves the XML/rels entries and updates the relationship target.
* refactor(diff): fold partsState into the main fingerprint instead of a separate partsFingerprint
Remove the dedicated partsFingerprint from snapshots, payloads, and
apply results. Instead, include partsState in the canonical diffable
state used to compute the single fingerprint, so part/media drift is
detected by the existing fingerprint mismatch check without adding
extra fields to the public API surface.
* refactor(diff): unify body and header/footer closure diffing into a single owned-parts strategy
Replace the separate body-if-docDiffs and header/footer-if-headerFootersDiff
branches with a unified approach: collect all "owned" parts from both
closures (excluding semantic roots like document.xml, styles.xml, and
header/footer XML files which are handled by their own diff channels),
then diff the two owned-part maps to produce upserts and deletes.
This simplifies the logic and correctly detects asset-only changes
(e.g. an image replacement) even when there are no semantic doc diffs.
* feat(diff): emit partChanged event after parts replay
Emit a `partChanged` event from `replayPartsDiff` listing all parts
that were created, mutated, or deleted during replay. This allows
downstream consumers (e.g. the layout engine) to react to part-level
changes without polling converter state.
* refactor(diff): simplify compareDocuments to accept a single target editor
Replace the multi-argument compareDocuments signature (doc, comments,
styles, numbering, headerFooters) with a single `targetEditor` param.
The command now derives all comparison inputs (comments, styles,
numbering, header/footer state, parts state) directly from the target
editor, eliminating boilerplate at every call site and ensuring parts
state is always captured.
* refactor(diff): inline resolveOpcTargetPath and tighten type annotations
Copy `resolveOpcTargetPath` into parts-diffing to remove the import
dependency on super-converter/helpers, making the diffing module
self-contained. Also fix the `cloneExtensionInstance` generic to avoid
exposing `constructor` on the public type, add explicit type aliases
for header/footer variant IDs and relationship elements, and widen
the `ReplayDiffsParams` editor shape to include `state.doc` and
`mediaFiles`.
* fix(diff): sync converter variant ID caches when replaying slot changes
After applying header/footer slot ref changes to the section properties,
also update the converter's `headerIds` and `footerIds` caches to match.
Without this, downstream code reading variant IDs (e.g. section
resolution) would see stale refs after a diff replay repoints a section
to a different header or footer.
* fix(diff): emit delete+create partChanged events for header/footer path renames
When a modified header/footer part has a different path than before,
emit a delete for the old path and a create for the new path instead
of a single mutate event. This ensures downstream consumers correctly
tear down the old part and initialize the new one.
* fix(diff): validate that payload coverage matches its declared version
Reject diff payloads whose coverage doesn't match the expected profile
for their version (e.g. a v1 payload claiming headerFooters coverage).
This prevents applying payloads that were manually tampered with or
constructed from mismatched version/coverage combinations.
* feat(diff): publish replayed media upserts to collaboration
Call `addImageToCollaboration` for binary media files under `word/media/`
during parts replay, so that images added via diff (e.g. header logos)
are synced to other collaboration participants.
* refactor(diff): restore resolveOpcTargetPath import and drop stale headerFooterUpdate event
Revert the inlined `resolveOpcTargetPath` in favor of the existing
import from super-converter/helpers. Also remove the unused
`headerFooterUpdate` event emission from header/footer replay, since
`partChanged` already covers the notification.
* fix(diffing-example): pass target editor to compareDocuments
* fix(diffing): mark parts replay as document modified
* test: remove logs from test
* fix: import error
* test: add missing test documents
* fix: emit slot clears for removed header/footer sections
* refactor: simplify replay parts media store initialization
* refactor: remove unused diffParts parameters
* refactor: share diffing rels path helper
* test: cover replay parts deletions
* test: add footer diff replay coverage
* test: cover snapshot compare for footer-only diffs
* test: add adapter dispatch and doc-api story tests for header/footer diffing
- Add diff-adapter.test.ts: verifies createDiffAdapter().apply() dispatches
the transaction for header-only diffs when tr.docChanged is false but
appliedOperations > 0
- Add header-footer-diff-roundtrip.ts: end-to-end doc-api story that diffs
two documents with different headers, applies the diff, saves to DOCX,
and verifies header content persists through reopen
---------
Co-authored-by: Caio Pizzol <caio@harbourshare.com>1 parent a4736c8 commit 4b6f30e
32 files changed
Lines changed: 3897 additions & 110 deletions
File tree
- examples/features/diffing/src
- packages
- document-api/src
- contract
- diff
- super-editor/src
- core
- super-converter
- document-api-adapters
- extensions/diffing
- algorithm
- replay
- service
- tests/data/diffing
- superdoc/src/dev/components
- tests/doc-api-stories/tests/diff
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
259 | 259 | | |
260 | 260 | | |
261 | 261 | | |
262 | | - | |
263 | | - | |
264 | | - | |
265 | | - | |
266 | | - | |
267 | | - | |
268 | | - | |
269 | | - | |
270 | | - | |
271 | | - | |
272 | | - | |
273 | | - | |
274 | | - | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
275 | 265 | | |
276 | 266 | | |
277 | 267 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2777 | 2777 | | |
2778 | 2778 | | |
2779 | 2779 | | |
2780 | | - | |
| 2780 | + | |
2781 | 2781 | | |
2782 | 2782 | | |
2783 | 2783 | | |
2784 | 2784 | | |
2785 | 2785 | | |
2786 | 2786 | | |
2787 | 2787 | | |
2788 | | - | |
| 2788 | + | |
| 2789 | + | |
| 2790 | + | |
| 2791 | + | |
2789 | 2792 | | |
2790 | 2793 | | |
2791 | 2794 | | |
2792 | 2795 | | |
| 2796 | + | |
| 2797 | + | |
2793 | 2798 | | |
2794 | | - | |
| 2799 | + | |
2795 | 2800 | | |
2796 | 2801 | | |
2797 | 2802 | | |
2798 | 2803 | | |
2799 | | - | |
| 2804 | + | |
2800 | 2805 | | |
2801 | 2806 | | |
2802 | 2807 | | |
| |||
2807 | 2812 | | |
2808 | 2813 | | |
2809 | 2814 | | |
2810 | | - | |
| 2815 | + | |
2811 | 2816 | | |
2812 | 2817 | | |
2813 | 2818 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
23 | | - | |
| 22 | + | |
| 23 | + | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
| 60 | + | |
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
81 | | - | |
| 81 | + | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
| 84 | + | |
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
| 26 | + | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
| 56 | + | |
55 | 57 | | |
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
59 | | - | |
| 61 | + | |
60 | 62 | | |
61 | 63 | | |
62 | 64 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
199 | 199 | | |
200 | 200 | | |
201 | 201 | | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
202 | 242 | | |
203 | 243 | | |
204 | 244 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
100 | 123 | | |
101 | 124 | | |
102 | 125 | | |
| |||
2003 | 2026 | | |
2004 | 2027 | | |
2005 | 2028 | | |
2006 | | - | |
2007 | | - | |
2008 | | - | |
2009 | | - | |
| 2029 | + | |
| 2030 | + | |
| 2031 | + | |
| 2032 | + | |
| 2033 | + | |
| 2034 | + | |
| 2035 | + | |
| 2036 | + | |
2010 | 2037 | | |
2011 | | - | |
| 2038 | + | |
2012 | 2039 | | |
2013 | 2040 | | |
2014 | 2041 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
| 69 | + | |
69 | 70 | | |
Lines changed: 133 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
| 42 | + | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| |||
0 commit comments