Skip to content

Commit 797b0b4

Browse files
feat(layout): enable odd/even header-footer support (w:evenAndOddHeaders) (#2804)
* feat(layout): enable odd/even header-footer support (w:evenAndOddHeaders) Thread alternateHeaders from document settings through the layout engine so the paginator selects the correct header/footer variant per page. Fixes margin calculation for documents with different odd and even page headers. Also fixes getVariantTypeForPage to use document page number (not section-relative) for even/odd selection, matching the rendering side (headerFooterUtils.ts). Closes #2803 * fix: address PR review — type guard + multi-section/footer/fallback tests - Guard alternateHeaders behind isSemanticFlow check to match ResolvedLayoutOptions type (paginated-only field) - Add multi-section test: section 2 starting on page 4 (even by document number, verifies the documentPageNumber fix) - Add footer test: even/odd footer heights with alternateHeaders - Add default-only fallback test: only default header defined * test(layout): strengthen alternateHeaders tests and thread flag via resolver Review round 2 follow-ups on PR #2804. Tests - Footer test now goes through sectionMetadata.footerRefs + footerContentHeightsByRId (the real path) and asserts page.margins.bottom. The old test only checked body-top y, which is footer-independent — stubbing getFooterHeightForPage to always return 0 left that assertion passing. Mutation-tested: forcing getVariantTypeForPage to always return 'default' now breaks it. - Default-only fallback test now exercises the production path (headerRefs.default + per-rId heights) and asserts the correct outcome (y ~= 90 via the step-3 default fallback). Old assertion of y ~= 50 codified a code path that never runs in production because document imports always supply section refs. Mutation-tested: removing the step-3 fallback breaks this test. - New combined test: multi-section + titlePg + alternateHeaders, where section 2 has titlePg=true and starts on an even document page. Guards both the titlePg interaction across a section boundary AND the documentPageNumber (not sectionPageNumber) rule on pages 5 and 6. Mutation-tested: reverting to sectionPageNumber breaks this test alongside the original multi-section case. Layout engine - getVariantTypeForPage now takes a named-params object. Two adjacent `number` params (sectionPageNumber, documentPageNumber) are swap-vulnerable. - JSDoc on LayoutOptions.alternateHeaders cross-references getHeaderFooterTypeForSection in layout-bridge — the two sides must agree on variant selection and the pointer helps future maintainers keep them in sync. PresentationEditor - alternateHeaders is now populated inside #resolveLayoutOptions, alongside the other paginated-only fields (columns, sectionMetadata). The render-site spread collapses back to the single ternary it was before, and the `as EditorWithConverter` cast there disappears. types.ts didn't need changes — the field was already declared on the paginated variant of ResolvedLayoutOptions but unpopulated; it's now legitimately set by the resolver. * test(alternate-headers): add unit + integration + behavior coverage Follow-up to round 2 review. Closes the three test gaps flagged in the gap analysis: Unit (PresentationEditor threading) - 3 tests in PresentationEditor.test.ts assert that `converter.pageStyles.alternateHeaders` is forwarded to the layout options that reach `incrementalLayout`. Covers true, unset, and falsy-non-boolean cases. A refactor that drops the threading no longer passes silently. Unit (docxImporter) - Export `isAlternatingHeadersOddEven` and add 4 tests covering the import-side signal: `<w:evenAndOddHeaders/>` present, absent, missing settings.xml, empty settings. Pins the contract between OOXML settings and `pageStyles.alternateHeaders`. Behavior (Playwright) - `tests/headers/odd-even-header-variants.spec.ts` loads `h_f-normal-odd-even.docx` (already in the corpus) and asserts: - pages 1/3 render the default/odd header text, pages 2/4 render the even header text - page 1 and page 3 use the same `data-block-id` (same rId) but differ from page 2 — catches regressions that produce the right text from the wrong rId - footers follow the same alternation The existing layout/visual corpus already includes `h_f-normal-odd-even*.docx` and `sd-2234-even-odd-headers.docx`, so rendering regressions show up in `pnpm test:layout` and `pnpm test:visual` without any additional wiring. --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com> Co-authored-by: Caio Pizzol <97641911+caio-pizzol@users.noreply.github.com>
1 parent 1f827f4 commit 797b0b4

8 files changed

Lines changed: 535 additions & 15 deletions

File tree

packages/layout-engine/layout-engine/src/index.test.ts

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5587,3 +5587,312 @@ describe('requirePageBoundary edge cases', () => {
55875587
});
55885588
});
55895589
});
5590+
5591+
describe('alternateHeaders (odd/even header differentiation)', () => {
5592+
// Two tall paragraphs (400px each) that force a 2-page layout.
5593+
const tallBlock = (id: string): FlowBlock => ({
5594+
kind: 'paragraph',
5595+
id,
5596+
runs: [],
5597+
});
5598+
const tallMeasure = makeMeasure([400]);
5599+
5600+
it('selects even/odd header heights when alternateHeaders is true', () => {
5601+
const options: LayoutOptions = {
5602+
pageSize: { w: 600, h: 800 },
5603+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5604+
alternateHeaders: true,
5605+
headerContentHeights: {
5606+
odd: 80, // Odd pages: header pushes body start down
5607+
even: 40, // Even pages: smaller header
5608+
},
5609+
};
5610+
5611+
const layout = layoutDocument([tallBlock('p1'), tallBlock('p2')], [tallMeasure, tallMeasure], options);
5612+
5613+
expect(layout.pages).toHaveLength(2);
5614+
5615+
// Page 1 is odd (documentPageNumber=1) → uses 'odd' header height (80px)
5616+
// Body should start at max(margin.top, margin.header + headerContentHeight) = max(50, 30+80) = 110
5617+
const p1Fragment = layout.pages[0].fragments.find((f) => f.blockId === 'p1');
5618+
expect(p1Fragment).toBeDefined();
5619+
expect(p1Fragment!.y).toBeCloseTo(110, 0);
5620+
5621+
// Page 2 is even (documentPageNumber=2) → uses 'even' header height (40px)
5622+
// Body should start at max(margin.top, margin.header + headerContentHeight) = max(50, 30+40) = 70
5623+
const p2Fragment = layout.pages[1].fragments.find((f) => f.blockId === 'p2');
5624+
expect(p2Fragment).toBeDefined();
5625+
expect(p2Fragment!.y).toBeCloseTo(70, 0);
5626+
});
5627+
5628+
it('uses default header height for all pages when alternateHeaders is false', () => {
5629+
const options: LayoutOptions = {
5630+
pageSize: { w: 600, h: 800 },
5631+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5632+
alternateHeaders: false,
5633+
headerContentHeights: {
5634+
default: 60,
5635+
odd: 80,
5636+
even: 40,
5637+
},
5638+
};
5639+
5640+
const layout = layoutDocument([tallBlock('p1'), tallBlock('p2')], [tallMeasure, tallMeasure], options);
5641+
5642+
expect(layout.pages).toHaveLength(2);
5643+
5644+
// Both pages use 'default' header height (60px)
5645+
// Body start = max(50, 30+60) = 90
5646+
const p1Fragment = layout.pages[0].fragments.find((f) => f.blockId === 'p1');
5647+
const p2Fragment = layout.pages[1].fragments.find((f) => f.blockId === 'p2');
5648+
expect(p1Fragment!.y).toBeCloseTo(90, 0);
5649+
expect(p2Fragment!.y).toBeCloseTo(90, 0);
5650+
});
5651+
5652+
it('defaults to false when alternateHeaders is omitted', () => {
5653+
const options: LayoutOptions = {
5654+
pageSize: { w: 600, h: 800 },
5655+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5656+
// alternateHeaders not set
5657+
headerContentHeights: {
5658+
default: 60,
5659+
odd: 80,
5660+
even: 40,
5661+
},
5662+
};
5663+
5664+
const layout = layoutDocument([tallBlock('p1'), tallBlock('p2')], [tallMeasure, tallMeasure], options);
5665+
5666+
expect(layout.pages).toHaveLength(2);
5667+
5668+
// Both pages should use 'default' (60px), not odd/even
5669+
const p1Fragment = layout.pages[0].fragments.find((f) => f.blockId === 'p1');
5670+
const p2Fragment = layout.pages[1].fragments.find((f) => f.blockId === 'p2');
5671+
expect(p1Fragment!.y).toBeCloseTo(90, 0);
5672+
expect(p2Fragment!.y).toBeCloseTo(90, 0);
5673+
});
5674+
5675+
it('first page uses first variant when titlePg is enabled with alternateHeaders', () => {
5676+
const sectionBreak: SectionBreakBlock = {
5677+
kind: 'sectionBreak',
5678+
id: 'sb',
5679+
attrs: { isFirstSection: true, source: 'sectPr', sectionIndex: 0 },
5680+
pageSize: { w: 600, h: 800 },
5681+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5682+
};
5683+
5684+
const options: LayoutOptions = {
5685+
pageSize: { w: 600, h: 800 },
5686+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5687+
alternateHeaders: true,
5688+
sectionMetadata: [{ sectionIndex: 0, titlePg: true }],
5689+
headerContentHeights: {
5690+
first: 100, // First page: tallest header
5691+
odd: 80,
5692+
even: 40,
5693+
},
5694+
};
5695+
5696+
const layout = layoutDocument(
5697+
[sectionBreak, tallBlock('p1'), tallBlock('p2'), tallBlock('p3')],
5698+
[{ kind: 'sectionBreak' }, tallMeasure, tallMeasure, tallMeasure],
5699+
options,
5700+
);
5701+
5702+
expect(layout.pages.length).toBeGreaterThanOrEqual(3);
5703+
5704+
// Page 1 (first page of section, titlePg=true) → 'first' variant → 100px
5705+
// Body start = max(50, 30+100) = 130
5706+
const p1Fragment = layout.pages[0].fragments.find((f) => f.blockId === 'p1');
5707+
expect(p1Fragment).toBeDefined();
5708+
expect(p1Fragment!.y).toBeCloseTo(130, 0);
5709+
5710+
// Page 2 (documentPageNumber=2, even) → 'even' variant → 40px
5711+
// Body start = max(50, 30+40) = 70
5712+
const p2Fragment = layout.pages[1].fragments.find((f) => f.blockId === 'p2');
5713+
expect(p2Fragment).toBeDefined();
5714+
expect(p2Fragment!.y).toBeCloseTo(70, 0);
5715+
5716+
// Page 3 (documentPageNumber=3, odd) → 'odd' variant → 80px
5717+
// Body start = max(50, 30+80) = 110
5718+
const p3Fragment = layout.pages[2].fragments.find((f) => f.blockId === 'p3');
5719+
expect(p3Fragment).toBeDefined();
5720+
expect(p3Fragment!.y).toBeCloseTo(110, 0);
5721+
});
5722+
5723+
it('multi-section: uses document page number for even/odd, not section-relative', () => {
5724+
// Section 1 has 3 pages (pages 1-3), section 2 starts on page 4.
5725+
// Page 4 is even by document number, but sectionPageNumber=1 (odd).
5726+
// The fix ensures document page number is used for even/odd.
5727+
const sb1: SectionBreakBlock = {
5728+
kind: 'sectionBreak',
5729+
id: 'sb1',
5730+
attrs: { isFirstSection: true, source: 'sectPr', sectionIndex: 0 },
5731+
pageSize: { w: 600, h: 800 },
5732+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5733+
};
5734+
const sb2: SectionBreakBlock = {
5735+
kind: 'sectionBreak',
5736+
id: 'sb2',
5737+
type: 'nextPage',
5738+
attrs: { source: 'sectPr', sectionIndex: 1 },
5739+
pageSize: { w: 600, h: 800 },
5740+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5741+
};
5742+
5743+
const options: LayoutOptions = {
5744+
pageSize: { w: 600, h: 800 },
5745+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5746+
alternateHeaders: true,
5747+
sectionMetadata: [{ sectionIndex: 0 }, { sectionIndex: 1 }],
5748+
headerContentHeights: {
5749+
odd: 80,
5750+
even: 40,
5751+
},
5752+
};
5753+
5754+
const layout = layoutDocument(
5755+
[sb1, tallBlock('p1'), tallBlock('p2'), tallBlock('p3'), sb2, tallBlock('p4')],
5756+
[{ kind: 'sectionBreak' }, tallMeasure, tallMeasure, tallMeasure, { kind: 'sectionBreak' }, tallMeasure],
5757+
options,
5758+
);
5759+
5760+
expect(layout.pages.length).toBeGreaterThanOrEqual(4);
5761+
5762+
// Page 4 (documentPageNumber=4, even) → should use 'even' header (40px)
5763+
// NOT 'odd' which would happen if sectionPageNumber (1) were used
5764+
// Body start = max(50, 30+40) = 70
5765+
const p4Fragment = layout.pages[3]?.fragments.find((f) => f.blockId === 'p4');
5766+
expect(p4Fragment).toBeDefined();
5767+
expect(p4Fragment!.y).toBeCloseTo(70, 0);
5768+
});
5769+
5770+
it('selects even/odd footer heights when alternateHeaders is true', () => {
5771+
// The footer-height path uses the per-rId map + sectionMetadata.footerRefs.
5772+
// Exposing the variant selection through `footerContentHeights` alone is not
5773+
// sufficient — without refs, the code falls back to 'default' for the footer
5774+
// variant regardless. We need the ref map to observe variant switching on
5775+
// `page.margins.bottom`.
5776+
const options: LayoutOptions = {
5777+
pageSize: { w: 600, h: 800 },
5778+
margins: { top: 50, right: 50, bottom: 50, left: 50, footer: 30 },
5779+
alternateHeaders: true,
5780+
sectionMetadata: [{ sectionIndex: 0, footerRefs: { odd: 'rIdFooterOdd', even: 'rIdFooterEven' } }],
5781+
footerContentHeightsByRId: new Map([
5782+
['rIdFooterOdd', 80], // Odd pages: larger footer
5783+
['rIdFooterEven', 40], // Even pages: smaller footer
5784+
]),
5785+
};
5786+
5787+
const layout = layoutDocument([tallBlock('p1'), tallBlock('p2')], [tallMeasure, tallMeasure], options);
5788+
5789+
expect(layout.pages).toHaveLength(2);
5790+
5791+
// Page 1 is odd → 'odd' footer (80px) → bottom = max(50, 30+80) = 110
5792+
// Page 2 is even → 'even' footer (40px) → bottom = max(50, 30+40) = 70
5793+
// Body-top Y is footer-independent, so assert on the effective bottom margin
5794+
// the paginator stamped on each page.
5795+
expect(layout.pages[0].margins?.bottom).toBeCloseTo(110, 0);
5796+
expect(layout.pages[1].margins?.bottom).toBeCloseTo(70, 0);
5797+
});
5798+
5799+
it('falls back to default header when only default is defined with alternateHeaders', () => {
5800+
// Production path: a document with `w:evenAndOddHeaders` on but only a
5801+
// `default` header authored. sectionMetadata supplies the `default` ref and
5802+
// the per-rId height map supplies its measurement. Step-3 fallback at
5803+
// index.ts:1345-1349 kicks in and `effectiveVariantType` drops to 'default'.
5804+
const options: LayoutOptions = {
5805+
pageSize: { w: 600, h: 800 },
5806+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5807+
alternateHeaders: true,
5808+
sectionMetadata: [{ sectionIndex: 0, headerRefs: { default: 'rIdHeaderDefault' } }],
5809+
headerContentHeightsByRId: new Map([['rIdHeaderDefault', 60]]),
5810+
};
5811+
5812+
const layout = layoutDocument([tallBlock('p1'), tallBlock('p2')], [tallMeasure, tallMeasure], options);
5813+
5814+
expect(layout.pages).toHaveLength(2);
5815+
5816+
// Both pages fall back to the default header (60px), so body start is the
5817+
// same on odd and even: max(50, 30+60) = 90.
5818+
const p1Fragment = layout.pages[0].fragments.find((f) => f.blockId === 'p1');
5819+
const p2Fragment = layout.pages[1].fragments.find((f) => f.blockId === 'p2');
5820+
expect(p1Fragment!.y).toBeCloseTo(90, 0);
5821+
expect(p2Fragment!.y).toBeCloseTo(90, 0);
5822+
// Effective top margin is also 90 on both pages.
5823+
expect(layout.pages[0].margins?.top).toBeCloseTo(90, 0);
5824+
expect(layout.pages[1].margins?.top).toBeCloseTo(90, 0);
5825+
});
5826+
5827+
it('multi-section + titlePg + alternateHeaders: first page of section 2 lands on an even doc-page', () => {
5828+
// Most realistic mixed case. Section 1 has 3 pages (docPN 1-3). Section 2
5829+
// has titlePg=true and starts on docPN=4.
5830+
// - Page 4 is sectionPageNumber=1 for section 2 + titlePg=true → 'first'
5831+
// - Page 5 is docPN=5 (odd) → 'odd' (regardless of section-relative number)
5832+
// - Page 6 is docPN=6 (even) → 'even'
5833+
// If the code used sectionPageNumber for even/odd, pages 5 and 6 would be
5834+
// swapped (section-relative 2 and 3 respectively). This guards both titlePg
5835+
// and the docPN rule across a section boundary.
5836+
const sb1: SectionBreakBlock = {
5837+
kind: 'sectionBreak',
5838+
id: 'sb1',
5839+
attrs: { isFirstSection: true, source: 'sectPr', sectionIndex: 0 },
5840+
pageSize: { w: 600, h: 800 },
5841+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5842+
};
5843+
const sb2: SectionBreakBlock = {
5844+
kind: 'sectionBreak',
5845+
id: 'sb2',
5846+
type: 'nextPage',
5847+
attrs: { source: 'sectPr', sectionIndex: 1 },
5848+
pageSize: { w: 600, h: 800 },
5849+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5850+
};
5851+
5852+
const options: LayoutOptions = {
5853+
pageSize: { w: 600, h: 800 },
5854+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 30 },
5855+
alternateHeaders: true,
5856+
sectionMetadata: [{ sectionIndex: 0 }, { sectionIndex: 1, titlePg: true }],
5857+
headerContentHeights: {
5858+
first: 100, // section 2 title-page header
5859+
odd: 80,
5860+
even: 40,
5861+
},
5862+
};
5863+
5864+
const layout = layoutDocument(
5865+
[sb1, tallBlock('p1'), tallBlock('p2'), tallBlock('p3'), sb2, tallBlock('p4'), tallBlock('p5'), tallBlock('p6')],
5866+
[
5867+
{ kind: 'sectionBreak' },
5868+
tallMeasure,
5869+
tallMeasure,
5870+
tallMeasure,
5871+
{ kind: 'sectionBreak' },
5872+
tallMeasure,
5873+
tallMeasure,
5874+
tallMeasure,
5875+
],
5876+
options,
5877+
);
5878+
5879+
expect(layout.pages.length).toBeGreaterThanOrEqual(6);
5880+
5881+
// Page 4: section 2 first page + titlePg → 'first' (100px) → y = max(50, 30+100) = 130
5882+
const p4Fragment = layout.pages[3]?.fragments.find((f) => f.blockId === 'p4');
5883+
expect(p4Fragment).toBeDefined();
5884+
expect(p4Fragment!.y).toBeCloseTo(130, 0);
5885+
5886+
// Page 5: docPN=5, odd → 'odd' (80px) → y = max(50, 30+80) = 110
5887+
// If sectionPageNumber were used: sectionPN=2 → 'even' (40) → y = 70 (wrong)
5888+
const p5Fragment = layout.pages[4]?.fragments.find((f) => f.blockId === 'p5');
5889+
expect(p5Fragment).toBeDefined();
5890+
expect(p5Fragment!.y).toBeCloseTo(110, 0);
5891+
5892+
// Page 6: docPN=6, even → 'even' (40px) → y = max(50, 30+40) = 70
5893+
// If sectionPageNumber were used: sectionPN=3 → 'odd' (80) → y = 110 (wrong)
5894+
const p6Fragment = layout.pages[5]?.fragments.find((f) => f.blockId === 'p6');
5895+
expect(p6Fragment).toBeDefined();
5896+
expect(p6Fragment!.y).toBeCloseTo(70, 0);
5897+
});
5898+
});

packages/layout-engine/layout-engine/src/index.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,17 @@ export type LayoutOptions = {
528528
* behavior for paragraph-free overlays.
529529
*/
530530
allowSectionBreakOnlyPageFallback?: boolean;
531+
/**
532+
* Whether the document has odd/even header/footer differentiation enabled.
533+
* Corresponds to the w:evenAndOddHeaders element in OOXML settings.xml.
534+
* When true, odd pages use the 'odd' variant and even pages use the 'even' variant.
535+
* When false or omitted, all pages use the 'default' variant.
536+
*
537+
* Must stay in sync with `getHeaderFooterTypeForSection` in
538+
* `layout-bridge/src/headerFooterUtils.ts` — both sides read this value
539+
* and must agree on variant selection.
540+
*/
541+
alternateHeaders?: boolean;
531542
};
532543

533544
export type HeaderFooterConstraints = {
@@ -669,23 +680,29 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
669680
/**
670681
* Determines the header/footer variant type for a given page based on section settings.
671682
*
672-
* @param sectionPageNumber - The page number within the current section (1-indexed)
683+
* Takes a params object because the two page-number fields have very similar
684+
* names and types — a positional call site is easy to get wrong.
685+
*
686+
* @param sectionPageNumber - The page number within the current section (1-indexed), used for titlePg
687+
* @param documentPageNumber - The absolute document page number (1-indexed), used for even/odd
673688
* @param titlePgEnabled - Whether the section has "different first page" enabled
674-
* @param alternateHeaders - Whether the section has odd/even differentiation enabled
689+
* @param alternateHeaders - Whether the document has odd/even differentiation enabled
675690
* @returns The variant type: 'first', 'even', 'odd', or 'default'
676691
*/
677-
const getVariantTypeForPage = (
678-
sectionPageNumber: number,
679-
titlePgEnabled: boolean,
680-
alternateHeaders: boolean,
681-
): 'default' | 'first' | 'even' | 'odd' => {
692+
const getVariantTypeForPage = (args: {
693+
sectionPageNumber: number;
694+
documentPageNumber: number;
695+
titlePgEnabled: boolean;
696+
alternateHeaders: boolean;
697+
}): 'default' | 'first' | 'even' | 'odd' => {
682698
// First page of section with titlePg enabled uses 'first' variant
683-
if (sectionPageNumber === 1 && titlePgEnabled) {
699+
if (args.sectionPageNumber === 1 && args.titlePgEnabled) {
684700
return 'first';
685701
}
686-
// Alternate headers (even/odd differentiation)
687-
if (alternateHeaders) {
688-
return sectionPageNumber % 2 === 0 ? 'even' : 'odd';
702+
// Alternate headers: even/odd based on document page number, matching
703+
// the rendering side (getHeaderFooterTypeForSection in headerFooterUtils.ts)
704+
if (args.alternateHeaders) {
705+
return args.documentPageNumber % 2 === 0 ? 'even' : 'odd';
689706
}
690707
return 'default';
691708
};
@@ -1295,11 +1312,15 @@ export function layoutDocument(blocks: FlowBlock[], measures: Measure[], options
12951312
// Get section metadata for titlePg setting
12961313
const sectionMetadata = sectionMetadataList[activeSectionIndex];
12971314
const titlePgEnabled = sectionMetadata?.titlePg ?? false;
1298-
// TODO: Support alternateHeaders (odd/even) when needed
1299-
const alternateHeaders = false;
1315+
const alternateHeaders = options.alternateHeaders ?? false;
13001316

13011317
// Determine which header/footer variant applies to this page
1302-
const variantType = getVariantTypeForPage(sectionPageNumber, titlePgEnabled, alternateHeaders);
1318+
const variantType = getVariantTypeForPage({
1319+
sectionPageNumber,
1320+
documentPageNumber: newPageNumber,
1321+
titlePgEnabled,
1322+
alternateHeaders,
1323+
});
13031324

13041325
// Resolve header/footer refs for margin calculation using OOXML inheritance model.
13051326
// This must match the rendering logic in PresentationEditor to ensure margins

0 commit comments

Comments
 (0)