comark-pdf

comark-pdf

PDF renderer for Comark. Convert Markdown to print-ready PDF bytes via jasy — no headless browser required.

npm version npm downloads CI license

comark-pdf — PDF renderer for Comark

Install

pnpm add comark-pdf

comark is a peer dependency.

Usage

Render to PDF bytes

import { renderPdf, createPdfRenderer } from 'comark-pdf'
import { writeFile } from 'node:fs/promises'

// One-shot
const bytes = await renderPdf(`
---
pdf:
  format: A4
  margin: 20mm
  footer: "Page {{ page }} of {{ totalPages }}"
---

# My Document

Content here.

::page-break
::

# Chapter 2

More content.
`)

await writeFile('output.pdf', bytes)

// Reusable renderer (parser initialized once)
const render = createPdfRenderer({
  pdf: { format: 'A4', margin: '20mm', footer: 'Page {{ page }} of {{ totalPages }}' },
})
const bytes2 = await render(markdownString)

Node.js file export

import { renderPdfToBuffer, renderPdfToFile } from 'comark-pdf/node'

const buffer = await renderPdfToBuffer(markdown)
await writeFile('output.pdf', buffer)

// Or write directly to a file
await renderPdfToFile(markdown, 'output.pdf')

Browser preview

import { renderPdf } from 'comark-pdf'
import { mount } from 'comark-pdf/preview'

const bytes = await renderPdf(markdownString)
const handle = mount(document.getElementById('preview'), bytes)

// Later, to free the Blob URL:
handle.revoke()

Options

renderPdf(), createPdfRenderer(), and the Node helpers accept PdfRendererOptions:

OptionTypeDescription
pdfPdfPageConfigPage size, margins, typography, metadata. String header / footer compile into chrome.
chromePdfChromePage header, footer, and watermark elements. Replaces compiled string templates.
visualsPdfVisualsPrint faces and table / quote / image map
pluginsComarkPlugin[]Parser plugins (math, mermaid, binding, …)
componentsRecord<string, JasyComponentFn>Custom jasy component factories by tag name
fontsRecord<string, Uint8Array | FontFaces>Font paths or bytes; registered with addFont

Also accepts Comark ParserOptions (autoClose, linkify, registerDefaultPlugins, …).

Merge order for page config (last write wins): frontmatter.pdfoptions.pdf.

pdf / frontmatter

Set defaults in code via options.pdf, or per document under a pdf: frontmatter block (same keys):

---
pdf:
  format: A4          # page size (A4, Letter, A3, A5, …); ignored when width + height are set
  width: 50mm         # custom page width (pair with height)
  height: 65mm        # custom page height (pair with width)
  orientation: portrait  # portrait | landscape
  margin: 20mm        # length string, points number, or { top, right, bottom, left }
  gap: 10             # space between block children (points)
  justify: start      # page main-axis alignment
  align: start        # page cross-axis alignment
  font: Helvetica     # document default font
  fontSize: 11        # document default text size (points)
  color: "#1b2433"    # document default text color
  lineHeight: 1.5     # document default line-height
  textAlign: left     # document default text align
  title: "My Report"  # PDF metadata + accessibility title
  author: "Acme"
  lang: en-US
  accessible: false
  onOverflow: error   # error | warn | ignore
  header: "My Report" # compiles to chrome.header; tokens: {{ page }}, {{ totalPages }}
  headerLeft: "Draft"
  headerRight: "Confidential"
  footer: "Page {{ page }} of {{ totalPages }}"
  footerLeft: "Company Name"
  footerRight: "2026"
---

margin is the paper inset only. Header and footer sit inside that box and take their own height. Do not add chrome height into margin. Pass chrome.header / chrome.footer to replace the compiled strings. Pass chrome.watermark as a Positioned overlay; it is not part of the header band.

Length values accept mm, cm, in, pt, px, or a bare number (treated as points).

const bytes = await renderPdf(markdown, {
  pdf: {
    format: 'Letter',
    margin: '25mm',
    footer: 'Page {{ page }} of {{ totalPages }}',
  },
})

Feature support

Supported: headings, paragraphs, bold / italic / strikethrough, links, lists, blockquotes, rules, tables, page size and margins, headers / footers with {{ page }} / {{ totalPages }} tokens, ::page-break, multi-page flow, browser mount, Node file export.

Degraded (source kept, no rich visual):

FeaturePDF outputReason
Code blocks (Shiki / rangi)Monospace text in a tinted boxHighlighters emit HTML
Math (KaTeX)LaTeX source as monospace textKaTeX emits HTML
MermaidDiagram source as monospace blockMermaid emits SVG
ImagesAlt-text placeholderRemote URL fetch not wired

Not yet: raw HTML blocks as layout, full footnote chrome, checkbox glyphs.

Plugins

import { renderPdf } from 'comark-pdf'
import math, { Math } from 'comark-pdf/plugins/math'
import mermaid, { Mermaid } from 'comark-pdf/plugins/mermaid'

const bytes = await renderPdf(markdown, {
  plugins: [math(), mermaid()],
  components: { Math, Mermaid },
})

Custom components

Override any Comark component tag with a jasy element factory:

import { renderPdf } from 'comark-pdf'
import { Box, Text } from '@jasy/pdf'

const bytes = await renderPdf(markdown, {
  components: {
    alert: ([, attrs, ...children], ctx) =>
      Box({ bg: '#fff3cd', padding: 12, radius: 4 }, ctx.mapNodes(children)),
  },
})

Development

pnpm install
pnpm test
pnpm play
pnpm play:nuxt
pnpm build

License

MIT