Pretext TypeScript chenglou/pretext

Getting Started with Your First Contribution to Pretext

Walk from clone to running tests in five stops, picking up the entry points and conventions a new contributor needs.

5 stops ~15 min Verified 2026-05-04
What you will learn
  • Where to clone, install, and run the demo pages with bun
  • Which package.json scripts gate every contribution (typecheck, lint, build, accuracy)
  • What the public API exports look like at the entry file
  • How tests document behavior using a deterministic fake canvas backend
  • Which long-form docs to read before opening a non-trivial PR
Prerequisites
  • Comfortable reading TypeScript
  • bun installed locally (https://bun.sh)
  • No prior knowledge of Pretext required
1 / 5

Install and Run the Demos

README.md:7

The README's install section names the published package and shows the exact two commands that take a fresh clone to a running demo page.

Before reading any source you want a working demo on screen, because Pretext is a layout library and reading the code without seeing the output is misleading. The README answers that in two commands: bun install followed by bun start, then visiting the local /demos path. The trailing-slash caveat is real; Bun's devserver returns wrong content if you add one. The hosted demos at chenglou.me/pretext work as a fallback when you cannot run bun locally, and the community demos at somnai-dreams.github.io/pretext-demos show what users have already built on top of the published package.

Key takeaway

Two commands take a fresh clone to a running demo page; everything else in the repo assumes you have seen Pretext render text.

## Installation

```sh
npm install @chenglou/pretext
```

## Demos

Clone the repo, run `bun install`, then `bun start`, and open the `/demos` in your browser (no trailing slash. Bun devserver bugs on those)
Alternatively, 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/)
2 / 5

The Scripts That Gate Every PR

package.json:39

The scripts block is the entry to every dev workflow: build, typecheck plus lint, and accuracy tooling that compares output against real browsers.

Two scripts do most of the work for a typical contributor. bun run check runs tsc followed by oxlint with type-aware mode on the src directory; if it fails, your PR will fail. bun run build:package emits dist/ via tsc -p tsconfig.build.json, which is the published artifact. The corpus scripts are heavier and matter when your change could shift wrapping output: they run text samples through real browsers and diff the result against checked-in JSON snapshots. You do not need them for a typo fix, but any change to line-break.ts, analysis.ts, or measurement.ts should be paired with a corpus run before review.

Key takeaway

Run bun run check on every change; run a corpus script when your change can affect wrapping decisions.

    "build:package": "rm -rf dist && tsc -p tsconfig.build.json",
    "check": "tsc && oxlint --type-aware src",
    "corpus-check": "bun run scripts/corpus-check.ts",
    "corpus-check:safari": "CORPUS_CHECK_BROWSER=safari bun run scripts/corpus-check.ts",
    "corpus-font-matrix": "bun run scripts/corpus-font-matrix.ts",
    "corpus-font-matrix:safari": "CORPUS_CHECK_BROWSER=safari bun run scripts/corpus-font-matrix.ts",
    "corpus-representative": "bun run scripts/corpus-representative.ts --output=corpora/representative.json",
    "corpus-status": "bun run scripts/corpus-status.ts",
    "corpus-status:refresh": "bun run scripts/corpus-representative.ts --output=corpora/representative.json && bun run scripts/corpus-sweep.ts --all --samples=9 --output=corpora/chrome-sampled.json && bun run scripts/corpus-sweep.ts --all --start=300 --end=900 --step=10 --output=corpora/chrome-step10.json && bun run scripts/corpus-status.ts",
    "corpus-sweep": "bun run scripts/corpus-sweep.ts",
    "corpus-sweep:safari": "CORPUS_CHECK_BROWSER=safari bun run scripts/corpus-sweep.ts",
3 / 5

The Public API Surface

src/layout.ts:472

Pretext exports two prepare functions and one layout function; together they encode the two-phase design directly in the type signatures.

If you are about to add an API, this is the file you are working in, so it pays to read the existing surface first. prepare() returns an opaque PreparedText handle that the public can pass to layout(); the internal shape is hidden behind a brand to keep callers from depending on the parallel-array layout. prepareWithSegments() is the rich variant used when the caller wants to render lines themselves (canvas, SVG, custom DOM). Both wrap a single prepareInternal() with a flag, so any change to preparation behavior lives in one place. The comment above layout() states the invariant explicitly: pure arithmetic, no canvas, no DOM, no allocations.

Key takeaway

Two prepare variants share one internal helper, and a layout function pinned to pure arithmetic completes the surface; new APIs slot into this shape.

export function prepare(text: string, font: string, options?: PrepareOptions): PreparedText {
  return prepareInternal(text, font, false, options) as PreparedText
}

// Rich variant used by callers that need enough information to render the
// laid-out lines themselves.
export function prepareWithSegments(text: string, font: string, options?: PrepareOptions): PreparedTextWithSegments {
  return prepareInternal(text, font, true, options) as PreparedTextWithSegments
}

function getInternalPrepared(prepared: PreparedText): InternalPreparedText {
  return prepared as InternalPreparedText
}

// Layout prepared text at a given max width and caller-provided lineHeight.
// Pure arithmetic on cached widths — no canvas calls, no DOM reads, no string
// operations, no allocations.
// ~0.0002ms per text block. Call on every resize.
4 / 5

How Tests Document Behavior

src/layout.test.ts:244

The first describe block shows the convention: short bun:test cases pinning one behavior using a deterministic fake canvas backend.

Most contribution-worthy test cases follow the shape on screen: a single test() that calls prepare() or prepareWithSegments() with one input, then asserts a structural property of the output (segments array, kinds array, line count, height). The header comment at the top of the file is explicit: keep the permanent suite small, durable, and run against the shipped exports with a deterministic fake canvas backend. Browser-specific investigations live in throwaway probes and the corpus scripts, not here. If you are adding a test alongside a fix, copy this shape; if your fix needs cross-browser evidence, that goes in scripts/ and shows up as updated JSON snapshots.

Key takeaway

Permanent tests pin one structural invariant per case; cross-browser evidence belongs in the corpus scripts, not here.

describe('prepare invariants', () => {
  test('whitespace-only input stays empty', () => {
    const prepared = prepare('  \t\n  ', FONT)
    expect(layout(prepared, 200, LINE_HEIGHT)).toEqual({ lineCount: 0, height: 0 })
  })

  test('collapses ordinary whitespace runs and trims the edges', () => {
    const prepared = prepareWithSegments('  Hello\t \n  World  ', FONT)
    expect(prepared.segments).toEqual(['Hello', ' ', 'World'])
  })

  test('pre-wrap mode keeps ordinary spaces instead of collapsing them', () => {
    const prepared = prepareWithSegments('  Hello   World  ', FONT, { whiteSpace: 'pre-wrap' })
    expect(prepared.segments).toEqual(['  ', 'Hello', '   ', 'World', '  '])
    expect(prepared.kinds).toEqual(['preserved-space', 'text', 'preserved-space', 'text', 'preserved-space'])
  })
5 / 5

Where to Read Next

DEVELOPMENT.md:49

The Current Sources Of Truth section in DEVELOPMENT.md points at every checked-in artifact a contributor should consult before opening a non-trivial PR.

This section is the contributor map. STATUS.md and status/dashboard.json answer the question of what is currently green and what is in flight. The accuracy and benchmark JSON files are checked into the repo, so a PR that changes wrapping output should also update them; reviewers will diff those numbers. corpora/dashboard.json and the sweep snapshots are how Pretext tracks long-form text behavior across widths. RESEARCH.md is the exploration log; if you find yourself adding a shim for a browser quirk, this is where the shim's evidence belongs. For a deeper read on the existing design, the architecture tour on this page walks through every source file in dependency order.

Key takeaway

STATUS plus accuracy JSONs answer what is green; RESEARCH.md is where empirical evidence for new shims belongs.

## Current Sources Of Truth

Use these for the current picture:
- [STATUS.md](STATUS.md) — prose pointers for the main status files
- [status/dashboard.json](status/dashboard.json) — machine-readable main status dashboard
- [accuracy/chrome.json](accuracy/chrome.json), [accuracy/safari.json](accuracy/safari.json), [accuracy/firefox.json](accuracy/firefox.json) — checked-in raw browser accuracy rows
- [benchmarks/chrome.json](benchmarks/chrome.json), [benchmarks/safari.json](benchmarks/safari.json) — checked-in benchmark snapshots
- [corpora/STATUS.md](corpora/STATUS.md) — prose pointers for long-form corpus status
- [corpora/dashboard.json](corpora/dashboard.json) — machine-readable long-form corpus dashboard
- [corpora/representative.json](corpora/representative.json) — machine-readable anchor subset
- [corpora/chrome-sampled.json](corpora/chrome-sampled.json), [corpora/chrome-step10.json](corpora/chrome-step10.json) — checked-in Chrome corpus sweep snapshots
- [RESEARCH.md](RESEARCH.md) — the exploration log and the durable conclusions behind the current model
Your codebase next

Create code tours for your project

Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.

Install Intraview Free