npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pre-markdown/layout

v0.2.1

Published

Pretext-based zero-DOM-reflow text layout engine

Readme

@pre-markdown/layout

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

npm version npm downloads TypeScript License: MIT


Overview

@pre-markdown/layout is the layout engine of PreMarkdown, built on @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 MeasurementPreparedText 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

npm install @pre-markdown/layout
pnpm add @pre-markdown/layout

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

Quick Start

Basic Layout

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

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:

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

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:

// 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:

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.

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

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

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

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.

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.

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

LineRenderer

Line number rendering engine.

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.

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:

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:

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 | AST types, visitors, events, plugins | | @pre-markdown/parser | Markdown → AST parser | | @pre-markdown/renderer | AST → HTML renderer | | @chenglou/pretext | Underlying text measurement engine |

License

MIT © 2024-2026 PreMarkdown Contributors