{"id":47587451,"url":"https://github.com/chenglou/pretext","last_synced_at":"2026-04-01T16:33:16.033Z","repository":{"id":342893098,"uuid":"1175538883","full_name":"chenglou/pretext","owner":"chenglou","description":null,"archived":false,"fork":false,"pushed_at":"2026-03-28T18:11:31.000Z","size":1418,"stargazers_count":2499,"open_issues_count":5,"forks_count":84,"subscribers_count":19,"default_branch":"main","last_synced_at":"2026-03-28T19:39:33.917Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chenglou.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-07T21:06:28.000Z","updated_at":"2026-03-28T19:39:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/chenglou/pretext","commit_stats":null,"previous_names":["chenglou/pretext"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/chenglou/pretext","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenglou%2Fpretext","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenglou%2Fpretext/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenglou%2Fpretext/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenglou%2Fpretext/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chenglou","download_url":"https://codeload.github.com/chenglou/pretext/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenglou%2Fpretext/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31290491,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-01T16:33:13.488Z","updated_at":"2026-04-01T16:33:16.019Z","avatar_url":"https://github.com/chenglou.png","language":"TypeScript","readme":"# Pretext\n\nPure JavaScript/TypeScript library for multiline text measurement \u0026 layout. Fast, accurate \u0026 supports all the languages you didn't even know about. Allows rendering to DOM, Canvas, SVG and soon, server-side.\n\nPretext 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).\n\n## Installation\n\n```sh\nnpm install @chenglou/pretext\n```\n\n## Demos\n\nClone the repo, run `bun install`, then `bun start`, and open the `/demos` in your browser (no trailing slash. Bun devserver bugs on those)\nAlternatively, see them live at [chenglou.me/pretext](https://chenglou.me/pretext/). Some more at [somnai-dreams.github.io/pretext-demos](https://somnai-dreams.github.io/pretext-demos/)\n\n## API\n\nPretext serves 2 use cases:\n\n### 1. Measure a paragraph's height _without ever touching DOM_\n\n```ts\nimport { prepare, layout } from '@chenglou/pretext'\n\nconst prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')\nconst { height, lineCount } = layout(prepared, textWidth, 20) // pure arithmetics. No DOM layout \u0026 reflow!\n```\n\n`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()`.\n\nIf you want textarea-like text where ordinary spaces, `\\t` tabs, and `\\n` hard breaks stay visible, pass `{ whiteSpace: 'pre-wrap' }` to `prepare()`:\n\n```ts\nconst prepared = prepare(textareaValue, '16px Inter', { whiteSpace: 'pre-wrap' })\nconst { height } = layout(prepared, textareaWidth, 20)\n```\n\nOn the current checked-in benchmark snapshot:\n- `prepare()` is about `19ms` for the shared 500-text batch\n- `layout()` is about `0.09ms` for that same batch\n\nWe support all the languages you can imagine, including emojis and mixed-bidi, and caters to specific browser quirks\n\nThe returned height is the crucial last piece for unlocking web UI's:\n- proper virtualization/occlusion without guesstimates \u0026 caching\n- fancy userland layouts: masonry, JS-driven flexbox-like implementations, nudging a few layout values without CSS hacks (imagine that), etc.\n- _development time_ verification (especially now with AI) that labels on e.g. buttons don't overflow to the next line, browser-free\n- prevent layout shift when new text loads and you wanna re-anchor the scroll position\n\n### 2. Lay out the paragraph lines manually yourself\n\nSwitch out `prepare` with `prepareWithSegments`, then:\n\n- `layoutWithLines()` gives you all the lines at a fixed width:\n\n```ts\nimport { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'\n\nconst prepared = prepareWithSegments('AGI 春天到了. بدأت الرحلة 🚀', '18px \"Helvetica Neue\"')\nconst { lines } = layoutWithLines(prepared, 320, 26) // 320px max width, 26px line height\nfor (let i = 0; i \u003c lines.length; i++) ctx.fillText(lines[i].text, 0, i * 26)\n```\n\n- `walkLineRanges()` gives you line widths and cursors without building the text strings:\n\n```ts\nlet maxW = 0\nwalkLineRanges(prepared, 320, line =\u003e { if (line.width \u003e maxW) maxW = line.width })\n// maxW is now the widest line — the tightest container width that still fits the text! This multiline \"shrink wrap\" has been missing from web\n```\n\n- `layoutNextLine()` lets you route text one row at a time when width changes as you go:\n\n```ts\nlet cursor = { segmentIndex: 0, graphemeIndex: 0 }\nlet y = 0\n\n// Flow text around a floated image: lines beside the image are narrower\nwhile (true) {\n  const width = y \u003c image.bottom ? columnWidth - image.width : columnWidth\n  const line = layoutNextLine(prepared, cursor, width)\n  if (line === null) break\n  ctx.fillText(line.text, 0, y)\n  cursor = line.end\n  y += 26\n}\n```\n\nThis usage allows rendering to canvas, SVG, WebGL and (eventually) server-side.\n\n### API Glossary\n\nUse-case 1 APIs:\n```ts\nprepare(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap' }): PreparedText // one-time text analysis + measurement pass, returns an opaque value to pass to `layout()`. Make sure `font` is synced with your css `font` declaration shorthand (e.g. size, weight, style, family) for the text you're measuring. `font` is the same format as what you'd use for `myCanvasContext.font = ...`, e.g. `16px Inter`.\nlayout(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.\n```\n\nUse-case 2 APIs:\n```ts\nprepareWithSegments(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap' }): PreparedTextWithSegments // same as `prepare()`, but returns a richer structure for manual line layouts needs\nlayoutWithLines(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\nwalkLineRanges(prepared: PreparedTextWithSegments, maxWidth: number, onLine: (line: LayoutLineRange) =\u003e 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.\nlayoutNextLine(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`.\ntype LayoutLine = {\n  text: string // Full text content of this line, e.g. 'hello world'\n  width: number // Measured width of this line, e.g. 87.5\n  start: LayoutCursor // Inclusive start cursor in prepared segments/graphemes\n  end: LayoutCursor // Exclusive end cursor in prepared segments/graphemes\n}\ntype LayoutLineRange = {\n  width: number // Measured width of this line, e.g. 87.5\n  start: LayoutCursor // Inclusive start cursor in prepared segments/graphemes\n  end: LayoutCursor // Exclusive end cursor in prepared segments/graphemes\n}\ntype LayoutCursor = {\n  segmentIndex: number // Segment index in prepareWithSegments' prepared rich segment stream\n  graphemeIndex: number // Grapheme index within that segment; `0` at segment boundaries\n}\n```\n\nOther helpers:\n```ts\nclearCache(): 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\nsetLocale(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)\n```\n\n## Caveats\n\nPretext doesn't try to be a full font rendering engine (yet?). It currently targets the common text setup:\n- `white-space: normal`\n- `word-break: normal`\n- `overflow-wrap: break-word`\n- `line-break: auto`\n- If you pass `{ whiteSpace: 'pre-wrap' }`, ordinary spaces, `\\t` tabs, and `\\n` hard breaks are preserved instead of collapsed. Tabs follow the default browser-style `tab-size: 8`. The other wrapping defaults stay the same: `word-break: normal`, `overflow-wrap: break-word`, and `line-break: auto`.\n- `system-ui` is unsafe for `layout()` accuracy on macOS. Use a named font.\n- Because the default target includes `overflow-wrap: break-word`, very narrow widths can still break inside words, but only at grapheme boundaries.\n\n## Develop\n\nSee [DEVELOPMENT.md](DEVELOPMENT.md) for the dev setup and commands.\n\n## Credits\n\nSebastian Markbage first planted the seed with [text-layout](https://github.com/chenglou/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.\n","funding_links":[],"categories":["Web Frontend","TypeScript","🔧 Utilities \u0026 Miscellaneous","Official Pretext Resources"],"sub_categories":["JS Libraries \u0026 Utilities"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchenglou%2Fpretext","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchenglou%2Fpretext","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchenglou%2Fpretext/lists"}