diff --git a/packages/layout-engine/contracts/src/index.test.ts b/packages/layout-engine/contracts/src/index.test.ts index a453d025eb..51286c7172 100644 --- a/packages/layout-engine/contracts/src/index.test.ts +++ b/packages/layout-engine/contracts/src/index.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { cloneColumnLayout, extractHeaderFooterSpace, normalizeColumnLayout, widthsEqual } from './index.js'; -import type { FlowBlock, Layout, PainterDOM, PainterPDF } from './index.js'; +import type { FlowBlock, Layout } from './index.js'; describe('contracts', () => { it('accepts a basic FlowBlock structure', () => { @@ -20,7 +20,7 @@ describe('contracts', () => { expect(block.id).toBe('block-1'); }); - it('describes a minimal layout', async () => { + it('describes a minimal layout', () => { const layout: Layout = { pageSize: { w: 612, h: 792 }, pages: [ @@ -62,25 +62,8 @@ describe('contracts', () => { }, }; - const domPainter: PainterDOM = { - paint(received, mount) { - mount.dataset.pageCount = String(received.pages.length); - }, - }; - - const pdfPainter: PainterPDF = { - async render(received) { - expect(received.pages.length).toBeGreaterThan(0); - return new Blob([JSON.stringify(received)]); - }, - }; - - const mount = document.createElement('div'); - domPainter.paint(layout, mount); - expect(mount.dataset.pageCount).toBe('1'); - - const blob = await pdfPainter.render(layout); - expect(blob).toBeInstanceOf(Blob); + expect(layout.pages.length).toBe(1); + expect(layout.pages[0].fragments.length).toBe(1); }); it('extracts header/footer spacing from margins', () => { diff --git a/packages/layout-engine/contracts/src/index.ts b/packages/layout-engine/contracts/src/index.ts index 294c4581b5..326af437f9 100644 --- a/packages/layout-engine/contracts/src/index.ts +++ b/packages/layout-engine/contracts/src/index.ts @@ -1880,23 +1880,6 @@ export type WrapExclusion = { wrapText: WrapTextMode; }; -export type RenderedLineInfo = { - el: HTMLElement; - top: number; - height: number; -}; - -/** - * Interface for position mapping from ProseMirror transactions. - * Used to efficiently update DOM position attributes without full re-render. - */ -export interface PositionMapping { - /** Transform a position from old to new document coordinates */ - map(pos: number, bias?: number): number; - /** Array of step maps - length indicates transaction complexity */ - readonly maps: readonly unknown[]; -} - /** * Rendering flow mode. * - `paginated`: discrete page surfaces @@ -1904,63 +1887,6 @@ export interface PositionMapping { */ export type FlowMode = 'paginated' | 'semantic'; -export interface PainterDOM { - paint(layout: Layout, mount: HTMLElement, mapping?: PositionMapping): void; - /** - * Updates the painter's internal block and measure data without reinstantiating. - * - * This method is an optimization for incremental rendering pipelines that need to - * refresh the underlying data (e.g., after content edits) without creating a new - * painter instance. It updates the painter's internal lookup tables to reflect - * the new blocks and measures. - * - * Header and footer blocks should be provided when the layout includes header/footer - * content that needs to be rendered. These are typically generated by the layout - * engine's header/footer adapter and should be passed through to the painter. - * - * @param blocks - Main document blocks to be rendered in the page body - * @param measures - Measurements corresponding to the main document blocks (must match blocks array length) - * @param headerBlocks - Optional array of blocks for header content. When provided, headerMeasures must also be provided. - * @param headerMeasures - Optional measurements for header blocks (must match headerBlocks array length when provided) - * @param footerBlocks - Optional array of blocks for footer content. When provided, footerMeasures must also be provided. - * @param footerMeasures - Optional measurements for footer blocks (must match footerBlocks array length when provided) - * - * @throws {Error} When blocks and measures array lengths don't match - * @throws {Error} When headerBlocks is provided without headerMeasures or vice versa - * @throws {Error} When headerBlocks and headerMeasures lengths don't match - * @throws {Error} When footerBlocks is provided without footerMeasures or vice versa - * @throws {Error} When footerBlocks and footerMeasures lengths don't match - * - * @example - * ```typescript - * // Basic usage with main document only - * painter.setData(blocks, measures); - * - * // With headers and footers - * painter.setData( - * mainBlocks, - * mainMeasures, - * headerBlocks, - * headerMeasures, - * footerBlocks, - * footerMeasures - * ); - * ``` - */ - setData?( - blocks: FlowBlock[], - measures: Measure[], - headerBlocks?: FlowBlock[], - headerMeasures?: Measure[], - footerBlocks?: FlowBlock[], - footerMeasures?: Measure[], - ): void; -} - -export interface PainterPDF { - render(layout: Layout): Promise; -} - export const extractHeaderFooterSpace = ( margins?: PageMargins | null, ): { diff --git a/packages/layout-engine/painters/dom/src/index.test.ts b/packages/layout-engine/painters/dom/src/index.test.ts index 7fbd6966b1..d2681ccc0e 100644 --- a/packages/layout-engine/painters/dom/src/index.test.ts +++ b/packages/layout-engine/painters/dom/src/index.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'; import { createDomPainter, sanitizeUrl, linkMetrics, applyRunDataAttributes } from './index.js'; import { DomPainter } from './renderer.js'; +import type { DomPainterOptions, DomPainterInput } from './index.js'; import { resolveListMarkerGeometry } from '../../../../../shared/common/list-marker-utils.js'; import type { FlowBlock, @@ -15,6 +16,68 @@ import type { TableMeasure, } from '@superdoc/contracts'; +const emptyResolved: ResolvedLayout = { version: 1, flowMode: 'paginated', pageGap: 0, pages: [] }; + +/** + * Test-only bridge: accepts old-style `{ blocks, measures, ...options }` and + * returns a painter whose `paint()` automatically builds a `DomPainterInput`. + * This lets existing tests exercise the new DomPainter code path without + * rewriting every call site. + */ +function createTestPainter(opts: { blocks?: FlowBlock[]; measures?: Measure[] } & DomPainterOptions) { + const { blocks: initBlocks, measures: initMeasures, ...painterOpts } = opts; + const painter = createDomPainter(painterOpts); + let currentBlocks: FlowBlock[] = initBlocks ?? []; + let currentMeasures: Measure[] = initMeasures ?? []; + let currentResolved: ResolvedLayout = emptyResolved; + let headerBlocks: FlowBlock[] | undefined; + let headerMeasures: Measure[] | undefined; + let footerBlocks: FlowBlock[] | undefined; + let footerMeasures: Measure[] | undefined; + + return { + paint(layout: Layout, mount: HTMLElement, mapping?: unknown) { + const input: DomPainterInput = { + resolvedLayout: currentResolved, + sourceLayout: layout, + blocks: currentBlocks, + measures: currentMeasures, + headerBlocks, + headerMeasures, + footerBlocks, + footerMeasures, + }; + painter.paint(input, mount, mapping as any); + }, + setData( + blocks: FlowBlock[], + measures: Measure[], + hb?: FlowBlock[], + hm?: Measure[], + fb?: FlowBlock[], + fm?: Measure[], + ) { + currentBlocks = blocks; + currentMeasures = measures; + headerBlocks = hb; + headerMeasures = hm; + footerBlocks = fb; + footerMeasures = fm; + }, + setResolvedLayout(rl: ResolvedLayout | null) { + currentResolved = rl ?? emptyResolved; + }, + setProviders: painter.setProviders, + setVirtualizationPins: painter.setVirtualizationPins, + setActiveComment: painter.setActiveComment, + getActiveComment: painter.getActiveComment, + getPaintSnapshot: painter.getPaintSnapshot, + onScroll: painter.onScroll, + setZoom: painter.setZoom, + setScrollContainer: painter.setScrollContainer, + }; +} + const block: FlowBlock = { kind: 'paragraph', id: 'block-1', @@ -226,7 +289,7 @@ describe('DomPainter', () => { }); it('renders pages and fragments into the mount', () => { - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); expect(mount.classList.contains('superdoc-layout')).toBe(true); @@ -293,7 +356,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [alignedBlock], measures: [alignedMeasure] }); + const painter = createTestPainter({ blocks: [alignedBlock], measures: [alignedMeasure] }); painter.paint(alignedLayout, mount); const line = mount.querySelector('.superdoc-line') as HTMLElement; @@ -361,7 +424,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [justifyBlock], measures: [justifyMeasure] }); + const painter = createTestPainter({ blocks: [justifyBlock], measures: [justifyMeasure] }); painter.paint(justifyLayout, mount); const lines = Array.from(mount.querySelectorAll('.superdoc-line')) as HTMLElement[]; @@ -435,7 +498,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [justifyWithBreakBlock], measures: [justifyWithBreakMeasure] }); + const painter = createTestPainter({ blocks: [justifyWithBreakBlock], measures: [justifyWithBreakMeasure] }); painter.paint(justifyWithBreakLayout, mount); const lines = Array.from(mount.querySelectorAll('.superdoc-line')) as HTMLElement[]; @@ -493,7 +556,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [singleLineBlock], measures: [singleLineMeasure] }); + const painter = createTestPainter({ blocks: [singleLineBlock], measures: [singleLineMeasure] }); painter.paint(singleLineLayout, mount); const lines = Array.from(mount.querySelectorAll('.superdoc-line')) as HTMLElement[]; @@ -550,7 +613,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [singleLineWithBreakBlock], measures: [singleLineWithBreakMeasure] }); + const painter = createTestPainter({ blocks: [singleLineWithBreakBlock], measures: [singleLineWithBreakMeasure] }); painter.paint(singleLineWithBreakLayout, mount); const lines = Array.from(mount.querySelectorAll('.superdoc-line')) as HTMLElement[]; @@ -639,7 +702,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [multiFragmentBlock], measures: [multiFragmentMeasure] }); + const painter = createTestPainter({ blocks: [multiFragmentBlock], measures: [multiFragmentMeasure] }); painter.paint(multiFragmentLayout, mount); const lines = Array.from(mount.querySelectorAll('.superdoc-line')) as HTMLElement[]; @@ -725,14 +788,14 @@ describe('DomPainter', () => { }; // Test right alignment - const rightPainter = createDomPainter({ blocks: [rightAlignBlock], measures: [singleLineMeasure] }); + const rightPainter = createTestPainter({ blocks: [rightAlignBlock], measures: [singleLineMeasure] }); rightPainter.paint(rightAlignLayout, mount); let line = mount.querySelector('.superdoc-line') as HTMLElement; expect(line.style.textAlign).toBe('right'); // Clear and test center alignment mount.innerHTML = ''; - const centerPainter = createDomPainter({ blocks: [centerAlignBlock], measures: [singleLineMeasure] }); + const centerPainter = createTestPainter({ blocks: [centerAlignBlock], measures: [singleLineMeasure] }); centerPainter.paint(centerAlignLayout, mount); line = mount.querySelector('.superdoc-line') as HTMLElement; expect(line.style.textAlign).toBe('center'); @@ -819,7 +882,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [listParaBlock], measures: [listParaMeasure] }); + const painter = createTestPainter({ blocks: [listParaBlock], measures: [listParaMeasure] }); painter.paint(listParaLayout, mount); const lines = Array.from(mount.querySelectorAll('.superdoc-line')) as HTMLElement[]; @@ -916,7 +979,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [listParaBlock], measures: [listParaMeasure] }); + const painter = createTestPainter({ blocks: [listParaBlock], measures: [listParaMeasure] }); painter.paint(listParaLayout, mount); const firstLine = mount.querySelector('.superdoc-line') as HTMLElement; @@ -999,7 +1062,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [singleLineListBlock], measures: [singleLineListMeasure] }); + const painter = createTestPainter({ blocks: [singleLineListBlock], measures: [singleLineListMeasure] }); painter.paint(singleLineListLayout, mount); const lines = Array.from(mount.querySelectorAll('.superdoc-line')) as HTMLElement[]; @@ -1092,7 +1155,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [tableBlock], measures: [tableMeasure] }); + const painter = createTestPainter({ blocks: [tableBlock], measures: [tableMeasure] }); painter.paint(tableLayout, mount); // Find the line inside the table cell @@ -1203,7 +1266,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [tableBlock], measures: [tableMeasure] }); + const painter = createTestPainter({ blocks: [tableBlock], measures: [tableMeasure] }); painter.paint(tableLayout, mount); // Find both lines inside the table cell @@ -1243,7 +1306,7 @@ describe('DomPainter', () => { // Intentionally empty - suppress expected error logging during this regression test. }); - const painter = createDomPainter({ blocks: [], measures: [] }); + const painter = createTestPainter({ blocks: [], measures: [] }); expect(() => painter.paint(missingTableLayout, mount)).not.toThrow(); const placeholder = mount.querySelector('.render-error-placeholder') as HTMLElement | null; @@ -1418,7 +1481,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [compressBlock], measures: [compressMeasure] }); + const painter = createTestPainter({ blocks: [compressBlock], measures: [compressMeasure] }); painter.paint(compressLayout, mount); const lines = mount.querySelectorAll('.superdoc-line') as NodeListOf; @@ -1502,7 +1565,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [negativeIndentBlock], measures: [negativeIndentMeasure] }); + const painter = createTestPainter({ blocks: [negativeIndentBlock], measures: [negativeIndentMeasure] }); painter.paint(negativeIndentLayout, mount); const lines = mount.querySelectorAll('.superdoc-line') as NodeListOf; @@ -1523,7 +1586,7 @@ describe('DomPainter', () => { }); it('emits pm metadata attributes', () => { - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const fragment = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -1540,7 +1603,8 @@ describe('DomPainter', () => { }); it('throws if blocks and measures length mismatch', () => { - expect(() => createDomPainter({ blocks: [block], measures: [] })).toThrow(/same number of blocks/); + const painter = createTestPainter({ blocks: [block], measures: [] }); + expect(() => painter.paint(layout, mount)).toThrow(/same number of blocks/); }); it('renders placeholder content for empty lines', () => { @@ -1585,7 +1649,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [blockWithEmptyRun], measures: [measureWithEmptyLine], }); @@ -1637,7 +1701,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [blockWithEmptyRun], measures: [measureWithEmptyLine], }); @@ -1680,7 +1744,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -1690,7 +1754,7 @@ describe('DomPainter', () => { }); it('annotates fragments and runs with SDT metadata', () => { - const painter = createDomPainter({ blocks: [sdtBlock], measures: [sdtMeasure] }); + const painter = createTestPainter({ blocks: [sdtBlock], measures: [sdtMeasure] }); painter.paint(sdtLayout, mount); const fragment = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -1762,7 +1826,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [sectionBlock], measures: [sectionMeasure] }); + const painter = createTestPainter({ blocks: [sectionBlock], measures: [sectionMeasure] }); painter.paint(sectionLayout, mount); const fragment = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -1840,7 +1904,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [tocBlock], measures: [tocMeasure] }); + const painter = createTestPainter({ blocks: [tocBlock], measures: [tocMeasure] }); painter.paint(tocLayout, mount); const fragment = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -1952,7 +2016,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [inlineScBlock], measures: [inlineScMeasure] }); + const painter = createTestPainter({ blocks: [inlineScBlock], measures: [inlineScMeasure] }); painter.paint(inlineScLayout, mount); // Should have exactly ONE wrapper for the grouped runs @@ -2051,7 +2115,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [markerBlock], measures: [markerMeasure], }); @@ -2140,7 +2204,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [nestedBlock], measures: [nestedMeasure], }); @@ -2226,7 +2290,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [tabBlock], measures: [tabMeasure], }); @@ -2315,7 +2379,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [longMarkerBlock], measures: [longMarkerMeasure], }); @@ -2408,7 +2472,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [rightMarkerBlock], measures: [rightMarkerMeasure], }); @@ -2494,7 +2558,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(listLayout, mount); const lineEl = mount.querySelector('.superdoc-line') as HTMLElement; @@ -2576,7 +2640,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(listLayout, mount); const lineEl = mount.querySelector('.superdoc-line') as HTMLElement; @@ -2685,7 +2749,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(listLayout, mount); const lineEl = mount.querySelector('.superdoc-line') as HTMLElement; @@ -2714,7 +2778,7 @@ describe('DomPainter', () => { }); it('reuses fragment DOM nodes when layout geometry changes', () => { - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const fragmentBefore = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -2741,7 +2805,7 @@ describe('DomPainter', () => { }); it('rebuilds fragment DOM when block content changes via setData', () => { - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const fragmentBefore = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -2767,7 +2831,7 @@ describe('DomPainter', () => { ], totalHeight: 20, }; - painter.setData?.([updatedBlock], [updatedMeasure]); + painter.setData([updatedBlock], [updatedMeasure]); const updatedLayout: Layout = { ...layout, @@ -2846,7 +2910,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [lockedBlock], measures: [lockedMeasure] }); + const painter = createTestPainter({ blocks: [lockedBlock], measures: [lockedMeasure] }); painter.paint(lockedLayout, mount); const fragmentBefore = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -2867,7 +2931,7 @@ describe('DomPainter', () => { }, }; - painter.setData?.([updatedLockedBlock], [lockedMeasure]); + painter.setData([updatedLockedBlock], [lockedMeasure]); painter.paint(lockedLayout, mount); const fragmentAfter = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -2875,7 +2939,7 @@ describe('DomPainter', () => { }); it('updates fragment positions in virtualized mode when layout changes without block diffs', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 2 }, @@ -2897,7 +2961,7 @@ describe('DomPainter', () => { }, }) as DOMRect; - painter.setData?.([block], [measure]); + painter.setData([block], [measure]); painter.paint(layout, virtualMount); const fragmentBefore = virtualMount.querySelector('.superdoc-fragment') as HTMLElement; expect(fragmentBefore.style.left).toBe('30px'); @@ -2915,7 +2979,7 @@ describe('DomPainter', () => { ], }; - painter.setData?.([block], [measure]); + painter.setData([block], [measure]); painter.paint(shiftedLayout, virtualMount); const fragmentAfter = virtualMount.querySelector('.superdoc-fragment') as HTMLElement; @@ -2923,7 +2987,7 @@ describe('DomPainter', () => { }); it('exposes a paint snapshot after rendering', () => { - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); @@ -2935,7 +2999,7 @@ describe('DomPainter', () => { }); it('uses actual page indices when collecting virtualized paint snapshots', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 2 }, @@ -3023,7 +3087,7 @@ describe('DomPainter', () => { width: 200, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, headerBlock], measures: [measure, headerMeasure], headerProvider: () => ({ fragments: [headerFragment], height: 16 }), @@ -3091,7 +3155,7 @@ describe('DomPainter', () => { behindDoc: false, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, behindDocImageBlock, normalImageBlock], measures: [measure, behindDocImageMeasure, normalImageMeasure], headerProvider: () => ({ @@ -3160,7 +3224,7 @@ describe('DomPainter', () => { isAnchored: true, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, behindDocImageBlock], measures: [measure, behindDocImageMeasure], headerProvider: () => ({ @@ -3214,7 +3278,7 @@ describe('DomPainter', () => { trackedBlock.runs[0].text.length, ); - const painter = createDomPainter({ blocks: [trackedBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [trackedBlock], measures: [paragraphMeasure] }); painter.paint(paragraphLayout, mount); const span = mount.querySelector('.superdoc-line span') as HTMLElement; @@ -3249,7 +3313,7 @@ describe('DomPainter', () => { trackedCommentBlock.runs[0].text.length, ); - const painter = createDomPainter({ blocks: [trackedCommentBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [trackedCommentBlock], measures: [paragraphMeasure] }); painter.setActiveComment('comment-1'); painter.paint(paragraphLayout, mount); @@ -3280,7 +3344,7 @@ describe('DomPainter', () => { highlightedCommentBlock.runs[0].text.length, ); - const painter = createDomPainter({ blocks: [highlightedCommentBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [highlightedCommentBlock], measures: [paragraphMeasure] }); painter.paint(paragraphLayout, mount); const span = mount.querySelector('.superdoc-comment-highlight') as HTMLElement; @@ -3311,7 +3375,7 @@ describe('DomPainter', () => { const { paragraphMeasure, paragraphLayout } = buildSingleParagraphData(block.id, block.runs[0].text.length); - const painter = createDomPainter({ blocks: [block], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [block], measures: [paragraphMeasure] }); painter.setActiveComment('comment-active-hl'); painter.paint(paragraphLayout, mount); @@ -3340,7 +3404,7 @@ describe('DomPainter', () => { const { paragraphMeasure, paragraphLayout } = buildSingleParagraphData(block.id, block.runs[0].text.length); - const painter = createDomPainter({ blocks: [block], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [block], measures: [paragraphMeasure] }); // Activate a different comment so this one gets faded painter.setActiveComment('some-other-comment'); painter.paint(paragraphLayout, mount); @@ -3372,7 +3436,7 @@ describe('DomPainter', () => { commentBlock.runs[0].text.length, ); - const painter = createDomPainter({ blocks: [commentBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [commentBlock], measures: [paragraphMeasure] }); painter.paint(paragraphLayout, mount); const span = mount.querySelector('.superdoc-comment-highlight') as HTMLElement; @@ -3401,7 +3465,7 @@ describe('DomPainter', () => { commentBlock.runs[0].text.length, ); - const painter = createDomPainter({ blocks: [commentBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [commentBlock], measures: [paragraphMeasure] }); // Initially (no active comment), should be highlighted painter.paint(paragraphLayout, mount); @@ -3447,7 +3511,7 @@ describe('DomPainter', () => { nestedCommentBlock.runs[0].text.length, ); - const painter = createDomPainter({ blocks: [nestedCommentBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [nestedCommentBlock], measures: [paragraphMeasure] }); // Select outer comment painter.setActiveComment('outer-comment'); @@ -3479,7 +3543,7 @@ describe('DomPainter', () => { commentBlock.runs[0].text.length, ); - const painter = createDomPainter({ blocks: [commentBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [commentBlock], measures: [paragraphMeasure] }); // First select a comment painter.setActiveComment('comment-X'); @@ -3521,7 +3585,7 @@ describe('DomPainter', () => { finalBlock.runs[0].text.length, ); - const painter = createDomPainter({ blocks: [finalBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [finalBlock], measures: [paragraphMeasure] }); painter.paint(paragraphLayout, mount); const span = mount.querySelector('[data-track-change-id="change-final"]') as HTMLElement; @@ -3556,7 +3620,7 @@ describe('DomPainter', () => { disabledBlock.runs[0].text.length, ); - const painter = createDomPainter({ blocks: [disabledBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [disabledBlock], measures: [paragraphMeasure] }); painter.paint(paragraphLayout, mount); const span = mount.querySelector('.superdoc-line span') as HTMLElement; @@ -3599,12 +3663,12 @@ describe('DomPainter', () => { const { paragraphMeasure, paragraphLayout } = buildSingleParagraphData(blockId, originalBlock.runs[0].text.length); - const painter = createDomPainter({ blocks: [originalBlock], measures: [paragraphMeasure] }); + const painter = createTestPainter({ blocks: [originalBlock], measures: [paragraphMeasure] }); painter.paint(paragraphLayout, mount); expect(mount.querySelector('[data-track-change-id]')).toBeNull(); - painter.setData?.([updatedBlock], [paragraphMeasure]); + painter.setData([updatedBlock], [paragraphMeasure]); painter.paint(paragraphLayout, mount); const trackedSpan = mount.querySelector('[data-track-change-id="tc-new"]') as HTMLElement; @@ -3649,7 +3713,7 @@ describe('DomPainter', () => { width: 200, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, footerBlock], measures: [measure, footerMeasure], footerProvider: () => ({ fragments: [footerFragment], height: 14 }), @@ -3697,7 +3761,7 @@ describe('DomPainter', () => { const contentHeight = 20; const footerOffset = 400; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, footerBlock], measures: [measure, footerMeasure], footerProvider: () => ({ @@ -3758,7 +3822,7 @@ describe('DomPainter', () => { width: 200, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, footerBlock], measures: [measure, footerMeasure], footerProvider: () => ({ fragments: [footerFragment], height: 14 }), @@ -3800,7 +3864,7 @@ describe('DomPainter', () => { behindDoc: true, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, headerImageBlock], measures: [measure, headerImageMeasure], headerProvider: () => ({ @@ -3855,7 +3919,7 @@ describe('DomPainter', () => { const footerHeight = 80; const footerContentHeight = 30; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, footerImageBlock], measures: [measure, footerImageMeasure], footerProvider: () => ({ @@ -3928,7 +3992,7 @@ describe('DomPainter', () => { width: 200, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, headerBlock], measures: [measure, headerMeasure], headerProvider: () => ({ fragments: [headerFragment], height: 16 }), @@ -3978,7 +4042,7 @@ describe('DomPainter', () => { width: 200, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, headerBlock], measures: [measure, headerMeasure], headerProvider: () => ({ fragments: [headerFragment], height: 16 }), @@ -4042,7 +4106,7 @@ describe('DomPainter', () => { width: 200, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, headerBlock], measures: [measure, headerMeasure], headerProvider: () => ({ fragments: [headerFragment], height: 16 }), @@ -4099,7 +4163,7 @@ describe('DomPainter', () => { width: 200, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block, headerBlock], measures: [measure, headerMeasure], headerProvider: () => ({ fragments: [headerFragment], height: 16 }), @@ -4165,7 +4229,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [listBlock], measures: [listMeasure] }); + const painter = createTestPainter({ blocks: [listBlock], measures: [listMeasure] }); painter.paint(listLayout, mount); const marker = mount.querySelector('.superdoc-list-marker'); @@ -4302,9 +4366,9 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [listBlock], measures: [listMeasure] }); + const painter = createTestPainter({ blocks: [listBlock], measures: [listMeasure] }); - painter.setResolvedLayout?.(initialResolvedLayout); + painter.setResolvedLayout(initialResolvedLayout); painter.paint(initialLayout, mount); const initialWrapper = mount.querySelector('.superdoc-fragment-list-item') as HTMLElement; @@ -4312,7 +4376,7 @@ describe('DomPainter', () => { expect(initialWrapper.style.top).toBe('40px'); expect(initialWrapper.style.width).toBe('290px'); - painter.setResolvedLayout?.(updatedResolvedLayout); + painter.setResolvedLayout(updatedResolvedLayout); painter.paint(updatedLayout, mount); const updatedWrapper = mount.querySelector('.superdoc-fragment-list-item') as HTMLElement; @@ -4429,12 +4493,12 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [anchoredDrawingBlock, inlineDrawingBlock], measures: [drawingMeasure, drawingMeasure], }); - painter.setResolvedLayout?.(resolvedLayout); + painter.setResolvedLayout(resolvedLayout); painter.paint(drawingLayout, mount); const anchoredDrawingEl = mount.querySelector('[data-block-id="drawing-anchored"]') as HTMLElement; @@ -4524,12 +4588,12 @@ describe('DomPainter', () => { }, }); - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [paragraphBlock], measures: [paragraphMeasure], }); - painter.setResolvedLayout?.(resolvedLayout); + painter.setResolvedLayout(resolvedLayout); painter.paint(paragraphLayout, mount); const lineEls = mount.querySelectorAll('.superdoc-line'); @@ -4617,12 +4681,12 @@ describe('DomPainter', () => { }, }); - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [paragraphBlock], measures: [paragraphMeasure], }); - painter.setResolvedLayout?.(resolvedLayout); + painter.setResolvedLayout(resolvedLayout); painter.paint(paragraphLayout, mount); const lineEl = mount.querySelector('.superdoc-line') as HTMLElement; @@ -4708,12 +4772,12 @@ describe('DomPainter', () => { }, }); - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [paragraphBlock], measures: [paragraphMeasure], }); - painter.setResolvedLayout?.(resolvedLayout); + painter.setResolvedLayout(resolvedLayout); painter.paint(paragraphLayout, mount); const dropCapEl = mount.querySelector('.superdoc-drop-cap') as HTMLElement; @@ -4782,7 +4846,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [decoratedBlock], measures: [decoratedMeasure] }); + const painter = createTestPainter({ blocks: [decoratedBlock], measures: [decoratedMeasure] }); painter.paint(decoratedLayout, mount); const anchor = mount.querySelector('a') as HTMLAnchorElement; @@ -4858,7 +4922,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(richLayout, mount); const anchor = mount.querySelector('a') as HTMLAnchorElement; @@ -4927,7 +4991,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(blockedLayout, mount); const span = mount.querySelector('.superdoc-fragment span') as HTMLSpanElement; @@ -4989,7 +5053,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(longUrlLayout, mount); // Should render as blocked span, not anchor @@ -5051,7 +5115,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(maxUrlLayout, mount); const anchor = mount.querySelector('a'); @@ -5088,7 +5152,7 @@ describe('DomPainter', () => { totalHeight: 18, }; - const painter = createDomPainter({ blocks: [blockWithTabs], measures: [measureWithLeaders] }); + const painter = createTestPainter({ blocks: [blockWithTabs], measures: [measureWithLeaders] }); const tabLayout: Layout = { pageSize: layout.pageSize, pages: [ @@ -5139,7 +5203,7 @@ describe('DomPainter', () => { runs: [{ text: 'Border test', fontFamily: 'Arial', fontSize: 16 }], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [blockWithBorders], measures: [measure], }); @@ -5189,7 +5253,7 @@ describe('DomPainter', () => { runs: [{ text: 'Shaded paragraph', fontFamily: 'Arial', fontSize: 16 }], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [shadedBlock], measures: [measure], }); @@ -5294,7 +5358,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [listBlock], measures: [listMeasure] }); + const painter = createTestPainter({ blocks: [listBlock], measures: [listMeasure] }); painter.paint(listLayout, mount); const content = mount.querySelector('.superdoc-list-content') as HTMLElement; @@ -5359,7 +5423,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [indentBlock], measures: [indentMeasure] }); + const painter = createTestPainter({ blocks: [indentBlock], measures: [indentMeasure] }); painter.paint(indentLayout, mount); const lines = mount.querySelectorAll('.superdoc-line') as NodeListOf; @@ -5428,7 +5492,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [firstLineBlock], measures: [firstLineMeasure] }); + const painter = createTestPainter({ blocks: [firstLineBlock], measures: [firstLineMeasure] }); painter.paint(firstLineLayout, mount); const lines = mount.querySelectorAll('.superdoc-line') as NodeListOf; @@ -5496,7 +5560,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [hangingBlock], measures: [hangingMeasure] }); + const painter = createTestPainter({ blocks: [hangingBlock], measures: [hangingMeasure] }); painter.paint(hangingLayout, mount); const lines = mount.querySelectorAll('.superdoc-line') as NodeListOf; @@ -5579,7 +5643,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [continuedBlock], measures: [continuedMeasure] }); + const painter = createTestPainter({ blocks: [continuedBlock], measures: [continuedMeasure] }); painter.paint(continuedLayout, mount); const pages = mount.querySelectorAll('.superdoc-page'); @@ -5638,7 +5702,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [doubleIndentBlock], measures: [doubleIndentMeasure] }); + const painter = createTestPainter({ blocks: [doubleIndentBlock], measures: [doubleIndentMeasure] }); painter.paint(doubleIndentLayout, mount); const fragment = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -5707,7 +5771,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -5778,7 +5842,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: imageBlocks, measures: imageMeasures }); + const painter = createTestPainter({ blocks: imageBlocks, measures: imageMeasures }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -5859,7 +5923,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: imageBlocks, measures: imageMeasures }); + const painter = createTestPainter({ blocks: imageBlocks, measures: imageMeasures }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -5927,7 +5991,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: imageBlocks, measures: imageMeasures }); + const painter = createTestPainter({ blocks: imageBlocks, measures: imageMeasures }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -5998,7 +6062,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6059,7 +6123,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6117,7 +6181,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6177,7 +6241,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const wrapper = mount.querySelector('.superdoc-inline-image-clip-wrapper'); @@ -6248,7 +6312,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6306,7 +6370,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6364,7 +6428,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6423,7 +6487,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6482,7 +6546,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6544,7 +6608,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img') as HTMLElement; @@ -6606,7 +6670,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img') as HTMLElement; @@ -6665,7 +6729,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6735,7 +6799,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6796,7 +6860,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6857,7 +6921,7 @@ describe('DomPainter', () => { ], }; - const painter = createDomPainter({ blocks: [imageBlock], measures: [imageMeasure] }); + const painter = createTestPainter({ blocks: [imageBlock], measures: [imageMeasure] }); painter.paint(imageLayout, mount); const img = mount.querySelector('img'); @@ -6904,7 +6968,7 @@ describe('DomPainter', () => { }; it('sets dir="rtl" and defaults text-align to right', () => { - const painter = createDomPainter({ blocks: [rtlBlock({})], measures: [rtlMeasure] }); + const painter = createTestPainter({ blocks: [rtlBlock({})], measures: [rtlMeasure] }); painter.paint(rtlLayout, mount); const line = mount.querySelector('.superdoc-line') as HTMLElement; @@ -6913,7 +6977,7 @@ describe('DomPainter', () => { }); it('preserves explicit left alignment on RTL paragraphs', () => { - const painter = createDomPainter({ blocks: [rtlBlock({ alignment: 'left' })], measures: [rtlMeasure] }); + const painter = createTestPainter({ blocks: [rtlBlock({ alignment: 'left' })], measures: [rtlMeasure] }); painter.paint(rtlLayout, mount); const line = mount.querySelector('.superdoc-line') as HTMLElement; @@ -6922,7 +6986,7 @@ describe('DomPainter', () => { }); it('uses text-align right for RTL justified paragraphs', () => { - const painter = createDomPainter({ blocks: [rtlBlock({ alignment: 'justify' })], measures: [rtlMeasure] }); + const painter = createTestPainter({ blocks: [rtlBlock({ alignment: 'justify' })], measures: [rtlMeasure] }); painter.paint(rtlLayout, mount); const line = mount.querySelector('.superdoc-line') as HTMLElement; @@ -6964,7 +7028,7 @@ describe('DomPainter', () => { totalHeight: 20, }; - const painter = createDomPainter({ blocks: [tabBlock], measures: [tabMeasure] }); + const painter = createTestPainter({ blocks: [tabBlock], measures: [tabMeasure] }); painter.paint(rtlLayout, mount); const line = mount.querySelector('.superdoc-line') as HTMLElement; @@ -7033,7 +7097,7 @@ describe('ImageFragment (block-level images)', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [watermarkBlock], measures: [watermarkMeasure], }); @@ -7090,7 +7154,7 @@ describe('ImageFragment (block-level images)', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [regularBlock], measures: [regularMeasure], }); @@ -7153,7 +7217,7 @@ describe('ImageFragment (block-level images)', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [regularBlock], measures: [regularMeasure], }); @@ -7293,7 +7357,7 @@ describe('normalizeAnchor XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); // Should render as blocked span, not anchor @@ -7312,7 +7376,7 @@ describe('normalizeAnchor XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const span = mount.querySelector('span[data-link-blocked="true"]'); @@ -7329,7 +7393,7 @@ describe('normalizeAnchor XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const span = mount.querySelector('span[data-link-blocked="true"]'); @@ -7346,7 +7410,7 @@ describe('normalizeAnchor XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7364,7 +7428,7 @@ describe('normalizeAnchor XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7441,7 +7505,7 @@ describe('appendDocLocation XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); // CRITICAL FIX: Should preserve the sanitized href and URL-encode the unsafe fragment @@ -7464,7 +7528,7 @@ describe('appendDocLocation XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); // CRITICAL FIX: Should preserve the sanitized href and URL-encode the unsafe fragment @@ -7486,7 +7550,7 @@ describe('appendDocLocation XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); // CRITICAL FIX: Should preserve the sanitized href and URL-encode the unsafe fragment @@ -7508,7 +7572,7 @@ describe('appendDocLocation XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7526,7 +7590,7 @@ describe('appendDocLocation XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7544,7 +7608,7 @@ describe('appendDocLocation XSS protection', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7623,7 +7687,7 @@ describe('appendDocLocation edge cases', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7647,7 +7711,7 @@ describe('appendDocLocation edge cases', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7667,7 +7731,7 @@ describe('appendDocLocation edge cases', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7688,7 +7752,7 @@ describe('appendDocLocation edge cases', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7712,7 +7776,7 @@ describe('appendDocLocation edge cases', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7737,7 +7801,7 @@ describe('appendDocLocation edge cases', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7757,7 +7821,7 @@ describe('appendDocLocation edge cases', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7841,7 +7905,7 @@ describe('Tooltip truncation signaling', () => { const measure = createMeasureForBlock(); const layout = createLayout([block]); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7861,7 +7925,7 @@ describe('Tooltip truncation signaling', () => { const measure = createMeasureForBlock(); const layout = createLayout([block]); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7881,7 +7945,7 @@ describe('Tooltip truncation signaling', () => { const measure = createMeasureForBlock(); const layout = createLayout([block]); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const anchor = mount.querySelector('a'); @@ -7958,7 +8022,7 @@ describe('Link accessibility - Focus styles', () => { ], }; - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); // Check that style tag exists @@ -8024,7 +8088,7 @@ describe('Link accessibility - Focus styles', () => { ], }; - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); painter.paint(testLayout, mount); @@ -8100,7 +8164,7 @@ describe('Link accessibility - ARIA labels', () => { const measure = createMeasureForRun(run.text.length); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const anchor = mount.querySelector('a'); @@ -8125,7 +8189,7 @@ describe('Link accessibility - ARIA labels', () => { const measure = createMeasureForRun(run.text.length); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const anchor = mount.querySelector('a'); @@ -8150,7 +8214,7 @@ describe('Link accessibility - ARIA labels', () => { const measure = createMeasureForRun(run.text.length); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const anchor = mount.querySelector('a'); @@ -8175,7 +8239,7 @@ describe('Link accessibility - ARIA labels', () => { const measure = createMeasureForRun(run.text.length); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const anchor = mount.querySelector('a'); @@ -8200,7 +8264,7 @@ describe('Link accessibility - ARIA labels', () => { const measure = createMeasureForRun(run.text.length); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const anchor = mount.querySelector('a'); @@ -8275,7 +8339,7 @@ describe('Link accessibility - Role attributes', () => { const measure = createMeasureForText(10); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const anchor = mount.querySelector('a'); @@ -8293,7 +8357,7 @@ describe('Link accessibility - Role attributes', () => { const measure = createMeasureForText(12); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const span = mount.querySelector('span[data-link-blocked="true"]'); @@ -8370,7 +8434,7 @@ describe('Link accessibility - Tooltip aria-describedby', () => { const measure = createMeasureForBlock(); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const anchor = mount.querySelector('a'); @@ -8394,7 +8458,7 @@ describe('Link accessibility - Tooltip aria-describedby', () => { const measure = createMeasureForBlock(); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const anchor = mount.querySelector('a'); @@ -8411,7 +8475,7 @@ describe('Link accessibility - Tooltip aria-describedby', () => { const measure = createMeasureForBlock(); const testLayout = createLayout(); - const painter = createDomPainter({ blocks: [block], measures: [measure] }); + const painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(testLayout, mount); const anchor = mount.querySelector('a'); @@ -8499,7 +8563,7 @@ describe('Link accessibility - Tooltip aria-describedby', () => { ], }; - const painter = createDomPainter({ blocks: [block1, block2], measures: [measure, measure] }); + const painter = createTestPainter({ blocks: [block1, block2], measures: [measure, measure] }); painter.paint(multiLayout, mount); const anchors = mount.querySelectorAll('a'); @@ -8590,7 +8654,7 @@ describe('Link rendering metrics', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const metrics = linkMetrics.getMetrics(); @@ -8607,7 +8671,7 @@ describe('Link rendering metrics', () => { const measure = createMeasureForBlock(); const layout = createLayout(); - painter = createDomPainter({ blocks: [block], measures: [measure] }); + painter = createTestPainter({ blocks: [block], measures: [measure] }); painter.paint(layout, mount); const metrics = linkMetrics.getMetrics(); @@ -8712,7 +8776,7 @@ describe('Link rendering metrics', () => { }; // Create single painter with all blocks - painter = createDomPainter({ + painter = createTestPainter({ blocks: [validBlock1, blockedBlock, validBlock2], measures: [measure, measure, measure], }); @@ -9034,14 +9098,14 @@ describe('applyRunDataAttributes', () => { totalHeight: 16, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], }); // Call setData with header and footer blocks expect(() => { - painter.setData?.([mainBlock], [mainMeasure], [headerBlock], [headerMeasure], [footerBlock], [footerMeasure]); + painter.setData([mainBlock], [mainMeasure], [headerBlock], [headerMeasure], [footerBlock], [footerMeasure]); }).not.toThrow(); }); @@ -9126,14 +9190,14 @@ describe('applyRunDataAttributes', () => { pmEnd: 6, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], headerProvider: () => ({ fragments: [headerFragment], height: 16 }), }); // Set data with header blocks - painter.setData?.([mainBlock], [mainMeasure], [headerBlock], [headerMeasure]); + painter.setData([mainBlock], [mainMeasure], [headerBlock], [headerMeasure]); // Paint should not throw errors about missing blocks expect(() => { @@ -9216,14 +9280,14 @@ describe('applyRunDataAttributes', () => { }, ]; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], }); // Should handle multiple header and footer blocks without errors expect(() => { - painter.setData?.([mainBlock], [mainMeasure], headerBlocks, headerMeasures, footerBlocks, footerMeasures); + painter.setData([mainBlock], [mainMeasure], headerBlocks, headerMeasures, footerBlocks, footerMeasures); }).not.toThrow(); }); @@ -9251,14 +9315,14 @@ describe('applyRunDataAttributes', () => { totalHeight: 20, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], }); // Should handle empty arrays gracefully expect(() => { - painter.setData?.([mainBlock], [mainMeasure], [], [], [], []); + painter.setData([mainBlock], [mainMeasure], [], [], [], []); }).not.toThrow(); }); @@ -9286,14 +9350,14 @@ describe('applyRunDataAttributes', () => { totalHeight: 20, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], }); // Should handle undefined parameters (backward compatibility) expect(() => { - painter.setData?.([mainBlock], [mainMeasure], undefined, undefined, undefined, undefined); + painter.setData([mainBlock], [mainMeasure], undefined, undefined, undefined, undefined); }).not.toThrow(); }); @@ -9321,14 +9385,14 @@ describe('applyRunDataAttributes', () => { totalHeight: 20, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], }); // Should work with just blocks and measures (original signature) expect(() => { - painter.setData?.([mainBlock], [mainMeasure]); + painter.setData([mainBlock], [mainMeasure]); }).not.toThrow(); const layoutData: Layout = { @@ -9441,14 +9505,14 @@ describe('applyRunDataAttributes', () => { pmEnd: 6, }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], headerProvider: () => ({ fragments: [headerFragment], height: 20 }), footerProvider: () => ({ fragments: [footerFragment], height: 20 }), }); - painter.setData?.([mainBlock], [mainMeasure], [headerBlock], [headerMeasure], [footerBlock], [footerMeasure]); + painter.setData([mainBlock], [mainMeasure], [headerBlock], [headerMeasure], [footerBlock], [footerMeasure]); // Paint should successfully render all blocks without errors expect(() => { @@ -9556,7 +9620,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], footerProvider: () => ({ @@ -9566,7 +9630,7 @@ describe('applyRunDataAttributes', () => { }), }); - painter.setData?.([mainBlock], [mainMeasure], undefined, undefined, [footerBlock], [footerMeasure]); + painter.setData([mainBlock], [mainMeasure], undefined, undefined, [footerBlock], [footerMeasure]); painter.paint(layout, mount); const footerEl = mount.querySelector('.superdoc-page-footer'); @@ -9671,7 +9735,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], footerProvider: () => ({ @@ -9681,7 +9745,7 @@ describe('applyRunDataAttributes', () => { }), }); - painter.setData?.([mainBlock], [mainMeasure], undefined, undefined, [footerBlock], [footerMeasure]); + painter.setData([mainBlock], [mainMeasure], undefined, undefined, [footerBlock], [footerMeasure]); painter.paint(layout, mount); const footerEl = mount.querySelector('.superdoc-page-footer'); @@ -9775,7 +9839,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], footerProvider: () => ({ @@ -9785,7 +9849,7 @@ describe('applyRunDataAttributes', () => { }), }); - painter.setData?.([mainBlock], [mainMeasure], undefined, undefined, [footerBlock], [footerMeasure]); + painter.setData([mainBlock], [mainMeasure], undefined, undefined, [footerBlock], [footerMeasure]); painter.paint(layout, mount); const footerEl = mount.querySelector('.superdoc-page-footer'); @@ -9843,7 +9907,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], footerProvider: () => ({ @@ -9852,7 +9916,7 @@ describe('applyRunDataAttributes', () => { }), }); - painter.setData?.([mainBlock], [mainMeasure]); + painter.setData([mainBlock], [mainMeasure]); expect(() => { painter.paint(layout, mount); }).not.toThrow(); @@ -9991,7 +10055,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], footerProvider: () => ({ @@ -10003,7 +10067,7 @@ describe('applyRunDataAttributes', () => { }), }); - painter.setData?.([mainBlock], [mainMeasure], undefined, undefined, footerBlocks, footerMeasures); + painter.setData([mainBlock], [mainMeasure], undefined, undefined, footerBlocks, footerMeasures); painter.paint(layout, mount); const footerEl = mount.querySelector('.superdoc-page-footer'); @@ -10102,7 +10166,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [mainBlock], measures: [mainMeasure], footerProvider: () => ({ @@ -10113,7 +10177,7 @@ describe('applyRunDataAttributes', () => { }), }); - painter.setData?.([mainBlock], [mainMeasure], undefined, undefined, [footerBlock], [footerMeasure]); + painter.setData([mainBlock], [mainMeasure], undefined, undefined, [footerBlock], [footerMeasure]); painter.paint(layout, mount); const footerEl = mount.querySelector('.superdoc-page-footer'); @@ -10193,7 +10257,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [lineBreakBlock], measures: [lineBreakMeasure], }); @@ -10267,7 +10331,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [lineBreakBlock], measures: [lineBreakMeasure], }); @@ -10327,7 +10391,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [lineBreakBlock], measures: [lineBreakMeasure], }); @@ -10409,7 +10473,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [multiLineBreakBlock], measures: [multiLineBreakMeasure], }); @@ -10476,7 +10540,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [lineBreakWithAttrsBlock], measures: [lineBreakWithAttrsMeasure], }); @@ -10556,7 +10620,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ blocks: [listBlock], measures: [listMeasure] }); + const painter = createTestPainter({ blocks: [listBlock], measures: [listMeasure] }); painter.paint(listLayout, mount); const fragmentBefore = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -10581,7 +10645,7 @@ describe('applyRunDataAttributes', () => { }, }; - painter.setData?.([updatedListBlock], [listMeasure]); + painter.setData([updatedListBlock], [listMeasure]); painter.paint(listLayout, mount); const fragmentAfter = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -10653,7 +10717,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ blocks: [listBlock], measures: [listMeasure] }); + const painter = createTestPainter({ blocks: [listBlock], measures: [listMeasure] }); painter.paint(listLayout, mount); const fragmentBefore = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -10676,7 +10740,7 @@ describe('applyRunDataAttributes', () => { }, }; - painter.setData?.([updatedListBlock], [listMeasure]); + painter.setData([updatedListBlock], [listMeasure]); painter.paint(listLayout, mount); const fragmentAfter = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -10745,13 +10809,13 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ blocks: [listBlock], measures: [listMeasure] }); + const painter = createTestPainter({ blocks: [listBlock], measures: [listMeasure] }); painter.paint(listLayout, mount); const fragmentBefore = mount.querySelector('.superdoc-fragment') as HTMLElement; // Set identical data - painter.setData?.([listBlock], [listMeasure]); + painter.setData([listBlock], [listMeasure]); painter.paint(listLayout, mount); const fragmentAfter = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -10821,7 +10885,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ blocks: [listBlock], measures: [listMeasure] }); + const painter = createTestPainter({ blocks: [listBlock], measures: [listMeasure] }); painter.paint(listLayout, mount); const fragmentBefore = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -10844,7 +10908,7 @@ describe('applyRunDataAttributes', () => { }, }; - painter.setData?.([updatedListBlock], [listMeasure]); + painter.setData([updatedListBlock], [listMeasure]); painter.paint(listLayout, mount); const fragmentAfter = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -10909,7 +10973,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ blocks: [blockSdtBlock], measures: [blockSdtMeasure] }); + const painter = createTestPainter({ blocks: [blockSdtBlock], measures: [blockSdtMeasure] }); painter.paint(blockSdtLayout, mount); const fragment = mount.querySelector('.superdoc-fragment') as HTMLElement; @@ -10984,7 +11048,7 @@ describe('applyRunDataAttributes', () => { pages: [{ number: 1, fragments: baseFragments }], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [paraA.block, paraB.block, paraC.block], measures: [paraA.measure, paraB.measure, paraC.measure], }); @@ -11010,7 +11074,7 @@ describe('applyRunDataAttributes', () => { ], }; - painter.setData?.( + painter.setData( [paraA.block, paraB.block, paraC.block, paraD.block], [paraA.measure, paraB.measure, paraC.measure, paraD.measure], ); @@ -11142,7 +11206,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [paraA.block, tableBlock, paraB.block], measures: [paraA.measure, tableMeasure, paraB.measure], }); @@ -11224,7 +11288,7 @@ describe('applyRunDataAttributes', () => { ], }; - const painter = createDomPainter({ blocks: [inlineSdtBlock], measures: [inlineSdtMeasure] }); + const painter = createTestPainter({ blocks: [inlineSdtBlock], measures: [inlineSdtMeasure] }); painter.paint(inlineSdtLayout, mount); const fragment = mount.querySelector('.superdoc-fragment') as HTMLElement; diff --git a/packages/layout-engine/painters/dom/src/index.ts b/packages/layout-engine/painters/dom/src/index.ts index 7c61fb9f68..e0e1139773 100644 --- a/packages/layout-engine/painters/dom/src/index.ts +++ b/packages/layout-engine/painters/dom/src/index.ts @@ -1,17 +1,7 @@ -import type { - FlowBlock, - Fragment, - Layout, - Measure, - Page, - PainterDOM, - PageMargins, - PositionMapping, - ResolvedLayout, -} from '@superdoc/contracts'; +import type { FlowBlock, Fragment, Layout, Measure, Page, PageMargins, ResolvedLayout } from '@superdoc/contracts'; import { DomPainter } from './renderer.js'; import type { PageStyles } from './styles.js'; -import type { PaintSnapshot, RulerOptions, FlowMode } from './renderer.js'; +import type { DomPainterInput, PaintSnapshot, PositionMapping, RulerOptions, FlowMode } from './renderer.js'; // Re-export constants export { DOM_CLASS_NAMES } from './constants.js'; @@ -36,6 +26,7 @@ export type { } from './ruler/index.js'; export type { RulerOptions } from './renderer.js'; export type { PaintSnapshot } from './renderer.js'; +export type { DomPainterInput, PositionMapping, RenderedLineInfo } from './renderer.js'; // Re-export utility functions for testing export { sanitizeUrl, linkMetrics, applyRunDataAttributes } from './renderer.js'; @@ -89,8 +80,16 @@ export type PageDecorationProvider = ( ) => PageDecorationPayload | null; export type DomPainterOptions = { - blocks: FlowBlock[]; - measures: Measure[]; + /** + * Legacy compatibility: initial body block data. + * New callers should pass block data through `paint(input, mount)`. + */ + blocks?: FlowBlock[]; + /** + * Legacy compatibility: initial body measures. + * New callers should pass measure data through `paint(input, mount)`. + */ + measures?: Measure[]; pageStyles?: PageStyles; layoutMode?: LayoutMode; flowMode?: FlowMode; @@ -125,20 +124,113 @@ export type DomPainterOptions = { ruler?: RulerOptions; }; -export const createDomPainter = ( - options: DomPainterOptions, -): PainterDOM & { - setProviders?: (header?: PageDecorationProvider, footer?: PageDecorationProvider) => void; - setVirtualizationPins?: (pageIndices: number[] | null | undefined) => void; - setActiveComment?: (commentId: string | null) => void; - getActiveComment?: () => string | null; - getPaintSnapshot?: () => PaintSnapshot | null; - onScroll?: () => void; - setZoom?: (zoom: number) => void; - setScrollContainer?: (el: HTMLElement | null) => void; - setResolvedLayout?: (resolvedLayout: ResolvedLayout | null) => void; -} => { - const painter = new DomPainter(options.blocks, options.measures, { +type LegacyDomPainterState = { + blocks: FlowBlock[]; + measures: Measure[]; + headerBlocks?: FlowBlock[]; + headerMeasures?: Measure[]; + footerBlocks?: FlowBlock[]; + footerMeasures?: Measure[]; + resolvedLayout: ResolvedLayout | null; +}; + +type BlockMeasurePair = { + blocks: FlowBlock[]; + measures: Measure[]; +}; + +export type DomPainterHandle = { + paint(input: DomPainterInput | Layout, mount: HTMLElement, mapping?: PositionMapping): void; + /** + * Legacy compatibility API. + * New callers should pass block/measure data via `paint(input, mount)`. + */ + setData( + blocks: FlowBlock[], + measures: Measure[], + headerBlocks?: FlowBlock[], + headerMeasures?: Measure[], + footerBlocks?: FlowBlock[], + footerMeasures?: Measure[], + ): void; + /** + * Legacy compatibility API. + * New callers should pass resolved data via `paint(input, mount)`. + */ + setResolvedLayout(resolvedLayout: ResolvedLayout | null): void; + setProviders(header?: PageDecorationProvider, footer?: PageDecorationProvider): void; + setVirtualizationPins(pageIndices: number[] | null | undefined): void; + setActiveComment(commentId: string | null): void; + getActiveComment(): string | null; + getPaintSnapshot(): PaintSnapshot | null; + onScroll(): void; + setZoom(zoom: number): void; + setScrollContainer(el: HTMLElement | null): void; +}; + +function assertRequiredBlockMeasurePair(label: string, blocks: FlowBlock[], measures: Measure[]): void { + if (blocks.length !== measures.length) { + throw new Error(`${label} blocks and measures must have the same length.`); + } +} + +function normalizeOptionalBlockMeasurePair( + label: 'header' | 'footer', + blocks: FlowBlock[] | undefined, + measures: Measure[] | undefined, +): BlockMeasurePair | undefined { + const hasBlocks = blocks !== undefined; + const hasMeasures = measures !== undefined; + + if (hasBlocks !== hasMeasures) { + throw new Error(`${label}Blocks and ${label}Measures must both be provided or both be omitted.`); + } + + if (!hasBlocks || !hasMeasures) { + return undefined; + } + + assertRequiredBlockMeasurePair(label, blocks, measures); + return { blocks, measures }; +} + +function createEmptyResolvedLayout(flowMode: FlowMode | undefined, pageGap: number | undefined): ResolvedLayout { + return { + version: 1, + flowMode: flowMode ?? 'paginated', + pageGap: pageGap ?? 0, + pages: [], + }; +} + +function isDomPainterInput(value: DomPainterInput | Layout): value is DomPainterInput { + return 'resolvedLayout' in value && 'sourceLayout' in value && 'blocks' in value && 'measures' in value; +} + +function buildLegacyPaintInput( + layout: Layout, + legacyState: LegacyDomPainterState, + flowMode: FlowMode | undefined, + pageGap: number | undefined, +): DomPainterInput { + return { + resolvedLayout: legacyState.resolvedLayout ?? createEmptyResolvedLayout(flowMode, pageGap), + sourceLayout: layout, + blocks: legacyState.blocks, + measures: legacyState.measures, + headerBlocks: legacyState.headerBlocks, + headerMeasures: legacyState.headerMeasures, + footerBlocks: legacyState.footerBlocks, + footerMeasures: legacyState.footerMeasures, + }; +} + +export const createDomPainter = (options: DomPainterOptions): DomPainterHandle => { + if ((options.blocks ?? []).length !== (options.measures ?? []).length) { + throw new Error('DomPainter requires the same number of blocks and measures'); + } + + const painter = new DomPainter({ pageStyles: options.pageStyles, layoutMode: options.layoutMode, flowMode: options.flowMode, @@ -149,9 +241,18 @@ export const createDomPainter = ( ruler: options.ruler, }); + const legacyState: LegacyDomPainterState = { + blocks: options.blocks ?? [], + measures: options.measures ?? [], + resolvedLayout: null, + }; + return { - paint(layout: Layout, mount: HTMLElement, mapping?: PositionMapping) { - painter.paint(layout, mount, mapping); + paint(input: DomPainterInput | Layout, mount: HTMLElement, mapping?: PositionMapping) { + const normalizedInput = isDomPainterInput(input) + ? input + : buildLegacyPaintInput(input, legacyState, options.flowMode, options.pageGap); + painter.paint(normalizedInput, mount, mapping); }, setData( blocks: FlowBlock[], @@ -161,9 +262,20 @@ export const createDomPainter = ( footerBlocks?: FlowBlock[], footerMeasures?: Measure[], ) { - painter.setData(blocks, measures, headerBlocks, headerMeasures, footerBlocks, footerMeasures); + assertRequiredBlockMeasurePair('body', blocks, measures); + const normalizedHeader = normalizeOptionalBlockMeasurePair('header', headerBlocks, headerMeasures); + const normalizedFooter = normalizeOptionalBlockMeasurePair('footer', footerBlocks, footerMeasures); + + legacyState.blocks = blocks; + legacyState.measures = measures; + legacyState.headerBlocks = normalizedHeader?.blocks; + legacyState.headerMeasures = normalizedHeader?.measures; + legacyState.footerBlocks = normalizedFooter?.blocks; + legacyState.footerMeasures = normalizedFooter?.measures; + }, + setResolvedLayout(resolvedLayout: ResolvedLayout | null) { + legacyState.resolvedLayout = resolvedLayout; }, - // Non-standard extension for demo app to avoid re-instantiating on provider changes setProviders(header?: PageDecorationProvider, footer?: PageDecorationProvider) { painter.setProviders(header, footer); }, @@ -179,20 +291,14 @@ export const createDomPainter = ( getPaintSnapshot() { return painter.getPaintSnapshot(); }, - // Trigger virtualization update when scroll container is external to the painter onScroll() { painter.onScroll(); }, - // Notify painter of CSS transform scale so virtualization maps scroll correctly setZoom(zoom: number) { painter.setZoom(zoom); }, - // Set the external scroll container for correct scrollY calculation setScrollContainer(el: HTMLElement | null) { painter.setScrollContainer(el); }, - setResolvedLayout(resolvedLayout: ResolvedLayout | null) { - painter.setResolvedLayout(resolvedLayout); - }, }; }; diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index b141a3b30d..a3ea285900 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -30,7 +30,6 @@ import type { ParagraphBorder, ParagraphMeasure, PositionedDrawingGeometry, - PositionMapping, Run, SdtMetadata, ShapeGroupChild, @@ -224,6 +223,47 @@ export type LayoutMode = 'vertical' | 'horizontal' | 'book'; // FlowMode is re-exported from @superdoc/contracts export type { FlowMode } from '@superdoc/contracts'; +/** + * Interface for position mapping from ProseMirror transactions. + * Used to efficiently update DOM position attributes without full re-render. + */ +export interface PositionMapping { + /** Transform a position from old to new document coordinates */ + map(pos: number, bias?: number): number; + /** Array of step maps - length indicates transaction complexity */ + readonly maps: readonly unknown[]; +} + +export type RenderedLineInfo = { + el: HTMLElement; + top: number; + height: number; +}; + +/** + * Input to `DomPainter.paint()`. + * + * `resolvedLayout` is the canonical resolved data. The remaining fields are + * bridge data carried for internal rendering of non-paragraph fragments + * (tables, images, drawings) that have not yet been migrated to resolved items. + */ +export type DomPainterInput = { + resolvedLayout: ResolvedLayout; + /** Raw Layout for internal fragment access (bridge — will be removed once all fragment types are resolved). */ + sourceLayout: Layout; + blocks: FlowBlock[]; + measures: Measure[]; + headerBlocks?: FlowBlock[]; + headerMeasures?: Measure[]; + footerBlocks?: FlowBlock[]; + footerMeasures?: Measure[]; +}; + +type OptionalBlockMeasurePair = { + blocks: FlowBlock[]; + measures: Measure[]; +}; + type PageDecorationPayload = { fragments: Fragment[]; height: number; @@ -1086,14 +1126,14 @@ export class DomPainter { private activeCommentId: string | null = null; private paintSnapshotBuilder: PaintSnapshotBuilder | null = null; private lastPaintSnapshot: PaintSnapshot | null = null; - /** Resolved layout for the next-gen paint pipeline (stored but not yet consumed). */ + /** Resolved layout for the next-gen paint pipeline. */ private resolvedLayout: ResolvedLayout | null = null; - constructor(blocks: FlowBlock[], measures: Measure[], options: PainterOptions = {}) { + constructor(options: PainterOptions = {}) { this.options = options; this.layoutMode = options.layoutMode ?? 'vertical'; this.isSemanticFlow = (options.flowMode ?? 'paginated') === 'semantic'; - this.blockLookup = this.buildBlockLookup(blocks, measures); + this.blockLookup = new Map(); this.headerProvider = options.headerProvider; this.footerProvider = options.footerProvider; @@ -1218,15 +1258,6 @@ export class DomPainter { return this.activeCommentId; } - /** - * Stores the resolved layout for the next-generation paint pipeline. - * When set, the painter sources page dimensions and fragment wrapper positioning - * from resolved data, falling back to legacy Layout when null. - */ - public setResolvedLayout(resolvedLayout: ResolvedLayout | null): void { - this.resolvedLayout = resolvedLayout; - } - /** Returns the resolved page for a given index, or null if resolved data is unavailable. */ private getResolvedPage(pageIndex: number): ResolvedPage | null { return this.resolvedLayout?.pages[pageIndex] ?? null; @@ -1386,79 +1417,47 @@ export class DomPainter { } /** - * Updates the painter's block and measure data. - * - * @param blocks - Main document blocks - * @param measures - Measures corresponding to main document blocks - * @param headerBlocks - Optional header blocks from header/footer layout results - * @param headerMeasures - Optional measures corresponding to header blocks - * @param footerBlocks - Optional footer blocks from header/footer layout results - * @param footerMeasures - Optional measures corresponding to footer blocks + * Builds a new block lookup from the input data, merging header/footer blocks, + * and tracks which blocks changed since the last paint cycle. */ - public setData( - blocks: FlowBlock[], - measures: Measure[], - headerBlocks?: FlowBlock[], - headerMeasures?: Measure[], - footerBlocks?: FlowBlock[], - footerMeasures?: Measure[], - ): void { - // Validate main blocks and measures arrays - if (blocks.length !== measures.length) { + private normalizeOptionalBlockMeasurePair( + label: 'header' | 'footer', + blocks: FlowBlock[] | undefined, + measures: Measure[] | undefined, + ): OptionalBlockMeasurePair | undefined { + const hasBlocks = blocks !== undefined; + const hasMeasures = measures !== undefined; + + if (hasBlocks !== hasMeasures) { throw new Error( - `setData: blocks and measures arrays must have the same length. ` + - `Got blocks.length=${blocks.length}, measures.length=${measures.length}`, + `DomPainter.paint requires ${label}Blocks and ${label}Measures to both be provided or both be omitted`, ); } - // Validate header blocks and measures - const hasHeaderBlocks = headerBlocks !== undefined; - const hasHeaderMeasures = headerMeasures !== undefined; - if (hasHeaderBlocks !== hasHeaderMeasures) { - throw new Error( - `setData: headerBlocks and headerMeasures must both be provided or both be omitted. ` + - `Got headerBlocks=${hasHeaderBlocks ? 'provided' : 'omitted'}, ` + - `headerMeasures=${hasHeaderMeasures ? 'provided' : 'omitted'}`, - ); - } - if (hasHeaderBlocks && hasHeaderMeasures && headerBlocks!.length !== headerMeasures!.length) { - throw new Error( - `setData: headerBlocks and headerMeasures arrays must have the same length. ` + - `Got headerBlocks.length=${headerBlocks!.length}, headerMeasures.length=${headerMeasures!.length}`, - ); + if (!hasBlocks || !hasMeasures) { + return undefined; } - // Validate footer blocks and measures - const hasFooterBlocks = footerBlocks !== undefined; - const hasFooterMeasures = footerMeasures !== undefined; - if (hasFooterBlocks !== hasFooterMeasures) { - throw new Error( - `setData: footerBlocks and footerMeasures must both be provided or both be omitted. ` + - `Got footerBlocks=${hasFooterBlocks ? 'provided' : 'omitted'}, ` + - `footerMeasures=${hasFooterMeasures ? 'provided' : 'omitted'}`, - ); - } - if (hasFooterBlocks && hasFooterMeasures && footerBlocks!.length !== footerMeasures!.length) { - throw new Error( - `setData: footerBlocks and footerMeasures arrays must have the same length. ` + - `Got footerBlocks.length=${footerBlocks!.length}, footerMeasures.length=${footerMeasures!.length}`, - ); - } + return { blocks, measures }; + } + + private updateBlockLookup(input: DomPainterInput): void { + const { blocks, measures, headerBlocks, headerMeasures, footerBlocks, footerMeasures } = input; // Build lookup for main document blocks const nextLookup = this.buildBlockLookup(blocks, measures); - // Merge header blocks into the lookup if provided - if (headerBlocks && headerMeasures) { - const headerLookup = this.buildBlockLookup(headerBlocks, headerMeasures); + const normalizedHeader = this.normalizeOptionalBlockMeasurePair('header', headerBlocks, headerMeasures); + if (normalizedHeader) { + const headerLookup = this.buildBlockLookup(normalizedHeader.blocks, normalizedHeader.measures); headerLookup.forEach((entry, id) => { nextLookup.set(id, entry); }); } - // Merge footer blocks into the lookup if provided - if (footerBlocks && footerMeasures) { - const footerLookup = this.buildBlockLookup(footerBlocks, footerMeasures); + const normalizedFooter = this.normalizeOptionalBlockMeasurePair('footer', footerBlocks, footerMeasures); + if (normalizedFooter) { + const footerLookup = this.buildBlockLookup(normalizedFooter.blocks, normalizedFooter.measures); footerLookup.forEach((entry, id) => { nextLookup.set(id, entry); }); @@ -1476,7 +1475,13 @@ export class DomPainter { this.changedBlocks = changed; } - public paint(layout: Layout, mount: HTMLElement, mapping?: PositionMapping): void { + public paint(input: DomPainterInput, mount: HTMLElement, mapping?: PositionMapping): void { + const layout = input.sourceLayout; + this.resolvedLayout = input.resolvedLayout; + + // Update block lookup and change tracking (absorbs former setData logic) + this.updateBlockLookup(input); + if (!(mount instanceof HTMLElement)) { throw new Error('DomPainter.paint requires a valid HTMLElement mount'); } diff --git a/packages/layout-engine/painters/dom/src/table/renderTableCell.ts b/packages/layout-engine/painters/dom/src/table/renderTableCell.ts index a69033491d..754f048458 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableCell.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableCell.ts @@ -10,7 +10,6 @@ import type { ParagraphIndent, ParagraphMeasure, PartialRowInfo, - RenderedLineInfo, SdtMetadata, TableBlock, TableFragment, @@ -18,13 +17,10 @@ import type { WrapExclusion, WrapTextMode, } from '@superdoc/contracts'; -import { effectiveTableCellSpacing, rescaleColumnWidths, normalizeZIndex, getCellSpacingPx } from '@superdoc/contracts'; +import { effectiveTableCellSpacing, rescaleColumnWidths, normalizeZIndex, getCellSpacingPx } from '@superdoc/contracts'; import { toCssFontFamily } from '@superdoc/font-utils'; -import type { FragmentRenderContext } from '../renderer.js'; -import { - applyParagraphBorderStyles, - applyParagraphShadingStyles, -} from '../features/paragraph-borders/index.js'; +import type { FragmentRenderContext, RenderedLineInfo } from '../renderer.js'; +import { applyParagraphBorderStyles, applyParagraphShadingStyles } from '../features/paragraph-borders/index.js'; import { applySquareWrapExclusionsToLines } from '../utils/anchor-helpers'; import { applyImageClipPath } from '../utils/image-clip-path.js'; import { diff --git a/packages/layout-engine/painters/dom/src/utils/anchor-helpers.ts b/packages/layout-engine/painters/dom/src/utils/anchor-helpers.ts index da04cba80d..0cd109fa74 100644 --- a/packages/layout-engine/painters/dom/src/utils/anchor-helpers.ts +++ b/packages/layout-engine/painters/dom/src/utils/anchor-helpers.ts @@ -1,4 +1,5 @@ -import type { WrapExclusion, RenderedLineInfo } from '@superdoc/contracts'; +import type { WrapExclusion } from '@superdoc/contracts'; +import type { RenderedLineInfo } from '../renderer.js'; const clampNumber = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value)); diff --git a/packages/layout-engine/painters/dom/src/virtualization.test.ts b/packages/layout-engine/painters/dom/src/virtualization.test.ts index ff0c9a816d..836ce4f932 100644 --- a/packages/layout-engine/painters/dom/src/virtualization.test.ts +++ b/packages/layout-engine/painters/dom/src/virtualization.test.ts @@ -1,6 +1,38 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createDomPainter } from './index.js'; -import type { FlowBlock, Measure, Layout, Fragment, PageMargins } from '@superdoc/contracts'; +import type { DomPainterOptions, DomPainterInput } from './index.js'; +import type { FlowBlock, Measure, Layout, Fragment, PageMargins, ResolvedLayout } from '@superdoc/contracts'; + +const emptyResolved: ResolvedLayout = { version: 1, flowMode: 'paginated', pageGap: 0, pages: [] }; + +/** Test-only bridge: see index.test.ts for full JSDoc. */ +function createTestPainter(opts: { blocks?: FlowBlock[]; measures?: Measure[] } & DomPainterOptions) { + const { blocks: initBlocks, measures: initMeasures, ...painterOpts } = opts; + const painter = createDomPainter(painterOpts); + let currentBlocks: FlowBlock[] = initBlocks ?? []; + let currentMeasures: Measure[] = initMeasures ?? []; + let currentResolved: ResolvedLayout = emptyResolved; + + return { + paint(layout: Layout, mount: HTMLElement, mapping?: unknown) { + const input: DomPainterInput = { + resolvedLayout: currentResolved, + sourceLayout: layout, + blocks: currentBlocks, + measures: currentMeasures, + }; + painter.paint(input, mount, mapping as any); + }, + setProviders: painter.setProviders, + setVirtualizationPins: painter.setVirtualizationPins, + setActiveComment: painter.setActiveComment, + getActiveComment: painter.getActiveComment, + getPaintSnapshot: painter.getPaintSnapshot, + onScroll: painter.onScroll, + setZoom: painter.setZoom, + setScrollContainer: painter.setScrollContainer, + }; +} // Minimal paragraph block/measure to satisfy painter const block: FlowBlock = { @@ -86,7 +118,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('renders only a window of pages with spacers', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 5, overscan: 0, gap: 72, paddingTop: 0 }, @@ -106,7 +138,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('defaults virtualization gap to 72px when no gap is provided', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 2 }, @@ -125,7 +157,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('updates the window on scroll', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 5, overscan: 0, gap: 72, paddingTop: 0 }, @@ -201,7 +233,7 @@ describe('DomPainter virtualization (vertical)', () => { })), }; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [sdtBlock], measures: [sdtMeasure], virtualization: { enabled: true, window: 1, overscan: 0, gap: 72, paddingTop: 0 }, @@ -228,7 +260,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('handles window size larger than total pages', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 10, overscan: 0, gap: 72, paddingTop: 0 }, @@ -241,7 +273,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('handles single page document', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 5, overscan: 0, gap: 72, paddingTop: 0 }, @@ -254,7 +286,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('maintains bounded DOM nodes with large document', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 5, overscan: 1, gap: 72, paddingTop: 0 }, @@ -268,7 +300,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('renders overscan pages correctly', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 3, overscan: 2, gap: 72, paddingTop: 0 }, @@ -283,7 +315,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('pins pages outside the scroll window', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 2, overscan: 0, gap: 72, paddingTop: 0 }, @@ -310,7 +342,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('updates providers without remounting pages', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], // Use non-virtualized path to focus on provider update semantics @@ -378,7 +410,7 @@ describe('DomPainter virtualization (vertical)', () => { const gap = 72; const pageCount = 20; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 3, overscan: 0, gap, paddingTop: 0 }, @@ -433,7 +465,7 @@ describe('DomPainter virtualization (vertical)', () => { const pageCount = 20; const toolbarHeight = 100; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 3, overscan: 0, gap, paddingTop: 0 }, @@ -537,7 +569,7 @@ describe('DomPainter virtualization (vertical)', () => { it('setScrollContainer triggers immediate updateVirtualWindow', () => { const pageCount = 20; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 3, overscan: 0, gap: 72, paddingTop: 0 }, @@ -599,7 +631,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('renders drawing fragments inside virtualized windows', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [drawingBlock], measures: [drawingMeasure], virtualization: { enabled: true, window: 2, overscan: 0, gap: 72, paddingTop: 0 }, @@ -623,7 +655,7 @@ describe('DomPainter virtualization (vertical)', () => { }); it('disables virtualization rendering paths in semantic flow mode', () => { - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], flowMode: 'semantic', @@ -671,7 +703,7 @@ describe('DomPainter virtualization (vertical)', () => { ], })); - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], flowMode: 'semantic', @@ -693,7 +725,7 @@ describe('DomPainter virtualization (vertical)', () => { // grows to fit content and scrollTop stays 0, so the scroll container branch // must fall through to the viewport-based getBoundingClientRect path. const pageCount = 20; - const painter = createDomPainter({ + const painter = createTestPainter({ blocks: [block], measures: [measure], virtualization: { enabled: true, window: 5, overscan: 1, gap: 72, paddingTop: 0 }, diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts index 6152fee586..4224a7b7d3 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts @@ -5,7 +5,7 @@ import { DecorationBridge } from './dom/DecorationBridge.js'; import { ProofingSessionManager } from './proofing/ProofingSessionManager.js'; import { applyProofingDecorations, clearProofingDecorations, createDomPainter } from '@superdoc/painter-dom'; import { resolveLayout } from '@superdoc/layout-resolved'; -import type { ProofingAnnotation, LayoutMode, PaintSnapshot } from '@superdoc/painter-dom'; +import type { DomPainterInput, ProofingAnnotation, LayoutMode, PaintSnapshot } from '@superdoc/painter-dom'; import type { ProofingConfig, ProofingPaintSlice } from './proofing/types.js'; import type { VisibilitySource } from './proofing/visibility-source.js'; import { @@ -4277,9 +4277,8 @@ export class PresentationEditor extends EventEmitter { this.#updateDecorationProviders(layout); } - const painter = this.#ensurePainter(blocksForLayout, measures); - painter.setResolvedLayout?.(resolvedLayout); - if (!isSemanticFlow && typeof painter.setProviders === 'function') { + const painter = this.#ensurePainter(); + if (!isSemanticFlow) { painter.setProviders( this.#headerFooterSession?.headerDecorationProvider, this.#headerFooterSession?.footerDecorationProvider, @@ -4327,18 +4326,6 @@ export class PresentationEditor extends EventEmitter { footerMeasures.push(...extraMeasures); } - // Pass all blocks (main document + headers + footers + extras) to the painter - const painterSetDataStart = perfNow(); - painter.setData?.( - blocksForLayout, - measures, - headerBlocks.length > 0 ? headerBlocks : undefined, - headerMeasures.length > 0 ? headerMeasures : undefined, - footerBlocks.length > 0 ? footerBlocks : undefined, - footerMeasures.length > 0 ? footerMeasures : undefined, - ); - const painterSetDataEnd = perfNow(); - perfLog(`[Perf] painter.setData: ${(painterSetDataEnd - painterSetDataStart).toFixed(2)}ms`); // Avoid MutationObserver overhead while repainting large DOM trees. this.#domIndexObserverManager?.pause(); // Pass the transaction mapping for efficient position attribute updates. @@ -4346,7 +4333,17 @@ export class PresentationEditor extends EventEmitter { const mapping = this.#pendingMapping; this.#pendingMapping = null; const painterPaintStart = perfNow(); - painter.paint(layout, this.#painterHost, mapping ?? undefined); + const paintInput: DomPainterInput = { + resolvedLayout, + sourceLayout: layout, + blocks: blocksForLayout, + measures, + headerBlocks: headerBlocks.length > 0 ? headerBlocks : undefined, + headerMeasures: headerMeasures.length > 0 ? headerMeasures : undefined, + footerBlocks: footerBlocks.length > 0 ? footerBlocks : undefined, + footerMeasures: footerMeasures.length > 0 ? footerMeasures : undefined, + }; + painter.paint(paintInput, this.#painterHost, mapping ?? undefined); const painterPaintEnd = perfNow(); perfLog(`[Perf] painter.paint: ${(painterPaintEnd - painterPaintStart).toFixed(2)}ms`); const painterPostStart = perfNow(); @@ -4406,7 +4403,7 @@ export class PresentationEditor extends EventEmitter { } } - #ensurePainter(blocks: FlowBlock[], measures: Measure[]) { + #ensurePainter() { if (!this.#domPainter) { // Ensure the virtualization gap matches the effective page gap so that // DomPainter's spacer/offset math stays consistent with #applyZoom() height calculations. @@ -4417,8 +4414,6 @@ export class PresentationEditor extends EventEmitter { : virtualization; this.#domPainter = createDomPainter({ - blocks, - measures, layoutMode: this.#layoutOptions.layoutMode ?? 'vertical', flowMode: this.#layoutOptions.flowMode ?? 'paginated', virtualization: normalizedVirtualization, diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.collaboration.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.collaboration.test.ts index 08823b746a..9045fcedd6 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.collaboration.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.collaboration.test.ts @@ -120,8 +120,6 @@ vi.mock('@superdoc/painter-dom', () => ({ setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), DOM_CLASS_NAMES: { PAGE: 'superdoc-page', diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.decorationSync.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.decorationSync.test.ts index b2109cddb4..ea6d5094f5 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.decorationSync.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.decorationSync.test.ts @@ -135,8 +135,6 @@ const { setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockEditorConverterStore: converterStore, mockEditorOverlayManager: vi.fn().mockImplementation(() => ({ diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.draggableFocus.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.draggableFocus.test.ts index 1558a13dec..7db80d002f 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.draggableFocus.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.draggableFocus.test.ts @@ -61,8 +61,6 @@ const { setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockEditorConverterStore: converterStore, mockEditorOverlayManager: vi.fn().mockImplementation(() => ({ diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.focusWrapping.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.focusWrapping.test.ts index b91a97baa9..ba1eff625a 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.focusWrapping.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.focusWrapping.test.ts @@ -74,8 +74,6 @@ const { setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockEditorConverterStore: converterStore, mockEditorOverlayManager: vi.fn().mockImplementation(() => ({ diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.footnotesPmMarkers.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.footnotesPmMarkers.test.ts index d28b51c761..d83ada97b1 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.footnotesPmMarkers.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.footnotesPmMarkers.test.ts @@ -96,8 +96,6 @@ vi.mock('@superdoc/painter-dom', () => ({ setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), DOM_CLASS_NAMES: { PAGE: '', FRAGMENT: '', LINE: '', INLINE_SDT_WRAPPER: '', BLOCK_SDT: '', DOCUMENT_SECTION: '' }, applyProofingDecorations: vi.fn(() => false), diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.getCurrentPageIndex.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.getCurrentPageIndex.test.ts index 2862323a9e..337b2a02de 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.getCurrentPageIndex.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.getCurrentPageIndex.test.ts @@ -62,8 +62,6 @@ const { setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockMeasureBlock: vi.fn(() => ({ width: 100, height: 100 })), mockEditorConverterStore: converterStore, diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.getElementAtPos.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.getElementAtPos.test.ts index a5f66ecb09..65ea2b0d7c 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.getElementAtPos.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.getElementAtPos.test.ts @@ -47,8 +47,6 @@ const { setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockEditorConverterStore: converterStore, mockEditorOverlayManager: vi.fn().mockImplementation(() => ({ diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.goToAnchor.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.goToAnchor.test.ts index 42e12e3d05..c1a9909e28 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.goToAnchor.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.goToAnchor.test.ts @@ -62,8 +62,6 @@ const { setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockMeasureBlock: vi.fn(() => ({ width: 100, height: 100 })), mockEditorConverterStore: converterStore, diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.media.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.media.test.ts index 64f35cbae5..d1810dfa7e 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.media.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.media.test.ts @@ -82,8 +82,6 @@ vi.mock('@superdoc/painter-dom', () => ({ setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), DOM_CLASS_NAMES: { PAGE: '', FRAGMENT: '', LINE: '', INLINE_SDT_WRAPPER: '', BLOCK_SDT: '', DOCUMENT_SECTION: '' }, applyProofingDecorations: vi.fn(() => false), diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.scrollToPosition.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.scrollToPosition.test.ts index 1a15d4f166..3a58eb87b9 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.scrollToPosition.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.scrollToPosition.test.ts @@ -80,8 +80,6 @@ const { setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockMeasureBlock: vi.fn(() => ({ width: 100, height: 100 })), mockEditorConverterStore: converterStore, diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.sectionPageStyles.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.sectionPageStyles.test.ts index b2b9fed47c..cfe97aff02 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.sectionPageStyles.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.sectionPageStyles.test.ts @@ -62,8 +62,6 @@ const { setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockMeasureBlock: vi.fn(() => ({ width: 100, height: 100 })), mockEditorConverterStore: converterStore, diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.test.ts index f961e5abe2..f5a78fd4e8 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.test.ts @@ -139,8 +139,6 @@ const { setLayoutMode: vi.fn(), setVirtualizationPins: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockMeasureBlock: vi.fn(() => ({ width: 100, height: 100 })), mockEditorConverterStore: converterStore, diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.zoom.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.zoom.test.ts index 368924d45a..0e9f451155 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.zoom.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.zoom.test.ts @@ -119,8 +119,6 @@ const { setZoom: vi.fn(), setLayoutMode: vi.fn(), setProviders: vi.fn(), - setData: vi.fn(), - setResolvedLayout: vi.fn(), })), mockMeasureBlock: vi.fn(() => ({ width: 100, height: 100 })), mockEditorConverterStore: converterStore,