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

marknative

v0.3.1

Published

Native Markdown rendering engine — produces paginated PNG/SVG documents without a browser

Readme

A native Markdown rendering engine that produces paginated PNG/SVG documents — no browser, no Chromium, no DOM.

Supports CommonMark, GFM (tables, task lists, strikethrough), syntax-highlighted code blocks (Shiki), and LaTeX math formulas (MathJax) — all rendered server-side.

📖 Documentation — Guide · Showcase · API Reference

Most Markdown rendering pipelines go through a browser:

Markdown → HTML → DOM/CSS → browser layout → screenshot

marknative takes a different path. It parses Markdown directly into a typed document model, runs its own block and inline layout engine, paginates the result into fixed-size pages, and paints each page using a native 2D canvas API.

The result is deterministic, server-renderable, and completely headless.


Gallery

Math formulas (MathJax block + inline) mixed with syntax-highlighted code:

Full syntax fixture, rendered across 10 pages with syntax-highlighted fenced code blocks:


Why

| Requirement | Browser-based | marknative | | --- | :---: | :---: | | Runs on the server without a browser | ✗ | ✓ | | Deterministic page breaks across runs | ✗ | ✓ | | Direct PNG / SVG output | ✗ | ✓ | | LaTeX math formulas (server-side MathJax) | ✗ | ✓ | | Syntax-highlighted code blocks (Shiki) | ✗ | ✓ | | Batch rendering at scale | slow | fast | | Embeddable as a library | heavy | lightweight |


Installation

bun add marknative
# or
npm install marknative

Peer dependency: marknative uses skia-canvas as its paint backend. It ships prebuilt native binaries for macOS, Linux, and Windows — no additional setup is needed in most environments.


Quick Start

import { renderMarkdown } from 'marknative'

const pages = await renderMarkdown(`
# Hello, marknative

A native Markdown rendering engine that produces **paginated PNG pages**
without a browser.

- CommonMark + GFM support
- Deterministic layout and pagination
- PNG and SVG output
`)

console.log(`Rendered ${pages.length} page(s)`)

for (const [i, page] of pages.entries()) {
  // page.format === 'png'
  // page.data   === Buffer
  await Bun.write(`page-${i + 1}.png`, page.data)
}

API

renderMarkdown(markdown, options?)

Parses, lays out, paginates, and paints a Markdown document. Returns one output entry per page.

function renderMarkdown(
  markdown: string,
  options?: {
    format?: 'png' | 'svg'                      // default: 'png'
    singlePage?: boolean                         // render into one image instead of paginating
    theme?: BuiltInThemeName | ThemeOverrides    // default: defaultTheme
    scale?: number                               // PNG pixel density multiplier — default: 2 (retina)
    painter?: Painter                            // override the paint backend
    codeHighlighting?: {
      theme?: string                             // Shiki theme — auto-detected from page background
    }
  },
): Promise<RenderPage[]>

Return type:

type RenderPage =
  | { format: 'png'; data: Buffer;  page: PaintPage }
  | { format: 'svg'; data: string;  page: PaintPage }

Each entry carries both the raw output (data) and the fully resolved page layout (page) so you can inspect fragment positions without re-rendering.


parseMarkdown(markdown)

Parses Markdown source into marknative's internal document model without running layout or paint. Useful for inspecting document structure or building custom renderers.

function parseMarkdown(markdown: string): MarkdownDocument

defaultTheme

The built-in default theme. Page size is 1080 × 1440 px (portrait card ratio). Font sizes, line heights, margins, and block spacing are all defined here.

import { defaultTheme } from 'marknative'

console.log(defaultTheme.page)
// { width: 1080, height: 1440, margin: { top: 80, right: 72, bottom: 80, left: 72 } }

Theme System

marknative ships with 10 built-in themes and a full theme customization API.

Built-in themes — pass a name string as the theme option:

// 'default' | 'github' | 'solarized' | 'sepia' | 'rose'
// 'dark' | 'nord' | 'dracula' | 'ocean' | 'forest'
const pages = await renderMarkdown(markdown, { theme: 'dark' })
const pages = await renderMarkdown(markdown, { theme: 'nord' })

Partial overrides — merged onto defaultTheme:

const pages = await renderMarkdown(markdown, {
  theme: {
    colors: { background: '#1e1e2e', text: '#cdd6f4' },
    page: { width: 800 },
  },
})

Full control with mergeTheme:

import { mergeTheme, getBuiltInTheme } from 'marknative'

const myTheme = mergeTheme(getBuiltInTheme('nord'), {
  colors: { link: '#ff6b6b' },
})

const pages = await renderMarkdown(markdown, { theme: myTheme })

Gradient backgrounds:

import { mergeTheme, defaultTheme } from 'marknative'

const theme = mergeTheme(defaultTheme, {
  colors: {
    background: '#0f0c29',
    backgroundGradient: {
      type: 'linear',
      angle: 135,
      stops: [
        { offset: 0,   color: '#24243e' },
        { offset: 0.5, color: '#302b63' },
        { offset: 1,   color: '#0f0c29' },
      ],
    },
    text: '#e8e0ff',
  },
})

See the Themes guide and Themes showcase for the full reference.


Performance

Benchmarks run on Apple M-series (warm, singletons already initialised). Run bun bench/perf.ts to reproduce.

Throughput by document type — PNG 2× (default)

| Document type | mean | p50 | p90 | | --- | ---: | ---: | ---: | | Plain text (prose + lists + blockquotes) | 116 ms | 115 ms | 120 ms | | Code-heavy (3 languages, shiki) | 101 ms | 101 ms | 104 ms | | Math-heavy (4 block + 3 inline formulas) | 100 ms | 99 ms | 103 ms | | Mixed (math + code) | 98 ms | 97 ms | 100 ms |

Output format — mixed doc

| Format | mean | p50 | p90 | note | | --- | ---: | ---: | ---: | --- | | SVG | 5.6 ms | 5.6 ms | 6.5 ms | layout + serialize only | | PNG 2× | 99 ms | 98 ms | 102 ms | full rasterize + encode |

SVG is ~17× faster than PNG. canvas.toBuffer('png') (pure CPU PNG compression) accounts for 94% of PNG render time; all parsing, layout, and drawing are <8 ms/page.

Scale factor — mixed doc, PNG only

| Scale | Resolution | mean | | --- | --- | ---: | | 1 | 1080 × ~650 (0.7 MP) | 29 ms | | 1.5 | 1620 × ~975 (1.6 MP) | 58 ms | | 2 (default) | 2160 × ~1300 (2.8 MP) | 99 ms | | 3 | 3240 × ~1950 (6.3 MP) | 214 ms |

PNG encode time scales linearly with pixel count. Use scale: 1 for previews, scale: 2 for retina output.

Concurrency — plain doc, PNG 2×

| Mode | mean per batch | | --- | ---: | | 1× sequential | 118 ms | | 2× parallel | 127 ms | | 4× parallel | 192 ms | | 8× parallel | 363 ms |

Parallel renders share CPU resources; throughput scales near-linearly up to core count.


Rendering Pipeline

Markdown source
  │
  ▼
CommonMark + GFM AST          (micromark, mdast-util-from-markdown)
  │
  ▼
MarkdownDocument               internal typed document model
  │
  ▼
BlockLayoutFragment[]          block + inline layout engine
  │
  ▼
Page[]                         paginator — slices fragments into fixed-height pages
  │
  ▼
PNG Buffer / SVG string        skia-canvas paint backend

Each stage is independently testable. The layout engine has no dependency on the paint backend, and the paint backend accepts a plain data structure — it does not re-run layout.


Supported Syntax

CommonMark

| Element | Support | | --- | :---: | | Headings (H1–H6) | ✓ | | Paragraphs | ✓ | | Bold, italic, bold italic | ✓ | | Inline code | ✓ | | Links | ✓ | | Fenced code blocks | ✓ | | Blockquotes (nested) | ✓ | | Ordered lists | ✓ | | Unordered lists (nested) | ✓ | | Images (block + inline) | ✓ | | Thematic breaks | ✓ | | Hard line breaks | ✓ |

GFM Extensions

| Element | Support | | --- | :---: | | Tables (with alignment) | ✓ | | Task lists | ✓ | | ~~Strikethrough~~ | ✓ |

Math (LaTeX via MathJax)

| Element | Support | | --- | :---: | | Block formulas $$...$$ | ✓ | | Inline formulas $...$ | ✓ | | Math in blockquotes / lists / tables | ✓ | | AMSmath, boldsymbol, mathtools | ✓ |


Recipes

Save all pages as PNG files

import { renderMarkdown } from 'marknative'
import { writeFile } from 'node:fs/promises'

const markdown = await Bun.file('article.md').text()
const pages = await renderMarkdown(markdown)

await Promise.all(
  pages.map((page, i) =>
    writeFile(`out/page-${String(i + 1).padStart(2, '0')}.png`, page.data)
  )
)

Serve rendered pages over HTTP with Bun

import { renderMarkdown } from 'marknative'

Bun.serve({
  routes: {
    '/render': {
      async POST(req) {
        const { markdown } = await req.json()
        const pages = await renderMarkdown(markdown, { format: 'png' })
        const first = pages[0]

        if (!first || first.format !== 'png') {
          return new Response('no output', { status: 500 })
        }

        return new Response(first.data, {
          headers: { 'Content-Type': 'image/png' },
        })
      },
    },
  },
})

Export as SVG

const pages = await renderMarkdown(markdown, { format: 'svg' })

for (const page of pages) {
  if (page.format === 'svg') {
    console.log(page.data) // inline SVG string
  }
}

Math formulas

Block and inline LaTeX — rendered server-side via MathJax, no browser required. Formula colors follow the active theme automatically.

const pages = await renderMarkdown(`
## Fourier Transform

$$
\\hat{f}(\\xi) = \\int_{-\\infty}^{\\infty} f(x)\\,e^{-2\\pi ix\\xi}\\,dx
$$

The gradient $\\nabla f$ points in the direction of steepest ascent.
Entropy: $H(X) = -\\sum p \\log p$.

\`\`\`python
import numpy as np
def dft(x):
    N, n = len(x), np.arange(len(x))
    return np.exp(-2j * np.pi * n.reshape(N,1) * n / N) @ x
\`\`\`

Complexity: $O(N^2)$ naïve, $O(N\\log N)$ with FFT.
`)

MathJax is initialised lazily on first use (~180 ms cold start). All subsequent renders reuse the singleton and cached SVGs — warm overhead is < 2 ms.

See the Math guide for the full reference.

Syntax highlighting for code blocks

// Auto-detected from page background (light → github-light, dark → github-dark)
const pages = await renderMarkdown(markdown)
const pages = await renderMarkdown(markdown, { theme: 'dark' })   // uses github-dark automatically

// Override the Shiki theme explicitly
const pages = await renderMarkdown(markdown, {
  codeHighlighting: { theme: 'nord' },
})

The Shiki theme is auto-selected based on the WCAG relative luminance of the page background — dark page themes automatically get a dark code theme. Code blocks without a language tag fall back to plain monochrome text.

Control PNG resolution with scale

const pages = await renderMarkdown(markdown, { scale: 1 })   // ~29 ms/page — fast preview
const pages = await renderMarkdown(markdown, { scale: 2 })   // ~99 ms/page — retina (default)
const pages = await renderMarkdown(markdown, { scale: 3 })   // ~214 ms/page — print quality

Tech Stack

| Layer | Library | | --- | --- | | Markdown parsing | micromark + mdast-util-from-markdown | | GFM extensions | micromark-extension-gfm + mdast-util-gfm | | Math rendering | mathjax-full (server-side SVG, liteAdaptor) | | Syntax highlighting | shiki | | Text shaping | @chenglou/pretext | | 2D rendering | skia-canvas | | Language | TypeScript |


Roadmap

  • [ ] Improve paragraph line-breaking quality for English prose
  • [ ] Refine CJK and mixed Chinese-English line-breaking rules
  • [x] Syntax highlighting for code blocks (Shiki, all themes, auto-detected from theme)
  • [x] LaTeX math rendering (MathJax, block + inline, all block types)
  • [x] Expose public theme and page configuration API
  • [x] PNG resolution control (scale option)
  • [ ] Support custom fonts
  • [ ] Complete GFM coverage (footnotes, autolinks)

License

MIT & Linux Do