Skip to content

Repository files navigation

marknative

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:

Math rendering example

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

Full syntax fixture page 1Full syntax fixture page 2
Full syntax fixture page 3Full syntax fixture page 4
Full syntax fixture page 5Full syntax fixture page 6
Full syntax fixture page 7Full syntax fixture page 8
Full syntax fixture page 9Full syntax fixture page 10

Why

RequirementBrowser-basedmarknative
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 scaleslowfast
Embeddable as a libraryheavylightweight

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'constpages=awaitrenderMarkdown(`# Hello, marknativeA 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]ofpages.entries()){// page.format === 'png'// page.data === BufferawaitBun.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.

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

Return type:

typeRenderPage=|{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.

functionparseMarkdown(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'constpages=awaitrenderMarkdown(markdown,{theme: 'dark'})constpages=awaitrenderMarkdown(markdown,{theme: 'nord'})

Partial overrides — merged onto defaultTheme:

constpages=awaitrenderMarkdown(markdown,{theme: {colors: {background: '#1e1e2e',text: '#cdd6f4'},page: {width: 800},},})

Full control with mergeTheme:

import{mergeTheme,getBuiltInTheme}from'marknative'constmyTheme=mergeTheme(getBuiltInTheme('nord'),{colors: {link: '#ff6b6b'},})constpages=awaitrenderMarkdown(markdown,{theme: myTheme})

Gradient backgrounds:

import{mergeTheme,defaultTheme}from'marknative'consttheme=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 typemeanp50p90
Plain text (prose + lists + blockquotes)116 ms115 ms120 ms
Code-heavy (3 languages, shiki)101 ms101 ms104 ms
Math-heavy (4 block + 3 inline formulas)100 ms99 ms103 ms
Mixed (math + code)98 ms97 ms100 ms

Output format — mixed doc

Formatmeanp50p90note
SVG5.6 ms5.6 ms6.5 mslayout + serialize only
PNG 2×99 ms98 ms102 msfull 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

ScaleResolutionmean
11080 × ~650 (0.7 MP)29 ms
1.51620 × ~975 (1.6 MP)58 ms
2 (default)2160 × ~1300 (2.8 MP)99 ms
33240 × ~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×

Modemean per batch
1× sequential118 ms
2× parallel127 ms
4× parallel192 ms
8× parallel363 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

ElementSupport
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

ElementSupport
Tables (with alignment)
Task lists
Strikethrough

Math (LaTeX via MathJax)

ElementSupport
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'constmarkdown=awaitBun.file('article.md').text()constpages=awaitrenderMarkdown(markdown)awaitPromise.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': {asyncPOST(req){const{ markdown }=awaitreq.json()constpages=awaitrenderMarkdown(markdown,{format: 'png'})constfirst=pages[0]if(!first||first.format!=='png'){returnnewResponse('no output',{status: 500})}returnnewResponse(first.data,{headers: {'Content-Type': 'image/png'},})},},},})

Export as SVG

constpages=awaitrenderMarkdown(markdown,{format: 'svg'})for(constpageofpages){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.

constpages=awaitrenderMarkdown(`## 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$.\`\`\`pythonimport numpy as npdef 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)constpages=awaitrenderMarkdown(markdown)constpages=awaitrenderMarkdown(markdown,{theme: 'dark'})// uses github-dark automatically// Override the Shiki theme explicitlyconstpages=awaitrenderMarkdown(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

constpages=awaitrenderMarkdown(markdown,{scale: 1})// ~29 ms/page — fast previewconstpages=awaitrenderMarkdown(markdown,{scale: 2})// ~99 ms/page — retina (default)constpages=awaitrenderMarkdown(markdown,{scale: 3})// ~214 ms/page — print quality

Tech Stack

LayerLibrary
Markdown parsingmicromark + mdast-util-from-markdown
GFM extensionsmicromark-extension-gfm + mdast-util-gfm
Math renderingmathjax-full (server-side SVG, liteAdaptor)
Syntax highlightingshiki
Text shaping@chenglou/pretext
2D renderingskia-canvas
LanguageTypeScript

Roadmap

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

License

MIT & Linux Do

About

A Markdown rendering engine that generates paginated PNG and SVG output — no browser, no Chromium, no DOM.

Resources

Stars

657 stars

Watchers

4 watching

Forks

Releases

Packages

Contributors

Languages