npm package: @pre-markdown/layout
Description: Pretext-based zero-DOM-reflow text layout engine
Author: tickli
Latest version: 0.2.1

# @pre-markdown/layout

<div align="center">

**Zero-DOM-reflow text layout engine** — Pretext-powered measurement, virtual scrolling, cursor positioning, and line rendering.

[![npm version](https://img.shields.io/npm/v/@pre-markdown/layout)](https://www.npmjs.com/package/@pre-markdown/layout)
[![npm downloads](https://img.shields.io/npm/dm/@pre-markdown/layout)](https://www.npmjs.com/package/@pre-markdown/layout)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.5+-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

</div>

---

## Overview

`@pre-markdown/layout` is the layout engine of [PreMarkdown](https://github.com/Leo555/pre-markdown), built on [@chenglou/pretext](https://github.com/chenglou/pretext). It provides **pixel-accurate text measurement without triggering browser DOM reflows**, enabling:

- **Zero-Reflow Layout** — Uses pretext's `prepare()` + `layout()` two-phase pipeline: no DOM queries, no forced layouts
- **LRU-Cached Measurement** — `PreparedText` results are cached; subsequent layouts are pure arithmetic (~0.0002ms)
- **Virtual Scrolling** — Only render visible lines in the viewport with configurable buffer zones
- **Incremental Document Layout** — Reuse cached heights for unchanged paragraphs on edits
- **Cursor Positioning** — Pretext-based cursor/caret positioning engine
- **Line Number Rendering** — Configurable line number renderer
- **Pluggable Backend** — Swap the measurement backend for testing or Web Worker offloading

## Installation

```bash
npm install @pre-markdown/layout
```

```bash
pnpm add @pre-markdown/layout
```

> **Note**: `@pre-markdown/core` and `@chenglou/pretext` are dependencies and will be installed automatically.

## Quick Start

### Basic Layout

```typescript
import { LayoutEngine } from '@pre-markdown/layout'

const engine = new LayoutEngine({
  font: '16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
  lineHeight: 24,
  maxWidth: 800,
})

const { height, lineCount } = engine.computeLayout('Hello world, this is a long paragraph...')
console.log(`Height: ${height}px, Lines: ${lineCount}`)
```

### Layout with Line Details

```typescript
const result = engine.computeLayoutWithLines('Hello world...')
for (const line of result.lines!) {
  console.log(`Line: "${line.text}" at y=${line.y}, width=${line.width}`)
}
```

### Viewport-only Layout (Virtual Scrolling)

Only compute lines visible in the current viewport:

```typescript
const viewport = engine.computeViewportLayout(text, scrollTop, viewportHeight)
console.log(`Rendering lines ${viewport.startIndex}–${viewport.endIndex}`)
console.log(`Total document height: ${viewport.totalHeight}px`)

for (const line of viewport.visibleLines) {
  renderLine(line.text, line.y)
}
```

### Multi-paragraph Document Layout

```typescript
const paragraphs = ['First paragraph...', 'Second paragraph...', 'Third...']
const { totalHeight, paragraphOffsets, paragraphHeights } = engine.computeDocumentLayout(paragraphs)

console.log(`Total height: ${totalHeight}px`)
paragraphOffsets.forEach((y, i) => {
  console.log(`Paragraph ${i} at y=${y}, height=${paragraphHeights[i]}`)
})
```

### Incremental Document Layout (Real-time Editing)

Only recompute layout for paragraphs that changed:

```typescript
// Initial layout
let result = engine.updateDocumentLayout(paragraphs)

// After editing paragraph 2
paragraphs[2] = 'Updated content...'
result = engine.updateDocumentLayout(paragraphs)
console.log(`Changed paragraphs:`, result.changedIndices) // [2]
```

### Hit Testing

Find which paragraph and line is at a scroll position:

```typescript
const hit = engine.hitTest(paragraphs, scrollTop)
if (hit) {
  console.log(`At paragraph ${hit.paragraphIndex}, line ${hit.lineIndex}`)
}
```

## API Reference

### `LayoutEngine`

The main class for text measurement and layout.

```typescript
class LayoutEngine {
  constructor(config: LayoutConfig, backend?: MeasurementBackend)

  // Configuration
  updateConfig(config: Partial<LayoutConfig>): void
  getConfig(): Readonly<LayoutConfig>
  setBackend(backend: MeasurementBackend): void
  setLocale(locale?: string): void

  // Core Layout
  computeLayout(text: string): LayoutResult
  computeCodeLayout(text: string): LayoutResult
  computeLayoutWithLines(text: string): LayoutResult
  computeViewportLayout(text: string, scrollTop: number, viewportHeight: number): ViewportLayoutResult

  // Multi-paragraph Layout
  computeDocumentLayout(paragraphs: string[]): { totalHeight, paragraphOffsets, paragraphHeights }
  hitTest(paragraphs: string[], scrollTop: number): { paragraphIndex, lineIndex } | null

  // Incremental Layout
  updateDocumentLayout(paragraphs: string[]): { totalHeight, paragraphOffsets, paragraphHeights, changedIndices }
  getCachedTotalHeight(): number

  // Cache Management
  invalidateCache(text?: string): void
  clearAllCaches(): void
  getCacheStats(): { preparedSize: number; segmentSize: number }
}
```

### `LayoutConfig`

```typescript
interface LayoutConfig {
  /** CSS font string (e.g., '16px Inter'). Must be loaded before use. */
  font: string
  /** Line height in pixels (must match CSS line-height) */
  lineHeight: number
  /** Maximum width for text wrapping (pixels) */
  maxWidth: number
  /** White-space mode: 'normal' (default) or 'pre-wrap' */
  whiteSpace?: 'normal' | 'pre-wrap'
  /** Viewport buffer multiplier (default 2 = 2x viewport above & below) */
  viewportBuffer?: number
  /** Font for code blocks (defaults to main font) */
  codeFont?: string
  /** Line height for code blocks (defaults to main lineHeight) */
  codeLineHeight?: number
}
```

### `LayoutResult`

```typescript
interface LayoutResult {
  height: number            // Total height of all lines (px)
  lineCount: number         // Number of visual lines
  lines?: LayoutLine[]      // Per-line info (when requested)
}

interface LayoutLine {
  text: string              // Line text content
  width: number             // Measured width (px)
  y: number                 // Y position from top (px)
  sourceIndex: number       // Source line index
}
```

### `ViewportLayoutResult`

```typescript
interface ViewportLayoutResult {
  visibleLines: LayoutLine[]  // Lines in the viewport
  totalHeight: number         // Full document height
  startY: number              // Y offset of first visible line
  startIndex: number          // Index of first visible line
  endIndex: number            // Index of last visible line (exclusive)
}
```

### `VirtualList`

Dynamic-height virtual scrolling list for rendering large documents.

```typescript
import { VirtualList } from '@pre-markdown/layout'
import type { VirtualListConfig, VirtualListItem, ViewportRange } from '@pre-markdown/layout'
```

### `CursorEngine`

Pretext-based cursor/caret positioning engine for accurate cursor placement without DOM measurement.

```typescript
import { CursorEngine } from '@pre-markdown/layout'
import type { Point, Rect, CursorPosition, VisualLineInfo, LineNumberInfo } from '@pre-markdown/layout'
```

### `LineRenderer`

Line number rendering engine.

```typescript
import { LineRenderer } from '@pre-markdown/layout'
import type { LineRendererConfig, RenderedLineNumber } from '@pre-markdown/layout'
```

### `MeasurementBackend`

Pluggable interface for text measurement — swap implementations for different environments.

```typescript
interface MeasurementBackend {
  prepare(text: string, font: string, options?): PreparedText
  prepareWithSegments(text: string, font: string, options?): PreparedTextWithSegments
  layout(prepared: PreparedText, maxWidth: number, lineHeight: number): PretextLayoutResult
  layoutWithLines(prepared: PreparedTextWithSegments, maxWidth: number, lineHeight: number): PretextLinesResult
  clearCache(): void
  setLocale(locale?: string): void
}
```

### `createFallbackBackend(avgCharWidth?)`

Creates a measurement backend that uses character-count heuristics instead of Canvas. Useful for Node.js / testing environments:

```typescript
import { LayoutEngine, createFallbackBackend } from '@pre-markdown/layout'

const engine = new LayoutEngine(config, createFallbackBackend(8))
```

### `createWorkerBackend`

Offload text measurement to a Web Worker for non-blocking layout computation:

```typescript
import { LayoutEngine, createWorkerBackend } from '@pre-markdown/layout'

const worker = createWorkerBackend()
const engine = new LayoutEngine(config, worker)
```

## Architecture: Two-Phase Pipeline

The layout engine uses pretext's two-phase pipeline for maximum performance:

```
Text → prepare() → PreparedText → layout() → LayoutResult
       ~1-5ms        (cached)      ~0.0002ms
       (LRU)                       (pure math)
```

1. **`prepare()`** — One-time text analysis: font metrics, grapheme segmentation, word boundaries. Results are cached in an LRU cache (512 entries).
2. **`layout()`** — Pure arithmetic: line breaking, height calculation. No DOM access, safe to call in animation frames.

This separation means:
- First layout of a paragraph: ~1-5ms (font measurement)
- Subsequent layouts (e.g., window resize): **~0.0002ms** (cache hit)
- Animation-frame safe layout: ✅

## Performance

| Operation | Time |
|-----------|------|
| First layout (cache miss) | ~1-5ms |
| Cached layout (cache hit) | ~0.0002ms |
| Document layout (100 paragraphs) | < 10ms |
| Incremental update (1 paragraph) | < 1ms |
| Viewport layout (visible only) | < 0.5ms |

## Module Format

| Format | Entry |
|--------|-------|
| ESM | `dist/index.js` |
| CJS | `dist/index.cjs` |
| Types | `dist/index.d.ts` |

## Related Packages

| Package | Description |
|---------|-------------|
| [@pre-markdown/core](https://www.npmjs.com/package/@pre-markdown/core) | AST types, visitors, events, plugins |
| [@pre-markdown/parser](https://www.npmjs.com/package/@pre-markdown/parser) | Markdown → AST parser |
| [@pre-markdown/renderer](https://www.npmjs.com/package/@pre-markdown/renderer) | AST → HTML renderer |
| [@chenglou/pretext](https://github.com/chenglou/pretext) | Underlying text measurement engine |

## License

[MIT](https://github.com/Leo555/pre-markdown/blob/master/LICENSE) © 2024-2026 PreMarkdown Contributors