A port of chenglou/pretext text layout engine to the Lynx platform.
npm install lynx-pretext
# or
pnpm add lynx-pretextSee live examples at lynx-pretext.vercel.app
Includes:
- ASCII Art: Wireframe torus, particles (pure MTS animation)
- Dynamic Layout: Logo rotation + text reflow (3 architectures: BTS/MTS/Hybrid)
- Editorial: Magazine layout with draggable orbs and text exclusion
- Basic: API usage examples and accuracy validation
Main APIs remain consistent between the original Pretext and Lynx Pretext:
// Preparation phase (same in both projects)
const prepared = prepare(text, font, options)
const preparedWithSegments = prepareWithSegments(text, font, options)
// Layout phase (same in both projects)
const result = layout(prepared, maxWidth, lineHeight)
const { lines } = layoutWithLines(preparedWithSegments, maxWidth, lineHeight)
// Line-by-line layout (same in both projects)
const line = layoutNextLine(preparedWithSegments, cursor, maxWidth)Both projects share the same two-phase architecture:
┌─────────────────┐ ┌─────────────────┐
│ Prepare Phase │ ──▶ │ Layout Phase │
│ (text appears) │ │ (on resize) │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
- Text analysis/segmentation Pure arithmetic
- Measure each segment Walk cached widths
- Cache results Compute lines & height
Core type definitions remain consistent:
SegmentBreakKind:'text' | 'space' | 'preserved-space' | 'tab' | 'glue' | 'zero-width-break' | 'soft-hyphen' | 'hard-break'PreparedText/PreparedTextWithSegmentsLayoutCursor/LayoutLine/LayoutResult
| File | Original Pretext | Lynx Pretext | Reuse Rate |
|---|---|---|---|
analysis.ts |
1,008 lines | ~1,016 lines | ~95% |
line-break.ts |
~1,056 lines | ~1,056 lines | ~98% |
layout.ts |
718 lines | 621 lines | ~85% |
measurement.ts |
232 lines | 149 lines | ~60% (main adaptation point) |
Overall Reuse Rate: Approximately 85-90% of core logic is directly reused, with differences concentrated in the platform adaptation layer.
- Analysis Layer: ~95% reuse, mainly import path adjustments
- Line Breaking Layer: ~98% reuse, almost direct port
- Layout Layer: ~85% reuse, removed browser-specific features
- Measurement Layer: ~60% reuse, main adaptation point (Canvas → Lynx API)
| Feature | Original Pretext (Browser) | Lynx Pretext |
|---|---|---|
| Measurement API | Canvas measureText() |
lynx.getTextInfo() |
| Runtime Environment | Browser main thread/Worker | Lynx main thread |
| Font Configuration | ctx.font = font |
Via fontSize/fontFamily parameters |
| Emoji Correction | Auto-detect and correct Canvas/DOM differences | Not supported (MVP version) |
Original Pretext Measurement:
// Browser version uses Canvas
const ctx = getMeasureContext() // OffscreenCanvas or DOM Canvas
ctx.font = font
const width = ctx.measureText(segment).widthLynx Pretext Measurement:
// Lynx version uses main thread API
const info = { fontSize: currentFontSizeStr }
if (currentFontFamily) info.fontFamily = currentFontFamily
const result = lynx.getTextInfo(segment, info)
const width = result.widthLynx Pretext adds the following adaptation files:
intl-shim.ts(6 lines): ProvidesIntlglobal object polyfill for PrimJSsegmenter-polyfill.ts(102 lines): LightweightIntl.Segmenteralternative (PrimJS's@formatjs/intl-segmentercrashes)rspeedy-env.d.ts(12 lines): Lynx/Rspeedy environment TypeScript declarations
| Feature | Original Pretext | Lynx Pretext |
|---|---|---|
| Bidirectional Text (Bidi) | Full support | Returns null in MVP (stub) |
| Emoji Width Correction | Supported (Canvas vs DOM differences) | Not supported (returns 0) |
| Browser Engine Config | Safari/Chromium differentiated handling | Fixed default values |
| System Font Detection | Supports system-ui, etc. |
Depends on Lynx underlying implementation |
Using Lynx native getTextInfo with maxWidth mode as the verification oracle:
// Native oracle
const native = lynx.getTextInfo(text, { fontSize, fontFamily, maxWidth })
// native.content = ['line 1 text', 'line 2 text', ...]
// Our implementation
const { lines } = layoutWithLines(prepared, maxWidthPx, lineHeight)
// lines[i].text should match native.content[i]lynx-pretext/
├── src/ # Core library
│ ├── analysis.ts # Text analysis/segmentation (~95% reused)
│ ├── line-break.ts # Line breaking algorithm (~98% reused)
│ ├── layout.ts # Layout API (~85% reused)
│ ├── measurement.ts # Measurement layer (Lynx adaptation)
│ ├── intl-shim.ts # PrimJS Intl polyfill
│ └── segmenter-polyfill.ts # Intl.Segmenter alternative
│
├── packages/ # Monorepo packages
│ └── devtools/ # @lynx-pretext/devtools (DevPanel component)
│
├── examples/ # Example projects
│ ├── basic/ # Basic API usage demos
│ ├── ascii-arts/ # ASCII art rendering (torus, particles)
│ ├── bubble/ # Bubble text layout
│ ├── dance/ # Dance sprite animation with text exclusion
│ ├── dynamic-layout/ # Dynamic layout with 3 architectures (BTS/MTS/Hybrid)
│ └── editorial/ # Editorial layout with draggable orbs
│
├── docs/ # Documentation
│ ├── blog.md # Project overview and journey
│ └── learning/ # Learning notes and migration guides
│ ├── mts-bts-architecture-patterns.md
│ ├── ascii-art-rendering.md
│ ├── bts-mts-compatible-components.md
│ └── ...
│
├── website/ # Project website
└── scripts/ # Build and utility scripts
- Rich-note demo: inline layout overflow at certain widths. The
layoutInlineItemsfunction (ported from the upstreamrich-notedemo) can produce lines that slightly exceedmaxWidthwhen chips and code spans interact with near-zero remaining space. The root cause is thatlayoutNextLineusesMath.max(1, remainingWidth)which can inflate a near-zero budget, causing the first grapheme to overflow. Tracked upstream: huxpro/lynx-pretext#3
Same as the original Pretext project