-
Notifications
You must be signed in to change notification settings - Fork 5.1k
feat(md): Zig markdown parser with Bun.markdown API #26440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 437cd0c
[autofix.ci] apply automated fixes
autofix-ci[bot] f52dab0
feat(md): split parser, add renderer abstraction, and Bun.Markdown.re…
Jarred-Sumner d08bb33
[autofix.ci] apply automated fixes
autofix-ci[bot] 41020e9
feat(md): implement GFM disallowed raw HTML tag filter
Jarred-Sumner 598c141
Merge branch 'main' into jarred/mdx
Jarred-Sumner 8af45e1
refactor(md): rename API to Bun.markdown.html/render, add custom rend…
Jarred-Sumner 343a83e
fix(md): GFM compatibility fixes, ban-words compliance, and type safety
Jarred-Sumner 1d5d72f
feat(md): add TypeScript types and documentation for Bun.markdown API
Jarred-Sumner 71819e2
[autofix.ci] apply automated fixes
autofix-ci[bot] 96e8d07
refactor(md): idiomatic Zig cleanup and deduplication
Jarred-Sumner 531dbef
Fix
Jarred-Sumner b0518cf
[autofix.ci] apply automated fixes
autofix-ci[bot] 9b4e95d
feat(md): add heading_ids and autolink_headings renderer options
Jarred-Sumner 47487bb
[autofix.ci] apply automated fixes
autofix-ci[bot] 9500d75
feat(md): use camelCase for JavaScript API options
Jarred-Sumner 4f9a815
fix(md): use bun.StringHashMapUnmanaged instead of std
Jarred-Sumner b2d4b76
fix(md): address code review feedback
Jarred-Sumner 9d6f52e
docs(md): update options to camelCase and add headingIds/autolinkHead…
Jarred-Sumner aec9a1f
refactor(md): deduplicate entity/slug code between renderers
Jarred-Sumner bdd71d2
fix(md): assorted cleanups across renderer and types
Jarred-Sumner 20f6901
fix(md): propagate errors through VTable and add stack overflow checks
Jarred-Sumner ae1a940
[autofix.ci] apply automated fixes
autofix-ci[bot] ebc2eb0
feat(md): add Bun.markdown.react() and Bun.markdown.render() APIs
Jarred-Sumner fe3e542
docs(md): update TypeScript types for render/react APIs
Jarred-Sumner c90018b
feat(md): react() returns a Fragment element, update types and docs
Jarred-Sumner 76fabbc
feat(md): restore callback-based render(), default reactVersion to 19
Jarred-Sumner cfeb087
feat(md): add specific typed props for render callbacks and fuzzer tests
Jarred-Sumner fb7b08f
[autofix.ci] apply automated fixes
autofix-ci[bot] 1c58726
refactor(md): split tests, bitset fast-path, HeadingIdTracker, metada…
Jarred-Sumner 4725921
[autofix.ci] apply automated fixes
autofix-ci[bot] 08dc571
fix(md): slug_counts memory safety, forward RenderOptions, move imports
Jarred-Sumner 403be5f
Update WebKit (#26449)
dylan-conway 3d1bec0
fix(napi): fix use-after-free in property names and external buffer l…
dylan-conway 3dec010
fix(build-jsc): enable REMOTE_INSPECTOR for macOS release builds (#26…
sosukesuzuki 244f447
fix(lint): remove unused variables in readline and ConsoleObject (#26…
sosukesuzuki 2738c5b
docs(test): update static-initializers.test.ts comments to reflect mi…
robobun 354aecf
Fix hash map use-after-free in macro (#26451)
kirillmarkelov 03e3228
test: add JSC JIT stress tests from WebKit (#26380)
sosukesuzuki a3ffcd8
Add benchmark for `[...set]` (#26452)
sosukesuzuki 26b5cae
feat: add native JSON5 parser (Bun.JSON5) (#26439)
dylan-conway 5ca44d7
fix(http2): prevent extra empty DATA frame on write()+end() pattern (…
robobun 6ae0390
feat(md): rename to unstable_markdown, restructure API args
Jarred-Sumner 678281e
docs(md): add unstable API note, remove individual autolink/heading o…
Jarred-Sumner c8b67b5
merge: resolve conflicts with main (md loader entries)
Jarred-Sumner 899d3d5
[autofix.ci] apply automated fixes
autofix-ci[bot] 0578daf
rename Bun.unstable_markdown to Bun.markdown
dylan-conway d5cb86f
[autofix.ci] apply automated fixes
autofix-ci[bot] 87ac50b
use arena
dylan-conway 82cf6b3
add Parser.Error
dylan-conway 36c428d
[autofix.ci] apply automated fixes
autofix-ci[bot] e2efc47
JSX.Element
dylan-conway e3810b3
Merge branch 'main' into jarred/mdx
dylan-conway ae37f72
Merge branch 'main' into jarred/mdx
dylan-conway c001356
introduce a jsx.d.ts which reaches for React's types when available, …
alii 7bd8073
bump .stdDir() ban limit to 42
dylan-conway File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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\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, | ||
| }); | ||
| ``` | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.