Skip to content

Commit 6803c4f

Browse files
committed
fix: review feedback
1 parent 4c0be90 commit 6803c4f

24 files changed

Lines changed: 1451 additions & 149 deletions

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

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,16 +210,31 @@ const logClickStage = (_level: 'log' | 'warn' | 'error', _stage: string, _payloa
210210
// No-op in production. Enable for debugging click-to-position mapping.
211211
};
212212

213+
const readSelectionDebugEnabled = (): boolean => {
214+
if (typeof globalThis === 'undefined') return false;
215+
return (globalThis as { __sdSelectionDebug?: boolean }).__sdSelectionDebug === true;
216+
};
217+
213218
const SELECTION_DEBUG_ENABLED = false;
214219
const logSelectionDebug = (payload: Record<string, unknown>): void => {
215-
if (!SELECTION_DEBUG_ENABLED) return;
220+
const enabled = SELECTION_DEBUG_ENABLED || readSelectionDebugEnabled();
221+
if (!enabled) return;
216222
try {
217223
console.log('[SELECTION-DEBUG]', JSON.stringify(payload));
218224
} catch {
219225
console.log('[SELECTION-DEBUG]', payload);
220226
}
221227
};
222228

229+
const pushSelectionDebugSnapshot = (payload: Record<string, unknown>): void => {
230+
if (typeof globalThis === 'undefined') return;
231+
const target = globalThis as { __sdSelectionDebugLog?: Record<string, unknown>[] };
232+
if (!Array.isArray(target.__sdSelectionDebugLog)) {
233+
target.__sdSelectionDebugLog = [];
234+
}
235+
target.__sdSelectionDebugLog.push(payload);
236+
};
237+
223238
/**
224239
* Debug flag for DOM and geometry position mapping.
225240
* Set to true to enable detailed logging of click-to-position operations.
@@ -636,7 +651,8 @@ export function selectionToRects(
636651
pageIndex,
637652
});
638653

639-
if (SELECTION_DEBUG_ENABLED) {
654+
const selectionDebugEnabled = SELECTION_DEBUG_ENABLED || readSelectionDebugEnabled();
655+
if (selectionDebugEnabled) {
640656
const runs = block.runs.slice(line.fromRun, line.toRun + 1).map((run: Run, idx: number) => {
641657
const isAtomic =
642658
'src' in run ||
@@ -657,7 +673,7 @@ export function selectionToRects(
657673
};
658674
});
659675

660-
debugEntries.push({
676+
const debugEntry = {
661677
pageIndex,
662678
blockId: block.id,
663679
lineIndex: index,
@@ -690,9 +706,18 @@ export function selectionToRects(
690706
Math.max(charOffsetFrom, charOffsetTo),
691707
),
692708
indent: (block.attrs as { indent?: unknown } | undefined)?.indent,
709+
alignment: (block.attrs as { alignment?: unknown } | undefined)?.alignment,
693710
marker: measure.marker,
711+
markerWidth,
712+
isListItemFlag,
713+
alignmentOverride,
694714
lineSegments: line.segments,
695-
});
715+
lineSpaceCount: (line as { spaceCount?: unknown }).spaceCount,
716+
lineNaturalWidth: (line as { naturalWidth?: unknown }).naturalWidth,
717+
lineMaxWidth: (line as { maxWidth?: unknown }).maxWidth,
718+
};
719+
debugEntries.push(debugEntry);
720+
pushSelectionDebugSnapshot(debugEntry);
696721
}
697722
});
698723
return;

packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterPerRidLayout.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,73 @@ describe('layoutPerRIdHeaderFooters', () => {
129129
expect(deps.headerLayoutsByRId.has('rId-header-first')).toBe(true);
130130
expect(deps.headerLayoutsByRId.has('rId-header-orphan')).toBe(false);
131131
});
132+
133+
it('lays out first-page header refs in multi-section documents with per-section constraints', async () => {
134+
const headerBlocksByRId = new Map<string, FlowBlock[]>([
135+
['rId-header-default', [makeBlock('block-default')]],
136+
['rId-header-first', [makeBlock('block-first')]],
137+
['rId-header-section-1', [makeBlock('block-section-1')]],
138+
]);
139+
140+
const headerFooterInput = {
141+
headerBlocksByRId,
142+
footerBlocksByRId: undefined,
143+
headerBlocks: undefined,
144+
footerBlocks: undefined,
145+
constraints: {
146+
width: 400,
147+
height: 80,
148+
pageWidth: 600,
149+
pageHeight: 800,
150+
margins: {
151+
top: 50,
152+
right: 50,
153+
bottom: 50,
154+
left: 50,
155+
header: 20,
156+
},
157+
},
158+
};
159+
160+
const layout = {
161+
pages: [
162+
{ number: 1, fragments: [], sectionIndex: 0 },
163+
{ number: 2, fragments: [], sectionIndex: 1 },
164+
],
165+
} as unknown as Layout;
166+
167+
const sectionMetadata: SectionMetadata[] = [
168+
{
169+
sectionIndex: 0,
170+
margins: { top: 50, right: 50, bottom: 50, left: 50, header: 20 },
171+
headerRefs: {
172+
default: 'rId-header-default',
173+
first: 'rId-header-first',
174+
},
175+
},
176+
{
177+
sectionIndex: 1,
178+
margins: { top: 55, right: 55, bottom: 55, left: 55, header: 20 },
179+
headerRefs: {
180+
default: 'rId-header-section-1',
181+
},
182+
},
183+
];
184+
185+
const deps = {
186+
headerLayoutsByRId: new Map(),
187+
footerLayoutsByRId: new Map(),
188+
};
189+
190+
await layoutPerRIdHeaderFooters(headerFooterInput, layout, sectionMetadata, deps);
191+
192+
const laidOutBlockIds = new Set(
193+
mockLayoutHeaderFooterWithCache.mock.calls.map((call) => call[0].default?.[0]?.id).filter(Boolean),
194+
);
195+
196+
expect(laidOutBlockIds).toEqual(new Set(['block-default', 'block-first', 'block-section-1']));
197+
expect(deps.headerLayoutsByRId.has('rId-header-default::s0')).toBe(true);
198+
expect(deps.headerLayoutsByRId.has('rId-header-first::s0')).toBe(true);
199+
expect(deps.headerLayoutsByRId.has('rId-header-section-1::s1')).toBe(true);
200+
});
132201
});

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

Lines changed: 39 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -189,36 +189,6 @@ function collectReferencedRIdsBySection(effectiveRefsBySection: Map<number, Head
189189
return result;
190190
}
191191

192-
/**
193-
* Resolve the default header/footer rId for each section.
194-
*
195-
* Multi-section layout has historically measured only the default variant with
196-
* section-specific constraints. Preserve that behavior to avoid changing
197-
* established rendering for documents that use first/even/odd variants.
198-
*/
199-
function resolveDefaultRIdPerSection(
200-
sectionMetadata: SectionMetadata[],
201-
kind: 'header' | 'footer',
202-
): Map<number, string> {
203-
const result = new Map<number, string>();
204-
let inheritedDefaultRId: string | undefined;
205-
206-
for (const section of sectionMetadata) {
207-
const refs = getRefsForKind(section, kind);
208-
const explicitDefaultRId = refs?.default;
209-
210-
if (explicitDefaultRId) {
211-
inheritedDefaultRId = explicitDefaultRId;
212-
}
213-
214-
if (inheritedDefaultRId) {
215-
result.set(section.sectionIndex, inheritedDefaultRId);
216-
}
217-
}
218-
219-
return result;
220-
}
221-
222192
/**
223193
* Layout header/footer blocks per rId, respecting per-section margins.
224194
*
@@ -411,7 +381,7 @@ async function layoutWithPerSectionConstraints(
411381
): Promise<void> {
412382
if (!blocksByRId) return;
413383

414-
const defaultRIdPerSection = resolveDefaultRIdPerSection(sectionMetadata, kind);
384+
const effectiveRefsBySection = buildEffectiveRefsBySection(sectionMetadata, kind);
415385

416386
// Extract table width specs per rId (SD-1837).
417387
// Word allows tables in headers/footers to extend beyond content margins.
@@ -429,36 +399,48 @@ async function layoutWithPerSectionConstraints(
429399
// Key: `${rId}::w${effectiveWidth}`, Value: { constraints, sections[] }
430400
const groups = new Map<
431401
string,
432-
{ sectionConstraints: Constraints; sectionIndices: number[]; rId: string; effectiveWidth: number }
402+
{ sectionConstraints: Constraints; sectionIndices: Set<number>; rId: string; effectiveWidth: number }
433403
>();
434404

435405
for (const section of sectionMetadata) {
436-
const rId = defaultRIdPerSection.get(section.sectionIndex);
437-
if (!rId || !blocksByRId.has(rId)) continue;
438-
439-
// Resolve the minimum width needed for tables in this section.
440-
// For pct tables, this depends on the section's content width.
441-
const contentWidth = buildSectionContentWidth(section, fallbackConstraints);
442-
const tableWidthSpec = tableWidthSpecByRId.get(rId);
443-
const tableMinWidth = resolveTableMinWidth(tableWidthSpec, contentWidth);
444-
const sectionConstraints = buildConstraintsForSection(section, fallbackConstraints, tableMinWidth || undefined);
445-
const effectiveWidth = sectionConstraints.width;
446-
// Include vertical geometry in the key so sections with different page heights,
447-
// vertical margins, or header distance get separate layouts (page-relative anchors
448-
// and header band origin resolve differently).
449-
const groupKey = `${rId}::w${effectiveWidth}::ph${sectionConstraints.pageHeight ?? ''}::mt${sectionConstraints.margins?.top ?? ''}::mb${sectionConstraints.margins?.bottom ?? ''}::mh${sectionConstraints.margins?.header ?? ''}`;
450-
451-
let group = groups.get(groupKey);
452-
if (!group) {
453-
group = {
454-
sectionConstraints,
455-
sectionIndices: [],
456-
rId,
457-
effectiveWidth,
458-
};
459-
groups.set(groupKey, group);
406+
const refs = effectiveRefsBySection.get(section.sectionIndex);
407+
if (!refs) continue;
408+
409+
const uniqueRIds = new Set<string>();
410+
for (const variant of HEADER_FOOTER_VARIANTS) {
411+
const rId = refs[variant];
412+
if (rId) {
413+
uniqueRIds.add(rId);
414+
}
415+
}
416+
417+
for (const rId of uniqueRIds) {
418+
if (!blocksByRId.has(rId)) continue;
419+
420+
// Resolve the minimum width needed for tables in this section.
421+
// For pct tables, this depends on the section's content width.
422+
const contentWidth = buildSectionContentWidth(section, fallbackConstraints);
423+
const tableWidthSpec = tableWidthSpecByRId.get(rId);
424+
const tableMinWidth = resolveTableMinWidth(tableWidthSpec, contentWidth);
425+
const sectionConstraints = buildConstraintsForSection(section, fallbackConstraints, tableMinWidth || undefined);
426+
const effectiveWidth = sectionConstraints.width;
427+
// Include vertical geometry in the key so sections with different page heights,
428+
// vertical margins, or header distance get separate layouts (page-relative anchors
429+
// and header band origin resolve differently).
430+
const groupKey = `${rId}::w${effectiveWidth}::ph${sectionConstraints.pageHeight ?? ''}::mt${sectionConstraints.margins?.top ?? ''}::mb${sectionConstraints.margins?.bottom ?? ''}::mh${sectionConstraints.margins?.header ?? ''}`;
431+
432+
let group = groups.get(groupKey);
433+
if (!group) {
434+
group = {
435+
sectionConstraints,
436+
sectionIndices: new Set<number>(),
437+
rId,
438+
effectiveWidth,
439+
};
440+
groups.set(groupKey, group);
441+
}
442+
group.sectionIndices.add(section.sectionIndex);
460443
}
461-
group.sectionIndices.push(section.sectionIndex);
462444
}
463445

464446
// Measure and layout each unique (rId, effectiveWidth) group

packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterRegistry.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,61 @@ describe('HeaderFooterLayoutAdapter', () => {
546546
expect(options?.storyKey).toBe('hf:part:rId-header-default');
547547
});
548548

549+
it('passes tracked change render config through to header/footer flow blocks', () => {
550+
const descriptor = { id: 'rId-header-default', kind: 'header', variant: 'default' };
551+
const doc = { type: 'doc', content: [{ type: 'paragraph' }] };
552+
553+
const manager = {
554+
rootEditor: {
555+
converter: {
556+
convertedXml: {},
557+
numbering: {},
558+
linkedStyles: {},
559+
},
560+
},
561+
getDescriptors: (kind: string) => (kind === 'header' ? [descriptor] : []),
562+
getDocumentJson: vi.fn(() => doc),
563+
} as unknown as HeaderFooterEditorManager;
564+
565+
const adapter = new HeaderFooterLayoutAdapter(manager);
566+
adapter.setTrackedChangesRenderConfig({ mode: 'final', enabled: false });
567+
568+
mockToFlowBlocks.mockClear();
569+
adapter.getBatch('header');
570+
571+
const [, options] = mockToFlowBlocks.mock.calls[0] || [];
572+
expect(options?.trackedChangesMode).toBe('final');
573+
expect(options?.enableTrackedChanges).toBe(false);
574+
});
575+
576+
it('invalidates cached header/footer flow blocks when tracked change render config changes', () => {
577+
const descriptor = { id: 'rId-header-default', kind: 'header', variant: 'default' };
578+
const doc = { type: 'doc', content: [{ type: 'paragraph' }] };
579+
580+
const manager = {
581+
rootEditor: {
582+
converter: {
583+
convertedXml: {},
584+
numbering: {},
585+
linkedStyles: {},
586+
},
587+
},
588+
getDescriptors: (kind: string) => (kind === 'header' ? [descriptor] : []),
589+
getDocumentJson: vi.fn(() => doc),
590+
} as unknown as HeaderFooterEditorManager;
591+
592+
const adapter = new HeaderFooterLayoutAdapter(manager);
593+
594+
mockToFlowBlocks.mockClear();
595+
adapter.getBatch('header');
596+
adapter.getBatch('header');
597+
expect(mockToFlowBlocks).toHaveBeenCalledTimes(1);
598+
599+
adapter.setTrackedChangesRenderConfig({ mode: 'final', enabled: true });
600+
adapter.getBatch('header');
601+
expect(mockToFlowBlocks).toHaveBeenCalledTimes(2);
602+
});
603+
549604
it('returns undefined when no descriptors have FlowBlocks', () => {
550605
const manager = {
551606
getDescriptors: () => [{ id: 'missing', kind: 'header', variant: 'default' }],

0 commit comments

Comments
 (0)