feat(session): header/footer + page-number authoring (DocxSession → editor) (#236)#274
Merged
Conversation
DocxSession could only *inspect* a section's header/footer parts
(SectionInfo.HeaderPartUris/FooterPartUris) — there was no surface to
create or edit them, blocking faithful reproduction of real filings (the
S-1 running footer + centered page number could not be authored). Add
three methods, rippled through every layer per CLAUDE.md.
- SetHeaderText/SetFooterText(bodyAnchor, HeaderFooterKind, markdown):
set the running header/footer for the section that owns any body block.
The governing w:sectPr is resolved exactly as GetSectionInfo resolves
it (synthesizing a trailing sectPr if the body has none). Creates the
HeaderPart/FooterPart + relationship + w:headerReference/w:footerReference
of the requested kind if absent, else replaces its content. Paragraphs
get the built-in Header/Footer style (centre/right tab stops); First
sets w:titlePg, Even sets w:evenAndOddHeaders. Created returns the new
hdr{N}/ftr{N} paragraph anchors.
- InsertPageNumberField(anchor, PageNumberField=CurrentPage): append a
native complex PAGE/NUMPAGES field to a paragraph.
- Undo/redo now reconciles header/footer part create/delete in
RestoreSnapshot (snapshot records each part's relationship id; re-create
uses AddNewPart<T>(relId) so the restored sectPr reference resolves),
resolving the long-standing TODO there.
Ripple: DocxSessionOps + DocxSessionJson, WASM DocxSessionBridge + npm
DocxSession (setHeaderText/setFooterText/insertPageNumberField, new
HeaderFooterKind/PageNumberField types), stdio Dispatcher + docx-scalpel
(set_header_text/set_footer_text/insert_page_number_field, new enums),
plus docs (docx_mutation_api.md, CHANGELOG, CLAUDE.md). The editor's
visual header/footer region is a deliberate follow-up.
Tests: DocxSessionTests DS250-DS262 and a WASM Playwright spec
(docx-session-headerfooter). Full .NET suite (2682) green; library
Release build clean.
…t reordering settings
Both the DocxDiff header/footer renderer and the new DocxSession
SetHeaderText(Even) routed the whole settings root through
WmlOrderElementsPerStandard, whose Order_settings table lacked
hdrShapeDefaults/shapeDefaults — children real Word documents carry —
so those children were sorted out of their CT_Settings schema slots and
the part failed schema validation ("unexpected child element
'hdrShapeDefaults'", caught smoke-testing this PR against
TestFiles/DB006-Source2.docx).
- Add the two missing entries to Order_settings (updateFields <
hdrShapeDefaults < footnotePr; smartTagType < shapeDefaults <
doNotEmbedSmartTags, per the transitional CT_Settings sequence).
- Replace both private EnsureEvenAndOddHeaders copies with one shared
WordprocessingMLUtil.EnsureEvenAndOddHeaders that inserts the flag at
its own slot (before the first child the table knows to come later)
and never moves any other settings child — unknown children
(extension content) stay exactly where Word wrote them.
- Document the evenAndOddHeaders sharp edge: the flag is
document-global and governs footers too, so an Even header alongside
only a Default footer blanks even-page footers (spec-correct Word
behavior, verified in LibreOffice).
Tests: DS263 (synthetic settings carrying hdrShapeDefaults /
shapeDefaults) + DS264 (the real Word-authored fixture that caught the
bug). Full .NET suite 2684 passed / 0 failed / 3 skipped.
# Conflicts: # CHANGELOG.md
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.
Closes #236.
Evaluation: the issue is present
I verified the gap in the current codebase (post-v7.0.1).
DocxSessioncould inspect a section's header/footer parts (SectionInfo.HeaderPartUris/FooterPartUris— read-only) but had no method to create or edit them, and no page-number field op. Confirmed across every layer named in the issue:DocxSession.cs,DocxSessionBridge.cs,editor.ts,Dispatcher.cs,docx_scalpel. This is not a redlining concern and does not touchWmlComparer/DocxDiff— it's theDocxSessionmutation/editor surface — so per the task's scoping it's implemented directly (no redline-engine changes).What this adds
Three new
DocxSessionmethods, rippled through all layers perCLAUDE.md:SetHeaderText(bodyAnchor, HeaderFooterKind, markdown)SetFooterText(bodyAnchor, HeaderFooterKind, markdown)InsertPageNumberField(anchor, PageNumberField = CurrentPage)PAGE/NUMPAGESfield to a paragraph.SetHeaderText/SetFooterTexttake any body block in the target section; the governingw:sectPris resolved exactly asGetSectionInforesolves it (forward mid-doc section break → body trailingsectPr), synthesizing a trailingsectPrif the body has none. A non-body anchor is anAnchorWrongKinderror (a story attaches to a body section).HeaderFooterKind=Default/First/Even→ the referencew:type.Firstsets the section'sw:titlePg;Evensetsw:evenAndOddHeadersin the settings part — without those Word ignores the story. Same-kind twice reuses the part and replaces content (idempotent "set"); a mismatched/stale reference self-heals.InsertParagraph; each un-styled paragraph gets the built-inHeader/Footerstyle so it inherits Word's centre/right tab stops.Createdreturns the newhdr{N}/ftr{N}paragraph anchors → feed one toInsertPageNumberField. The field is a native complex field (fldChar/instrText, cached"1").RestoreSnapshotTODO). The snapshot records each part's relationship id; restore deletes parts the snapshot lacks and re-creates the ones it has with their original relId (AddNewPart<T>(relId)) so the restoredsectPrreference resolves. One documented edge: thew:evenAndOddHeadersflag (only set byEven) isn't reverted by undo — idempotent, no visual effect without an even story.Recipe — the S-1 running footer this issue was found reproducing
Ripple
DocxSessionOps+DocxSessionJson(single-owner facade first) → WASMDocxSessionBridge+ npmDocxSession(setHeaderText/setFooterText/insertPageNumberField, newHeaderFooterKind/PageNumberFieldstring-union types) → stdioDispatcher+docx-scalpel(set_header_text/set_footer_text/insert_page_number_field, new enums) → docs (docx_mutation_api.mdsection + anchor-lifecycle rows + decision tree,CHANGELOG,CLAUDE.md).Testing
DocxSessionTestsDS250–DS262 (create / reuse-replaces-content / footer+page-number compose /Firsttitle-page /Evensettings flag / undo↔redo of part creation / no-sectPrsynthesis / wrong-kind + non-paragraph errors /NUMPAGES/ saved-bytes reopen / empty payload). Full suite 2682 passed, 0 failed, 3 skipped.pyhostbuilds.tsc --noEmitclean; fullnpm run build(WASM + bundles) succeeds; new Playwright specdocx-session-headerfooter.spec.tsexercisesSetFooterText+InsertPageNumberFieldthrough the WASM bridge (compose → project → save → reopen) plus the non-body error envelope — both green.HeaderFooterKind/PageNumberFieldimport; thedocx-scalpelmethods mirror the existinginsert_paragraphshape (stdio integration not run in this environment).Out of scope (deliberate follow-ups, noted in the issue)
w:pgNumType,\* roman).🤖 Generated with Claude Code
https://claude.ai/code/session_01RjdqsSuxy8GJjK9TLVsfKd
Generated by Claude Code