Skip to content

Commit 81717eb

Browse files
feat: honor PAGE/NUMPAGES field format switches (SD-2990) (#3262)
* 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
1 parent 85802da commit 81717eb

98 files changed

Lines changed: 4869 additions & 1010 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { describe, expect, it } from 'vitest';
2+
import {
3+
resolveInheritedHeaderFooterRef,
4+
resolveInheritedHeaderFooterRefWithType,
5+
} from './header-footer-inheritance.js';
6+
7+
describe('header/footer inheritance', () => {
8+
it('uses legacy refs when section maps are empty', () => {
9+
const ref = resolveInheritedHeaderFooterRef({
10+
identifier: {
11+
headerIds: { default: 'legacy-default' },
12+
sectionHeaderIds: new Map(),
13+
},
14+
sectionIndex: 0,
15+
kind: 'header',
16+
variantType: 'default',
17+
});
18+
19+
expect(ref).toBe('legacy-default');
20+
});
21+
22+
it('returns null for section zero when no page, section, or legacy refs exist', () => {
23+
const ref = resolveInheritedHeaderFooterRef({
24+
identifier: { sectionHeaderIds: new Map() },
25+
sectionIndex: 0,
26+
kind: 'header',
27+
variantType: 'default',
28+
});
29+
30+
expect(ref).toBeNull();
31+
});
32+
33+
it('walks back past intermediate sections with no entry', () => {
34+
const ref = resolveInheritedHeaderFooterRef({
35+
identifier: {
36+
sectionHeaderIds: new Map([[0, { first: 'section-0-first' }]]),
37+
},
38+
sectionIndex: 3,
39+
kind: 'header',
40+
variantType: 'first',
41+
});
42+
43+
expect(ref).toBe('section-0-first');
44+
});
45+
46+
it('prefers page refs over section refs', () => {
47+
const resolved = resolveInheritedHeaderFooterRefWithType({
48+
identifier: {
49+
sectionFooterIds: new Map([[0, { default: 'section-default' }]]),
50+
},
51+
sectionIndex: 0,
52+
kind: 'footer',
53+
variantType: 'default',
54+
pageRefs: { default: 'page-default' },
55+
});
56+
57+
expect(resolved).toEqual({ ref: 'page-default', variantType: 'default' });
58+
});
59+
60+
it('uses default refs for odd pages and reports the effective variant', () => {
61+
const resolved = resolveInheritedHeaderFooterRefWithType({
62+
identifier: {
63+
headerIds: { default: 'legacy-default' },
64+
},
65+
sectionIndex: 0,
66+
kind: 'header',
67+
variantType: 'odd',
68+
});
69+
70+
expect(resolved).toEqual({ ref: 'legacy-default', variantType: 'default' });
71+
});
72+
73+
it('does not fall back from first to default', () => {
74+
const ref = resolveInheritedHeaderFooterRef({
75+
identifier: {
76+
headerIds: { default: 'legacy-default' },
77+
},
78+
sectionIndex: 0,
79+
kind: 'header',
80+
variantType: 'first',
81+
});
82+
83+
expect(ref).toBeNull();
84+
});
85+
86+
it('does not fall back from even to default', () => {
87+
const ref = resolveInheritedHeaderFooterRef({
88+
identifier: {
89+
headerIds: { default: 'legacy-default' },
90+
},
91+
sectionIndex: 0,
92+
kind: 'header',
93+
variantType: 'even',
94+
});
95+
96+
expect(ref).toBeNull();
97+
});
98+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { HeaderFooterType } from './index.js';
2+
3+
export type HeaderFooterRefMap = Partial<Record<HeaderFooterType, string | null | undefined>>;
4+
5+
export type HeaderFooterRefIdentifier = {
6+
headerIds?: HeaderFooterRefMap;
7+
footerIds?: HeaderFooterRefMap;
8+
sectionCount?: number;
9+
sectionHeaderIds?: Map<number, HeaderFooterRefMap>;
10+
sectionFooterIds?: Map<number, HeaderFooterRefMap>;
11+
};
12+
13+
export type ResolveInheritedHeaderFooterRefInput = {
14+
identifier: HeaderFooterRefIdentifier;
15+
sectionIndex: number;
16+
kind: 'header' | 'footer';
17+
variantType: HeaderFooterType;
18+
pageRefs?: HeaderFooterRefMap;
19+
};
20+
21+
export type ResolvedInheritedHeaderFooterRef = {
22+
ref: string;
23+
variantType: HeaderFooterType;
24+
};
25+
26+
function resolveVariantRef(
27+
refs: HeaderFooterRefMap | undefined,
28+
variantType: HeaderFooterType,
29+
): ResolvedInheritedHeaderFooterRef | null {
30+
if (!refs) return null;
31+
const direct = refs[variantType];
32+
if (direct) return { ref: direct, variantType };
33+
if (variantType === 'odd' && refs.default) return { ref: refs.default, variantType: 'default' };
34+
return null;
35+
}
36+
37+
export function resolveInheritedHeaderFooterRefWithType({
38+
identifier,
39+
sectionIndex,
40+
kind,
41+
variantType,
42+
pageRefs,
43+
}: ResolveInheritedHeaderFooterRefInput): ResolvedInheritedHeaderFooterRef | null {
44+
const fromPage = resolveVariantRef(pageRefs, variantType);
45+
if (fromPage) return fromPage;
46+
47+
const sectionMap = kind === 'header' ? identifier.sectionHeaderIds : identifier.sectionFooterIds;
48+
const legacyIds = kind === 'header' ? identifier.headerIds : identifier.footerIds;
49+
50+
const sectionIds = sectionMap?.get(sectionIndex);
51+
const fromSection = resolveVariantRef(sectionIds, variantType);
52+
if (fromSection) return fromSection;
53+
54+
if (sectionMap) {
55+
for (let index = sectionIndex - 1; index >= 0; index -= 1) {
56+
const inherited = resolveVariantRef(sectionMap.get(index), variantType);
57+
if (inherited) return inherited;
58+
}
59+
}
60+
61+
return resolveVariantRef(legacyIds, variantType);
62+
}
63+
64+
export function resolveInheritedHeaderFooterRef(input: ResolveInheritedHeaderFooterRefInput): string | null {
65+
return resolveInheritedHeaderFooterRefWithType(input)?.ref ?? null;
66+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { resolveEffectiveHeaderFooterRef, selectHeaderFooterVariantForPage } from './header-footer-resolution.js';
3+
import type { HeaderFooterResolutionSection } from './header-footer-resolution.js';
4+
5+
describe('header/footer effective ref resolution', () => {
6+
it('inherits matching variants across more than one previous section', () => {
7+
const sections: HeaderFooterResolutionSection[] = [
8+
{ sectionIndex: 0, titlePg: true, headerRefs: { first: 'h0-first' } },
9+
{ sectionIndex: 1, titlePg: true, headerRefs: { default: 'h1-default' } },
10+
{ sectionIndex: 2, titlePg: true, headerRefs: {} },
11+
];
12+
13+
expect(
14+
resolveEffectiveHeaderFooterRef({ sections, sectionIndex: 2, kind: 'header', variant: 'first' }),
15+
).toMatchObject({
16+
refId: 'h0-first',
17+
matchedSectionIndex: 0,
18+
matchedVariant: 'first',
19+
});
20+
});
21+
22+
it('preserves inherited missing variants when a later section partially overrides another variant', () => {
23+
const sections: HeaderFooterResolutionSection[] = [
24+
{ sectionIndex: 0, footerRefs: { default: 'f0-default', even: 'f0-even' } },
25+
{ sectionIndex: 1, footerRefs: { default: 'f1-default' } },
26+
];
27+
28+
expect(
29+
resolveEffectiveHeaderFooterRef({ sections, sectionIndex: 1, kind: 'footer', variant: 'even' }),
30+
).toMatchObject({
31+
refId: 'f0-even',
32+
matchedSectionIndex: 0,
33+
matchedVariant: 'even',
34+
});
35+
});
36+
37+
it('does not let first inherit default when titlePg selects first', () => {
38+
const sections: HeaderFooterResolutionSection[] = [
39+
{ sectionIndex: 0, titlePg: true, headerRefs: { default: 'h0-default' } },
40+
];
41+
42+
const variant = selectHeaderFooterVariantForPage({
43+
documentPageNumber: 1,
44+
sectionPageNumber: 1,
45+
titlePg: true,
46+
alternateHeaders: false,
47+
});
48+
49+
expect(variant).toBe('first');
50+
expect(resolveEffectiveHeaderFooterRef({ sections, sectionIndex: 0, kind: 'header', variant: 'first' })).toBeNull();
51+
});
52+
53+
it('does not let even inherit default when odd/even headers are enabled', () => {
54+
const sections: HeaderFooterResolutionSection[] = [{ sectionIndex: 0, headerRefs: { default: 'h0-default' } }];
55+
56+
expect(resolveEffectiveHeaderFooterRef({ sections, sectionIndex: 0, kind: 'header', variant: 'even' })).toBeNull();
57+
});
58+
59+
it('resolves odd from explicit odd before OOXML default', () => {
60+
const sections: HeaderFooterResolutionSection[] = [
61+
{ sectionIndex: 0, headerRefs: { default: 'h0-default' } },
62+
{ sectionIndex: 1, headerRefs: { odd: 'h1-odd', default: 'h1-default' } },
63+
];
64+
65+
expect(
66+
resolveEffectiveHeaderFooterRef({ sections, sectionIndex: 1, kind: 'header', variant: 'odd' }),
67+
).toMatchObject({
68+
refId: 'h1-odd',
69+
matchedVariant: 'odd',
70+
});
71+
});
72+
73+
it('resolves odd from OOXML default when explicit odd is absent', () => {
74+
const sections: HeaderFooterResolutionSection[] = [{ sectionIndex: 0, headerRefs: { default: 'h0-default' } }];
75+
76+
expect(
77+
resolveEffectiveHeaderFooterRef({ sections, sectionIndex: 0, kind: 'header', variant: 'odd' }),
78+
).toMatchObject({
79+
refId: 'h0-default',
80+
matchedVariant: 'default',
81+
});
82+
});
83+
84+
it('uses document page number for even/odd selection', () => {
85+
expect(
86+
selectHeaderFooterVariantForPage({
87+
documentPageNumber: 4,
88+
sectionPageNumber: 1,
89+
titlePg: false,
90+
alternateHeaders: true,
91+
}),
92+
).toBe('even');
93+
});
94+
95+
it('accepts non-positive document page numbers for parity when the section page is valid', () => {
96+
expect(
97+
selectHeaderFooterVariantForPage({
98+
documentPageNumber: 0,
99+
sectionPageNumber: 1,
100+
titlePg: false,
101+
alternateHeaders: true,
102+
}),
103+
).toBe('even');
104+
expect(
105+
selectHeaderFooterVariantForPage({
106+
documentPageNumber: -1,
107+
sectionPageNumber: 1,
108+
titlePg: false,
109+
alternateHeaders: true,
110+
}),
111+
).toBe('odd');
112+
});
113+
114+
it('returns null when the section page number is invalid', () => {
115+
expect(
116+
selectHeaderFooterVariantForPage({
117+
documentPageNumber: 1,
118+
sectionPageNumber: 0,
119+
titlePg: false,
120+
alternateHeaders: false,
121+
}),
122+
).toBeNull();
123+
expect(
124+
selectHeaderFooterVariantForPage({
125+
documentPageNumber: -1,
126+
sectionPageNumber: -1,
127+
titlePg: false,
128+
alternateHeaders: true,
129+
}),
130+
).toBeNull();
131+
});
132+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
export type HeaderFooterKind = 'header' | 'footer';
2+
export type HeaderFooterVariant = 'default' | 'first' | 'even' | 'odd';
3+
4+
export type HeaderFooterSectionRefs = Partial<Record<HeaderFooterVariant, string | null>>;
5+
6+
export type HeaderFooterResolutionSection = {
7+
sectionIndex: number;
8+
titlePg?: boolean;
9+
headerRefs?: HeaderFooterSectionRefs | null;
10+
footerRefs?: HeaderFooterSectionRefs | null;
11+
};
12+
13+
export type HeaderFooterVariantSelectionInput = {
14+
documentPageNumber: number;
15+
sectionPageNumber: number;
16+
titlePg?: boolean;
17+
alternateHeaders?: boolean;
18+
};
19+
20+
export type HeaderFooterEffectiveRefInput = {
21+
sections: readonly HeaderFooterResolutionSection[];
22+
sectionIndex: number;
23+
kind: HeaderFooterKind;
24+
variant: HeaderFooterVariant;
25+
};
26+
27+
export type HeaderFooterEffectiveRefResult = {
28+
refId: string;
29+
matchedSectionIndex: number;
30+
matchedVariant: HeaderFooterVariant;
31+
};
32+
33+
export function selectHeaderFooterVariantForPage({
34+
documentPageNumber,
35+
sectionPageNumber,
36+
titlePg,
37+
alternateHeaders,
38+
}: HeaderFooterVariantSelectionInput): HeaderFooterVariant | null {
39+
if (!Number.isFinite(documentPageNumber) || !Number.isFinite(sectionPageNumber)) return null;
40+
if (sectionPageNumber < 1) return null;
41+
if (sectionPageNumber === 1 && titlePg === true) return 'first';
42+
if (alternateHeaders === true) return documentPageNumber % 2 === 0 ? 'even' : 'odd';
43+
return 'default';
44+
}
45+
46+
function candidateVariantsFor(variant: HeaderFooterVariant): readonly HeaderFooterVariant[] {
47+
return variant === 'odd' ? ['odd', 'default'] : [variant];
48+
}
49+
50+
function sectionRefsFor(
51+
section: HeaderFooterResolutionSection | undefined,
52+
kind: HeaderFooterKind,
53+
): HeaderFooterSectionRefs | null | undefined {
54+
return kind === 'header' ? section?.headerRefs : section?.footerRefs;
55+
}
56+
57+
export function resolveEffectiveHeaderFooterRef({
58+
sections,
59+
sectionIndex,
60+
kind,
61+
variant,
62+
}: HeaderFooterEffectiveRefInput): HeaderFooterEffectiveRefResult | null {
63+
if (sectionIndex < 0) return null;
64+
65+
const sectionsByIndex = new Map<number, HeaderFooterResolutionSection>();
66+
for (const section of sections) {
67+
sectionsByIndex.set(section.sectionIndex, section);
68+
}
69+
70+
const candidates = candidateVariantsFor(variant);
71+
for (let currentIndex = sectionIndex; currentIndex >= 0; currentIndex -= 1) {
72+
const refs = sectionRefsFor(sectionsByIndex.get(currentIndex), kind);
73+
if (!refs) continue;
74+
75+
for (const candidate of candidates) {
76+
const refId = refs[candidate];
77+
if (refId) {
78+
return {
79+
refId,
80+
matchedSectionIndex: currentIndex,
81+
matchedVariant: candidate,
82+
};
83+
}
84+
}
85+
}
86+
87+
return null;
88+
}

0 commit comments

Comments
 (0)