GitHub - chenglou/pretext
Key Points
- 1Pretext is a JavaScript/TypeScript library for fast and accurate multiline text measurement and layout, designed to bypass DOM reflows by implementing its own text sizing logic using the browser's font engine.
- 2It serves two primary use cases: obtaining precise paragraph height and line count without DOM interaction, and providing detailed control for manual line layout across various rendering environments like Canvas, SVG, or server-side.
- 3Supporting a wide range of languages, emojis, and mixed bidirectionality, Pretext enables critical web UI features such as proper virtualization, prevention of layout shifts, and advanced user-land layout implementations.
Pretext is a JavaScript/TypeScript library designed for fast and accurate multiline text measurement and layout, sidestepping the performance bottlenecks of DOM measurements (e.g., getBoundingClientRect, offsetHeight) which trigger expensive layout reflows in the browser. It implements its own text measurement logic using the browser's native font engine (via the Canvas API's measureText) as the ground truth for character and segment widths. This allows for pure arithmetic calculations for layout after an initial precomputation step, making it suitable for high-performance UI components like virtualization, custom layouts, and preventing layout shifts.
The core methodology involves a two-phase process:
- Preparation (
prepare/prepareWithSegments): This is the one-time, more computationally intensive phase. It takes the text string and font style as input. Internally, it normalizes whitespace (unlesswhiteSpace: 'pre-wrap'is specified, preserving spaces, tabs, and newlines), segments the text into logical units (e.g., words, graphemes), applies internal "glue rules" for word breaking and hyphenation, and crucially, measures the precise width of each segment using theCanvasRenderingContext2D.measureText()API. This operation leverages the browser's native font rendering capabilities, ensuring accuracy across various languages, emojis, and mixed-bidi text. The results of these measurements are then cached and encapsulated into an opaquePreparedTextorPreparedTextWithSegmentsobject. This phase is relatively slow (e.g., for a batch of 500 texts in benchmarks) but only needs to be run once per unique text and font configuration. - Layout (
layout/layoutWithLines/walkLineRanges/layoutNextLine): This is the computationally cheap "hot path." Once the text is prepared, subsequent layout calculations involve pure arithmetic operations over the precomputed and cached segment widths. This avoids any DOM interaction or reflows. Layout functions take the prepared text object, amaxWidth, and optionally alineHeight, to determine line breaks and overall dimensions. This phase is extremely fast (e.g., for a batch of 500 texts).
Pretext provides two main API use cases:
- Measuring Paragraph Height Without Touching the DOM:
prepare(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap' }) : PreparedText
font string should match the CSS font shorthand.
layout(prepared: PreparedText, maxWidth: number, lineHeight: number) : { height: number, lineCount: number }
- Manual Line Layout and Rendering (e.g., for Canvas, SVG, WebGL):
prepareWithSegments(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap' }) : PreparedTextWithSegments
prepare, but returns a richer structure (PreparedTextWithSegments) that exposes granular segment and grapheme information necessary for manual line manipulation.
layoutWithLines(prepared: PreparedTextWithSegments, maxWidth: number, lineHeight: number) : { height: number, lineCount: number, lines: LayoutLine[] }
LayoutLine objects. Each LayoutLine contains the full text content, width, and start/end cursors for a given line, assuming a fixed maxWidth for all lines.
maxWidth. It calls the onLine callback with a LayoutLineRange object, providing the width and start/end cursors for each line, but without constructing the full line text string. This is useful for speculative width testing (e.g., binary searching for an optimal container width that fits text elegantly). It returns the total line count.
layoutNextLine(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number) : LayoutLine | null
maxWidth values for each subsequent line (e.g., wrapping text around a floated image). It returns a LayoutLine object for the next line, or null if the text is exhausted. The start cursor from the previous line's end cursor is passed to continue the layout.The library supports standard CSS text properties white-space: normal, word-break: normal, overflow-wrap: break-word, and line-break: auto by default. When whiteSpace: 'pre-wrap' is used, ordinary spaces, tabs (\t), and hard breaks (\n) are preserved. overflow-wrap: break-word ensures that even very narrow widths can break inside words at grapheme boundaries. It also provides clearCache() and setLocale() utilities for managing internal caches and locale-specific text behaviors.