[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-873":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":25,"hasPages":25,"topics":26,"createdAt":10,"pushedAt":10,"updatedAt":27,"readmeContent":28,"aiSummary":29,"trendingCount":16,"starSnapshotCount":16,"syncStatus":30,"lastSyncTime":31,"discoverSource":32},873,"pretext","chenglou\u002Fpretext","chenglou","Fast, accurate & comprehensive text measurement & layout","http:\u002F\u002Fchenglou.me\u002Fpretext\u002F",null,"TypeScript",48349,2694,150,38,0,35,208,1659,177,45,"MIT License",false,"main",true,[],"2026-06-12 02:00:19","# Pretext\n\nPure JavaScript\u002FTypeScript 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.\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\u002Fpretext\n```\n\n## Demos\n\nClone the repo, run `bun install`, then `bun start`, and open `\u002Fdemos\u002Findex` in your browser. On Windows, use `bun run start:windows`.\nAlternatively, see them live at [chenglou.me\u002Fpretext](https:\u002F\u002Fchenglou.me\u002Fpretext\u002F). Some more at [somnai-dreams.github.io\u002Fpretext-demos](https:\u002F\u002Fsomnai-dreams.github.io\u002Fpretext-demos\u002F)\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\u002Fpretext'\n\nconst prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀‎', '16px Inter')\nconst { height, lineCount } = layout(prepared, 320, 20) \u002F\u002F pure arithmetic. No DOM layout & 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\nOther `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).\n\nThe returned height is the crucial last piece for unlocking web UIs:\n- proper virtualization\u002Focclusion without guesstimates & 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\u002Fpretext'\n\nconst prepared = prepareWithSegments('AGI 春天到了. بدأت الرحلة 🚀', '18px \"Helvetica Neue\"')\nconst { lines } = layoutWithLines(prepared, 320, 26) \u002F\u002F 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- `measureLineStats()` and `walkLineRanges()` give you line counts, widths and cursors without building the text strings:\n\n```ts\nimport { measureLineStats, walkLineRanges } from '@chenglou\u002Fpretext'\n\nconst { lineCount, maxLineWidth } = measureLineStats(prepared, 320)\nlet maxW = 0\nwalkLineRanges(prepared, 320, line => { if (line.width > maxW) maxW = line.width })\n\u002F\u002F 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- `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:\n\n```ts\nimport { layoutNextLineRange, materializeLineRange, prepareWithSegments, type LayoutCursor } from '@chenglou\u002Fpretext'\n\nconst prepared = prepareWithSegments(article, BODY_FONT)\nlet cursor: LayoutCursor = { segmentIndex: 0, graphemeIndex: 0 }\nlet y = 0\n\n\u002F\u002F 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 range = layoutNextLineRange(prepared, cursor, width)\n  if (range === null) break\n\n  const line = materializeLineRange(prepared, range)\n  ctx.fillText(line.text, 0, y)\n  cursor = range.end\n  y += 26\n}\n```\n\nThis usage allows rendering to canvas, SVG, WebGL and (eventually) server-side. See the `\u002Fdemos\u002Fdynamic-layout` demo for a richer example.\n\nFor hyphenation in manual layout, insert soft hyphens before `prepare()` \u002F `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.\n\nIf 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\u002Fpretext\u002Frich-inline`. It stays inline-only and `white-space: normal`-only on purpose:\n\n```ts\nimport { materializeRichInlineLineRange, prepareRichInline, walkRichInlineLineRanges } from '@chenglou\u002Fpretext\u002Frich-inline'\n\nconst prepared = prepareRichInline([\n  { text: 'Ship ', font: '500 17px Inter' },\n  { text: '@maya', font: '700 12px Inter', break: 'never', extraWidth: 22 },\n  { text: \"'s rich-note\", font: '500 17px Inter' },\n])\n\nwalkRichInlineLineRanges(prepared, 320, range => {\n  const line = materializeRichInlineLineRange(prepared, range)\n  \u002F\u002F each fragment keeps its source item index, text slice, gapBefore, and cursors\n})\n```\n\nIt is intentionally narrow:\n- raw inline text in, including boundary spaces\n- caller-owned `extraWidth` for pill chrome\n- `break: 'never'` for atomic items like chips and mentions\n- `white-space: normal` only\n- not a nested markup tree and not a general CSS inline formatting engine\n\n### API Glossary\n\nUse-case 1 APIs:\n```ts\nprepare(text: string, font: string, options?: { whiteSpace?: 'normal' | 'pre-wrap', wordBreak?: 'normal' | 'keep-all', letterSpacing?: number }): PreparedText \u002F\u002F 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.\nlayout(prepared: PreparedText, maxWidth: number, lineHeight: number): { height: number, lineCount: number } \u002F\u002F 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', wordBreak?: 'normal' | 'keep-all', letterSpacing?: number }): PreparedTextWithSegments \u002F\u002F same as `prepare()`, but returns a richer structure for manual line layout needs\nlayoutWithLines(prepared: PreparedTextWithSegments, maxWidth: number, lineHeight: number): { height: number, lineCount: number, lines: LayoutLine[] } \u002F\u002F 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) => void): number \u002F\u002F 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\u002Fend 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.\nmeasureLineStats(prepared: PreparedTextWithSegments, maxWidth: number): { lineCount: number, maxLineWidth: number } \u002F\u002F returns only how many lines this width produces, and how wide the widest one is. Avoids line\u002Fstring allocations.\nmeasureNaturalWidth(prepared: PreparedTextWithSegments): number \u002F\u002F returns the widest forced line when width itself is not the thing causing wraps\nlayoutNextLine(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLine | null \u002F\u002F 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`.\nlayoutNextLineRange(prepared: PreparedTextWithSegments, start: LayoutCursor, maxWidth: number): LayoutLineRange | null \u002F\u002F same as layoutNextLine(), but without allocating line text strings. Useful for variable-width manual layout, occlusion, and virtualization measurements.\nmaterializeLineRange(prepared: PreparedTextWithSegments, line: LayoutLineRange): LayoutLine \u002F\u002F turns a LayoutLineRange from layoutNextLineRange() or walkLineRanges() into a full line with text\ntype LineStats = {\n  lineCount: number \u002F\u002F Number of wrapped lines, e.g. 3\n  maxLineWidth: number \u002F\u002F Widest wrapped line, e.g. 192.5\n}\ntype LayoutLine = {\n  text: string \u002F\u002F Full text content of this line, e.g. 'hello world'\n  width: number \u002F\u002F Measured width of this line, e.g. 87.5\n  start: LayoutCursor \u002F\u002F Inclusive start cursor in prepared segments\u002Fgraphemes\n  end: LayoutCursor \u002F\u002F Exclusive end cursor in prepared segments\u002Fgraphemes\n}\ntype LayoutLineRange = {\n  width: number \u002F\u002F Measured width of this line, e.g. 87.5\n  start: LayoutCursor \u002F\u002F Inclusive start cursor in prepared segments\u002Fgraphemes\n  end: LayoutCursor \u002F\u002F Exclusive end cursor in prepared segments\u002Fgraphemes\n}\ntype LayoutCursor = {\n  segmentIndex: number \u002F\u002F Segment index in prepareWithSegments' prepared rich segment stream\n  graphemeIndex: number \u002F\u002F Grapheme index within that segment; `0` at segment boundaries\n}\n```\n\nHelper for rich-text inline flow:\n```ts\nprepareRichInline(items: RichInlineItem[]): PreparedRichInline \u002F\u002F compile raw inline items with their original text. The compiler owns cross-item collapsed whitespace and caches each item's natural width\nlayoutNextRichInlineLineRange(prepared: PreparedRichInline, maxWidth: number, start?: RichInlineCursor): RichInlineLineRange | null \u002F\u002F stream one line of rich-text inline flow at a time without building fragment text strings\nwalkRichInlineLineRanges(prepared: PreparedRichInline, maxWidth: number, onLine: (line: RichInlineLineRange) => void): number \u002F\u002F non-materializing line walker for rich-text inline flow shrinkwrap\u002Fstats work\nmaterializeRichInlineLineRange(prepared: PreparedRichInline, line: RichInlineLineRange): RichInlineLine \u002F\u002F turns one previously computed rich-inline line range back into full fragment text\nmeasureRichInlineStats(prepared: PreparedRichInline, maxWidth: number): { lineCount: number, maxLineWidth: number } \u002F\u002F returns only how many lines this width produces, and how wide the widest one is. Avoids fragment-text allocations.\ntype RichInlineItem = {\n  text: string \u002F\u002F raw author text, including leading\u002Ftrailing collapsible spaces\n  font: string \u002F\u002F canvas font shorthand for this item\n  letterSpacing?: number \u002F\u002F extra horizontal spacing between graphemes, in CSS px\n  break?: 'normal' | 'never' \u002F\u002F `never` keeps the item atomic, like a chip\n  extraWidth?: number \u002F\u002F caller-owned horizontal chrome, e.g. padding + border width\n}\ntype RichInlineCursor = {\n  itemIndex: number \u002F\u002F Which source RichInlineItem this cursor is currently in\n  segmentIndex: number \u002F\u002F Segment index within that item's prepared text\n  graphemeIndex: number \u002F\u002F Grapheme index within that segment; `0` at segment boundaries\n}\ntype RichInlineFragment = {\n  itemIndex: number \u002F\u002F index back into the original RichInlineItem array\n  text: string \u002F\u002F Text slice for this fragment\n  gapBefore: number \u002F\u002F collapsed boundary gap paid before this fragment on this line\n  occupiedWidth: number \u002F\u002F text width plus extraWidth\n  start: LayoutCursor \u002F\u002F Start cursor within the item's prepared text\n  end: LayoutCursor \u002F\u002F End cursor within the item's prepared text\n}\ntype RichInlineLine = {\n  fragments: RichInlineFragment[] \u002F\u002F Materialized fragments on this line\n  width: number \u002F\u002F Measured width of this line, including gapBefore\u002FextraWidth\n  end: RichInlineCursor \u002F\u002F Exclusive end cursor for continuing the next line\n}\ntype RichInlineFragmentRange = {\n  itemIndex: number \u002F\u002F index back into the original RichInlineItem array\n  gapBefore: number \u002F\u002F collapsed boundary gap paid before this fragment on this line\n  occupiedWidth: number \u002F\u002F text width plus extraWidth\n  start: LayoutCursor \u002F\u002F Start cursor within the item's prepared text\n  end: LayoutCursor \u002F\u002F End cursor within the item's prepared text\n}\ntype RichInlineLineRange = {\n  fragments: RichInlineFragmentRange[] \u002F\u002F Non-materialized fragment ownership\u002Franges on this line\n  width: number \u002F\u002F Measured width of this line, including gapBefore\u002FextraWidth\n  end: RichInlineCursor \u002F\u002F Exclusive end cursor for continuing the next line\n}\ntype RichInlineStats = {\n  lineCount: number \u002F\u002F Number of wrapped lines, e.g. 3\n  maxLineWidth: number \u002F\u002F Widest wrapped line, e.g. 192.5\n}\n```\n\nOther helpers:\n```ts\nclearCache(): void \u002F\u002F 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 \u002F\u002F 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\nNotes:\n- `PreparedText` is the opaque fast-path handle. `PreparedTextWithSegments` is the richer manual-layout handle.\n- `LayoutCursor` is a segment\u002Fgrapheme cursor, not a raw string offset.\n- `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.\n- The richer handle also includes `segLevels` for custom bidi-aware rendering. The line-breaking APIs do not read it.\n- Segment widths are browser-canvas widths for line breaking, not exact glyph-position data for custom Arabic or mixed-direction x-coordinate reconstruction.\n- If a soft hyphen wins the break, materialized line text includes the visible trailing `-`.\n- `measureNaturalWidth()` returns the widest forced line. Hard breaks still count.\n- `prepare()` and `prepareWithSegments()` do horizontal-only work. `lineHeight` stays a layout-time input.\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` and `pre-wrap`\n- `word-break: normal` and `keep-all`\n- `overflow-wrap: break-word`. Very narrow widths can still break inside words, but only at grapheme boundaries.\n- `line-break: auto`\n- `letter-spacing` as a numeric pixel value passed to `prepare()` \u002F `prepareWithSegments()`\n- Tabs follow the default browser-style `tab-size: 8`\n- `{ wordBreak: 'keep-all' }` is supported too. It behaves like you'd expect for CJK\u002FHangul and no-space mixed Latin\u002Fnumeric\u002FCJK text, while keeping the same `overflow-wrap: break-word` fallback for overlong runs.\n- `system-ui` is unsafe for `layout()` accuracy on macOS. Use a named font.\n- Runtime requires `Intl.Segmenter` and Canvas 2D text measurement. Browsers or runtimes without `Intl.Segmenter` are currently unsupported.\n- 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.\n\n## Develop\n\nSee [DEVELOPMENT.md](https:\u002F\u002Fgithub.com\u002Fchenglou\u002Fpretext\u002Fblob\u002Fmain\u002FDEVELOPMENT.md) for the dev setup and commands.\n\n## Credits\n\nSebastian Markbage first planted the seed with [text-layout](https:\u002F\u002Fgithub.com\u002Fchenglou\u002Ftext-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","Pretext 是一个用于多行文本测量与布局的纯 JavaScript\u002FTypeScript 库。它提供了快速、准确且全面的文本测量功能，支持多种语言，并允许渲染到 DOM、Canvas 和 SVG，未来还将支持服务器端渲染。Pretext 通过实现自己的文本测量逻辑，避免了依赖 DOM 测量（如 `getBoundingClientRect`）导致的布局重排，从而显著提升了性能。该库特别适用于需要精确控制文本布局的应用场景，例如虚拟化列表、自定义布局算法以及防止文本加载时布局偏移等。此外，Pretext 还可以在开发阶段帮助验证 UI 元素（如按钮标签）不会因文本过长而换行，从而提高用户体验。",2,"2026-06-11 02:39:58","top_all"]