Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 50 additions & 1 deletion docs/runtime/markdown.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ description: Parse and render Markdown with Bun's built-in Markdown API, support
**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 Rust. It supports GitHub Flavored Markdown (GFM) extensions and provides three APIs:
Bun includes a fast, built-in Markdown parser written in Rust. It supports GitHub Flavored Markdown (GFM) extensions and provides four APIs:

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

Expand Down Expand Up @@ -84,6 +85,15 @@ Bun.markdown.html("Visit www.example.com", {
});
```

The autolink rules are the permissive autolinks of [md4c](https://github.com/mity/md4c), the parser that Bun's parser is a port of. They are not the autolink extension as GitHub implements it in [cmark-gfm](https://github.com/github/cmark-gfm). Both link common input such as `www.example.com`, `https://example.com/path`, and `user@example.com`. Unlike cmark-gfm, Bun does not link:

- `mailto:` and `xmpp:` URIs (`mailto:user@example.com`)
- a URL that contains a non-ASCII character (`https://example.com/å`)
- a URL that directly follows a quote character (`"https://example.com"`)
- a URL whose host name has no dot (`http://localhost:3000`)

To link any of these, use the `<https://example.com/å>` or `[text](url)` syntax.

#### Heading IDs

Pass `true` to enable both heading IDs and autolink headings, or an object for granular control:
Expand All @@ -100,6 +110,45 @@ Bun.markdown.html("## Hello World", { headings: { ids: true } });

---

## `Bun.markdown.ansi()`

Render a Markdown string to an ANSI-colored string for the terminal. `bun ./file.md` uses the same renderer.

```ts
const out = Bun.markdown.ansi("# Hello\n\n**bold** and *italic*\n");
process.stdout.write(out);
```

`ansi()` renders headings, lists, tables, blockquotes, links, images, inline styles, and fenced code blocks. It highlights the syntax of JavaScript and TypeScript code blocks.

`ansi()` takes no parser options. It always enables tables, strikethrough, task lists, autolinks, wiki links, underline, and LaTeX math.

### Theme

Pass a theme object as the second argument:

| Option | Default | Description |
| --------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `colors` | `true` | Emit ANSI color and style escape codes. When `false`, the output is plain text with ASCII borders |
| `hyperlinks` | `false` | Emit OSC 8 hyperlinks, which modern terminals make clickable. When `false`, a link is `text (url)` |
| `light` | detected | Use the palette for a light terminal background. Detected from the `COLORFGBG` environment variable |
| `columns` | `80` | Line width for word wrapping. Pass `0` to disable wrapping |
| `kittyGraphics` | `false` | Show images from local files inline with the Kitty Graphics Protocol |

```ts
// Plain text, no escape codes
Bun.markdown.ansi("# Hello\n\n[docs](https://bun.com)\n", { colors: false });
// "Hello\n=====\n\ndocs (https://bun.com)\n"

// Clickable links
Bun.markdown.ansi("[docs](https://bun.com)", { hyperlinks: true });

// Wrap at 60 columns
Bun.markdown.ansi(longText, { columns: 60 });
```

---

## `Bun.markdown.render()`

Parse Markdown and render it using custom JavaScript callbacks. The callbacks give you full control over the output format. You can generate HTML with custom classes, ANSI terminal output, or any other string format.
Expand Down
171 changes: 170 additions & 1 deletion docs/runtime/yaml.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Use Bun's built-in support for YAML files through both runtime APIs

In Bun, YAML is a first-class citizen alongside JSON and TOML. You can:

- Parse YAML strings with `Bun.YAML.parse`
- Parse and stringify YAML with `Bun.YAML.parse` and `Bun.YAML.stringify`
- `import` & `require` YAML files as modules at runtime (including hot reloading & watch mode support)
- `import` & `require` YAML files in frontend apps with Bun's bundler

Expand Down Expand Up @@ -121,6 +121,175 @@ try {
}
```

### `Bun.YAML.stringify()`

Convert a JavaScript value into a YAML string. The signature matches `JSON.stringify`:

```ts
YAML.stringify(value, replacer?, space?)
```

- `value`: the value to convert to YAML
- `replacer`: not supported. Pass `null` or `undefined`. Any other value throws an error.
- `space`: the number of spaces for each level of indentation (for example `2`), or a string to use as the indentation. Bun clamps a number to the range 0 to 10 and uses the first 10 characters of a string. YAML indentation must be spaces, so use a string of spaces.

Without `space`, Bun writes flow-style YAML on one line. With `space`, Bun writes block-style YAML on multiple lines.

#### Basic Usage

```ts
import { YAML } from "bun";

const data = {
name: "John Doe",
age: 30,
hobbies: ["reading", "coding"],
};

// Without space: flow style (single line)
console.log(YAML.stringify(data));
// {name: John Doe,age: 30,hobbies: [reading,coding]}

// With space: block style (multiple lines)
console.log(YAML.stringify(data, null, 2));
// name: John Doe
// age: 30
// hobbies:
// - reading
// - coding
```

Arrays follow the same rule:

```ts
const arr = [1, 2, 3];

console.log(YAML.stringify(arr));
// [1,2,3]

console.log(YAML.stringify(arr, null, 2));
// - 1
// - 2
// - 3
```

#### String Quoting

`YAML.stringify()` double-quotes a string when a YAML parser would otherwise read it as something else:

- Strings that YAML reads as keywords (`true`, `false`, `null`, `yes`, `no`, `~`, and so on)
- Strings that YAML reads as numbers
- Empty strings, and strings with special characters or escape sequences

```ts
const examples = {
keyword: "true",
number: "123",
text: "hello world",
empty: "",
};

console.log(YAML.stringify(examples, null, 2));
// keyword: "true"
// number: "123"
// text: hello world
// empty: ""
```

#### Cycles and References

`YAML.stringify()` writes an object that appears more than once as a YAML anchor (`&name`) and aliases (`*name`). Circular references use the same syntax:

```ts
const obj: Record<string, unknown> = { name: "root" };
obj.self = obj; // Circular reference

console.log(YAML.stringify(obj, null, 2));
// &root
// name: root
// self:
// *root

// Objects with shared references
const shared = { id: 1 };
const data = {
first: shared,
second: shared,
};

console.log(YAML.stringify(data, null, 2));
// first:
// &first
// id: 1
// second:
// *first
```

`Bun.YAML.parse()` reads the anchors back, so the parsed objects share identity again.

#### Special Values

```ts
// Special numeric values
console.log(YAML.stringify(Infinity)); // .inf
console.log(YAML.stringify(-Infinity)); // -.inf
console.log(YAML.stringify(NaN)); // .nan
console.log(YAML.stringify(0)); // 0
console.log(YAML.stringify(-0)); // -0

// null and undefined
console.log(YAML.stringify(null)); // null
console.log(YAML.stringify(undefined)); // undefined (the return value is undefined, not a string)

// Booleans
console.log(YAML.stringify(true)); // true
console.log(YAML.stringify(false)); // false
```

#### Complex Objects

```ts
const config = {
server: {
port: 3000,
host: "localhost",
ssl: {
enabled: true,
cert: "/path/to/cert.pem",
key: "/path/to/key.pem",
},
},
database: {
connections: [
{ name: "primary", host: "db1.example.com" },
{ name: "replica", host: "db2.example.com" },
],
},
features: {
auth: true,
"rate-limit": 100,
},
};

console.log(YAML.stringify(config, null, 2));
// server:
// port: 3000
// host: localhost
// ssl:
// enabled: true
// cert: /path/to/cert.pem
// key: /path/to/key.pem
// database:
// connections:
// - name: primary
// host: db1.example.com
// - name: replica
// host: db2.example.com
// features:
// auth: true
// rate-limit: 100
```

---

## Module Import
Expand Down
16 changes: 11 additions & 5 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1548,8 +1548,9 @@ declare module "bun" {
* const cycle = {};
* cycle.obj = cycle;
* console.log(YAML.stringify(cycle, null, 2));
* // &1
* // obj: *1
* // &root
* // obj:
* // *root
* ```
*/
export function stringify(input: unknown, replacer?: undefined | null, space?: string | number): string;
Comment thread
robobun marked this conversation as resolved.
Expand All @@ -1564,8 +1565,9 @@ declare module "bun" {
* - `render()` — render with custom callbacks for each element
* - `react()` — parse to React-compatible JSX elements
*
* Supports GFM extensions (tables, strikethrough, task lists, autolinks) and
* component overrides to replace default HTML tags with custom components.
* Supports GFM extensions (tables, strikethrough, task lists), md4c's
* permissive autolinks, and component overrides to replace default HTML tags
* with custom components.
*
* @example
* ```tsx
Expand Down Expand Up @@ -1627,7 +1629,11 @@ declare module "bun" {
tagFilter?: boolean;
/**
* Enable autolinks. Pass `true` to enable all autolink types (URL, WWW, email),
* or an object to enable individually.
* or an object to enable individually. Default: `false`.
*
* The rules are md4c's permissive autolinks, not the GFM autolink extension as
* cmark-gfm implements it. For example, `mailto:` URIs, URLs with a non-ASCII
* character, and URLs that directly follow a quote character are not linked.
*
* @example
* ```ts
Expand Down
33 changes: 31 additions & 2 deletions test/js/bun/yaml/yaml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2717,6 +2717,30 @@ config:
});

describe("stringify", () => {
// Editors show this example for `YAML.stringify`. It runs here as written, and each console.log()
// must print the `// ...` lines under it. A comment cannot show the space that follows `key:`
// in front of a nested block, so trailing spaces are not compared.
test("the example in bun.d.ts prints what its comments say", async () => {
const dts = await file(join(import.meta.dir, "../../../../packages/bun-types/bun.d.ts")).text();
const declaration = dts.indexOf("export function stringify(", dts.indexOf("namespace YAML {"));
const jsdoc = dts.slice(dts.lastIndexOf("/**", declaration), declaration).replace(/^ *\* ?/gm, "");
const example = /```ts\n([\s\S]*?)```/.exec(jsdoc)![1];

const documented: string[] = [];
const lines = example.split("\n");
for (let i = 0; i < lines.length; i++) {
if (!lines[i].startsWith("console.log(")) continue;
while (lines[i + 1]?.startsWith("// ")) documented.push(lines[++i].slice(3));
}

await using proc = Bun.spawn({ cmd: [bunExe(), "-e", example], env: bunEnv, stderr: "pipe" });
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(stdout.split("\n").map(line => line.trimEnd())).toEqual([...documented, ""]);
expect(exitCode).toBe(0);
});

// Basic data type tests
test("stringifies null", () => {
expect(YAML.stringify(null)).toBe("null");
Expand Down Expand Up @@ -3860,11 +3884,16 @@ config:
expect(parsed).toEqual([{ a: 1, c: 2 }, { y: 3 }, { valid: "data" }]);
});

// A Linux release build overflows between 30,000 and 50,000 levels, a debug or ASAN build below
// 5,000. The loop that builds 1,000,000 levels takes 2.8 s on a debug build, which put the second
// test over the 5 s limit in a whole-file run.
const overflowDepth = isDebug || isASAN ? 100_000 : 1_000_000;

test("handles stack overflow protection", () => {
// Create deeply nested structure approaching stack limit
let deep = {};
let current = deep;
for (let i = 0; i < 1000000; i++) {
for (let i = 0; i < overflowDepth; i++) {
current.next = {};
current = current.next;
}
Expand All @@ -3876,7 +3905,7 @@ config:
test("stack overflow protection in the write pass", () => {
let deep = {};
let current = deep;
for (let i = 0; i < 1000000; i++) {
for (let i = 0; i < overflowDepth; i++) {
current.next = {};
current = current.next;
}
Expand Down
Loading