|
| 1 | +/** |
| 2 | + * Test-only helpers. |
| 3 | + * |
| 4 | + * These mirror the legacy {@link createDomPainter} surface (blocks/measures |
| 5 | + * options, `paint(layout)`, `setData`, `setResolvedLayout`) so existing tests |
| 6 | + * can keep their shape while the production API stays strict |
| 7 | + * (resolved-layout-only). Production code MUST NOT import from this file — |
| 8 | + * the architecture-boundary tests enforce that. |
| 9 | + */ |
| 10 | +import { createDomPainter } from './index.js'; |
| 11 | +import type { DomPainterOptions, DomPainterInput, PaintSnapshot } from './index.js'; |
| 12 | +import type { PageDecorationProvider } from './renderer.js'; |
| 13 | +import { resolveLayout } from '@superdoc/layout-resolved'; |
| 14 | +import type { FlowBlock, Fragment, Layout, Measure, ResolvedLayout, ResolvedPaintItem } from '@superdoc/contracts'; |
| 15 | + |
| 16 | +export const emptyResolved: ResolvedLayout = { version: 1, flowMode: 'paginated', pageGap: 0, pages: [] }; |
| 17 | + |
| 18 | +/** |
| 19 | + * Test-only bridge: accepts old-style `{ blocks, measures, ...options }` and |
| 20 | + * returns a painter whose `paint()` automatically builds a `DomPainterInput`. |
| 21 | + */ |
| 22 | +export function createTestPainter(opts: { blocks?: FlowBlock[]; measures?: Measure[] } & DomPainterOptions) { |
| 23 | + const { blocks: initBlocks, measures: initMeasures, headerProvider, footerProvider, ...painterOpts } = opts; |
| 24 | + let lastPaintSnapshot: PaintSnapshot | null = null; |
| 25 | + |
| 26 | + let currentBlocks: FlowBlock[] = initBlocks ?? []; |
| 27 | + let currentMeasures: Measure[] = initMeasures ?? []; |
| 28 | + let currentResolved: ResolvedLayout = emptyResolved; |
| 29 | + let headerBlocks: FlowBlock[] | undefined; |
| 30 | + let headerMeasures: Measure[] | undefined; |
| 31 | + let footerBlocks: FlowBlock[] | undefined; |
| 32 | + let footerMeasures: Measure[] | undefined; |
| 33 | + let resolvedLayoutOverridden = false; |
| 34 | + |
| 35 | + const resolveDecorationItems = ( |
| 36 | + fragments: readonly Fragment[], |
| 37 | + kind: 'header' | 'footer', |
| 38 | + ): ResolvedPaintItem[] | undefined => { |
| 39 | + const decorationBlocks = kind === 'header' ? headerBlocks : footerBlocks; |
| 40 | + const decorationMeasures = kind === 'header' ? headerMeasures : footerMeasures; |
| 41 | + const mergedBlocks = [...(currentBlocks ?? []), ...(decorationBlocks ?? [])]; |
| 42 | + const mergedMeasures = [...(currentMeasures ?? []), ...(decorationMeasures ?? [])]; |
| 43 | + if (mergedBlocks.length !== mergedMeasures.length || mergedBlocks.length === 0) { |
| 44 | + return undefined; |
| 45 | + } |
| 46 | + const fakeLayout: Layout = { pageSize: { w: 400, h: 500 }, pages: [{ number: 1, fragments: [...fragments] }] }; |
| 47 | + try { |
| 48 | + const resolved = resolveLayout({ |
| 49 | + layout: fakeLayout, |
| 50 | + flowMode: opts.flowMode ?? 'paginated', |
| 51 | + blocks: mergedBlocks, |
| 52 | + measures: mergedMeasures, |
| 53 | + }); |
| 54 | + return resolved.pages[0]?.items; |
| 55 | + } catch { |
| 56 | + return undefined; |
| 57 | + } |
| 58 | + }; |
| 59 | + |
| 60 | + const wrapProvider = ( |
| 61 | + provider: PageDecorationProvider | undefined, |
| 62 | + kind: 'header' | 'footer', |
| 63 | + ): PageDecorationProvider | undefined => { |
| 64 | + if (!provider) return undefined; |
| 65 | + return (pageNumber, pageMargins, page) => { |
| 66 | + const payload = provider(pageNumber, pageMargins, page); |
| 67 | + if (!payload) return payload; |
| 68 | + if (payload.items) return payload; |
| 69 | + const items = resolveDecorationItems(payload.fragments, kind); |
| 70 | + return items ? { ...payload, items } : { ...payload, items: [] }; |
| 71 | + }; |
| 72 | + }; |
| 73 | + |
| 74 | + const userOnPaintSnapshot = painterOpts.onPaintSnapshot; |
| 75 | + const painter = createDomPainter({ |
| 76 | + ...painterOpts, |
| 77 | + headerProvider: wrapProvider(headerProvider, 'header'), |
| 78 | + footerProvider: wrapProvider(footerProvider, 'footer'), |
| 79 | + onPaintSnapshot: (snapshot) => { |
| 80 | + lastPaintSnapshot = snapshot; |
| 81 | + userOnPaintSnapshot?.(snapshot); |
| 82 | + }, |
| 83 | + }); |
| 84 | + |
| 85 | + return { |
| 86 | + paint(layout: Layout, mount: HTMLElement, mapping?: unknown) { |
| 87 | + const effectiveResolved = resolvedLayoutOverridden |
| 88 | + ? currentResolved |
| 89 | + : resolveLayout({ |
| 90 | + layout, |
| 91 | + flowMode: opts.flowMode ?? 'paginated', |
| 92 | + blocks: currentBlocks, |
| 93 | + measures: currentMeasures, |
| 94 | + }); |
| 95 | + const input: DomPainterInput = { |
| 96 | + resolvedLayout: effectiveResolved, |
| 97 | + }; |
| 98 | + painter.paint(input, mount, mapping as never); |
| 99 | + }, |
| 100 | + setData( |
| 101 | + blocks: FlowBlock[], |
| 102 | + measures: Measure[], |
| 103 | + hb?: FlowBlock[], |
| 104 | + hm?: Measure[], |
| 105 | + fb?: FlowBlock[], |
| 106 | + fm?: Measure[], |
| 107 | + ) { |
| 108 | + currentBlocks = blocks; |
| 109 | + currentMeasures = measures; |
| 110 | + headerBlocks = hb; |
| 111 | + headerMeasures = hm; |
| 112 | + footerBlocks = fb; |
| 113 | + footerMeasures = fm; |
| 114 | + }, |
| 115 | + setResolvedLayout(rl: ResolvedLayout | null) { |
| 116 | + currentResolved = rl ?? emptyResolved; |
| 117 | + resolvedLayoutOverridden = true; |
| 118 | + }, |
| 119 | + setProviders(header?: PageDecorationProvider, footer?: PageDecorationProvider) { |
| 120 | + painter.setProviders(wrapProvider(header, 'header'), wrapProvider(footer, 'footer')); |
| 121 | + }, |
| 122 | + setVirtualizationPins(pageIndices: number[] | null | undefined) { |
| 123 | + painter.setVirtualizationPins(pageIndices); |
| 124 | + }, |
| 125 | + getMountedPageIndices() { |
| 126 | + return painter.getMountedPageIndices(); |
| 127 | + }, |
| 128 | + getPaintSnapshot() { |
| 129 | + return lastPaintSnapshot; |
| 130 | + }, |
| 131 | + onScroll() { |
| 132 | + painter.onScroll(); |
| 133 | + }, |
| 134 | + setZoom(zoom: number) { |
| 135 | + painter.setZoom(zoom); |
| 136 | + }, |
| 137 | + setScrollContainer(el: HTMLElement | null) { |
| 138 | + painter.setScrollContainer(el); |
| 139 | + }, |
| 140 | + }; |
| 141 | +} |
0 commit comments