Skip to content

liyown/marknative

Repository files navigation

marknative

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

📖 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

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

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

Why

Requirement Browser-based marknative
Runs on the server without a browser
Deterministic page breaks across runs
Direct PNG / SVG output
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
    painter?: Painter                            // override the paint backend
    codeHighlighting?: {
      theme?: string                             // Shiki theme — default: 'github-light'
    }
  },
): 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.


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

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
  }
}

Syntax highlighting for code blocks

// Light theme (default — github-light)
const pages = await renderMarkdown(markdown)

// Pair a dark marknative theme with a matching Shiki theme
const pages = await renderMarkdown(markdown, {
  format: 'png',
  theme: 'dark',
  codeHighlighting: { theme: 'github-dark' },
})

// Nord palette
const pages = await renderMarkdown(markdown, {
  codeHighlighting: { theme: 'nord' },
})

Code blocks without a language tag fall back to plain monochrome text automatically.


Tech Stack

Layer Library
Markdown parsing micromark + mdast-util-from-markdown
GFM extensions micromark-extension-gfm + mdast-util-gfm
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
  • Syntax highlighting for code blocks (Shiki, all themes)
  • Expose public theme and page configuration API
  • 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

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors