From ecdb2f1b7faa1c9eaccbb95995adf5639051fcaf Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 31 May 2026 13:36:39 +0300 Subject: [PATCH] refactor(shared): remove deprecated TextLayoutEngine and its re-exports --- .../TextLayoutEngine.svelte.ts | 183 ------------------ .../TextLayoutEngine/TextLayoutEngine.test.ts | 89 --------- src/shared/lib/helpers/index.ts | 18 -- src/shared/lib/index.ts | 3 - 4 files changed, 293 deletions(-) delete mode 100644 src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts delete mode 100644 src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.test.ts diff --git a/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts b/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts deleted file mode 100644 index 3b7b1f9..0000000 --- a/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { - layoutWithLines, - prepareWithSegments, -} from '@chenglou/pretext'; - -/** - * A single laid-out line of text, with per-grapheme x offsets and widths. - * - * `chars` is indexed by grapheme cluster (not UTF-16 code unit), so emoji - * sequences and combining characters each produce exactly one entry. - */ -export interface LayoutLine { - /** - * Full text of this line as returned by pretext. - */ - text: string; - /** - * Rendered width of this line in pixels. - */ - width: number; - /** - * Individual character metadata for this line - */ - chars: Array<{ - /** - * The grapheme cluster string (may be >1 code unit for emoji, etc.). - */ - char: string; - /** - * X offset from the start of the line, in pixels. - */ - x: number; - /** - * Advance width of this grapheme, in pixels. - */ - width: number; - }>; -} - -/** - * Aggregated output of a single-font layout pass. - */ -export interface LayoutResult { - /** - * Per-line grapheme data. Empty when input text is empty. - */ - lines: LayoutLine[]; - /** - * Total height in pixels. Equals `lines.length * lineHeight` (pretext guarantee). - */ - totalHeight: number; -} - -/** - * Single-font text layout engine backed by `@chenglou/pretext`. - * - * Replaces the canvas-DOM hybrid `createCharacterComparison` for cases where - * only one font is needed. For dual-font comparison use `CharacterComparisonEngine`. - * - * **Usage** - * ```ts - * const engine = new TextLayoutEngine(); - * const result = engine.layout('Hello World', '400 16px "Inter"', 320, 24); - * // result.lines[0].chars → [{ char: 'H', x: 0, width: 9 }, ...] - * ``` - * - * **Font string format:** `"${weight} ${size}px \"${family}\""` — e.g. `'400 16px "Inter"'`. - * This matches what SliderArea constructs from `typography.weight` and `typography.renderedSize`. - * - * **Canvas requirement:** pretext calls `document.createElement('canvas').getContext('2d')` on - * first use and caches the context for the process lifetime. Tests must install a canvas mock - * (see `__mocks__/canvas.ts`) before the first `layout()` call. - * - * @deprecated No live consumers remain — the only previous caller - * (`createFontRowSizeResolver`) now invokes pretext's `prepare` + `layout` - * directly (per pretext's "hot-path resize function" guidance). If you need - * single-font height-only measurement, use `prepare` + `layout` from - * `@chenglou/pretext` directly. If you need per-grapheme x/width data, see - * `CharacterComparisonEngine` (dual-font) or revive a slimmer wrapper. - * Slated for removal once it has been absent from `main` for a release cycle. - */ -export class TextLayoutEngine { - /** - * Grapheme segmenter used to split segment text into individual clusters. - * - * Pretext maintains its own internal segmenter for line-breaking decisions. - * We keep a separate one here so we can iterate graphemes in `layout()` - * without depending on pretext internals — the two segmenters produce - * identical boundaries because both use `{ granularity: 'grapheme' }`. - */ - #segmenter: Intl.Segmenter; - - /** - * @param locale BCP 47 language tag passed to Intl.Segmenter. Defaults to the runtime locale. - */ - constructor(locale?: string) { - this.#segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' }); - } - - /** - * Lay out `text` in the given `font` within `width` pixels. - * - * @param text Raw text to lay out. - * @param font CSS font string: `"weight sizepx \"family\""`. - * @param width Available line width in pixels. - * @param lineHeight Line height in pixels (passed directly to pretext). - * @returns Per-line grapheme data. Empty `lines` when `text` is empty. - */ - layout(text: string, font: string, width: number, lineHeight: number): LayoutResult { - if (!text) { - return { lines: [], totalHeight: 0 }; - } - - // prepareWithSegments measures the text and builds the segment data structure - // (widths, breakableFitAdvances, etc.) that the line-walker consumes. - const prepared = prepareWithSegments(text, font); - const { lines, height } = layoutWithLines(prepared, width, lineHeight); - - // `PreparedTextWithSegments` has these fields in its public type definition - // but the TypeScript signature only exposes `segments`. We cast to `any` to - // access the parallel numeric arrays — they are documented in the plan and - // verified against the pretext source at node_modules/@chenglou/pretext/src/layout.ts. - const internal = prepared as any; - const breakableFitAdvances = internal.breakableFitAdvances as (number[] | null)[]; - const widths = internal.widths as number[]; - - const resultLines: LayoutLine[] = lines.map(line => { - const chars: LayoutLine['chars'] = []; - let currentX = 0; - - const start = line.start; - const end = line.end; - - // Walk every segment that falls within this line's [start, end] cursors. - // Both cursors are grapheme-level: start is inclusive, end is exclusive. - for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) { - const segmentText = prepared.segments[sIdx]; - if (segmentText === undefined) { - continue; - } - - const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment); - const advances = breakableFitAdvances[sIdx]; - - // For the first and last segments of the line the cursor may point - // into the middle of the segment — respect those boundaries. - // All intermediate segments are walked in full (gStart=0, gEnd=length). - const gStart = sIdx === start.segmentIndex ? start.graphemeIndex : 0; - const gEnd = sIdx === end.segmentIndex ? end.graphemeIndex : graphemes.length; - - for (let gIdx = gStart; gIdx < gEnd; gIdx++) { - const char = graphemes[gIdx]; - - // `breakableFitAdvances[sIdx]` is an array of per-grapheme advance - // widths when the segment has >1 grapheme (multi-character words). - // It is `null` for single-grapheme segments (spaces, punctuation, - // emoji, etc.) — in that case the entire segment width is attributed - // to this single grapheme. - const charWidth = advances != null ? advances[gIdx]! : widths[sIdx]!; - - chars.push({ - char, - x: currentX, - width: charWidth, - }); - currentX += charWidth; - } - } - - return { - text: line.text, - width: line.width, - chars, - }; - }); - - return { - lines: resultLines, - // pretext guarantees height === lineCount * lineHeight (see layout.ts source). - totalHeight: height, - }; - } -} diff --git a/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.test.ts b/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.test.ts deleted file mode 100644 index 7aeac46..0000000 --- a/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// @vitest-environment jsdom -import { clearCache } from '@chenglou/pretext'; -import { - beforeEach, - describe, - expect, - it, -} from 'vitest'; -import { installCanvasMock } from '../__mocks__/canvas'; -import { TextLayoutEngine } from './TextLayoutEngine.svelte'; - -// Fixed-width mock: every segment is measured as (text.length * 10) px. -// This is font-independent so we can reason about wrapping precisely. -const CHAR_WIDTH = 10; - -describe('TextLayoutEngine', () => { - let engine: TextLayoutEngine; - - beforeEach(() => { - // Install mock BEFORE any prepareWithSegments call. - // clearMeasurementCaches resets pretext's cached canvas context - // and segment metric caches so each test gets a clean slate. - installCanvasMock((_font, text) => text.length * CHAR_WIDTH); - clearCache(); - engine = new TextLayoutEngine(); - }); - - it('returns empty result for empty string', () => { - const result = engine.layout('', '400 16px "Inter"', 500, 20); - expect(result.lines).toHaveLength(0); - expect(result.totalHeight).toBe(0); - }); - - it('returns a single line when text fits within width', () => { - // 'ABC' = 3 chars × 10px = 30px, fits in 500px - const result = engine.layout('ABC', '400 16px "Inter"', 500, 20); - expect(result.lines).toHaveLength(1); - expect(result.lines[0].text).toBe('ABC'); - }); - - it('breaks text into multiple lines when it exceeds width', () => { - // 'Hello World' — pretext will split at the space. - // 'Hello' = 50px, ' ' hangs, 'World' = 50px. Width = 60px forces wrap after 'Hello '. - const result = engine.layout('Hello World', '400 16px "Inter"', 60, 20); - expect(result.lines.length).toBeGreaterThan(1); - // First line must not exceed the container width. - expect(result.lines[0].width).toBeLessThanOrEqual(60); - }); - - it('assigns correct x positions to characters on a single line', () => { - // 'ABC': A=10px, B=10px, C=10px; all on one line in 500px container. - const result = engine.layout('ABC', '400 16px "Inter"', 500, 20); - const chars = result.lines[0].chars; - - expect(chars).toHaveLength(3); - expect(chars[0].char).toBe('A'); - expect(chars[0].x).toBe(0); - expect(chars[0].width).toBe(CHAR_WIDTH); - - expect(chars[1].char).toBe('B'); - expect(chars[1].x).toBe(CHAR_WIDTH); - expect(chars[1].width).toBe(CHAR_WIDTH); - - expect(chars[2].char).toBe('C'); - expect(chars[2].x).toBe(CHAR_WIDTH * 2); - expect(chars[2].width).toBe(CHAR_WIDTH); - }); - - it('x positions are monotonically increasing across a line', () => { - const result = engine.layout('ABCDE', '400 16px "Inter"', 500, 20); - const chars = result.lines[0].chars; - for (let i = 1; i < chars.length; i++) { - expect(chars[i].x).toBeGreaterThan(chars[i - 1].x); - } - }); - - it('each line has at least one char', () => { - const result = engine.layout('Hello World', '400 16px "Inter"', 60, 20); - for (const line of result.lines) { - expect(line.chars.length).toBeGreaterThan(0); - } - }); - - it('totalHeight equals lineCount * lineHeight', () => { - const lineHeight = 24; - const result = engine.layout('Hello World', '400 16px "Inter"', 60, lineHeight); - expect(result.totalHeight).toBe(result.lines.length * lineHeight); - }); -}); diff --git a/src/shared/lib/helpers/index.ts b/src/shared/lib/helpers/index.ts index 56a185c..4422645 100644 --- a/src/shared/lib/helpers/index.ts +++ b/src/shared/lib/helpers/index.ts @@ -115,24 +115,6 @@ export { type EntityStore, } from './createEntityStore/createEntityStore.svelte'; -/** - * Text layout - */ -export { - /** - * Single line layout information - */ - type LayoutLine as TextLayoutLine, - /** - * Full multi-line layout information - */ - type LayoutResult as TextLayoutResult, - /** - * High-level text measurement engine - */ - TextLayoutEngine, -} from './TextLayoutEngine/TextLayoutEngine.svelte'; - /** * Persistence */ diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index 81cd249..c2eaef9 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -24,9 +24,6 @@ export { type Property, type ResponsiveManager, responsiveManager, - TextLayoutEngine, - type TextLayoutLine, - type TextLayoutResult, type TypographyControl, type VirtualItem, type Virtualizer,