chenglou/pretextPublic

Fast, accurate & comprehensive text measurement & layout

AI summary: A pure JS/TS library for fast, DOM-free multiline text measurement and layout.

Stars
49.7K
+33 today
Forks
2.7K
Watchers
149
Open issues
46
Open PRs
44
Contributors
~7
Commits
410
Branches
7

TypeScriptMITCreated Mar 7, 2026Last push 1mo ago+158 stars this week+232 this month

Star history

since Mar 8, 2026
020K40KMar 2026Apr 2026Jun 2026Aug 2026
49.7K stars as of Aug 7, 2026, tracked back to Mar 8, 2026. Historical curve reconstructed from public GitHub event archives, calibrated to the current total.

Contribution activity

commits per day, last 52 weeks
AugSepOctNovDecJanFebMarAprMayJunJulAugMonWedFri2025-08-09: 0 commits2025-08-10: 0 commits2025-08-11: 0 commits2025-08-12: 0 commits2025-08-13: 0 commits2025-08-14: 0 commits2025-08-15: 0 commits2025-08-16: 0 commits2025-08-17: 0 commits2025-08-18: 0 commits2025-08-19: 0 commits2025-08-20: 0 commits2025-08-21: 0 commits2025-08-22: 0 commits2025-08-23: 0 commits2025-08-24: 0 commits2025-08-25: 0 commits2025-08-26: 0 commits2025-08-27: 0 commits2025-08-28: 0 commits2025-08-29: 0 commits2025-08-30: 0 commits2025-08-31: 0 commits2025-09-01: 0 commits2025-09-02: 0 commits2025-09-03: 0 commits2025-09-04: 0 commits2025-09-05: 0 commits2025-09-06: 0 commits2025-09-07: 0 commits2025-09-08: 0 commits2025-09-09: 0 commits2025-09-10: 0 commits2025-09-11: 0 commits2025-09-12: 0 commits2025-09-13: 0 commits2025-09-14: 0 commits2025-09-15: 0 commits2025-09-16: 0 commits2025-09-17: 0 commits2025-09-18: 0 commits2025-09-19: 0 commits2025-09-20: 0 commits2025-09-21: 0 commits2025-09-22: 0 commits2025-09-23: 0 commits2025-09-24: 0 commits2025-09-25: 0 commits2025-09-26: 0 commits2025-09-27: 0 commits2025-09-28: 0 commits2025-09-29: 0 commits2025-09-30: 0 commits2025-10-01: 0 commits2025-10-02: 0 commits2025-10-03: 0 commits2025-10-04: 0 commits2025-10-05: 0 commits2025-10-06: 0 commits2025-10-07: 0 commits2025-10-08: 0 commits2025-10-09: 0 commits2025-10-10: 0 commits2025-10-11: 0 commits2025-10-12: 0 commits2025-10-13: 0 commits2025-10-14: 0 commits2025-10-15: 0 commits2025-10-16: 0 commits2025-10-17: 0 commits2025-10-18: 0 commits2025-10-19: 0 commits2025-10-20: 0 commits2025-10-21: 0 commits2025-10-22: 0 commits2025-10-23: 0 commits2025-10-24: 0 commits2025-10-25: 0 commits2025-10-26: 0 commits2025-10-27: 0 commits2025-10-28: 0 commits2025-10-29: 0 commits2025-10-30: 0 commits2025-10-31: 0 commits2025-11-01: 0 commits2025-11-02: 0 commits2025-11-03: 0 commits2025-11-04: 0 commits2025-11-05: 0 commits2025-11-06: 0 commits2025-11-07: 0 commits2025-11-09: 0 commits2025-11-10: 0 commits2025-11-11: 0 commits2025-11-12: 0 commits2025-11-13: 0 commits2025-11-14: 0 commits2025-11-15: 0 commits2025-11-16: 0 commits2025-11-17: 0 commits2025-11-18: 0 commits2025-11-19: 0 commits2025-11-20: 0 commits2025-11-21: 0 commits2025-11-22: 0 commits2025-11-23: 0 commits2025-11-24: 0 commits2025-11-25: 0 commits2025-11-26: 0 commits2025-11-27: 0 commits2025-11-28: 0 commits2025-11-29: 0 commits2025-11-30: 0 commits2025-12-01: 0 commits2025-12-02: 0 commits2025-12-03: 0 commits2025-12-04: 0 commits2025-12-05: 0 commits2025-12-06: 0 commits2025-12-07: 0 commits2025-12-08: 0 commits2025-12-09: 0 commits2025-12-10: 0 commits2025-12-11: 0 commits2025-12-12: 0 commits2025-12-13: 0 commits2025-12-14: 0 commits2025-12-15: 0 commits2025-12-16: 0 commits2025-12-17: 0 commits2025-12-18: 0 commits2025-12-19: 0 commits2025-12-20: 0 commits2025-12-21: 0 commits2025-12-22: 0 commits2025-12-23: 0 commits2025-12-24: 0 commits2025-12-25: 0 commits2025-12-26: 0 commits2025-12-27: 0 commits2025-12-28: 0 commits2025-12-29: 0 commits2025-12-30: 0 commits2025-12-31: 0 commits2026-01-01: 0 commits2026-01-02: 0 commits2026-01-03: 0 commits2026-01-04: 0 commits2026-01-05: 0 commits2026-01-06: 0 commits2026-01-07: 0 commits2026-01-08: 0 commits2026-01-09: 0 commits2026-01-10: 0 commits2026-01-11: 0 commits2026-01-12: 0 commits2026-01-13: 0 commits2026-01-14: 0 commits2026-01-15: 0 commits2026-01-16: 0 commits2026-01-17: 0 commits2026-01-18: 0 commits2026-01-19: 0 commits2026-01-20: 0 commits2026-01-21: 0 commits2026-01-22: 0 commits2026-01-23: 0 commits2026-01-24: 0 commits2026-01-25: 0 commits2026-01-26: 0 commits2026-01-27: 0 commits2026-01-28: 0 commits2026-01-29: 0 commits2026-01-30: 0 commits2026-01-31: 0 commits2026-02-01: 0 commits2026-02-02: 0 commits2026-02-03: 0 commits2026-02-04: 0 commits2026-02-05: 0 commits2026-02-06: 0 commits2026-02-07: 0 commits2026-02-08: 0 commits2026-02-09: 0 commits2026-02-10: 0 commits2026-02-11: 0 commits2026-02-12: 0 commits2026-02-13: 0 commits2026-02-14: 0 commits2026-02-15: 0 commits2026-02-16: 0 commits2026-02-17: 0 commits2026-02-18: 0 commits2026-02-19: 0 commits2026-02-20: 0 commits2026-02-21: 0 commits2026-02-22: 0 commits2026-02-23: 0 commits2026-02-24: 0 commits2026-02-25: 0 commits2026-02-26: 0 commits2026-02-27: 0 commits2026-02-28: 0 commits2026-03-01: 0 commits2026-03-02: 0 commits2026-03-03: 25 commits2026-03-04: 14 commits2026-03-05: 0 commits2026-03-06: 5 commits2026-03-07: 14 commits2026-03-08: 15 commits2026-03-09: 14 commits2026-03-10: 38 commits2026-03-11: 21 commits2026-03-12: 20 commits2026-03-13: 0 commits2026-03-14: 0 commits2026-03-15: 0 commits2026-03-16: 0 commits2026-03-17: 4 commits2026-03-18: 0 commits2026-03-19: 0 commits2026-03-20: 2 commits2026-03-21: 0 commits2026-03-22: 3 commits2026-03-23: 6 commits2026-03-24: 0 commits2026-03-25: 0 commits2026-03-26: 9 commits2026-03-27: 27 commits2026-03-28: 22 commits2026-03-29: 21 commits2026-03-30: 5 commits2026-03-31: 8 commits2026-04-01: 2 commits2026-04-02: 8 commits2026-04-03: 1 commit2026-04-04: 9 commits2026-04-05: 19 commits2026-04-06: 19 commits2026-04-07: 0 commits2026-04-08: 11 commits2026-04-09: 5 commits2026-04-10: 0 commits2026-04-11: 0 commits2026-04-12: 0 commits2026-04-13: 0 commits2026-04-14: 0 commits2026-04-15: 1 commit2026-04-16: 0 commits2026-04-17: 0 commits2026-04-18: 12 commits2026-04-19: 1 commit2026-04-20: 0 commits2026-04-21: 4 commits2026-04-22: 5 commits2026-04-23: 0 commits2026-04-24: 0 commits2026-04-25: 0 commits2026-04-26: 0 commits2026-04-27: 0 commits2026-04-28: 0 commits2026-04-29: 0 commits2026-04-30: 0 commits2026-05-01: 0 commits2026-05-02: 0 commits2026-05-03: 0 commits2026-05-04: 0 commits2026-05-05: 0 commits2026-05-06: 0 commits2026-05-07: 8 commits2026-05-08: 3 commits2026-05-09: 3 commits2026-05-10: 3 commits2026-05-11: 0 commits2026-05-12: 0 commits2026-05-13: 0 commits2026-05-14: 0 commits2026-05-15: 0 commits2026-05-16: 0 commits2026-05-17: 0 commits2026-05-18: 0 commits2026-05-19: 0 commits2026-05-20: 0 commits2026-05-21: 0 commits2026-05-22: 1 commit2026-05-23: 0 commits2026-05-24: 0 commits2026-05-25: 0 commits2026-05-26: 0 commits2026-05-27: 0 commits2026-05-28: 0 commits2026-05-29: 0 commits2026-05-30: 0 commits2026-05-31: 0 commits2026-06-01: 0 commits2026-06-02: 0 commits2026-06-03: 0 commits2026-06-04: 0 commits2026-06-05: 0 commits2026-06-06: 0 commits2026-06-07: 0 commits2026-06-08: 0 commits2026-06-09: 0 commits2026-06-10: 0 commits2026-06-11: 2 commits2026-06-12: 0 commits2026-06-13: 0 commits2026-06-14: 0 commits2026-06-15: 0 commits2026-06-16: 0 commits2026-06-17: 0 commits2026-06-18: 0 commits2026-06-19: 0 commits2026-06-20: 0 commits2026-06-21: 0 commits2026-06-22: 12 commits2026-06-23: 0 commits2026-06-24: 0 commits2026-06-25: 0 commits2026-06-26: 0 commits2026-06-27: 0 commits2026-06-28: 0 commits2026-06-29: 0 commits2026-06-30: 0 commits2026-07-01: 0 commits2026-07-02: 0 commits2026-07-03: 0 commits2026-07-04: 0 commits2026-07-05: 0 commits2026-07-06: 0 commits2026-07-07: 0 commits2026-07-08: 0 commits2026-07-09: 0 commits2026-07-10: 0 commits2026-07-11: 0 commits2026-07-12: 0 commits2026-07-13: 0 commits2026-07-14: 0 commits2026-07-15: 0 commits2026-07-16: 0 commits2026-07-17: 0 commits2026-07-18: 0 commits2026-07-19: 0 commits2026-07-20: 0 commits2026-07-21: 0 commits2026-07-22: 0 commits2026-07-23: 0 commits2026-07-24: 0 commits2026-07-25: 0 commits2026-07-26: 0 commits2026-07-27: 0 commits2026-07-28: 0 commits2026-07-29: 0 commits2026-07-30: 0 commits2026-07-31: 0 commits2026-08-01: 0 commits2026-08-02: 0 commits2026-08-03: 0 commits2026-08-04: 0 commits2026-08-05: 0 commits2026-08-06: 0 commits2026-08-07: 0 commits2026-08-08: 0 commits
402 commits in the last yearLessMore

Signals and awards

derived from tracked data
  • Widely adopted

    49,713 stars

  • Permissive license

    MIT

  • Continuous integration

    Automated checks passing

  • Repeat trending

    10 trending appearances

What pretext does

Pretext is a highly optimized JavaScript/TypeScript library designed to handle multiline text measurement and layout entirely outside the DOM. By avoiding expensive DOM operations like `getBoundingClientRect`, which trigger massive layout reflows, Pretext offers significantly faster performance. It uses the browser's own font engine via the Canvas API as the ground truth to accurately measure text segments, handling complex scripts and whitespace normalization. This approach allows developers to pre-calculate layouts for DOM, SVG, Canvas, or server-side rendering with pure arithmetic speed.

Frontend engineers and creative coders building high-performance UIs, data visualizations, or Canvas/WebGL applications. It is crucial for projects where DOM reflow costs cause unacceptable frame drops.

  • DOM-free measurement: Calculates dimensions purely in memory, completely eliminating layout reflows.
  • High performance: Utilizes a fast hot-path of pure arithmetic over cached segment widths.
  • Universal language support: Accurately handles complex scripts and all languages supported by the browser.
  • Multi-target rendering: Provides layouts ready for DOM, Canvas, SVG, or upcoming server-side environments.
  • AI-friendly iteration: Uses standard font engines as ground truth, making the output highly predictable.

Where teams use it

Virtual list optimization

Calculate exact row heights for a massive, dynamically sized list before rendering any elements to the DOM.

Canvas text rendering

Implement complex, multiline text wrapping in WebGL or Canvas applications where native DOM layout isn't available.

Performance-critical UIs

Eliminate jank in highly interactive applications by moving all text measurement off the main rendering path.

Server-side layout prediction

Predict the visual layout of text blocks on the server to send pre-calculated dimensions to the client.

Getting started: npm install @chenglou/pretext

README

main branch

Pretext

Pure JavaScript/TypeScript library for multiline text measurement & layout. Fast, accurate & supports all the languages you didn't even know about. Allows rendering to DOM, Canvas, SVG and soon, server-side.

Pretext side-steps the need for DOM measurements (e.g. getBoundingClientRect, offsetHeight), which trigger layout reflow, one of the most expensive operations in the browser. It implements its own text measurement logic, using the browsers' own font engine as ground truth (very AI-friendly iteration method).

Installation

npm install @chenglou/pretext

Demos

Clone the repo, run bun install, then bun start, and open /demos/index in your browser. On Windows, use bun run start:windows. Alternatively, see them live at chenglou.me/pretext. Some more at somnai-dreams.github.io/pretext-demos

API

Pretext serves 2 use cases:

1. Measure a paragraph's height without ever touching DOM

import { prepare, layout } from '@chenglou/pretext'

const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀‎', '16px Inter')
const { height, lineCount } = layout(prepared, 320, 20) // pure arithmetic. No DOM layout & reflow!

prepare() does the one-time work: normalize whitespace, segment the text, apply glue rules, measure the segments with canvas, and return an opaque handle. layout() is the cheap hot path after that: pure arithmetic over cached widths. Do not rerun prepare() for the same text and configs; that'd defeat its precomputation. For example, on resize, only rerun layout().

If you want textarea-like text where ordinary spaces, \t tabs, and \n hard breaks stay visible, pass { whiteSpace: 'pre-wrap' } to prepare():

const prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })
const { height } = layout(prepared, textareaWidth, 20)

Other prepare() options are { wordBreak: 'keep-all' } for CSS-like word-break: keep-all, and { letterSpacing: n } to match CSS letter-spacing (n is treated as a px value).

The returned height is the crucial last piece for unlocking web UIs:

  • proper virtualization/occlusion without guesstimates & caching
  • fancy userland layouts: masonry, JS-driven flexbox-like implementations, nudging a few layout values without CSS hacks (imagine that), etc.
  • development time verification (especially now with AI) that labels on e.g. buttons don't overflow to the next line, browser-free
  • prevent layout shift when new text loads and you wanna re-anchor the scroll position

2. Lay out the paragraph lines manually yourself

Switch out prepare with prepareWithSegments, then:

  • layoutWithLines() gives you all the lines at a fixed width:
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'

const prepared = prepareWithSegments('AGI 春天到了. بدأت الرحلة 🚀', '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26) // 320px max width, 26px line height
for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i].text, 0, i * 26)
  • measureLineStats() and walkLineRanges() give you line counts, widths and cursors without building the text strings:
import { measureLineStats, walkLineRanges } from '@chenglou/pretext'

const { lineCount, maxLineWidth } = measureLineStats(prepared, 320)
let maxW = 0
walkLineRanges(prepared, 320, line => { if (line.width > maxW) maxW = line.width })
// maxW is now the widest line — the tightest container width that still fits the text! This multiline "shrink wrap" has been missing from web
  • layoutNextLineRange() lets you route text one row at a time when width changes as you go. If you want the actual string too, materializeLineRange() turns that one range back into a full line:
import { layoutNextLineRange, materializeLineRange, prepareWithSegments, type LayoutCursor } from '@chenglou/pretext'

const prepared = prepareWithSegments(article, BODY_FONT)
let cursor: LayoutCursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0

// Flow text around a floated image: lines beside the image are narrower
while (true) {
  const width = y < image.bottom ? columnWidth - image.width : columnWidth
  const range = layoutNextLineRange(prepared, cursor, width)
  if (range === null) break

  const line = materializeLineRange(prepared, range)
  ctx.fillText(line.text, 0, y)
  cursor = range.end
  y += 26
}

This usage allows rendering to canvas, SVG, WebGL and (eventually) server-side. See the /demos/dynamic-layout demo for a richer example.

For hyphenation in manual layout, insert soft hyphens before prepare() / prepareWithSegments(). Pretext treats them as optional break points: unchosen soft hyphens stay invisible, while chosen breaks materialize as a trailing -. For mixed-language or user-generated app text, prefer conservative, locale-aware insertion over aggressive pattern hyphenation. Automatic hyphenation is not built in today.

If your manual layout needs a small helper for rich-text inline flow, code spans, mentions, chips, and browser-like boundary whitespace collapse, there is a helper at @chenglou/pretext/rich-inline. It stays inline-only and white-space: normal-only on purpose:

import { materializeRichInlineLineRange, prepareRichInline, walkRichInlineLineRanges } from '@chenglou/pretext/rich-inline'

const prepared = prepareRichInline([
  { text: 'Ship ', font: '500 17px Inter' },
  { text: '@maya', font: '700 12px Inter', break: 'never', extraWidth: 22 },
  { text: "'s rich-note", font: '500 17px Inter' },
])

walkRichInlineLineRanges(prepared, 320, range => {
  const line = materializeRichInlineLineRange(prepared, range)
  // each fragment keeps its source item index, text slice, gapBefore, and cursors
})

It is intentionally narrow:

  • raw inline text in, including boundary spaces
  • caller-owned extraWidth for pill chrome
  • break: 'never' for atomic items like chips and mentions
  • white-space: normal only
  • not a nested markup tree and not a general CSS inline formatting engine

API Glossary

Use-case 1 APIs:

prepare(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap', wordBreak?: 'normal' | 'keep-all', letterSpacing?: number }): PreparedText // one-time text analysis + measurement pass, returns an opaque value to pass to `layout()`. Make sure `font` and `letterSpacing` are synced with your CSS for the text you're measuring. `font` is the same format as what you'd use for `myCanvasContext.font = ...`, e.g. `16px Inter`; `letterSpacing` is a CSS pixel value.
layout(prepared: PreparedText, maxWidth: number, lineHeight: number): { height: number, lineCount: number } // calculates text height given a max width and lineHeight. Make sure `lineHeight` is synced with your css `line-height` declaration for the text you're measuring.

Use-case 2 APIs:

prepareWithSegments(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap', wordBreak?: 'normal' | 'keep-all', letterSpacing?: number }): PreparedTextWithSegments // same as `prepare()`, but returns a richer structure for manual line layout needs
layoutWithLines(prepared: PreparedTextWithSegments, maxWidth: number, lineHeight: number): { height: number, lineCount: number, lines: LayoutLine[] } // high-level api for manual layout needs. Accepts a fixed max width for all lines. Similar to `layout()`'s return, but additionally returns the lines info
walkLineRanges(prepared: PreparedTextWithSegments, maxWidth: number, onLine: (line: LayoutLineRange) => void): number // low-level api for manual layout needs. Accepts a fixed max width for all lines. Calls `onLine` once per line with its actual calculated line width and start/end cursors, without building line text strings. Very useful for certain cases where you wanna speculatively test a few width and height boundaries (e.g. binary search a nice width value by repeatedly calling walkLineRanges and checking the line count, and therefore height, is "nice" too). You can have text messages shrinkwrap and balanced text layout this way. After walkLineRanges calls, you'd call layoutWithLines once, with your satisfying max width, to get the actual lines info.
measureLineStats(prepared: PreparedTextWithSegments, maxWidth: number): { lineCount: number, maxLineWidth: number } // returns only how many lines this width produces, and how wide the widest one is. Avoids line/string allocations.
measureNaturalWidth(prepared: PreparedTextWithSegments): number // returns the widest forced line when width itself is not the thing causing wraps
layoutNextLine(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLine | null // iterator-like api for laying out each line with a different width! Returns the LayoutLine starting from `start`, or `null` when the paragraph's exhausted. Pass the previous line's `end` cursor as the next `start`.
layoutNextLineRange(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLineRange | null // same as layoutNextLine(), but without allocating line text strings. Useful for variable-width manual layout, occlusion, and virtualization measurements.
materializeLineRange(prepared: PreparedTextWithSegments, line: LayoutLineRange): LayoutLine // turns a LayoutLineRange from layoutNextLineRange() or walkLineRanges() into a full line with text
type LineStats = {
  lineCount: number // Number of wrapped lines, e.g. 3
  maxLineWidth: number // Widest wrapped line, e.g. 192.5
}
type LayoutLine = {
  text: string // Full text content of this line, e.g. 'hello world'
  width: number // Measured width of this line, e.g. 87.5
  start: LayoutCursor // Inclusive start cursor in prepared segments/graphemes
  end: LayoutCursor // Exclusive end cursor in prepared segments/graphemes
}
type LayoutLineRange = {
  width: number // Measured width of this line, e.g. 87.5
  start: LayoutCursor // Inclusive start cursor in prepared segments/graphemes
  end: LayoutCursor // Exclusive end cursor in prepared segments/graphemes
}
type LayoutCursor = {
  segmentIndex: number // Segment index in prepareWithSegments' prepared rich segment stream
  graphemeIndex: number // Grapheme index within that segment; `0` at segment boundaries
}

Helper for rich-text inline flow:

prepareRichInline(items: RichInlineItem[]): PreparedRichInline // compile raw inline items with their original text. The compiler owns cross-item collapsed whitespace and caches each item's natural width
layoutNextRichInlineLineRange(prepared: PreparedRichInline, maxWidth: number, start?: RichInlineCursor): RichInlineLineRange | null // stream one line of rich-text inline flow at a time without building fragment text strings
walkRichInlineLineRanges(prepared: PreparedRichInline, maxWidth: number, onLine: (line: RichInlineLineRange) => void): number // non-materializing line walker for rich-text inline flow shrinkwrap/stats work
materializeRichInlineLineRange(prepared: PreparedRichInline, line: RichInlineLineRange): RichInlineLine // turns one previously computed rich-inline line range back into full fragment text
measureRichInlineStats(prepared: PreparedRichInline, maxWidth: number): { lineCount: number, maxLineWidth: number } // returns only how many lines this width produces, and how wide the widest one is. Avoids fragment-text allocations.
type RichInlineItem = {
  text: string // raw author text, including leading/trailing collapsible spaces
  font: string // canvas font shorthand for this item
  letterSpacing?: number // extra horizontal spacing between graphemes, in CSS px
  break?: 'normal' | 'never' // `never` keeps the item atomic, like a chip
  extraWidth?: number // caller-owned horizontal chrome, e.g. padding + border width
}
type RichInlineCursor = {
  itemIndex: number // Which source RichInlineItem this cursor is currently in
  segmentIndex: number // Segment index within that item's prepared text
  graphemeIndex: number // Grapheme index within that segment; `0` at segment boundaries
}
type RichInlineFragment = {
  itemIndex: number // index back into the original RichInlineItem array
  text: string // Text slice for this fragment
  gapBefore: number // collapsed boundary gap paid before this fragment on this line
  occupiedWidth: number // text width plus extraWidth
  start: LayoutCursor // Start cursor within the item's prepared text
  end: LayoutCursor // End cursor within the item's prepared text
}
type RichInlineLine = {
  fragments: RichInlineFragment[] // Materialized fragments on this line
  width: number // Measured width of this line, including gapBefore/extraWidth
  end: RichInlineCursor // Exclusive end cursor for continuing the next line
}
type RichInlineFragmentRange = {
  itemIndex: number // index back into the original RichInlineItem array
  gapBefore: number // collapsed boundary gap paid before this fragment on this line
  occupiedWidth: number // text width plus extraWidth
  start: LayoutCursor // Start cursor within the item's prepared text
  end: LayoutCursor // End cursor within the item's prepared text
}
type RichInlineLineRange = {
  fragments: RichInlineFragmentRange[] // Non-materialized fragment ownership/ranges on this line
  width: number // Measured width of this line, including gapBefore/extraWidth
  end: RichInlineCursor // Exclusive end cursor for continuing the next line
}
type RichInlineStats = {
  lineCount: number // Number of wrapped lines, e.g. 3
  maxLineWidth: number // Widest wrapped line, e.g. 192.5
}

Other helpers:

clearCache(): void // clears Pretext's shared internal caches used by prepare() and prepareWithSegments(). Useful if your app cycles through many different fonts or text variants and you want to release the accumulated cache
setLocale(locale?: string): void // optional (by default we use the current locale). Sets locale for future prepare() and prepareWithSegments(). Internally, it also calls clearCache(). Setting a new locale doesn't affect existing prepare() and prepareWithSegments() states (no mutations to them)

Notes:

  • PreparedText is the opaque fast-path handle. PreparedTextWithSegments is the richer manual-layout handle.
  • LayoutCursor is a segment/grapheme cursor, not a raw string offset.
  • layout() with an empty string returns { lineCount: 0, height: 0 }. Browsers still size an empty block to one line-height, so clamp with Math.max(1, lineCount) * lineHeight if you need that behavior.
  • The richer handle also includes segLevels for custom bidi-aware rendering. The line-breaking APIs do not read it.
  • Segment widths are browser-canvas widths for line breaking, not exact glyph-position data for custom Arabic or mixed-direction x-coordinate reconstruction.
  • If a soft hyphen wins the break, materialized line text includes the visible trailing -.
  • measureNaturalWidth() returns the widest forced line. Hard breaks still count.
  • prepare() and prepareWithSegments() do horizontal-only work. lineHeight stays a layout-time input.

Caveats

Pretext doesn't try to be a full font rendering engine (yet?). It currently targets the common text setup:

  • white-space: normal and pre-wrap
  • word-break: normal and keep-all
  • overflow-wrap: break-word. Very narrow widths can still break inside words, but only at grapheme boundaries.
  • line-break: auto
  • letter-spacing as a numeric pixel value passed to prepare() / prepareWithSegments()
  • Tabs follow the default browser-style tab-size: 8
  • { wordBreak: 'keep-all' } is supported too. It behaves like you'd expect for CJK/Hangul and no-space mixed Latin/numeric/CJK text, while keeping the same overflow-wrap: break-word fallback for overlong runs.
  • system-ui is unsafe for layout() accuracy on macOS. Use a named font. See the platform bug ledger for the Chrome and Firefox issues.
  • Runtime requires Intl.Segmenter and Canvas 2D text measurement. Browsers or runtimes without Intl.Segmenter are currently unsupported.
  • CSS text features outside the canvas font shorthand, such as font-optical-sizing, font-feature-settings, and standalone font-variation-settings, are not modeled separately. Variable-font axes only help when the active axis is reflected in the canvas font string, for example via weight.

Develop

See DEVELOPMENT.md for the dev setup and commands.

Credits

Sebastian Markbage first planted the seed with text-layout last decade. His design — canvas measureText for shaping, bidi from pdf.js, streaming line breaking — informed the architecture we kept pushing forward here.

View on GitHub

Recent activity

commits and pull requests

Code frequency

additions and deletions
+243.6K-243.6KWeek of 2026-03-01: +9,148 linesWeek of 2026-03-01: -1,151 linesWeek of 2026-03-08: +26,960 linesWeek of 2026-03-08: -10,103 linesWeek of 2026-03-15: +323 linesWeek of 2026-03-15: -202 linesWeek of 2026-03-22: +243,551 linesWeek of 2026-03-22: -7,594 linesWeek of 2026-03-29: +18,414 linesWeek of 2026-03-29: -9,909 linesWeek of 2026-04-05: +12,148 linesWeek of 2026-04-05: -8,694 linesWeek of 2026-04-12: +119 linesWeek of 2026-04-12: -55 linesWeek of 2026-04-19: +3,544 linesWeek of 2026-04-19: -1,332 linesWeek of 2026-04-26: +0 linesWeek of 2026-04-26: -0 linesWeek of 2026-05-03: +2,655 linesWeek of 2026-05-03: -2,401 linesWeek of 2026-05-10: +1,126 linesWeek of 2026-05-10: -645 linesWeek of 2026-05-17: +168 linesWeek of 2026-05-17: -54 linesWeek of 2026-05-24: +0 linesWeek of 2026-05-24: -0 linesWeek of 2026-05-31: +0 linesWeek of 2026-05-31: -0 linesWeek of 2026-06-07: +16 linesWeek of 2026-06-07: -5 linesWeek of 2026-06-14: +0 linesWeek of 2026-06-14: -0 linesWeek of 2026-06-21: +1,729 linesWeek of 2026-06-21: -2,632 linesWeek of 2026-06-28: +0 linesWeek of 2026-06-28: -0 linesWeek of 2026-07-05: +0 linesWeek of 2026-07-05: -0 linesWeek of 2026-07-12: +0 linesWeek of 2026-07-12: -0 linesWeek of 2026-07-19: +0 linesWeek of 2026-07-19: -0 linesWeek of 2026-07-26: +0 linesWeek of 2026-07-26: -0 linesMar 1, 2026Jul 26, 2026
+319.9K lines added, -44.8K removed over the last year.

Commits per week

last 52 weeks
1080Week of 2025-08-09: 0 commitsWeek of 2025-08-16: 0 commitsWeek of 2025-08-23: 0 commitsWeek of 2025-08-30: 0 commitsWeek of 2025-09-06: 0 commitsWeek of 2025-09-13: 0 commitsWeek of 2025-09-20: 0 commitsWeek of 2025-09-27: 0 commitsWeek of 2025-10-04: 0 commitsWeek of 2025-10-11: 0 commitsWeek of 2025-10-18: 0 commitsWeek of 2025-10-25: 0 commitsWeek of 2025-11-01: 0 commitsWeek of 2025-11-09: 0 commitsWeek of 2025-11-16: 0 commitsWeek of 2025-11-23: 0 commitsWeek of 2025-11-30: 0 commitsWeek of 2025-12-07: 0 commitsWeek of 2025-12-14: 0 commitsWeek of 2025-12-21: 0 commitsWeek of 2025-12-28: 0 commitsWeek of 2026-01-04: 0 commitsWeek of 2026-01-11: 0 commitsWeek of 2026-01-18: 0 commitsWeek of 2026-01-25: 0 commitsWeek of 2026-02-01: 0 commitsWeek of 2026-02-08: 0 commitsWeek of 2026-02-15: 0 commitsWeek of 2026-02-22: 0 commitsWeek of 2026-03-01: 58 commitsWeek of 2026-03-08: 108 commitsWeek of 2026-03-15: 6 commitsWeek of 2026-03-22: 67 commitsWeek of 2026-03-29: 54 commitsWeek of 2026-04-05: 54 commitsWeek of 2026-04-12: 13 commitsWeek of 2026-04-19: 10 commitsWeek of 2026-04-26: 0 commitsWeek of 2026-05-03: 14 commitsWeek of 2026-05-10: 3 commitsWeek of 2026-05-17: 1 commitsWeek of 2026-05-24: 0 commitsWeek of 2026-05-31: 0 commitsWeek of 2026-06-07: 2 commitsWeek of 2026-06-14: 0 commitsWeek of 2026-06-21: 12 commitsWeek of 2026-06-28: 0 commitsWeek of 2026-07-05: 0 commitsWeek of 2026-07-12: 0 commitsWeek of 2026-07-19: 0 commitsWeek of 2026-07-26: 0 commitsWeek of 2026-08-02: 0 commitsAug 9, 2025Aug 2, 2026
402 commits in the last 52 weeks.

When work happens

weekday and hour
SunMonTueWedThuFriSat036912151821Sun 0:00 — 2 commitsSun 1:00 — 9 commitsSun 2:00 — 6 commitsSun 3:00 — 4 commitsSun 4:00 — 0 commitsSun 5:00 — 0 commitsSun 6:00 — 0 commitsSun 7:00 — 0 commitsSun 8:00 — 0 commitsSun 9:00 — 0 commitsSun 10:00 — 1 commitsSun 11:00 — 0 commitsSun 12:00 — 1 commitsSun 13:00 — 3 commitsSun 14:00 — 3 commitsSun 15:00 — 1 commitsSun 16:00 — 1 commitsSun 17:00 — 2 commitsSun 18:00 — 5 commitsSun 19:00 — 0 commitsSun 20:00 — 5 commitsSun 21:00 — 7 commitsSun 22:00 — 7 commitsSun 23:00 — 5 commitsMon 0:00 — 9 commitsMon 1:00 — 5 commitsMon 2:00 — 10 commitsMon 3:00 — 0 commitsMon 4:00 — 2 commitsMon 5:00 — 0 commitsMon 6:00 — 0 commitsMon 7:00 — 0 commitsMon 8:00 — 0 commitsMon 9:00 — 0 commitsMon 10:00 — 0 commitsMon 11:00 — 0 commitsMon 12:00 — 4 commitsMon 13:00 — 0 commitsMon 14:00 — 1 commitsMon 15:00 — 1 commitsMon 16:00 — 3 commitsMon 17:00 — 2 commitsMon 18:00 — 4 commitsMon 19:00 — 4 commitsMon 20:00 — 3 commitsMon 21:00 — 4 commitsMon 22:00 — 2 commitsMon 23:00 — 2 commitsTue 0:00 — 4 commitsTue 1:00 — 7 commitsTue 2:00 — 13 commitsTue 3:00 — 6 commitsTue 4:00 — 1 commitsTue 5:00 — 0 commitsTue 6:00 — 0 commitsTue 7:00 — 0 commitsTue 8:00 — 0 commitsTue 9:00 — 0 commitsTue 10:00 — 0 commitsTue 11:00 — 1 commitsTue 12:00 — 5 commitsTue 13:00 — 1 commitsTue 14:00 — 1 commitsTue 15:00 — 0 commitsTue 16:00 — 4 commitsTue 17:00 — 0 commitsTue 18:00 — 2 commitsTue 19:00 — 5 commitsTue 20:00 — 4 commitsTue 21:00 — 4 commitsTue 22:00 — 11 commitsTue 23:00 — 10 commitsWed 0:00 — 10 commitsWed 1:00 — 8 commitsWed 2:00 — 8 commitsWed 3:00 — 2 commitsWed 4:00 — 0 commitsWed 5:00 — 0 commitsWed 6:00 — 0 commitsWed 7:00 — 0 commitsWed 8:00 — 0 commitsWed 9:00 — 3 commitsWed 10:00 — 4 commitsWed 11:00 — 1 commitsWed 12:00 — 3 commitsWed 13:00 — 2 commitsWed 14:00 — 0 commitsWed 15:00 — 3 commitsWed 16:00 — 0 commitsWed 17:00 — 0 commitsWed 18:00 — 2 commitsWed 19:00 — 3 commitsWed 20:00 — 0 commitsWed 21:00 — 0 commitsWed 22:00 — 0 commitsWed 23:00 — 5 commitsThu 0:00 — 7 commitsThu 1:00 — 8 commitsThu 2:00 — 1 commitsThu 3:00 — 0 commitsThu 4:00 — 1 commitsThu 5:00 — 0 commitsThu 6:00 — 0 commitsThu 7:00 — 0 commitsThu 8:00 — 0 commitsThu 9:00 — 0 commitsThu 10:00 — 1 commitsThu 11:00 — 0 commitsThu 12:00 — 0 commitsThu 13:00 — 0 commitsThu 14:00 — 1 commitsThu 15:00 — 1 commitsThu 16:00 — 1 commitsThu 17:00 — 3 commitsThu 18:00 — 2 commitsThu 19:00 — 2 commitsThu 20:00 — 2 commitsThu 21:00 — 9 commitsThu 22:00 — 7 commitsThu 23:00 — 6 commitsFri 0:00 — 2 commitsFri 1:00 — 1 commitsFri 2:00 — 2 commitsFri 3:00 — 2 commitsFri 4:00 — 3 commitsFri 5:00 — 1 commitsFri 6:00 — 0 commitsFri 7:00 — 0 commitsFri 8:00 — 0 commitsFri 9:00 — 0 commitsFri 10:00 — 0 commitsFri 11:00 — 0 commitsFri 12:00 — 1 commitsFri 13:00 — 0 commitsFri 14:00 — 5 commitsFri 15:00 — 1 commitsFri 16:00 — 1 commitsFri 17:00 — 6 commitsFri 18:00 — 1 commitsFri 19:00 — 3 commitsFri 20:00 — 3 commitsFri 21:00 — 1 commitsFri 22:00 — 3 commitsFri 23:00 — 3 commitsSat 0:00 — 11 commitsSat 1:00 — 7 commitsSat 2:00 — 4 commitsSat 3:00 — 1 commitsSat 4:00 — 1 commitsSat 5:00 — 0 commitsSat 6:00 — 0 commitsSat 7:00 — 1 commitsSat 8:00 — 0 commitsSat 9:00 — 1 commitsSat 10:00 — 6 commitsSat 11:00 — 2 commitsSat 12:00 — 2 commitsSat 13:00 — 6 commitsSat 14:00 — 5 commitsSat 15:00 — 0 commitsSat 16:00 — 4 commitsSat 17:00 — 3 commitsSat 18:00 — 0 commitsSat 19:00 — 0 commitsSat 20:00 — 0 commitsSat 21:00 — 2 commitsSat 22:00 — 2 commitsSat 23:00 — 2 commits
Commit volume by weekday and hour (UTC). Larger dots mean more commits.

Who is committing

last 52 weeks
Maintainer commits403 (98%)
Community commits7 (2%)

410 commits in total over the last year.

DateListRankStars gained
Apr 7, 2026daily#21+173
Apr 5, 2026daily#25+219
Apr 4, 2026daily#8+382
Apr 3, 2026daily#3+739
Apr 2, 2026daily#4+560
Apr 1, 2026daily#7+455
Mar 31, 2026daily#3+782
Mar 30, 2026daily#1+922
Mar 29, 2026daily#1+1,344
Mar 28, 2026daily#1+468
  • freeCodeCamp/freeCodeCamp

    freeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.

    453.6K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    385.5K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    384.4K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    384.4K stars · TypeScript

  • openclaw/openclaw

    Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

    384.4K stars · TypeScript

  • anomalyco/opencode

    The open source coding agent.

    194.7K stars · TypeScript