Skip to content

fix(header-footer): centralize OOXML ref inheritance for first-page headers (SD-2997)#3264

Open
luccas-harbour wants to merge 12 commits into
luccas/sd-2990-feature-headerfooter-page-numbersfrom
luccas/sd-2997-feature-different-first-page-headers
Open

fix(header-footer): centralize OOXML ref inheritance for first-page headers (SD-2997)#3264
luccas-harbour wants to merge 12 commits into
luccas/sd-2990-feature-headerfooter-page-numbersfrom
luccas/sd-2997-feature-different-first-page-headers

Conversation

@luccas-harbour
Copy link
Copy Markdown
Contributor

Summary

Fixes SD-2997 (different first-page headers) by centralizing the OOXML
header/footer ref inheritance rules into a single helper and pointing the
three places that used to re-implement them at the shared implementation.
Along the way, this corrects several inheritance bugs that broke first
and even variants in multi-section documents.

What changed

  • New @superdoc/contracts helperresolveInheritedHeaderFooterRef
    / resolveInheritedHeaderFooterRefWithType encode the OOXML resolution
    order (page refs → current section → walk back to the nearest prior
    section that defines the variant → legacy identifier) plus the rule
    that odd may fall back to default but never vice-versa when
    w:evenAndOddHeaders is enabled.
  • Three call sites unifiedlayout-engine/index.ts,
    layout-bridge/headerFooterUtils.ts, and
    HeaderFooterSessionManager.ts now delegate to the shared helper
    instead of carrying three near-duplicate copies of the rules.
  • Walk back through intermediate sections — previously the resolver
    only consulted the immediately prior section, so a first ref defined
    in section 0 was lost once section 1 (with only default) sat between
    it and a later section that also lacked an explicit first ref. The
    shared resolver now scans back to the nearest prior section that
    defines the requested variant.
  • Header height uses the resolved variant — the page-margin
    calculation in layout-engine now uses the resolved variant
    (headerResolution.variantType) for the height lookup, so inherited
    default refs don't end up sized against a non-existent variant slot.
  • Per-section titlePg honored in the fallback path
    HeaderFooterSessionManager was classifying first pages with the
    document-level titlePg flag only; multi-section docs that override
    titlePg per section now consult the section's own value via the
    multi-section identifier's sectionTitlePg map.
  • Layout-bridge type/variant gatinggetHeaderFooterTypeForSection
    now returns null when neither the section nor its ancestors define a
    usable ref for the chosen parity (or default), so missing even-page
    headers no longer render as the odd/default ref.
  • Converter fallback refs preserved@superdoc/contracts
    inheritance helpers no longer let a later section's empty refs shadow
    an earlier section's resolved ones during fallback computation.

Tests

  • New header-footer-inheritance.test.ts in contracts covering the
    walk-back, parity, and odd → default fallback rules.
  • Expanded headerFooterUtils.test.ts and layout-engine/index.test.ts
    to assert the resolved variant flows through to height and margin
    calculations.
  • Expanded HeaderFooterSessionManager.test.ts to cover the
    per-section titlePg decoration path, including the missing-map case.

…regions

When inferring header/footer region variants without explicit instance metadata,
the fallback path only consulted the document-level titlePg flag. Multi-section
documents that override titlePg per section ended up classifying the first page
as 'default' instead of 'first'. Use the multi-section identifier's
sectionTitlePg map when available so each section's variant is respected.
Extract the OOXML header/footer ref inheritance logic into a shared
helper (`resolveInheritedHeaderFooterRef`) in `@superdoc/contracts` and
use it from layout-engine, layout-bridge, and HeaderFooterSessionManager.
This replaces three near-duplicate copies of the same resolution rules.

While unifying the logic, fix inheritance through intermediate sections
that omit `first`/`even` refs: previously the resolver only looked at the
immediately prior section, so a `first` ref defined in section 0 was lost
once section 1 (with only a `default` ref) sat between section 0 and a
later section that also lacked an explicit `first` ref. The shared
resolver now walks back to the nearest prior section that defines the
requested variant.
@luccas-harbour luccas-harbour requested a review from a team as a code owner May 13, 2026 13:21
@linear
Copy link
Copy Markdown

linear Bot commented May 13, 2026

SD-2997

@github-actions
Copy link
Copy Markdown
Contributor

Status: PASS

The two translator files change how sd:autoPageNumber and sd:totalPageNumber round-trip PAGE/NUMPAGES field instructions. I verified the new behavior against ECMA-376 Part 1 §17.16 (complex fields) and §17.16.4 (field switches).

What the PR emits on the OOXML side:

  • w:fldChar with w:fldCharType of begin / separate / end — all three are valid ST_FldCharType values per the spec, and w:dirty="true" is conditionally added on the begin run only when the cache is stale (consistent with §17.16.5.34).
  • w:instrText xml:space="preserve" containing strings like " PAGE", " PAGE \* roman", " PAGE \* ArabicDash", " NUMPAGES \# \"00\"". Leading space + xml:space="preserve" is the standard Word emission for field instructions.
  • The cached-result run between separate and end in totalPageNumber is a w:t with xml:space="preserve", which is required since zero-padded values can be "007" (significant leading characters).

Switch handling (verified against §17.16.4.1 general-format and §17.16.4.2 numeric-formatting):

  • \* mappings (Arabic, roman/Roman/ROMAN, alphabetic/ALPHABETIC, ArabicDash) are all Word-recognized format-switch names. ArabicDash is the Word-emitted form for the numberInDash page-number style and round-trips correctly.
  • \# with all-zeros picture ("00", 000) is a valid numeric picture per the spec; using it as a zero-padding hint and defaulting pageNumberFormat to decimal is consistent with Word's behavior.

The custom attributes (instruction, pageNumberFormat, pageNumberZeroPadding) live only on SuperDoc's own sd:-namespaced elements, so they don't create any vocabulary conflicts with the WordprocessingML schema. The exported OOXML is the original Word field-instruction syntax — nothing invented.

No spec violations found.

@luccas-harbour luccas-harbour changed the base branch from main to luccas/sd-2990-feature-headerfooter-page-numbers May 13, 2026 13:27
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

3 participants