Skip to content

Huxpro/lynx-pretext

Repository files navigation

lynx-pretext

A port of chenglou/pretext text layout engine to the Lynx platform.

Installation

npm install lynx-pretext
# or
pnpm add lynx-pretext

Demos

See 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

What Works the Same

API Compatibility

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)

Architecture Consistency

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 / PreparedTextWithSegments
  • LayoutCursor / LayoutLine / LayoutResult

Code Reuse

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)

Differences

1. Measurement Backend (Key Difference)

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).width

Lynx 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.width

2. Platform Adaptation Layer

Lynx Pretext adds the following adaptation files:

  • intl-shim.ts (6 lines): Provides Intl global object polyfill for PrimJS
  • segmenter-polyfill.ts (102 lines): Lightweight Intl.Segmenter alternative (PrimJS's @formatjs/intl-segmenter crashes)
  • rspeedy-env.d.ts (12 lines): Lynx/Rspeedy environment TypeScript declarations

3. Feature Differences

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

4. Verification Strategy

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]

Project Structure

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

Known Issues

  • Rich-note demo: inline layout overflow at certain widths. The layoutInlineItems function (ported from the upstream rich-note demo) can produce lines that slightly exceed maxWidth when chips and code spans interact with near-zero remaining space. The root cause is that layoutNextLine uses Math.max(1, remainingWidth) which can inflate a near-zero budget, causing the first grapheme to overflow. Tracked upstream: huxpro/lynx-pretext#3

License

Same as the original Pretext project

About

Pretext for Lynxjs

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors