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

pretext-markdown

v0.2.2

Published

High-performance Canvas-virtualized markdown preview for large documents

Readme

pretext-markdown

High-performance Canvas-based Markdown preview for React, based on pretext. Parses Markdown into discrete sections, caches each section's layout independently, and renders only what is visible — so even large documents open instantly and scroll smoothly.

How it works

| Layer | Technology | |---|---| | Text layout engine | @chenglou/pretext Pure JavaScript/TypeScript library for multiline text measurement | | Parsing | marked Lexer — tokenizes to MarkdownBlock[] | | Rendering | Canvas 2D API — only visible sections are drawn each frame | | Code highlighting | Shiki with JavaScript regex engine (no WASM) |

Installation

npm install pretext-markdown

Usage

import { useRef } from 'react'
import { PretextMarkdown } from 'pretext-markdown'
import type { PretextMarkdownHandle } from 'pretext-markdown'

export function Preview({ content }: { content: string }) {
  const mdRef = useRef<PretextMarkdownHandle>(null)

  return (
    <div style={{ height: 600 }}>
      <PretextMarkdown
        ref={mdRef}
        value={content}
      />
    </div>
  )
}

The component fills its parent — give the parent an explicit height.

Props

| Prop | Type | Default | Description | |---|---|---|---| | value | string | — | Markdown source (controlled) | | fontSize | number | 14 | Body font size in px | | fontFamily | string | 'Menlo, Monaco, "Courier New", monospace' | CSS font-family string | | className | string | — | Class on the scroll container | | style | CSSProperties | — | Inline style on the scroll container | | ref | Ref<PretextMarkdownHandle> | — | Imperative handle for scroll control |

Imperative handle

PretextMarkdown forwards a ref exposing section-level scroll control, designed for syncing scroll position when toggling between preview and edit modes:

const mdRef = useRef<PretextMarkdownHandle>(null)

// Index of the section at the top of the viewport
const topBlock = mdRef.current?.getTopBlock()

// Scroll to a specific section by index
mdRef.current?.scrollToBlock(3)

// Replace one section's layout (e.g. after an in-place edit)
mdRef.current?.updateBlock(3, newMarkdownBlock)

| Method | Returns | Description | |---|---|---| | getTopBlock() | number | Index of the MarkdownBlock at the top of the visible area | | scrollToBlock(idx) | void | Scroll the viewport so section idx appears at the top | | updateBlock(idx, block) | void | Re-layout a single section and update total height incrementally |

Exported utilities

import { parseMarkdown } from 'pretext-markdown'
import type { MarkdownBlock } from 'pretext-markdown'

// Parse Markdown source into blocks
const blocks = parseMarkdown(source)

// Each block carries its starting source line
// Useful for mapping editor cursor line → preview section index:
const blockIdx = blocks.findLastIndex(b => b.startLine <= editorLine)

MarkdownBlock variants: heading · paragraph · code · blockquote · list-item · hr — each tagged with startLine: number.

Performance architecture

Section model

Every top-level Markdown construct is one section:

| Markdown | Section kind | |---|---| | # Heading | heading (its own section, never merged with body) | | Normal paragraph | paragraph | | ```lang … ``` | code | | > quote | blockquote | | - item / 1. item | list-item (one section per item) | | --- | hr |

Each section is laid out independently. Y coordinates inside a section are relative to the section top (start from 0), so cached layouts can be repositioned without recomputation when other sections change height.

Section cache

SectionCache {
  layouts: (SectionLayout | null)[]   // null = not yet computed
  heights:  number[]                  // actual height or fast estimate
  startYs:  number[]                  // PV + cumulative prefix sums
  totalHeight: number
  gen:      number                    // generation counter
}

When a section is first laid out, heights[i] is updated from the estimate to the real value and startYs is recomputed from i onward in O(n − i). The scrollable div height (totalHeight) is set as React state, so the native scrollbar is always accurate even before all sections are laid out.

Progressive rendering

On mount (or when value changes), sections are laid out in batches using requestIdleCallback. Batch size is measured in source lines and follows a doubling schedule:

Batch 1  ≥  200 source lines → paint
Batch 2  ≥  400 source lines → paint
Batch 3  ≥  800 source lines → paint
Batch 4  ≥ 1600 source lines → paint
Batch 5+ ≥ 1600 source lines → paint  (cap at 1600)

Files with fewer than 200 total source lines are laid out in a single synchronous pass — no idle scheduling needed.

Virtual rendering

On every scroll event, repaint() iterates startYs to find sections that intersect [scrollTop, scrollTop + viewHeight]. Only those sections are passed to renderMarkdown. Sections outside the viewport are skipped entirely — layout cost is already paid and cached; drawing cost is O(visible sections).

totalHeight ──────────────── scrollable div height (native scrollbar)
                   ┌──────── scrollTop
    section 0      │
    section 1      │  ← these sections are passed to renderMarkdown
    section 2      │
                   └──────── scrollTop + viewHeight
    section 3      (skipped)
    …

Async safety: generation counter

The progressive layout loop runs across multiple requestIdleCallback ticks. If the document changes mid-flight (file switch, value prop update), the generation counter gen is incremented. Each idle callback captures gen at dispatch time:

function layoutBatch() {
  if (gen !== cache.gen) return   // stale — discard
  …
  requestIdleCallback(() => layoutBatch())
}

This prevents a slow layout pass for a previous document from writing into the cache of the current one.

Incremental update via updateBlock

When the host knows exactly which section changed (e.g. the user edited one paragraph in the editor before switching to preview), it can call updateBlock(idx, newBlock) instead of changing value:

  1. layoutSingleBlock re-computes only section idx.
  2. If its height changed, startYs is updated from idx onward.
  3. totalHeight state is updated so the scrollbar adjusts.
  4. repaint() is called synchronously.

All other sections' cached layouts remain untouched.

Mode-switch scroll sync

parseMarkdown and MarkdownBlock are exported so the host layer can maintain a source-line → section-index mapping without parsing twice:

// editor line → preview section
const blocks = parseMarkdown(currentContent)
const blockIdx = blocks.findLastIndex(b => b.startLine <= editorTopLine)
mdRef.current?.scrollToBlock(blockIdx)

// preview section → editor line
const topBlockIdx = mdRef.current?.getTopBlock() ?? 0
const sourceLine = blocks[topBlockIdx]?.startLine ?? 0
editorRef.current?.scrollToLine(sourceLine)