Skip to content

Commit fa24d9a

Browse files
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.
1 parent fb9bf1d commit fa24d9a

4 files changed

Lines changed: 289 additions & 46 deletions

File tree

packages/layout-engine/layout-bridge/src/headerFooterUtils.ts

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -179,50 +179,41 @@ export const defaultMultiSectionIdentifier = (): MultiSectionHeaderFooterIdentif
179179
sections: [],
180180
});
181181

182-
function mergeSection0FallbackRefs(
183-
sectionRefs: SectionHeaderFooterIds | undefined,
184-
fallbackRefs: SectionHeaderFooterIds,
185-
): SectionHeaderFooterIds {
186-
return {
187-
default: sectionRefs?.default ?? fallbackRefs.default,
188-
first: sectionRefs?.first ?? fallbackRefs.first,
189-
even: sectionRefs?.even ?? fallbackRefs.even,
190-
odd: sectionRefs?.odd ?? fallbackRefs.odd,
191-
};
192-
}
193-
194182
function refreshResolutionSections(identifier: MultiSectionHeaderFooterIdentifier): void {
183+
if (
184+
identifier.sectionCount === 0 &&
185+
identifier.sectionHeaderIds.size === 0 &&
186+
identifier.sectionFooterIds.size === 0 &&
187+
identifier.sectionTitlePg.size === 0
188+
) {
189+
identifier.sections = [];
190+
return;
191+
}
192+
195193
const maxIndex = Math.max(
196194
identifier.sectionCount - 1,
197195
...Array.from(identifier.sectionHeaderIds.keys()),
198196
...Array.from(identifier.sectionFooterIds.keys()),
199197
...Array.from(identifier.sectionTitlePg.keys()),
200-
0,
201198
);
202199

203200
const sections: HeaderFooterResolutionSection[] = [];
204201
for (let sectionIndex = 0; sectionIndex <= maxIndex; sectionIndex += 1) {
205202
sections.push({
206203
sectionIndex,
207-
titlePg: identifier.sectionTitlePg.has(sectionIndex)
208-
? identifier.sectionTitlePg.get(sectionIndex)
209-
: sectionIndex === 0
210-
? identifier.titlePg
211-
: false,
212-
headerRefs:
213-
sectionIndex === 0
214-
? mergeSection0FallbackRefs(identifier.sectionHeaderIds.get(sectionIndex), identifier.headerIds)
215-
: identifier.sectionHeaderIds.get(sectionIndex),
216-
footerRefs:
217-
sectionIndex === 0
218-
? mergeSection0FallbackRefs(identifier.sectionFooterIds.get(sectionIndex), identifier.footerIds)
219-
: identifier.sectionFooterIds.get(sectionIndex),
204+
titlePg: identifier.sectionTitlePg.get(sectionIndex) ?? false,
205+
headerRefs: identifier.sectionHeaderIds.get(sectionIndex),
206+
footerRefs: identifier.sectionFooterIds.get(sectionIndex),
220207
});
221208
}
222209

223210
identifier.sections = sections;
224211
}
225212

213+
function getSectionTitlePg(identifier: MultiSectionHeaderFooterIdentifier, sectionIndex: number): boolean {
214+
return identifier.sectionTitlePg.get(sectionIndex) ?? false;
215+
}
216+
226217
/**
227218
* Builds a multi-section header/footer identifier from section metadata.
228219
*
@@ -400,10 +391,9 @@ export function getHeaderFooterTypeForSection(
400391
const kind = options?.kind ?? 'header';
401392
const sectionPageNumber = options?.sectionPageNumber ?? pageNumber;
402393

403-
// Check titlePg for this specific section
404-
const sectionTitlePg = identifier.sectionTitlePg.has(sectionIndex)
405-
? identifier.sectionTitlePg.get(sectionIndex)!
406-
: identifier.titlePg;
394+
// Check titlePg for this specific section. Omitted section metadata means false;
395+
// legacy converter titlePg is only used by the non-section-aware path.
396+
const sectionTitlePg = getSectionTitlePg(identifier, sectionIndex);
407397
const variant = selectHeaderFooterVariantForPage({
408398
documentPageNumber: pageNumber,
409399
sectionPageNumber,
@@ -448,9 +438,7 @@ export function getHeaderFooterIdForPage(
448438
const kind = options?.kind ?? 'header';
449439
const sectionIndex = page.sectionIndex ?? 0;
450440
const sectionPageNumber = options?.sectionPageNumber ?? page.number;
451-
const sectionTitlePg = identifier.sectionTitlePg.has(sectionIndex)
452-
? identifier.sectionTitlePg.get(sectionIndex)!
453-
: identifier.titlePg;
441+
const sectionTitlePg = getSectionTitlePg(identifier, sectionIndex);
454442
const variantType = selectHeaderFooterVariantForPage({
455443
documentPageNumber: page.number,
456444
sectionPageNumber,
@@ -522,9 +510,7 @@ export function resolveHeaderFooterForPageAndSection(
522510
const firstPageInSection = sectionFirstPageNumbers.get(sectionIndex);
523511
const sectionPageNumber = typeof firstPageInSection === 'number' ? pageNumber - firstPageInSection + 1 : pageNumber;
524512

525-
const sectionTitlePg = identifier.sectionTitlePg.has(sectionIndex)
526-
? identifier.sectionTitlePg.get(sectionIndex)!
527-
: identifier.titlePg;
513+
const sectionTitlePg = getSectionTitlePg(identifier, sectionIndex);
528514
const type = selectHeaderFooterVariantForPage({
529515
documentPageNumber: pageNumber,
530516
sectionPageNumber,

packages/layout-engine/layout-bridge/test/headerFooterUtils.test.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ describe('headerFooterUtils', () => {
377377
expect(identifier.footerIds.even).toBe('converter-f-even');
378378
});
379379

380-
it('should expose converter fallbacks through section-aware resolution', () => {
380+
it('keeps converter fallbacks on legacy fields without exposing them through section-aware resolution', () => {
381381
const sectionMetadata: SectionMetadata[] = [
382382
{
383383
sectionIndex: 0,
@@ -391,17 +391,44 @@ describe('headerFooterUtils', () => {
391391
footerIds: { default: 'converter-f-default' },
392392
});
393393

394-
expect(getHeaderFooterTypeForSection(1, 0, identifier, { kind: 'header' })).toBe('default');
394+
expect(identifier.headerIds.default).toBe('converter-h-default');
395+
expect(identifier.footerIds.default).toBe('converter-f-default');
396+
expect(getHeaderFooterTypeForSection(1, 0, identifier, { kind: 'header' })).toBeNull();
395397
expect(
396398
getHeaderFooterIdForPage({ number: 1, fragments: [], sectionIndex: 0 }, identifier, { kind: 'header' }),
397-
).toBe('converter-h-default');
398-
expect(getHeaderFooterTypeForSection(1, 0, identifier, { kind: 'footer' })).toBe('default');
399+
).toBeNull();
400+
expect(getHeaderFooterTypeForSection(1, 0, identifier, { kind: 'footer' })).toBeNull();
399401
expect(
400402
getHeaderFooterIdForPage({ number: 1, fragments: [], sectionIndex: 0 }, identifier, { kind: 'footer' }),
401-
).toBe('converter-f-default');
403+
).toBeNull();
402404
});
403405

404-
it('should preserve converter titlePg fallback for section 0 variant selection', () => {
406+
it('does not apply a legacy converter default footer to a footerless first section', () => {
407+
const sectionMetadata: SectionMetadata[] = [
408+
{
409+
sectionIndex: 0,
410+
titlePg: false,
411+
},
412+
{
413+
sectionIndex: 15,
414+
footerRefs: { default: 'rId22' },
415+
titlePg: false,
416+
},
417+
];
418+
419+
const identifier = buildMultiSectionIdentifier(sectionMetadata, undefined, {
420+
footerIds: { default: 'rId22' },
421+
});
422+
423+
expect(
424+
getHeaderFooterIdForPage({ number: 1, fragments: [], sectionIndex: 0 }, identifier, { kind: 'footer' }),
425+
).toBeNull();
426+
expect(
427+
getHeaderFooterIdForPage({ number: 2, fragments: [], sectionIndex: 15 }, identifier, { kind: 'footer' }),
428+
).toBe('rId22');
429+
});
430+
431+
it('keeps converter titlePg fallback on legacy fields without exposing first refs through section-aware resolution', () => {
405432
const sectionMetadata: SectionMetadata[] = [
406433
{
407434
sectionIndex: 0,
@@ -413,9 +440,30 @@ describe('headerFooterUtils', () => {
413440
headerIds: { first: 'converter-h-first', titlePg: true },
414441
});
415442

443+
expect(identifier.titlePg).toBe(true);
444+
expect(identifier.headerIds.first).toBe('converter-h-first');
445+
expect(
446+
getHeaderFooterIdForPage({ number: 1, fragments: [], sectionIndex: 0 }, identifier, { kind: 'header' }),
447+
).toBeNull();
448+
});
449+
450+
it('does not apply legacy converter titlePg to section-aware variant selection when titlePg is omitted', () => {
451+
const sectionMetadata: SectionMetadata[] = [
452+
{
453+
sectionIndex: 0,
454+
headerRefs: { default: 'h0-default' },
455+
},
456+
];
457+
458+
const identifier = buildMultiSectionIdentifier(sectionMetadata, undefined, {
459+
headerIds: { first: 'legacy-first', titlePg: true },
460+
});
461+
462+
expect(identifier.titlePg).toBe(true);
463+
expect(getHeaderFooterTypeForSection(1, 0, identifier, { kind: 'header', sectionPageNumber: 1 })).toBe('default');
416464
expect(
417465
getHeaderFooterIdForPage({ number: 1, fragments: [], sectionIndex: 0 }, identifier, { kind: 'header' }),
418-
).toBe('converter-h-first');
466+
).toBe('h0-default');
419467
});
420468

421469
it('should NOT override existing section metadata with converter IDs', () => {

packages/super-editor/src/editors/v1/core/presentation-editor/header-footer/HeaderFooterSessionManager.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ type SurfacePmEntry = {
7777
el: HTMLElement;
7878
};
7979

80+
function hasSectionRefsForKind(
81+
identifier: MultiSectionHeaderFooterIdentifier | null | undefined,
82+
kind: 'header' | 'footer',
83+
): identifier is MultiSectionHeaderFooterIdentifier {
84+
const refKey = kind === 'header' ? 'headerRefs' : 'footerRefs';
85+
return Boolean(identifier?.sections?.some((section) => section[refKey] !== undefined));
86+
}
87+
8088
// AIDEV-NOTE: compat-fallback - header/footer session interaction still keys
8189
// off `data-pm-*` (prep-002). DomPainter also stamps the parallel neutral
8290
// dataset (`data-layout-fragment-id` etc.) which a future v2 consumer can
@@ -2379,21 +2387,22 @@ export class HeaderFooterSessionManager {
23792387
sectionFirstPageNumbers.set(idx, p.number);
23802388
}
23812389
}
2390+
const hasSectionResolution = hasSectionRefsForKind(multiSectionId, kind);
23822391

23832392
return (pageNumber, pageMargins, page) => {
23842393
const sectionIndex = page?.sectionIndex ?? 0;
23852394
const firstPageInSection = sectionFirstPageNumbers.get(sectionIndex);
23862395
const sectionPageNumber =
23872396
typeof firstPageInSection === 'number' ? pageNumber - firstPageInSection + 1 : pageNumber;
2388-
const headerFooterType = multiSectionId
2397+
const headerFooterType = hasSectionResolution
23892398
? getHeaderFooterTypeForSection(pageNumber, sectionIndex, multiSectionId, { kind, sectionPageNumber })
23902399
: getHeaderFooterType(pageNumber, legacyIdentifier, { kind });
23912400

23922401
if (!headerFooterType) {
23932402
return null;
23942403
}
23952404

2396-
const effectiveRef = multiSectionId?.sections?.length
2405+
const effectiveRef = hasSectionResolution
23972406
? resolveEffectiveHeaderFooterRef({
23982407
sections: multiSectionId.sections,
23992408
sectionIndex,
@@ -2404,7 +2413,8 @@ export class HeaderFooterSessionManager {
24042413
const pageSectionRefs = kind === 'header' ? page?.sectionRefs?.headerRefs : page?.sectionRefs?.footerRefs;
24052414
const legacyRefs = kind === 'header' ? legacyIdentifier.headerIds : legacyIdentifier.footerIds;
24062415
const fallbackRef =
2407-
refForVariant(pageSectionRefs, headerFooterType) ?? refForVariant(legacyRefs, headerFooterType);
2416+
refForVariant(pageSectionRefs, headerFooterType) ??
2417+
(!hasSectionResolution ? refForVariant(legacyRefs, headerFooterType) : undefined);
24082418
const sectionRId = effectiveRef?.refId ?? fallbackRef?.refId;
24092419
const layoutVariantType = effectiveRef?.matchedVariant ?? fallbackRef?.matchedVariant ?? headerFooterType;
24102420

0 commit comments

Comments
 (0)