skills/bun-runtime-markdown/SKILL.md
Parse and render Markdown with Bun's built-in Markdown API, supporting GFM extensions and custom rendering callbacks
npx skillsauth add jarle/bun-skills Bun MarkdownInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
<Callout type="note"> **Unstable API** — This API is under active development and may change in future versions of Bun. </Callout>Parse and render Markdown with Bun's built-in Markdown API, supporting GFM extensions and custom rendering callbacks
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 stringBun.markdown.render() — render Markdown with custom callbacks for each elementBun.markdown.react() — render Markdown to React JSX elementsBun.markdown.html()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 |
`);
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 |
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 },
});
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>'
Each callback receives:
children — the accumulated content of the element as a stringmeta (optional) — an object with element-specific metadataReturn 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.
| 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 |
| 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 |
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>`,
});
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"
Return null or undefined to remove an element from the output:
const result = Bun.markdown.render("# Title\n\n\n\nHello", {
image: () => null, // Remove all images
heading: children => children,
paragraph: children => children + "\n",
});
// "Title\nHello\n"
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`,
});
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 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);
}
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>"
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 } },
);
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) |
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 });
}
All parser options are passed as the third argument:
const el = Bun.markdown.react("## Hello World", undefined, {
headings: { ids: true },
autolinks: true,
});
development
Using TypeScript with Bun, including type definitions and compiler options
development
Learn how to write tests using Bun's Jest-compatible API with support for async tests, timeouts, and various test modifiers
testing
Learn how to use snapshot testing in Bun to save and compare output between test runs
testing
Learn about Bun test's runtime integration, environment variables, timeouts, and error handling