feat: edit and track-change footnote/endnote bodies#646
Open
jacobjove wants to merge 8 commits into
Open
Conversation
Note bodies were parsed but never serialized: rezip copied footnotes.xml / endnotes.xml verbatim, so edits and tracked changes inside notes were dropped on save. The run parser also discarded the separator markers (w:separator / w:continuationSeparator) and the in-body auto-number marks (w:footnoteRef / w:endnoteRef), making the note model lossy. Add SeparatorContent and NoteRefMarkContent to the run-content model, parse them, and serialize them, so note bodies round-trip through the same serializeBlockContent the document body uses — preserving w:ins/w:del, run / paragraph properties, fields, and tables. Add serializeFootnotes / serializeEndnotes (noteSerializer.ts) reusing that block serializer. This is the model layer only; rezip wiring follows separately. Refs eigenpal#378 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
repackDocx and repackDocxFromRaw copied word/footnotes.xml and word/endnotes.xml verbatim from the original ZIP, so edits and tracked changes made to note bodies never persisted. Add serializeFootnotesToZip / serializeEndnotesToZip (mirroring serializeCommentsToZip) and call them from both repack paths. Separator notes are parsed out of package.footnotes/endnotes (which feed layout and must stay normal-only), so retain them in new package.footnoteSeparators / endnoteSeparators fields and re-emit them ahead of the normal notes. Content-type / rels registration is intentionally skipped: a document carrying notes already declares the part, and notes-from-scratch is out of scope. Verified end-to-end against a 38-endnote document: an edit to a note body persists through repack and reparse, separator count is preserved, and the body endnote-reference count is unchanged. Refs eigenpal#378 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getChanges() walked only the document body, so tracked changes inside note
bodies were invisible to the review pipeline. Add opt-in includeFootnotes /
includeEndnotes filter flags; when set, walk the package's footnote/endnote
bodies too. Each note change carries noteId + noteType ('footnote'|'endnote');
paragraphIndex is note-local for those.
Default behavior is unchanged — body-only unless a caller opts in. Note bodies
live on the package, so DocxReviewer.getChanges threads them into the walk;
the new forEachNoteParagraph mirrors forEachParagraph over a note's blocks.
Refs eigenpal#378
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a synthetic endnotes-tracked-changes.docx fixture (separators, the w:endnoteRef number mark, and a w:ins inside an endnote body) plus a generator in generate-fixtures.ts. Two integration tests exercise the full path against it: core parses → edits a note → repacks → reparses (edit + separators + endnoteRef survive; body refs unchanged), and agents' DocxReviewer.getChanges surfaces the in-note insertion with noteType/noteId only when includeEndnotes is set. Refs eigenpal#378 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refs eigenpal#378 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Someone is attempting to deploy a commit to the EigenPal Team on Vercel. A member of the Team first needs to authorize it. |
Contributor
|
All contributors have signed the CLA ✍️ ✅ Posted by the CLA bot. |
A tracked-change w:id is unique only within its part (document.xml / footnotes.xml / endnotes.xml), so the same id can appear in the body and in a note. The change map was keyed by id alone, so with includeFootnotes/ includeEndnotes set, a note change could clobber a body change sharing its id. Key on location + id instead. Add a regression test for the collision. Also document that note changes are surfaced for discovery only — accept/reject still operate on the body, so a change carrying noteId can't yet be accepted/rejected. Refs eigenpal#378 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
|
I have read the CLA Document and I hereby sign the CLA |
Hoist the full OOXML_NAMESPACES + MC_IGNORABLE block into serializer/ xmlUtils.ts so commentSerializer and noteSerializer share one copy instead of two verbatim ~40-line duplicates. Collapse forEachParagraph and forEachNoteParagraph onto a shared walkParagraphs helper, dropping the duplicated table walk and a dead `as Table` cast. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jacobjove
pushed a commit
to jacobjove/docx-editor
that referenced
this pull request
May 31, 2026
eigenpal#646 surfaced note-body tracked changes for discovery only — accept/reject operated on the document body, so a change carrying noteId/noteType could not be resolved. Lift that: pass a ReviewChange from getChanges to acceptChange/ rejectChange to resolve it inside its footnote/endnote, or use acceptAll/ rejectAll with { includeFootnotes, includeEndnotes } for bulk. The result persists via eigenpal#646's note save path on toBuffer(). The per-item mutation (applyChangeAtIndex) and the numeric acceptChange(id) body path are unchanged; note resolution reuses forEachNoteParagraph and a shared processParagraph helper. Within a located note all matching items are processed (notes are small, bounded); the body keeps its first-paragraph-stop. Programmatic DocxReviewer surface only — not exposed as an agent/MCP tool, per wordCompat's "accept/reject is human-only" non-goal for the tool surface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
#378 / #415 routed footnote and endnote rendering through the body pipeline. The save path was never updated:
repackDocxcopiesword/footnotes.xmlandword/endnotes.xmlverbatim from the original archive, so any edit or tracked change made to a note body is silently dropped on save.getChanges()also walks only the document body, so tracked changes inside notes are invisible to the review pipeline.This PR adds the save-side and review-side counterparts, symmetric across footnotes and endnotes.
Changes
noteSerializer.ts):serializeFootnotes/serializeEndnotesreuse the document body'sserializeBlockContent, so note bodies round-trip with full fidelity —w:ins/w:del, run/paragraph properties, fields, tables — instead of a minimal hand-rolled serializer.<w:separator>/<w:continuationSeparator>and the in-body auto-number marks<w:footnoteRef>/<w:endnoteRef>. AddedSeparatorContentandNoteRefMarkContentso note bodies round-trip losslessly (all existing run consumers use permissive defaults; typecheck clean).repackDocx/repackDocxFromRawwrite the note parts viaserialize{Footnotes,Endnotes}ToZip. Separator notes are kept in newpackage.footnoteSeparators/endnoteSeparators(out offootnotes/endnotes, which feed layout/font/variable code and must stay normal-only) and re-emitted ahead of the normal notes.DocxReviewer.getChanges()gains opt-inincludeFootnotes/includeEndnotes; in-note changes carrynoteId/noteType. Default behavior is unchanged (body-only).endnotes-tracked-changes.docxfixture + integration tests (parse → edit a note → repack → reparse;getChangesdiscovery from a parsed doc).Notes for review
A few choices I'd value your steer on — none of this was covered by #378/#415, which were render-only:
serializeBlockContentcan be reused unchanged. The alternative was selective raw-XML passthrough of unedited notes, which doesn't help edited notes.*Separatorspackage fields rather than mixed intofootnotes/endnotes, since layout/font/variable detection expect those to be normal notes only.serializeCommentsToZip— the same contract as the body, whichrepackDocxalready rewrites every save. Happy to gate behind an edit flag if you'd prefer.Verification
bun run typecheckclean across all packages;bun test941 pass / 0 fail;bun run lint0 errors;api:extractcommitted;bun changesetadded (minor). Also checked end-to-end against a real 38-endnote document: an edit to a note body persists through repack + reparse, separator andw:endnoteRefmarkers survive, and body endnote-reference count is unchanged.Follow-up to #378 / #415.
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.