feat: add header and footer diffing support (SD-2238)#2575
feat: add header and footer diffing support (SD-2238)#2575caio-pizzol merged 41 commits intomainfrom
Conversation
…tionship 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.
… 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.
…ules Export SLOT_VARIANTS from header-footer-diffing.ts and import it in replay-header-footers.ts instead of defining it independently in both.
Remove buildSectionMarginsForAttrs and toInches from replay-header-footers.ts in favor of the canonical readSectPrMargins from sections-xml.ts.
…hanged 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.
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.
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.
…e 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.
… 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.
…egrity 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
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.
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.
…ers) 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.
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.
…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.
…ingle 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.
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.
…ditor 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.
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`.
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.
…th 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.
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.
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.
…aderFooterUpdate 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.
caio-pizzol
left a comment
There was a problem hiding this comment.
@luccas-harbour approach looks good, no blocking issues.
left a few inline comments — mostly small cleanup stuff (unused params, duplicated helper, readability).
on tests: the delete path in replay-parts is never exercised (every test passes empty deletes). also no test uses footers — only headers — so a bug in footer-specific code wouldn't be caught. the adapter dispatch change in diff-adapter.ts could also use a test for header/footer-only diffs.
…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
There was a problem hiding this comment.
@luccas-harbour round 1 feedback is all fixed, looks good.
i pushed two tests that were missing: one for the adapter dispatch path (header-only diffs), and a doc-api story that diffs headers end-to-end and checks they survive save/reopen.
two things worth cleaning up later (not blocking): the test helpers are copy-pasted between two test files (~150 lines), and computeDiff now takes 13 arguments — an options object would be easier to read.
|
🎉 This PR is included in vscode-ext v1.1.0-next.21 |
|
🎉 This PR is included in superdoc v1.24.0-next.20 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-cli v0.5.0-next.20 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.3.0-next.20 |
|
🎉 This PR is included in esign v2.2.0-next.3 The release is available on GitHub release |
|
🎉 This PR is included in template-builder v1.3.0-next.2 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.24.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-cli v0.5.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.3.0 |
|
🎉 This PR is included in vscode-ext v2.2.0 |
Summary
.rels, media binaries) so that header/footer assets (images, etc.) are correctly transferred during diff replay, including collaboration-aware media upsertsKey changes
header-footer-diffing.tscaptures and diffs section slot assignments and part XML content;parts-diffing.tscaptures OPC part closures (.relstraversal) and produces coarse upsert/delete diffsreplay-header-footers.tsapplies slot ref changes, replays part XML diffs via ProseMirror transactions, and syncs converter caches (headers/footers/IDs/titlePage);replay-parts.tsupserts/deletes raw parts and publishes media to collaborationcaptureSnapshotnow includesheaderFootersandpartsStatein the payload;compareToSnapshotandapplyDiffPayloadthread the new slots through the pipeline with version-aware coverage validationV2_COVERAGEenablesheaderFooters: true; snapshot versionsd-diff-snapshot/v2and payload versionsd-diff-payload/v2are introduced alongside v1 compatibilityEditor.tsclones extension instances duringcreateExtensionServiceso multiple editors don't share mutable storageDiffCoverage.headerFooterswidened fromfalsetoboolean;DiffSummary.changedComponentsextended withheaderFootersandparts