Skip to content

refactor(layout): centralize header/footer ref resolution in a shared contract (SD-2989)#3577

Merged
harbournick merged 28 commits into
luccas/sd-2990-feature-headerfooter-page-numbersfrom
luccas/sd-2989-feature-headerfooter-section-references
Jun 3, 2026
Merged

refactor(layout): centralize header/footer ref resolution in a shared contract (SD-2989)#3577
harbournick merged 28 commits into
luccas/sd-2990-feature-headerfooter-page-numbersfrom
luccas/sd-2989-feature-headerfooter-section-references

Conversation

@luccas-harbour
Copy link
Copy Markdown
Contributor

Summary

Header/footer reference resolution β€” the OOXML inheritance logic that decides which variant (default / first / even / odd) applies to a page and which rId backs it β€” was duplicated across four call sites, each with subtly different fallback rules. This drift caused mismatches between the margins computed during pagination and the content actually painted, especially around titlePg, evenAndOddHeaders, and cross-section inheritance.

This PR extracts that logic into a single shared module in @superdoc/contracts and routes every consumer through it, so pagination, rendering, the presentation editor, and the document-api adapters all resolve refs identically.

What changed

New shared contract (packages/layout-engine/contracts/src/header-footer-resolution.ts):

  • selectHeaderFooterVariantForPage(...) β€” pure variant selection from titlePg, alternateHeaders, and the document/section page numbers. First page of a section with titlePg β†’ first; alternating headers β†’ even/odd by document page parity; otherwise default.
  • resolveEffectiveHeaderFooterRef(...) β€” walks sections backward (toward section 0) to find the effective rId for a (kind, variant), returning the matched section index and the matched variant. Encodes the OOXML rule that an odd request may fall back to default content, while even must not borrow default.

Consumers now delegate to the shared resolver (removing their bespoke inheritance/fallback blocks):

  • layout-bridge/headerFooterUtils.ts β€” getHeaderFooterTypeForSection, getHeaderFooterIdForPage, and resolveHeaderFooterForPageAndSection. The identifier now carries an ordered sections array (built in buildMultiSectionIdentifier) for the resolver to consume.
  • layout-engine/index.ts β€” per-page margin calculation during pagination. The old inline getVariantTypeForPage helper and the multi-step manual inheritance are replaced with calls to the shared functions; heights resolve to 0 when no ref applies.
  • presentation-editor/HeaderFooterSessionManager.ts β€” drops its duplicated header/footer rId resolution and uses the matched variant for layout-slot lookup.
  • document-api-adapters/header-footer-refs-mutation.ts β€” resolveEffectiveRef is now a thin wrapper over the shared resolver (and no longer needs the editor argument, simplifying its three call sites in the adapter, slot materialization, and story runtime).

Notable fixes folded in

  • Sparse section metadata resolved by index, not array position (layout-engine/index.ts): getSectionMetadata now matches on section.sectionIndex rather than the array slot, so documents with non-contiguous section metadata resolve correctly. Runtime section refs are tracked in a Map keyed by section index.
  • Converter-provided header refs preserved: section 0 now merges fallback refs from the converter (mergeSection0FallbackRefs) so explicitly-imported refs aren't dropped during resolution.

Tests

  • New unit suite for the shared resolver (header-footer-resolution.test.ts, +121).
  • Expanded coverage in headerFooterUtils.test.ts (+105), layout-engine/index.test.ts (+159), and header-footer-refs-mutation.test.ts (+46) covering variant selection, oddβ†’default fallback, even-without-default, cross-section inheritance, and sparse metadata.

@luccas-harbour luccas-harbour requested a review from a team as a code owner May 29, 2026 19:39
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 29, 2026

SD-2989

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89d7cd2058

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/layout-engine/layout-engine/src/index.ts Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

cubic analysis

3 issues found across 14 files

Linked issue analysis

Linked issue: SD-2989: Feature: Header/Footer Section References

Status Acceptance criteria Notes
βœ… Add a shared resolver implementing selectHeaderFooterVariantForPage(...) and resolveEffectiveHeaderFooterRef(...) (centralize variant selection and OOXML inheritance) New module with both functions was added and exported from contracts; unit tests for the resolver were added.
βœ… Ensure oddβ†’default fallback but prevent even/from-default borrowing (OOXML semantics) Resolver encodes oddβ†’default candidate order and tests assert odd falls back to default and even/first do not inherit default.
βœ… Select 'first' for section first page when titlePg is enabled and treat missing first as absent (no implicit default inheritance for first) Variant selection returns 'first' for titlePg; resolver/consumers return null when no matching first exists (tests updated to expect null).
βœ… Wire consumers to delegate to the shared resolver (layout-bridge, layout-engine, presentation-editor, document-api-adapters) Multiple call sites were refactored to import and call the shared functions instead of bespoke logic.
βœ… Preserve/merge converter-provided fallback refs for section 0 when building multi-section identifiers Section-0 merge logic was added and tests verify converter fallbacks are exposed and prefer non-null section refs.
βœ… Layout engine uses shared resolver for margin/header/footer height calculations, including sparse section metadata and inheritance Layout engine now builds resolution sections and resolves effective refs for header/footer heights; many layout tests added to validate inherited first/even refs, sparse metadata, reset-to-base when blank, and default-backed odd heights.
βœ… Presentation editor uses matchedVariant from resolver for layout-slot lookup (consistent resolved variant vs requested variant) Presentation editor removes duplicated resolution and uses resolved.matchedVariant when choosing the layout variant.
βœ… Document API adapters use shared resolver (simplify resolveEffectiveRef and remove editor dependency) and tests for adapter resolver behavior Adapter helpers were refactored to call the shared resolver and tests were added to assert inheritance rules.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/layout-engine/layout-engine/src/index.ts Outdated
Comment thread packages/layout-engine/layout-bridge/src/headerFooterUtils.ts Outdated
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.

πŸ“’ Thoughts on this report? Let us know!

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/layout-engine/layout-engine/src/index.ts Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/layout-engine/layout-bridge/src/headerFooterUtils.ts
@luccas-harbour luccas-harbour requested a review from tupizz June 2, 2026 19:35
Copy link
Copy Markdown
Contributor

@tupizz tupizz left a comment

Choose a reason for hiding this comment

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

Lgtm! Great work as always

@luccas-harbour luccas-harbour force-pushed the luccas/sd-2989-feature-headerfooter-section-references branch 2 times, most recently from 8e46277 to e06e7f4 Compare June 3, 2026 12:19
@luccas-harbour luccas-harbour changed the base branch from main to luccas/sd-2997-feature-different-first-page-headers June 3, 2026 12:31
…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.
…oc media

Stop shifting normal footer/header fragments when the layout's minY is
negative purely because of explicit behindDoc anchored drawings/images
(e.g. page-relative background shapes). Decoration normalization now
computes its own minY that ignores those explicit behindDoc media, so
in-flow content stays at its original coordinates while the negative
minY is preserved on the payload for downstream painters.
… contract

Introduce `selectHeaderFooterVariantForPage` and `resolveEffectiveHeaderFooterRef`
in `@superdoc/contracts` as the single source of truth for picking a page's
header/footer variant and walking section inheritance to a concrete rId.

Replace the four divergent copies of this logic β€” in layout-bridge
(`getHeaderFooterTypeForSection` / `getHeaderFooterIdForPage` /
`resolveHeaderFooterForPageAndSection`), the layout-engine margin pass, the
PresentationEditor `HeaderFooterSessionManager`, and the document-api
`resolveEffectiveRef` helper β€” with calls into the shared resolver.

This corrects the OOXML inheritance model: `first` and `even` variants no
longer fall back to a `default` ref (only `odd` may resolve from `default`
under `w:evenAndOddHeaders`), and inheritance now walks across all prior
sections rather than just the immediately preceding one. Pages with no
matching ref now resolve to null/zero height instead of inferring default
content, keeping layout margins consistent with rendered output.
…tion

buildMultiSectionIdentifier previously merged the converter's legacy
header/footer refs into section 0's resolution entry. This let a
footerless first section inherit a converter-level default that belonged
to a later section, painting a footer where the document declares none.

Section-aware resolution now reads only per-section refs; converter
fallbacks remain on the legacy identifier fields for legacy lookups but
are no longer exposed through resolveEffectiveHeaderFooterRef. Guard
HeaderFooterSessionManager so it only consults legacy refs when section
resolution is unavailable, and skip building resolution sections for an
empty identifier.
@luccas-harbour luccas-harbour force-pushed the luccas/sd-2997-feature-different-first-page-headers branch from a79892c to 92e4a03 Compare June 3, 2026 14:45
@luccas-harbour luccas-harbour force-pushed the luccas/sd-2989-feature-headerfooter-section-references branch from e06e7f4 to bade825 Compare June 3, 2026 14:45
Base automatically changed from luccas/sd-2997-feature-different-first-page-headers to luccas/sd-2990-feature-headerfooter-page-numbers June 3, 2026 15:58
luccas-harbour and others added 2 commits June 3, 2026 09:00
…itive field dispatch (SD-3006) (#3599)

* feat(super-converter): match field dispatch keywords case-insensitively

OOXML field type names are case-insensitive, but the field-reference
preprocessors dispatched on the raw first token (e.g. only "PAGE",
not "page"). A lowercase PAGE/NUMPAGES field in a repeated footer fell
through to the cached static text and showed the same number on every
page.

Add a shared extractFieldKeyword helper that normalizes the dispatch
token to upper case while leaving the original instruction text intact
for downstream processors, and route fldSimple/fldChar dispatch and the
header/footer page-field scan through it. Make the HYPERLINK target
regex case-insensitive and anchored. Cover the new behavior with unit
tests and a behavior spec asserting a lowercase PAGE footer resolves
per page.

* test(super-converter): cover field keyword dispatch

* fix(super-converter): trust header footer field keyword

* feat(page-number): support PAGE field value-format switches

Parse the `\*` value-format switches on PAGE field instructions (Arabic,
Roman/roman, ALPHABETIC/alphabetic, ArabicDash) into a run-local
pageNumberFormat override, and apply it independently of section numbering
when resolving page-number tokens.

- add parsePageInstruction / pageNumberFormatToInstructionSwitch in a new
  page-instruction.js; page-preprocessor stores the original instruction and
  parsed format on sd:autoPageNumber
- round-trip instruction + pageNumberFormat through the autoPageNumber
  translator and the page-number extension node (preserve imported
  instruction text, synthesize a switch for new formatted nodes)
- add pageNumberFormat to TextRun and thread it through layout-bridge,
  layout-resolved, painters (resolveRunText), and stamp section-aware
  displayNumber on pages so formatting uses the pre-format numeric value
- move formatPageNumber + PageNumberFormat into @superdoc/contracts as the
  single source of truth; re-export from pageNumbering
- include pageNumberFormat in block-version, merge, and hash signatures so
  format changes invalidate cached layouts

upperLetter/lowerLetter now render as repeated letters (AA, BB, CC) to match
Word instead of the previous Excel-style sequence (AA, AB).

* fix(page-number): render ArabicDash spacing

* fix(layout-bridge): hash page number formats

* fix(page-number): fall back for unknown formats

* test(behavior): cover formatted footer page fields

* fix(page-number): address PAGE field review feedback

* fix(sequence-field): preserve cached numbering for lowercase seq fields

Only dispatch the SEQ pre-processor for uppercase SEQ instructions so
lowercase `seq` fields keep their cached visible result runs instead of
being re-resolved. Also recurse into run-wrapped content when extracting
resolved text so cached numbers nested inside runs are captured.

* fix(painter): rebuild drawing page fields on context changes

* fix: footnote formatter parity test

* fix(layout): remove duplicate displayNumber fields and fix page signature ref

Drop the redundant displayNumber declarations from HeaderFooterPage,
ResolvedHeaderFooterPage, and the layout-bridge page builder, keeping the
section-aware variant. Correct the renderer page context signature to read
displayPageNumber instead of the nonexistent pageNumberDisplayNumber.

* test(layout): update page-number field expectations

Adjust header/footer token and footer rendering expectations to the
spaced "- N -" format, and migrate the renderer page-context test to the
pageNumberFieldFormat shape.
…luccas/sd-2989-feature-headerfooter-section-references
@harbournick harbournick merged commit 985f183 into luccas/sd-2990-feature-headerfooter-page-numbers Jun 3, 2026
4 checks passed
@harbournick harbournick deleted the luccas/sd-2989-feature-headerfooter-section-references branch June 3, 2026 16:08
harbournick pushed a commit that referenced this pull request Jun 3, 2026
* fix(layout-engine): use section-aware page number for odd/even header parity

OOXML (ECMA-376 Β§17.10.1) selects even/odd headers based on the printed page
number β€” which respects per-section numbering restarts and offsets β€” not the
physical page index. Track the post-restart/offset value as `displayNumber` on
each page and thread it through pagination, header/footer resolution, and the
HeaderFooterSessionManager so a section that starts at page 2 picks the `even`
variant on its first page.

* fix(layout-bridge): handle negative odd header parity

* fix(layout-resolved): expose header footer display numbers

* test(super-editor): cover header footer display parity

* fix(layout-bridge): allow section parity override

* feat(super-editor): honor PAGE/NUMPAGES field format switches

Parse `\*` general-format and `\#` numeric-picture switches when
importing PAGE/NUMPAGES fields and thread the requested format
(roman/alphabetic/zero-padded decimal/etc.) plus the section-aware
numeric page value through the converter, pm-adapter, layout engine,
and DOM painter so page-number fields render in the format Word stored
rather than always decimal. The original instruction is preserved on
the node so export round-trips back to the same field code.

* fix(super-editor): format NUMPAGES cached exports

* fix(super-editor): pass display number to rId header layouts

* fix(layout-bridge): avoid bucketing formatted page tokens

* fix(super-editor): preserve field-run page number styling

* fix(contracts): centralize page number formatting

* refactor(pm-adapter): share page field format extraction

* fix(super-editor): pass page field options explicitly

* refactor(super-editor): use field processor options object

* refactor(contracts): move page number formatting

* fix(super-editor): preserve active header display numbers

* fix(contracts): remove duplicate display number fields

* fix(layout-bridge): bucket zero-padded page numbers

* fix(super-editor): parse numeric page switch casing

* fix(converter): parse field dispatch whitespace

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

* fix(super-editor): honor per-section titlePg when inferring fallback 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.

* refactor(layout-engine): centralize header/footer ref inheritance

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.

* fix(contracts): preserve header footer fallback refs

* fix(layout-engine): use resolved header footer height slot

* fix(contracts): ignore later refs for fallback resolution

* fix(layout-bridge): render inherited default refs

* fix(super-editor): tolerate missing section titlePg map

* fix(contracts): inherit converter fallback refs

* test(contracts): cover header footer inheritance helper

* fix(layout-bridge): drop unused inheritance re-export

* test(super-editor): cover section titlePg decoration provider

* fix(layout-bridge): skip missing even header refs

* fix(header-footer): preserve negative minY from page-relative behindDoc media

Stop shifting normal footer/header fragments when the layout's minY is
negative purely because of explicit behindDoc anchored drawings/images
(e.g. page-relative background shapes). Decoration normalization now
computes its own minY that ignores those explicit behindDoc media, so
in-flow content stays at its original coordinates while the negative
minY is preserved on the payload for downstream painters.

* fix(header-footer): honor identifier alternate header state

* refactor(layout): centralize header/footer ref resolution in a shared contract (SD-2989) (#3577)

* fix(super-editor): honor per-section titlePg when inferring fallback 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.

* refactor(layout-engine): centralize header/footer ref inheritance

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.

* fix(contracts): preserve header footer fallback refs

* fix(layout-engine): use resolved header footer height slot

* fix(contracts): ignore later refs for fallback resolution

* fix(layout-bridge): render inherited default refs

* fix(super-editor): tolerate missing section titlePg map

* fix(contracts): inherit converter fallback refs

* test(contracts): cover header footer inheritance helper

* fix(layout-bridge): drop unused inheritance re-export

* test(super-editor): cover section titlePg decoration provider

* fix(layout-bridge): skip missing even header refs

* fix(header-footer): preserve negative minY from page-relative behindDoc media

Stop shifting normal footer/header fragments when the layout's minY is
negative purely because of explicit behindDoc anchored drawings/images
(e.g. page-relative background shapes). Decoration normalization now
computes its own minY that ignores those explicit behindDoc media, so
in-flow content stays at its original coordinates while the negative
minY is preserved on the payload for downstream painters.

* fix(header-footer): honor identifier alternate header state

* refactor(layout): centralize header/footer ref resolution in a shared contract

Introduce `selectHeaderFooterVariantForPage` and `resolveEffectiveHeaderFooterRef`
in `@superdoc/contracts` as the single source of truth for picking a page's
header/footer variant and walking section inheritance to a concrete rId.

Replace the four divergent copies of this logic β€” in layout-bridge
(`getHeaderFooterTypeForSection` / `getHeaderFooterIdForPage` /
`resolveHeaderFooterForPageAndSection`), the layout-engine margin pass, the
PresentationEditor `HeaderFooterSessionManager`, and the document-api
`resolveEffectiveRef` helper β€” with calls into the shared resolver.

This corrects the OOXML inheritance model: `first` and `even` variants no
longer fall back to a `default` ref (only `odd` may resolve from `default`
under `w:evenAndOddHeaders`), and inheritance now walks across all prior
sections rather than just the immediately preceding one. Pages with no
matching ref now resolve to null/zero height instead of inferring default
content, keeping layout margins consistent with rendered output.

* fix(layout): preserve converter header refs in section resolver

* fix(layout): resolve sparse section metadata by index

* refactor(layout): trim header footer resolver surface

* chore(editor): remove stale header footer imports

* fix(layout): preserve section header ref resolution

* fix(layout): preserve converter title page refs

* fix(document-api): inherit converter header refs

* fix(editor): resolve per-rId header refs for decorations

* fix(editor): align header footer fallback variant

* fix(layout): stop leaking converter fallback refs into section resolution

buildMultiSectionIdentifier previously merged the converter's legacy
header/footer refs into section 0's resolution entry. This let a
footerless first section inherit a converter-level default that belonged
to a later section, painting a footer where the document declares none.

Section-aware resolution now reads only per-section refs; converter
fallbacks remain on the legacy identifier fields for legacy lookups but
are no longer exposed through resolveEffectiveHeaderFooterRef. Guard
HeaderFooterSessionManager so it only consults legacy refs when section
resolution is unavailable, and skip building resolution sections for an
empty identifier.

* fix(layout): use effective page number for header parity

* feat(page-number): per-field PAGE value-format switches & case-insensitive field dispatch (SD-3006) (#3599)

* feat(super-converter): match field dispatch keywords case-insensitively

OOXML field type names are case-insensitive, but the field-reference
preprocessors dispatched on the raw first token (e.g. only "PAGE",
not "page"). A lowercase PAGE/NUMPAGES field in a repeated footer fell
through to the cached static text and showed the same number on every
page.

Add a shared extractFieldKeyword helper that normalizes the dispatch
token to upper case while leaving the original instruction text intact
for downstream processors, and route fldSimple/fldChar dispatch and the
header/footer page-field scan through it. Make the HYPERLINK target
regex case-insensitive and anchored. Cover the new behavior with unit
tests and a behavior spec asserting a lowercase PAGE footer resolves
per page.

* test(super-converter): cover field keyword dispatch

* fix(super-converter): trust header footer field keyword

* feat(page-number): support PAGE field value-format switches

Parse the `\*` value-format switches on PAGE field instructions (Arabic,
Roman/roman, ALPHABETIC/alphabetic, ArabicDash) into a run-local
pageNumberFormat override, and apply it independently of section numbering
when resolving page-number tokens.

- add parsePageInstruction / pageNumberFormatToInstructionSwitch in a new
  page-instruction.js; page-preprocessor stores the original instruction and
  parsed format on sd:autoPageNumber
- round-trip instruction + pageNumberFormat through the autoPageNumber
  translator and the page-number extension node (preserve imported
  instruction text, synthesize a switch for new formatted nodes)
- add pageNumberFormat to TextRun and thread it through layout-bridge,
  layout-resolved, painters (resolveRunText), and stamp section-aware
  displayNumber on pages so formatting uses the pre-format numeric value
- move formatPageNumber + PageNumberFormat into @superdoc/contracts as the
  single source of truth; re-export from pageNumbering
- include pageNumberFormat in block-version, merge, and hash signatures so
  format changes invalidate cached layouts

upperLetter/lowerLetter now render as repeated letters (AA, BB, CC) to match
Word instead of the previous Excel-style sequence (AA, AB).

* fix(page-number): render ArabicDash spacing

* fix(layout-bridge): hash page number formats

* fix(page-number): fall back for unknown formats

* test(behavior): cover formatted footer page fields

* fix(page-number): address PAGE field review feedback

* fix(sequence-field): preserve cached numbering for lowercase seq fields

Only dispatch the SEQ pre-processor for uppercase SEQ instructions so
lowercase `seq` fields keep their cached visible result runs instead of
being re-resolved. Also recurse into run-wrapped content when extracting
resolved text so cached numbers nested inside runs are captured.

* fix(painter): rebuild drawing page fields on context changes

* fix: footnote formatter parity test

* fix(layout): remove duplicate displayNumber fields and fix page signature ref

Drop the redundant displayNumber declarations from HeaderFooterPage,
ResolvedHeaderFooterPage, and the layout-bridge page builder, keeping the
section-aware variant. Correct the renderer page context signature to read
displayPageNumber instead of the nonexistent pageNumberDisplayNumber.

* test(layout): update page-number field expectations

Adjust header/footer token and footer rendering expectations to the
spaced "- N -" format, and migrate the renderer page-context test to the
pageNumberFieldFormat shape.

* chore: fix locks

* fix(contracts): remove duplicate header/footer displayNumber fields

* fix(layout-engine): correct section-aware header/footer parity and page-number bucketing
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.

5 participants