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.4.0

Published

High-performance Canvas-virtualized markdown preview for large documents based @chenglou/pretext

Readme

pretext-markdown

High-performance Canvas-based Markdown preview — React, Vue, Angular, Svelte, and vanilla HTML5. Parses Markdown into discrete sections, caches each section's layout independently, and renders only what is visible — large documents scroll smoothly.

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

Installation

npm install pretext-markdown

Core API (pretext-markdown)

The root entry exports framework-agnostic utilities usable from any environment:

import { parseMarkdown, PretextBinding, searchLayouts, VanillaSearchBar } from 'pretext-markdown'
import type { MarkdownBlock, MdSearchMatch, SearchState, SearchActions } from 'pretext-markdown'

parseMarkdown(src: string): MarkdownBlock[]

Parses Markdown source into blocks. Each block carries a startLine (source line number).

MarkdownBlock

Variants: heading · paragraph · code · blockquote · list · table · image · hr

PretextBinding

Simple object for editor↔preview scroll sync. Two components share one binding:

const binding = new PretextBinding()

// Editor side — report top visible line on scroll
binding.reportSourceLine(editorTopLine)

// Preview side — read where to scroll when activated
const line = binding.getSourceLine()

searchLayouts(layouts, query, caseSensitive, wholeWord, useRegex)

Searches through section layouts and returns { matches: MdSearchMatch[], regexError: string | null }. Used to build custom search UIs on top of the engine.

VanillaSearchBar

A zero-dependency DOM-based search bar. Mount it inside a container, then call .update(state, actions) from the onSearchStateChange callback. Imports require CSS:

import 'pretext-markdown/icons/icons.css'
import 'pretext-markdown/styles/search-bar.css'

React

Import from pretext-markdown/react:

import { PretextMarkdown, usePretextBinding } from 'pretext-markdown/react'
import type { PretextMarkdownHandle } from 'pretext-markdown/react'

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

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

React 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 | | className | string | — | Class on the scroll container | | style | CSSProperties | — | Inline style on the scroll container | | binding | PretextBinding | — | Shared binding for scroll sync | | active | boolean | false | When true, scrolls to binding position on mount/change | | contextMenuItems | (builtins) => ContextMenuItem[] | — | Custom right-click menu items | | renderSearchBar | (state: SearchState, actions: SearchActions) => ReactNode | — | Override the default search bar UI | | ref | Ref<PretextMarkdownHandle> | — | Imperative handle |

Imperative Handle

const mdRef = useRef<PretextMarkdownHandle>(null)

// Get visible section range
const { from, to } = mdRef.current?.getVisibleBlocks() ?? { from: 0, to: 0 }

// Scroll to section by index
mdRef.current?.showBlocks(3)

// Re-layout a single section after inline edit
mdRef.current?.updateBlock(3, updatedBlock)

// Search
mdRef.current?.openSearch('keyword')
mdRef.current?.searchNext()
mdRef.current?.closeSearch()

Search (React)

Press Ctrl/Cmd+F to open search, Escape to close. The default UI is a floating bar in the top-right corner.

Custom search UI via renderSearchBar:

import type { SearchState, SearchActions } from 'pretext-markdown/react'

<PretextMarkdown
  value={content}
  renderSearchBar={(state: SearchState, actions: SearchActions) => (
    <MySearchBar state={state} actions={actions} />
  )}
/>

SearchState fields: isOpen · query · caseSensitive · matchCount · currentIndex

SearchActions methods: setQuery(q) · next() · prev() · setCaseSensitive(v) · close()

Editor↔Preview Scroll Sync (React)

import { PretextBinding } from 'pretext-markdown'

function EditorPreview({ content }: { content: string }) {
  const binding = useRef(new PretextBinding()).current
  const [mode, setMode] = useState<'edit' | 'preview'>('edit')
  const mdRef = useRef<PretextMarkdownHandle>(null)

  return (
    <div>
      <button onClick={() => setMode(m => m === 'edit' ? 'preview' : 'edit')}>Toggle</button>
      {mode === 'edit' ? (
        <Editor binding={binding} />
      ) : (
        <PretextMarkdown ref={mdRef} value={content} binding={binding} active />
      )}
    </div>
  )
}

Vue

Import from pretext-markdown/vue:

<script setup lang="ts">
import { ref } from 'vue'
import { PretextMarkdown, usePretextBinding } from 'pretext-markdown/vue'
import type { PretextMarkdownHandle } from 'pretext-markdown/vue'

const content = ref('# Hello World')
const mdRef = ref<PretextMarkdownHandle>()
</script>

<template>
  <div style="height: 600px">
    <PretextMarkdown ref="mdRef" :value="content" />
  </div>
</template>

Vue Props

| Prop | Type | Default | |---|---|---| | value | string | required | | fontSize | number | 14 | | fontFamily | string | 'Menlo, Monaco, "Courier New", monospace' | | className | string | '' | | style | string | '' | | binding | PretextBinding \| null | null | | active | boolean | false |

The imperative handle is exposed via defineExpose — access via template ref.

Search (Vue)

Press Ctrl/Cmd+F to open search, Escape to close. The default SearchBar is rendered inside the component.

<script setup lang="ts">
import { ref } from 'vue'
import type { PretextMarkdownHandle, SearchState, SearchActions } from 'pretext-markdown/vue'

const mdRef = ref<PretextMarkdownHandle>()

function openSearch() {
  mdRef.value?.openSearch('keyword')
}
</script>

SearchState / SearchActions types are re-exported from pretext-markdown/vue.


Angular

Import from pretext-markdown/angular.

Ensure experimentalDecorators is enabled in your tsconfig.json:

{ "compilerOptions": { "experimentalDecorators": true } }
import { Component, ViewChild } from '@angular/core'
import { PretextMarkdownComponent } from 'pretext-markdown/angular'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PretextMarkdownComponent],
  template: `
    <div style="height:600px">
      <pretext-markdown [value]="content" #md></pretext-markdown>
    </div>
  `,
})
export class AppComponent {
  content = '# Hello World'
  @ViewChild('md') mdRef!: PretextMarkdownComponent

  scrollToTop() {
    this.mdRef.showBlocks(0)
  }
}

Angular Inputs

| Input | Type | Default | |---|---|---| | value | string | '' | | fontSize | number | 14 | | fontFamily | string | 'Menlo, Monaco, "Courier New", monospace' | | className | string | '' | | style | string | '' | | binding | PretextBinding \| undefined | — | | active | boolean | false |

Search (Angular)

Press Ctrl/Cmd+F to open search. A <pretext-search-bar> is rendered inside the component.

export class AppComponent {
  @ViewChild('md') mdRef!: PretextMarkdownComponent

  openSearch() {
    this.mdRef.openSearch('keyword')
  }
}

The component exposes search methods: openSearch(q?), closeSearch(), searchNext(), searchPrev(), replace(), replaceAll(), and setters for all toggles.


Svelte

Import from pretext-markdown/svelte:

<script lang="ts">
  import PretextMarkdown from 'pretext-markdown/svelte'
  import type { MarkdownBlock } from 'pretext-markdown'

  let content = $state('# Hello World')
  let mdRef = $state<{ getVisibleBlocks(): { from: number; to: number }; showBlocks(n: number): void; updateBlock(i: number, b: MarkdownBlock): void } | null>(null)
</script>

<div style="height:600px">
  <PretextMarkdown bind:this={mdRef} value={content} fontSize={15} />
</div>

Svelte Props

| Prop | Type | Default | |---|---|---| | value | string | '' | | fontSize | number | 14 | | fontFamily | string | 'Menlo, Monaco, "Courier New", monospace' | | className | string | '' | | style | string | '' | | binding | PretextBinding \| null | null | | active | boolean | false |

Imperative methods are exposed as component exports — access by binding the component to a variable with bind:this.

Search (Svelte)

Press Ctrl/Cmd+F to open search. A <SearchBar> is rendered inside the component automatically.

<script lang="ts">
  import PretextMarkdown from 'pretext-markdown/svelte'

  let mdRef: { openSearch(q?: string): void; closeSearch(): void; searchNext(): void; searchPrev(): void; replace(): void; replaceAll(): void } | null = null

  function openSearch() {
    mdRef?.openSearch('keyword')
  }
</script>

<PretextMarkdown bind:this={mdRef} value={content} />

Vanilla HTML5

Use PretextCore directly:

import { PretextCore, VanillaSearchBar } from 'pretext-markdown'
import 'pretext-markdown/styles/pretext-markdown.css'
import 'pretext-markdown/styles/search-bar.css'
import 'pretext-markdown/icons/icons.css'

const root = document.getElementById('md-root')!
root.classList.add('ptmic-root')

const host = document.createElement('div')
host.className = 'ptmic-host'
root.appendChild(host)

// Search bar (renders as overlay inside root)
const searchBar = new VanillaSearchBar(root)

const core = new PretextCore(host, {
  value: '# Hello World',
  fontSize: 14,
  fontFamily: 'Menlo, Monaco, "Courier New", monospace',
  onSearchStateChange: (state, actions) => searchBar.update(state, actions),
})

// Update content
core.updateOptions({ value: '# New Content' })

// Imperative methods
const { from, to } = core.handle.getVisibleBlocks()
core.handle.showBlocks(5)
core.handle.updateBlock(3, updatedBlock)
core.handle.openSearch('keyword')

// Clean up
core.destroy()
searchBar.remove()

Performance Architecture

Every top-level Markdown construct is one section (heading, paragraph, code block, etc.). Each section is laid out independently — Y coordinates inside a section are relative to section top (start from 0), so cached layouts can be repositioned without recomputation.

Progressive Layout

On mount or content change, sections are laid out in batches using requestIdleCallback. Batch size doubles each tick from 200 → 400 → 800 → 1600 source lines. Files under 200 total lines are laid out synchronously.

Virtual Rendering

On scroll, only sections intersecting the viewport are drawn to the canvas. Non-visible sections cost nothing — their layout is already cached.

Generation Counter

If content changes mid-layout, the generation counter gen is incremented. Each async callback captures gen at dispatch time and returns early if stale, preventing old layout passes from corrupting the current cache.

Incremental Update (updateBlock)

When editing a single section, call updateBlock(idx, newBlock) instead of changing the whole value. Only that section is re-laid out, and if its height changed, subsequent section positions are updated incrementally.