Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
11f1984
feat(md): add Zig markdown parser with .md loader and Bun.Markdown API
Jarred-Sumner Jan 25, 2026
437cd0c
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 25, 2026
f52dab0
feat(md): split parser, add renderer abstraction, and Bun.Markdown.re…
Jarred-Sumner Jan 25, 2026
d08bb33
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 25, 2026
41020e9
feat(md): implement GFM disallowed raw HTML tag filter
Jarred-Sumner Jan 25, 2026
598c141
Merge branch 'main' into jarred/mdx
Jarred-Sumner Jan 25, 2026
8af45e1
refactor(md): rename API to Bun.markdown.html/render, add custom rend…
Jarred-Sumner Jan 25, 2026
343a83e
fix(md): GFM compatibility fixes, ban-words compliance, and type safety
Jarred-Sumner Jan 26, 2026
1d5d72f
feat(md): add TypeScript types and documentation for Bun.markdown API
Jarred-Sumner Jan 26, 2026
71819e2
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 26, 2026
96e8d07
refactor(md): idiomatic Zig cleanup and deduplication
Jarred-Sumner Jan 26, 2026
531dbef
Fix
Jarred-Sumner Jan 26, 2026
b0518cf
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 26, 2026
9b4e95d
feat(md): add heading_ids and autolink_headings renderer options
Jarred-Sumner Jan 26, 2026
47487bb
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 26, 2026
9500d75
feat(md): use camelCase for JavaScript API options
Jarred-Sumner Jan 26, 2026
4f9a815
fix(md): use bun.StringHashMapUnmanaged instead of std
Jarred-Sumner Jan 26, 2026
b2d4b76
fix(md): address code review feedback
Jarred-Sumner Jan 26, 2026
9d6f52e
docs(md): update options to camelCase and add headingIds/autolinkHead…
Jarred-Sumner Jan 26, 2026
aec9a1f
refactor(md): deduplicate entity/slug code between renderers
Jarred-Sumner Jan 26, 2026
bdd71d2
fix(md): assorted cleanups across renderer and types
Jarred-Sumner Jan 26, 2026
20f6901
fix(md): propagate errors through VTable and add stack overflow checks
Jarred-Sumner Jan 26, 2026
ae1a940
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 26, 2026
ebc2eb0
feat(md): add Bun.markdown.react() and Bun.markdown.render() APIs
Jarred-Sumner Jan 26, 2026
fe3e542
docs(md): update TypeScript types for render/react APIs
Jarred-Sumner Jan 26, 2026
c90018b
feat(md): react() returns a Fragment element, update types and docs
Jarred-Sumner Jan 26, 2026
76fabbc
feat(md): restore callback-based render(), default reactVersion to 19
Jarred-Sumner Jan 26, 2026
cfeb087
feat(md): add specific typed props for render callbacks and fuzzer tests
Jarred-Sumner Jan 26, 2026
fb7b08f
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 26, 2026
1c58726
refactor(md): split tests, bitset fast-path, HeadingIdTracker, metada…
Jarred-Sumner Jan 26, 2026
4725921
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 26, 2026
08dc571
fix(md): slug_counts memory safety, forward RenderOptions, move imports
Jarred-Sumner Jan 26, 2026
403be5f
Update WebKit (#26449)
dylan-conway Jan 26, 2026
3d1bec0
fix(napi): fix use-after-free in property names and external buffer l…
dylan-conway Jan 26, 2026
3dec010
fix(build-jsc): enable REMOTE_INSPECTOR for macOS release builds (#26…
sosukesuzuki Jan 26, 2026
244f447
fix(lint): remove unused variables in readline and ConsoleObject (#26…
sosukesuzuki Jan 26, 2026
2738c5b
docs(test): update static-initializers.test.ts comments to reflect mi…
robobun Jan 26, 2026
354aecf
Fix hash map use-after-free in macro (#26451)
kirillmarkelov Jan 26, 2026
03e3228
test: add JSC JIT stress tests from WebKit (#26380)
sosukesuzuki Jan 26, 2026
a3ffcd8
Add benchmark for `[...set]` (#26452)
sosukesuzuki Jan 26, 2026
26b5cae
feat: add native JSON5 parser (Bun.JSON5) (#26439)
dylan-conway Jan 26, 2026
5ca44d7
fix(http2): prevent extra empty DATA frame on write()+end() pattern (…
robobun Jan 26, 2026
6ae0390
feat(md): rename to unstable_markdown, restructure API args
Jarred-Sumner Jan 26, 2026
678281e
docs(md): add unstable API note, remove individual autolink/heading o…
Jarred-Sumner Jan 26, 2026
c8b67b5
merge: resolve conflicts with main (md loader entries)
Jarred-Sumner Jan 27, 2026
899d3d5
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 27, 2026
0578daf
rename Bun.unstable_markdown to Bun.markdown
dylan-conway Jan 29, 2026
d5cb86f
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 29, 2026
87ac50b
use arena
dylan-conway Jan 29, 2026
82cf6b3
add Parser.Error
dylan-conway Jan 29, 2026
36c428d
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 29, 2026
e2efc47
JSX.Element
dylan-conway Jan 29, 2026
e3810b3
Merge branch 'main' into jarred/mdx
dylan-conway Jan 29, 2026
ae37f72
Merge branch 'main' into jarred/mdx
dylan-conway Jan 29, 2026
c001356
introduce a jsx.d.ts which reaches for React's types when available, …
alii Jan 29, 2026
7bd8073
bump .stdDir() ban limit to 42
dylan-conway Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions cmake/Sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@
},
{
"output": "JavaScriptSources.txt",
"paths": [
"src/js/**/*.{js,ts}",
"src/install/PackageManager/scanner-entry.ts"
]
"paths": ["src/js/**/*.{js,ts}", "src/install/PackageManager/scanner-entry.ts"]
},
{
"output": "JavaScriptCodegenSources.txt",
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"/runtime/secrets",
"/runtime/console",
"/runtime/yaml",
"/runtime/markdown",
"/runtime/json5",
"/runtime/jsonl",
"/runtime/html-rewriter",
Expand Down
2 changes: 1 addition & 1 deletion docs/runtime/bun-apis.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ Click the link in the right column to jump to the associated documentation.
| Stream Processing | [`Bun.readableStreamTo*()`](/runtime/utils#bun-readablestreamto), `Bun.readableStreamToBytes()`, `Bun.readableStreamToBlob()`, `Bun.readableStreamToFormData()`, `Bun.readableStreamToJSON()`, `Bun.readableStreamToArray()` |
| Memory & Buffer Management | `Bun.ArrayBufferSink`, `Bun.allocUnsafe`, `Bun.concatArrayBuffers` |
| Module Resolution | [`Bun.resolveSync()`](/runtime/utils#bun-resolvesync) |
| Parsing & Formatting | [`Bun.semver`](/runtime/semver), `Bun.TOML.parse`, [`Bun.color`](/runtime/color) |
| Parsing & Formatting | [`Bun.semver`](/runtime/semver), `Bun.TOML.parse`, [`Bun.markdown`](/runtime/markdown), [`Bun.color`](/runtime/color) |
| Low-level / Internals | `Bun.mmap`, `Bun.gc`, `Bun.generateHeapSnapshot`, [`bun:jsc`](https://bun.com/reference/bun/jsc) |
344 changes: 344 additions & 0 deletions docs/runtime/markdown.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
---
title: Markdown
description: Parse and render Markdown with Bun's built-in Markdown API, supporting GFM extensions and custom rendering callbacks
---

{% callout type="note" %}
**Unstable API** — This API is under active development and may change in future versions of Bun.
{% /callout %}

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

- `Bun.markdown.html()` — render Markdown to an HTML string
- `Bun.markdown.render()` — render Markdown with custom callbacks for each element
- `Bun.markdown.react()` — render Markdown to React JSX elements

---

## `Bun.markdown.html()`

Convert a Markdown string to HTML.

```ts
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:

```ts
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:

```ts
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](#autolinks) |
| `headings` | `false` | Heading IDs and autolinks — see [Heading IDs](#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:

```ts
// 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:

```ts
// 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.

```ts
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: number, id?: string }` | Heading level 1–6. `id` is set when `headings: { ids: true }` is enabled |
| `paragraph` | — | Paragraph block |
| `blockquote` | — | Blockquote block |
| `code` | `{ language?: string }` | Fenced or indented code block. `language` is the info-string when specified on the fence |
| `list` | `{ ordered: boolean, start?: number }` | Ordered or unordered list. `start` is the start number for ordered lists |
| `listItem` | `{ checked?: boolean }` | List item. `checked` is set for task list items (`- [x]` / `- [ ]`) |
| `hr` | — | Horizontal rule |
| `table` | — | Table block |
| `thead` | — | Table head |
| `tbody` | — | Table body |
| `tr` | — | Table row |
| `th` | `{ align?: "left" \| "center" \| "right" }` | Table header cell. `align` is set when alignment is specified |
| `td` | `{ align?: "left" \| "center" \| "right" }` | Table data cell. `align` is set when alignment is specified |
| `html` | — | Raw HTML content |

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

```ts
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

```ts
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:

```ts
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

```ts
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`,
});
```

#### Code block syntax highlighting

````ts
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:

```ts
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.

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

### Server-side rendering

Works with `renderToString()` and React Server Components:

```tsx
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:

```tsx
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 |
| ------------ | ---------------------------- | --------------------------------------------------------------- |
| `h1`–`h6` | `{ 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):

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

### Parser options

All [parser options](#options) are passed as the third argument:

```tsx
const el = Bun.markdown.react("## Hello World", undefined, {
headings: { ids: true },
autolinks: true,
});
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading