GitHub - chenglou/pretext
Service

GitHub - chenglou/pretext

chenglou
2026.03.30
ยทGitHubยทby ๋„ค๋ฃจ
#JavaScript#Layout#Text Measurement#TypeScript#Web UI

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:

  1. 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 (unless whiteSpace: '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 the CanvasRenderingContext2D.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 opaque PreparedText or PreparedTextWithSegments object. This phase is relatively slow (e.g., โ‰ˆ19ms\approx 19ms for a batch of 500 texts in benchmarks) but only needs to be run once per unique text and font configuration.
  2. 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, a maxWidth, and optionally a lineHeight, to determine line breaks and overall dimensions. This phase is extremely fast (e.g., โ‰ˆ0.09ms\approx 0.09ms for a batch of 500 texts).

Pretext provides two main API use cases:

  1. Measuring Paragraph Height Without Touching the DOM:
    • prepare(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap' }) : PreparedText
Performs the initial text analysis and measurement, returning an opaque handle. The font string should match the CSS font shorthand.
  • layout(prepared: PreparedText, maxWidth: number, lineHeight: number) : { height: number, lineCount: number }
Calculates the total height and line count of the text given a maximum width and line height, using only cached data. This is ideal for scenarios requiring accurate dimensions for virtualization or pre-rendering checks.

  1. Manual Line Layout and Rendering (e.g., for Canvas, SVG, WebGL):
    • prepareWithSegments(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap' }) : PreparedTextWithSegments
Similar to 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[] }
A high-level API that returns an array of 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.
  • walkLineRanges(prepared:PreparedTextWithSegments,maxWidth:number,onLine:(line:LayoutLineRange)=>void):numberwalkLineRanges(prepared: PreparedTextWithSegments, maxWidth: number, onLine: (line: LayoutLineRange) => void) : number
A low-level API that iterates over each potential line within the given 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
An iterator-like API that allows laying out text line by line, even with varying 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.