rembrembdocs

Bun includes a fast, built-in Markdown parser written in Zig. It supports GitHub Flavored Markdown (GFM) extensions and provides three APIs:


Convert a Markdown string to HTML.

const html = Bun.markdown.html("# Hello **world**");
// "<h1>Hello <strong>world</strong></h1>\n"

GFM extensions like tables, strikethrough, and task lists are enabled by default:

const html = Bun.markdown.html(`
| Feature      | Status |
|-------------|--------|
| Tables       | ~~done~~ |
| Strikethrough| ~~done~~ |
| Task lists   | done |
`);

Options

Pass an options object as the second argument to configure the parser:

const html = Bun.markdown.html("some markdown", {
  tables: true, // GFM tables (default: true)
  strikethrough: true, // GFM strikethrough (default: true)
  tasklists: true, // GFM task lists (default: true)
  tagFilter: true, // GFM tag filter for disallowed HTML tags
  autolinks: true, // Autolink URLs, emails, and www. links
});

All available options:

Option

Default

Description

tables

false

GFM tables

strikethrough

false

GFM strikethrough (~~text~~)

tasklists

false

GFM task lists (- [x] item)

autolinks

false

Enable autolinks — see Autolinks

headings

false

Heading IDs and autolinks — see Heading IDs

hardSoftBreaks

false

Treat soft line breaks as hard breaks

wikiLinks

false

Enable [[wiki links]]

underline

false

__text__ renders as <u> instead of <strong>

latexMath

false

Enable $inline$ and $$display$$ math

collapseWhitespace

false

Collapse whitespace in text

permissiveAtxHeaders

false

ATX headers without space after #

noIndentedCodeBlocks

false

Disable indented code blocks

noHtmlBlocks

false

Disable HTML blocks

noHtmlSpans

false

Disable inline HTML

tagFilter

false

GFM tag filter for disallowed HTML tags

Autolinks

Pass true to enable all autolink types, or an object for granular control:

// Enable all autolinks (URL, WWW, email)
Bun.markdown.html("Visit www.example.com", { autolinks: true });

// Enable only specific types
Bun.markdown.html("Visit www.example.com", {
  autolinks: { url: true, www: true },
});

Heading IDs

Pass true to enable both heading IDs and autolink headings, or an object for granular control:

// Enable heading IDs and autolink headings
Bun.markdown.html("## Hello World", { headings: true });
// '<h2 id="hello-world"><a href="#hello-world">Hello World</a></h2>\n'

// Enable only heading IDs (no autolink)
Bun.markdown.html("## Hello World", { headings: { ids: true } });
// '<h2 id="hello-world">Hello World</h2>\n'

Bun.markdown.render()

Parse Markdown and render it using custom JavaScript callbacks. This gives you full control over the output format — you can generate HTML with custom classes, React elements, ANSI terminal output, or any other string format.

const result = Bun.markdown.render("# Hello **world**", {
  heading: (children, { level }) => `<h${level} class="title">${children}</h${level}>`,
  strong: children => `<b>${children}</b>`,
  paragraph: children => `<p>${children}</p>`,
});
// '<h1 class="title">Hello <b>world</b></h1>'

Callback signature

Each callback receives:

  1. children — the accumulated content of the element as a string
  2. meta (optional) — an object with element-specific metadata

Return a string to replace the element’s rendering. Return null or undefined to omit the element from the output entirely. If no callback is registered for an element, its children pass through unchanged.

Block callbacks

Callback

Meta

Description

heading

{ level, id? }

Heading level 1–6. id is set when headings: { ids: true } is enabled

paragraph

Paragraph block

blockquote

Blockquote block

code

{ language? }

Fenced or indented code block. language is the info-string when specified on the fence

list

{ ordered, start?, depth }

depth is nesting level (0 = top-level). start is set for ordered lists

listItem

{ index, depth, ordered, start?, checked? }

See List item meta below

hr

Horizontal rule

table

Table block

thead

Table head

tbody

Table body

tr

Table row

th

{ align? }

Table header cell. align is "left", "center", "right", or absent

td

{ align? }

Table data cell

html

Raw HTML content

List item meta

The listItem callback receives everything needed to render markers directly:

Inline callbacks

Callback

Meta

Description

strong

Strong emphasis (**text**)

emphasis

Emphasis (*text*)

link

{ href: string, title?: string }

Link

image

{ src: string, title?: string }

Image

codespan

Inline code (`code`)

strikethrough

Strikethrough (~~text~~)

text

Plain text content

Examples

Custom HTML with classes

const html = Bun.markdown.render("# Title\n\nHello **world**", {
  heading: (children, { level }) => `<h${level} class="heading heading-${level}">${children}</h${level}>`,
  paragraph: children => `<p class="body">${children}</p>`,
  strong: children => `<strong class="bold">${children}</strong>`,
});

Stripping all formatting

const plaintext = Bun.markdown.render("# Hello **world**", {
  heading: children => children,
  paragraph: children => children,
  strong: children => children,
  emphasis: children => children,
  link: children => children,
  image: () => "",
  code: children => children,
  codespan: children => children,
});
// "Hello world"

Omitting elements

Return null or undefined to remove an element from the output:

const result = Bun.markdown.render("# Title\n\n![logo](img.png)\n\nHello", {
  image: () => null, // Remove all images
  heading: children => children,
  paragraph: children => children + "\n",
});
// "Title\nHello\n"

ANSI terminal output

const ansi = Bun.markdown.render("# Hello\n\nThis is **bold** and *italic*", {
  heading: (children, { level }) => `\x1b[1;4m${children}\x1b[0m\n`,
  paragraph: children => children + "\n",
  strong: children => `\x1b[1m${children}\x1b[22m`,
  emphasis: children => `\x1b[3m${children}\x1b[23m`,
});

Nested list numbering

The listItem callback receives everything needed to render markers directly — no post-processing:

const result = Bun.markdown.render("1. first\n   1. sub-a\n   2. sub-b\n2. second", {
  listItem: (children, { index, depth, ordered, start }) => {
    const n = (start ?? 1) + index;
    // 1. 2. 3. at depth 0, a. b. c. at depth 1, i. ii. iii. at depth 2
    const marker = !ordered
      ? "-"
      : depth === 0
        ? `${n}.`
        : depth === 1
          ? `${String.fromCharCode(96 + n)}.`
          : `${toRoman(n)}.`;
    return "  ".repeat(depth) + marker + " " + children.trimEnd() + "\n";
  },
  // Prepend a newline so nested lists are separated from their parent item's text
  list: children => "\n" + children,
});
// 1. first
//   a. sub-a
//   b. sub-b
// 2. second

Code block syntax highlighting

const result = Bun.markdown.render("```js\nconsole.log('hi')\n```", {
  code: (children, meta) => {
    const lang = meta?.language ?? "";
    return `<pre><code class="language-${lang}">${children}</code></pre>`;
  },
});

Parser options

Parser options are passed as a separate third argument:

const result = Bun.markdown.render(
  "Visit www.example.com",
  {
    link: (children, { href }) => `[${children}](${href})`,
    paragraph: children => children,
  },
  { autolinks: true },
);

Bun.markdown.react()

Render Markdown directly to React elements. Returns a <Fragment> that you can use as a component return value.

function Markdown({ text }: { text: string }) {
  return Bun.markdown.react(text);
}

Server-side rendering

Works with renderToString() and React Server Components:

import { renderToString } from "react-dom/server";

const html = renderToString(Bun.markdown.react("# Hello **world**"));
// "<h1>Hello <strong>world</strong></h1>"

Component overrides

Replace any HTML element with a custom React component by passing it in the second argument, keyed by tag name:

function Code({ language, children }) {
  return (
    <pre data-language={language}>
      <code>{children}</code>
    </pre>
  );
}

function Link({ href, title, children }) {
  return (
    <a href={href} title={title} target="_blank" rel="noopener noreferrer">
      {children}
    </a>
  );
}

function Heading({ id, children }) {
  return (
    <h2 id={id}>
      <a href={`#${id}`}>{children}</a>
    </h2>
  );
}

const el = Bun.markdown.react(
  content,
  {
    pre: Code,
    a: Link,
    h2: Heading,
  },
  { headings: { ids: true } },
);

Available overrides

Every HTML tag produced by the parser can be overridden:

Option

Props

Description

h1h6

{ id?, children }

Headings. id is set when headings: { ids: true } is enabled

p

{ children }

Paragraph

blockquote

{ children }

Blockquote

pre

{ language?, children }

Code block. language is the info string (e.g. "js")

hr

{}

Horizontal rule (no children)

ul

{ children }

Unordered list

ol

{ start, children }

Ordered list. start is the first item number

li

{ checked?, children }

List item. checked is set for task list items

table

{ children }

Table

thead

{ children }

Table head

tbody

{ children }

Table body

tr

{ children }

Table row

th

{ align?, children }

Table header cell

td

{ align?, children }

Table data cell

em

{ children }

Emphasis (*text*)

strong

{ children }

Strong (**text**)

a

{ href, title?, children }

Link

img

{ src, alt?, title? }

Image (no children)

code

{ children }

Inline code

del

{ children }

Strikethrough (~~text~~)

br

{}

Hard line break (no children)

React 18 and older

By default, elements use Symbol.for('react.transitional.element') as the $$typeof symbol. For React 18 and older, pass reactVersion: 18 in the options (third argument):

function Markdown({ text }: { text: string }) {
  return Bun.markdown.react(text, undefined, { reactVersion: 18 });
}

Parser options

All parser options are passed as the third argument:

const el = Bun.markdown.react("## Hello World", undefined, {
  headings: { ids: true },
  autolinks: true,
});