Skip to content

feat: add header and footer diffing support (SD-2238)#2575

Merged
caio-pizzol merged 41 commits intomainfrom
luccas/sd-2238-feature-add-header-and-footer-diffing-support
Mar 26, 2026
Merged

feat: add header and footer diffing support (SD-2238)#2575
caio-pizzol merged 41 commits intomainfrom
luccas/sd-2238-feature-add-header-and-footer-diffing-support

Conversation

@luccas-harbour
Copy link
Copy Markdown
Contributor

Summary

  • Adds full header/footer diffing and replay support to the diff engine, introducing a v2 snapshot and payload format that captures section slot references, part content, and associated media closures
  • Implements parts-level diffing (.rels, media binaries) so that header/footer assets (images, etc.) are correctly transferred during diff replay, including collaboration-aware media upserts
  • Isolates extension storage across editors sharing the same extension list, preventing cross-editor state leakage during diff operations
  • Maintains backward compatibility with v1 snapshots/payloads (body, comments, styles, numbering only)

Key changes

  • New algorithm modules: header-footer-diffing.ts captures and diffs section slot assignments and part XML content; parts-diffing.ts captures OPC part closures (.rels traversal) and produces coarse upsert/delete diffs
  • New replay modules: replay-header-footers.ts applies slot ref changes, replays part XML diffs via ProseMirror transactions, and syncs converter caches (headers/footers/IDs/titlePage); replay-parts.ts upserts/deletes raw parts and publishes media to collaboration
  • Diff service (v2): captureSnapshot now includes headerFooters and partsState in the payload; compareToSnapshot and applyDiffPayload thread the new slots through the pipeline with version-aware coverage validation
  • Coverage & versioning: V2_COVERAGE enables headerFooters: true; snapshot version sd-diff-snapshot/v2 and payload version sd-diff-payload/v2 are introduced alongside v1 compatibility
  • Editor isolation: Editor.ts clones extension instances during createExtensionService so multiple editors don't share mutable storage
  • Contract updates: DiffCoverage.headerFooters widened from false to boolean; DiffSummary.changedComponents extended with headerFooters and parts

…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.
Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment thread packages/super-editor/src/extensions/diffing/replay/replay-parts.ts Outdated
Comment thread packages/super-editor/src/extensions/diffing/replay/replay-header-footers.ts Outdated
…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
Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@caio-pizzol caio-pizzol merged commit 4b6f30e into main Mar 26, 2026
48 of 49 checks passed
@caio-pizzol caio-pizzol deleted the luccas/sd-2238-feature-add-header-and-footer-diffing-support branch March 26, 2026 21:32
@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Mar 26, 2026

🎉 This PR is included in vscode-ext v1.1.0-next.21

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Mar 26, 2026

🎉 This PR is included in superdoc v1.24.0-next.20

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Mar 26, 2026

🎉 This PR is included in superdoc-cli v0.5.0-next.20

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Mar 26, 2026

🎉 This PR is included in superdoc-sdk v1.3.0-next.20

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Mar 30, 2026

🎉 This PR is included in esign v2.2.0-next.3

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Mar 30, 2026

🎉 This PR is included in template-builder v1.3.0-next.2

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Mar 30, 2026

🎉 This PR is included in superdoc v1.24.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Mar 31, 2026

🎉 This PR is included in superdoc-cli v0.5.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Apr 2, 2026

🎉 This PR is included in superdoc-sdk v1.3.0

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented Apr 2, 2026

🎉 This PR is included in vscode-ext v2.2.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants