From f49164e6eb98cf6feb3ae71af8a9380a7b0f5b92 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:14:42 +0000 Subject: [PATCH 1/2] docs, bun-types: restore the YAML.stringify section, document markdown.ansi, correct two JSDoc statements - docs/runtime/yaml.mdx: restore the Bun.YAML.stringify() section that #22921 added to docs/api/yaml.md and the docs move (#24201) dropped. - docs/runtime/markdown.mdx: the page listed three APIs. Bun.markdown.ansi() is the fourth. Add it to the list and add a section for it. - docs/runtime/markdown.mdx, bun.d.ts: autolinks follow md4c's permissive autolink rules, not the cmark-gfm autolink extension. Say so, and list the inputs that cmark-gfm links and Bun does not. - bun.d.ts: the YAML.stringify cycle sample showed &1 / *1. The output is &root / *root. --- docs/runtime/markdown.mdx | 51 ++++++++++- docs/runtime/yaml.mdx | 171 +++++++++++++++++++++++++++++++++++- packages/bun-types/bun.d.ts | 16 ++-- 3 files changed, 231 insertions(+), 7 deletions(-) diff --git a/docs/runtime/markdown.mdx b/docs/runtime/markdown.mdx index 3a30fb9990bd..95f7d4bf607a 100644 --- a/docs/runtime/markdown.mdx +++ b/docs/runtime/markdown.mdx @@ -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. -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 @@ -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 `` or `[text](url)` syntax. + #### Heading IDs Pass `true` to enable both heading IDs and autolink headings, or an object for granular control: @@ -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. diff --git a/docs/runtime/yaml.mdx b/docs/runtime/yaml.mdx index 631d4ef41c38..156e5b67d73a 100644 --- a/docs/runtime/yaml.mdx +++ b/docs/runtime/yaml.mdx @@ -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 @@ -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 = { 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 diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 1cef2b84928f..8a3a63c47789 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -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; @@ -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 @@ -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 From 3aff0b210fc85ce63638eb705c580c0ba9555d0d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:46:49 +0000 Subject: [PATCH 2/2] test(yaml): run the bun.d.ts example for YAML.stringify and compare it with its comments The test takes the @example block from the JSDoc of YAML.stringify, runs it with bun -e, and compares stdout with the comment lines under each console.log(). It fails with the bun.d.ts of main, which shows &1 / *1. Two older tests build a 1,000,000-deep chain. On debug and ASAN builds they now build 100,000 levels: the loop takes 2.8 s there, and the second test hit the 5 s limit in a whole-file run. Those builds overflow below 5,000 levels. --- test/js/bun/yaml/yaml.test.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/test/js/bun/yaml/yaml.test.ts b/test/js/bun/yaml/yaml.test.ts index d3a8ac84eda5..7264aef0be59 100644 --- a/test/js/bun/yaml/yaml.test.ts +++ b/test/js/bun/yaml/yaml.test.ts @@ -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"); @@ -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; } @@ -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; }