Install
$ agentstack add skill-yaniv-golan-pretext-skill-pretext ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Pretext Integration Guide
You are helping a developer use @chenglou/pretext — a small, tree-shakable TypeScript library by Cheng Lou that computes exact text metrics using pure math (no DOM reflows). It uses CanvasRenderingContext2D.measureText internally, segments text, measures once, caches, then does arithmetic for all subsequent layouts. The package ships proper ESM with subpath exports (. for core, ./rich-inline for inline rich text) and is sideEffects: false — modern bundlers tree-shake unused entrypoints.
When Pretext Is the Right Tool
Use Pretext when the developer needs to:
- Know text dimensions before rendering — virtual scrolling, masonry layouts, card height estimation
- Auto-fit text to a container — find the largest font size that keeps text within N lines (CSS has no equivalent)
- Flow text around obstacles — magazine-style layouts where text wraps around shapes, images, or interactive elements
- Measure text in canvas/SVG/WebGL — Pretext's measurements are exact for
fillText - Render rich inline text — mentions, chips, code spans with browser-like boundary whitespace collapse, via the
@chenglou/pretext/rich-inlinesubpath - Measure many text items fast — each
layout()call is ~0.0002ms after the firstprepare()per font
When NOT to Use Pretext
- CSS float/flex already handles it — don't reimplement text flow that CSS does natively
- Content is HTML, not plain text — Pretext measures plain text strings. Tables, code blocks, nested elements need DOM measurement
- TanStack Virtual + Pretext height estimation — this integration is fragile. Height errors compound over many items, and
measureElementcorrection loops cause desyncing. Foror SVG` and let CSS do the wrapping, the CSS may break to 3 lines OR to 4 — the rules differ on hyphenation, soft-hyphens, kerning thresholds, and fallback-font width.
Either:
- Render per pretext's break points. Use
prepareWithSegments+layoutWithLines(orlayoutNextLine) to get the actual line strings, render each line as its own element (`,,` per line). Pixel-perfect match. See [Rendering Per-Line in SVG](references/patterns.md#rendering-per-line-in-svg). - Don't use pretext for wrap. Let CSS handle measurement AND rendering (use
getBoundingClientRectafter the DOM lays out).
Mixing measurement modes is the silent-overflow bug. Symptom: a balloon/box sized correctly for 3 lines actually renders 4, last line sticks out the bottom.
Gotchas #11 and #12 share a theme with the streaming-chat anti-pattern: anything you do AFTER pretext returns its result invalidates the result. Keep the measured input and the rendered output identical.
Which API Do I Need?
Start here. Match the developer's goal to the right API path — this avoids the most common mistake (using prepare when prepareWithSegments is needed, or vice versa).
Decision tree: Need per-line text content (the actual strings)? → Manual / rich layout rows. Need only geometry (height, widths, line counts)? → Core layout rows. Need mentions/chips/code spans inline? → Rich Inline row.
| Developer wants to... | prepare variant | layout function | |---|---|---| | Core layout | | | | Get text height/line count at a given width | prepare | layout | | Auto-fit font size (binary search over sizes) | prepare (in a loop) | layout | | Auto-height a textarea | prepare with { whiteSpace: 'pre-wrap' } | layout | | ─────────────── | | | | Manual / rich layout | | | | Get per-line text content (render, animate) | prepareWithSegments | layoutWithLines | | Find widest line (shrink-wrap containers) | prepareWithSegments | measureLineStats or walkLineRanges | | Get line count + widest in one call, no strings | prepareWithSegments | measureLineStats | | Flow text around obstacles (variable width/line) | prepareWithSegments | layoutNextLine in a loop | | Variable widths, no string allocation (virtualization) | prepareWithSegments | layoutNextLineRange + lazy materializeLineRange | | Get intrinsic/widest forced line (hard breaks only) | prepareWithSegments | measureNaturalWidth | | Render mentions, chips, code spans inline | prepareRichInline (from /rich-inline) | walkRichInlineLineRanges / layoutNextRichInlineLineRange |
The key decision is prepare vs prepareWithSegments vs prepareRichInline:
prepare→ only gives youlayout()(height + line count). Fastest path.prepareWithSegments→ gives you ALL core layout functions includinglayout(). Use this the moment you need per-line data, variable widths, or geometry without strings. There is no reason to call both for the same text.prepareRichInline(from@chenglou/pretext/rich-inline) → its own prepare path for arrays of mixed-font items. Not interchangeable with the core handles.
prepare and prepareWithSegments both accept { whiteSpace, wordBreak, letterSpacing } as a third argument. wordBreak: 'keep-all' is the CSS-equivalent for CJK/Hangul. letterSpacing is a CSS pixel number (see gotcha #9). prepareRichInline is different — it takes only the items array; per-item letterSpacing and break live inside each item.
Common API selection mistakes:
- Using
preparethen callinglayoutWithLines→ crashes at runtime (no.segments) - Using
layoutWithLineswhen only widths are needed →walkLineRangesormeasureLineStatsis cheaper (no string allocation) - Using
layoutWithLineswhen width varies per line → must uselayoutNextLine(orlayoutNextLineRange) instead - Using
measureNaturalWidthfor shrink-wrap of soft-wrapping text → it returns the widest forced line (hard breaks only); usemeasureLineStats/walkLineRangesfor soft-wrap shrink-wrap - Re-calling
prepareon container resize → just calllayoutagain with the new width (it's pure arithmetic)
For full signatures, types, and examples, see the [API Reference](references/api.md). For rich inline specifically, jump to [Rich Inline](references/api.md#rich-inline).
Integration Patterns
For detailed code examples of each pattern, see the [Patterns Reference](references/patterns.md). Here's when to reach for each:
Wrapper Module (Recommended First Step)
Create a thin wrapper that converts lineHeight from CSS multiplier to pixels and returns null on failure. This prevents the critical lineHeight bug and enables progressive enhancement. An [extended variant](references/patterns.md#wrapper-module) supports wordBreak/letterSpacing for CJK or spaced-out type.
Auto-Fit Font Size
Binary search for the largest font that keeps text within N lines. This is Pretext's killer feature — CSS has no equivalent. Use for hero headlines, card titles, quote displays. Shares its prepare-once-layout-many shape with all [iterative width searches](references/patterns.md#iterative-width-search) — re-preparing inside the loop is the most common performance bug.
Height Estimation for Card Layouts
Measure variable text parts with Pretext, add fixed parts (padding, border, gaps) manually. Good for simple cards. When you only need lineCount or widest-line numbers (not actual height in px), measureLineStats is cheaper than layoutWithLines. Remember: this is inherently approximate — don't use for pixel-perfect virtualization.
Text Around Obstacles (layoutNextLine / layoutNextLineRange)
The creative powerhouse. Feed a different maxWidth per line based on obstacle position. This enables magazine layouts, text flowing around images, and all the impressive community demos. For virtualization or hit-testing where you don't need the rendered text yet, layoutNextLineRange + lazy materializeLineRange avoids the per-line string allocation.
Rich Inline Text (Mentions, Chips, Code Spans)
Use the @chenglou/pretext/rich-inline subpath when items have mixed fonts AND some items must stay atomic (chip pills with rounded chrome that can't break mid-name). Pretext handles browser-like boundary whitespace collapse so your @maya mention sits flush against surrounding text the way CSS inline rendering would. See the [Rich Inline pattern](references/patterns.md#rich-inline-text).
Shrink-Wrap Without String Allocation
For chat bubbles, balloon containers, or any "tightest box around this text" UI, use measureLineStats (single call returning { lineCount, maxLineWidth }) instead of running layoutWithLines and inspecting strings. Pretext skips the string-construction step entirely.
Rendering Per-Line in SVG
When the surface is SVG (speech balloons, badges, posters) and you want pixel-perfect alignment between what was measured and what's drawn, render each line as its own ` with explicit x reset and a dy shift — don't rely on ` + CSS wrap (see gotcha #12). See the [SVG per-line pattern](references/patterns.md#rendering-per-line-in-svg).
Balanced / Justified Layout (Knuth-Plass)
The upstream package ships a justification-comparison demo showing Knuth-Plass-style paragraph layout next to greedy hyphenation and native CSS justification. Use the same line-walking primitives (walkLineRanges / layoutNextLineRange) plus a width-balancing pass for paragraphs that need even raggedness or proper justification. See the [Balanced layout pattern](references/patterns.md#balanced-layout).
Progressive Enhancement
Always load Pretext as enhancement — the page should work without it. Use type="module" as a natural feature gate.
Vendoring (No Build Step)
Modern bundlers handle Pretext natively — proper ESM exports, sideEffects: false, subpath exports. Reach for the [vendoring recipe](references/patterns.md#vendoring-without-a-bundler) only when you can't run a bundler at all.
Creative Demos & Advanced Patterns
The npm package now ships its own demos under pages/demos/ — clone the repo and run bun start to play with them locally. They are the canonical reference for the patterns below, and they're versioned with the package so they always reflect the current API:
accordion— Pretext-predicted heights for smooth expand/collapse with no layout thrashingbubbles— multiline chat bubbles that shrink-wrap to their longest linemasonry— virtualized masonry using exact pre-measured heightsdynamic-layout— variable-width manual layout (live obstacle routing)editorial-engine— accessible magazine-style multi-column spread with obstacle-aware title routingjustification-comparison— Knuth-Plass paragraph layout side-by-side with greedy hyphenation and native CSS justificationmarkdown-chat— virtualized chat with rich-inline + Markdown parsing (canonical streaming-chat reference)rich-note— rich inline notes with mentions and chipsvariable-typographic-ascii— particle-driven ASCII art using proportional glyph measurements
The community showcase has 70+ more creative uses (fluid simulations, 3D text, games, dragon-shaped reflows). All use the same core API — the difference is how creatively you compute widths and route lines.
See the [Patterns Reference](references/patterns.md) for code-level walkthroughs of the recurring idioms.
Performance Notes
The two functions have very different cost profiles. Confusing them is the root cause of the streaming-chat anti-pattern.
prepare()is O(N) in the input text length. Each call re-runs the Unicode segmenter and walks every segment. First call per font also measures character widths (~1–5ms). Subsequent calls with the same font reuse the per-font segment-width cache, but the segmentation work still scales with the text. Don't re-prepare the same growing text every frame.- Factor
prepare()OUT of any iterative-width search. Patterns like balanced shrink-wrap, auto-fit, and balanced-paragraph layout binary-search the maxWidth (or font size, or target line count). Each iteration must calllayout()/measureLineStats/walkLineRangeson an already-prepared handle — NOT callprepare()again. Re-preparing inside the loop multiplies cost by the iteration count and produces visible UI hangs on long text. See [Iterative Width Search](references/patterns.md#iterative-width-search) for the correct shape. layout()is ~0.0002ms. Pure arithmetic over a prepared handle. This is what's safe to call inrequestAnimationFrame, scroll handlers, and workers — call it on every resize tick, notprepare().- Typical batch cost: 500 different texts in the same font ≈ 19ms total prepare, 0.09ms per layout.
- For streaming AI chat (text that grows token-by-token), see the tiered guidance at [Streaming AI Chat](references/patterns.md#streaming-ai-chat) — re-preparing every token is O(N²) across the stream and burns CPU on long messages.
Key Resources
- GitHub: https://github.com/chenglou/pretext
- Official demos: https://chenglou.me/pretext/ — also ship inside the npm package under
pages/demos/(clone +bun start) - Community showcase (70+ projects): https://pretextwall.xyz/
- Curated collection: https://www.pretext.cool/
- Additional community demos: https://somnai-dreams.github.io/pretext-demos/
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: yaniv-golan
- Source: yaniv-golan/pretext-skill
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.