# brookmd — full documentation bundle Generated by web/scripts/build-llms.mjs. Do not edit by hand. Short index: https://md.hsingh.app/llms.txt ============================================================================== # Source: packages/brookmd/README.md ============================================================================== # brookmd Zero-dep streaming markdown for the browser. Rust→WASM core, one Web Worker per stream, incremental parse with speculative closure for mid-stream constructs. Drop in a streaming-aware renderer — **React, Vue, Svelte, Solid, a framework-agnostic `` Web Component, or the vanilla DOM mount** — wire each LLM stream to a `BrookClient`, and the markdown renders incrementally off the main thread, block by block, with stable identities so unchanged blocks never re-reconcile. Parsing runs entirely **off the main thread** — each stream gets its own pooled Web Worker, so many concurrent LLM responses render without contending for the UI thread. On each token the parser re-parses only the **active tail**, not the whole document; patches cross the worker boundary as **verified splices** (not full re-sends, so emitted bytes stay O(n) even for one giant growing block); and heavy renderers (math, mermaid) are **deferred until a block closes** — code fences highlight as they stream, re-tokenizing only the last line rather than the whole block per chunk. The result is low retained memory and a main thread that stays responsive while streaming. See [the live demo](https://md.hsingh.app/). > **Beyond the browser:** the same Rust core also powers experimental React > Native, Swift (iOS/macOS), Kotlin/Android, Flutter, and C-ABI bindings — > every platform speaks the same versioned wire, byte-for-byte. See the > [platform matrix](https://github.com/siinghd/brookmd#platforms) in the > repository README. ## Install ```bash bun add brookmd # or: npm i brookmd / pnpm add brookmd ``` brookmd ships **compiled, non-minified ESM** (`dist/*.js` + `.d.ts` types) plus the compiled WASM — no raw `.ts`/`.tsx` source. The worker and WASM asset are referenced with the **web-standard `new URL(asset, import.meta.url)`** pattern, so any bundler with asset-module support resolves them: **Vite** (the reference setup), **webpack 5**, **Rollup** (with asset modules), **Parcel**, and **Next.js** (App Router — Turbopack *and* webpack; **verified on Next.js 16**, see the [Next.js callout](#nextjs) below). The streaming client (`` / `BrookClient`) is **browser-only** (it constructs Web Workers). For **server-side / static rendering of finished content** — SSR, React Server Components, build steps — use the worker-free, synchronous [`brookmd/server`](#server-side-rendering) entry. The framework packages — `react`, `vue`, `svelte`, `solid-js` — are all **optional** peer dependencies; you only need the one whose binding you import. The framework-free entries (`brookmd/client`, `brookmd/dom`, `brookmd/element`, and `brookmd/server`) need none. (The bare `brookmd` entry re-exports the React component surface, so it pulls `react` — import from `brookmd/client` if you want a framework-free core.) > **Vite — one-line config.** Vite's dependency pre-bundling (esbuild) hoists > the wasm-bindgen glue into `.vite/deps/`, which breaks the relative > `new URL("…_bg.wasm", import.meta.url)` lookup so the worker can't load WASM > (you'll see a 404 / "magic word" error). Exclude brookmd from pre-bundling: > > ```ts > // vite.config.ts > export default defineConfig({ > optimizeDeps: { exclude: ["brookmd"] }, > }); > ``` > > No other bundler needs this — it's specific to Vite's optimizer. > **Next.js (App Router) — one requirement.** Works on **Next.js** with > **Turbopack** (the default for both `next dev` and `next build`) or webpack. > Since 0.17.0 brookmd ships **compiled ESM**, so **no `transpilePackages` or > other build config is needed** — earlier versions required it only because the > package shipped raw TypeScript, which Next does not compile inside > `node_modules`. That no longer applies. > > **Use it from a Client Component.** `` uses React hooks (and > spawns a Web Worker on mount), so it must carry `"use client"` — it can't be > a Server Component. (It is still SSR-safe: on the server it renders an empty > shell and only starts streaming after hydration, so there's no SSR crash — > the constraint is hooks, not the worker.) > > ```tsx > "use client"; > import { BrookMarkdown } from "brookmd/react"; > > export default function Answer({ stream }: { stream: AsyncIterable }) { > return ; > } > ``` > > **Create the `stream` in Client Component code, not in a Server Component.** > A `Response` / `ReadableStream` / `AsyncIterable` isn't serializable, so it > can't be passed as a prop from a Server Component (e.g. `page.tsx`) — that > throws *"Only plain objects can be passed to Client Components."* Pass a > serializable prop (a URL, the chat messages) from the server and open the > stream on the client — e.g. `stream={await fetch("/api/chat")}` from a client > effect, or the `useBrookStream` hook (see [Quick start](#quick-start)). > > That's it — Turbopack bundles the worker and emits the `.wasm` to > `_next/static/media` itself, so no extra asset/loader config is needed (and the > Vite `optimizeDeps` workaround above does **not** apply). Both `next dev` and > `next build && next start` are verified to spawn the worker, load the WASM, and > stream markdown. _Dev tip:_ open the app on `localhost` — Next dev blocks > cross-origin dev resources (HMR, chunks) from other hosts (e.g. `127.0.0.1`) > unless you add them to `allowedDevOrigins` in `next.config`. ## Quick start ```ts import { BrookClient, BrookMarkdown } from "brookmd"; // One client per stream. Spawns a Web Worker that owns a Rust parser. const client = new BrookClient(); // Feed chunks as they arrive from your SSE / fetch reader. for await (const delta of streamFromAi()) { client.append(delta); } client.finalize(); ``` In React — pass the stream straight to ``. It owns the client, pipes the stream, supersedes it if it changes, and cleans up on unmount: ```tsx import { BrookMarkdown } from "brookmd/react"; export function ChatMessage({ stream }: { stream: AsyncIterable }) { return ; } ``` `stream` accepts an `AsyncIterable` (e.g. SSE deltas), a `Response`, or a `ReadableStream` — so `` works too. Need the client handle (for `outline()` / `getMetrics()` / a shared client)? Use the `useBrookStream` hook — same lifecycle, returns the owned client: ```tsx import { BrookMarkdown, useBrookStream } from "brookmd/react"; export function ChatMessage({ stream }: { stream: AsyncIterable }) { const client = useBrookStream(stream); return ; } ``` ### Chat UI defaults Five flags an LLM chat UI almost always wants — each off by default because the library's default is strict CommonMark, not because it is the better choice here: ```tsx import { getDefaultPool } from "brookmd"; import { BrookMarkdown, useBrookStream } from "brookmd/react"; import { useEffect } from "react"; // Hoist the config and the overrides — a fresh object each render busts the // per-block memo, so every block re-renders on every patch. const chatConfig = { softBreaks: true, // a lone \n renders as
— models write chat prose, not CommonMark dirAuto: true, // per-block dir="auto", so an Arabic answer renders RTL beside an English one a11y: true, // task-list