mdast-util-to-markdown
Serializes an mdast syntax tree back into CommonMark-compatible markdown text, with pluggable handlers for custom syntax extensions.
Repository Health
Technical Analysis
mdast-util-to-markdown takes an mdast syntax tree — the abstract syntax tree format used across the unified/remark ecosystem — and turns it back into serialized markdown text. It handles the low-level mechanics of markdown generation: choosing safe escape characters so content doesn’t accidentally form new syntax, tracking line/column position for wrapped text, and picking consistent stylistic choices like quote characters, list bullets, and fence styles.
The package is intentionally low-level; it’s the engine underneath remark-stringify, which most projects use for an easier, plugin-based interface. Its real value is extensibility: syntax extensions like mdast-util-gfm, mdast-util-mdx, mdast-util-frontmatter, mdast-util-math, and mdast-util-directive all register custom node handlers, escaping rules, and block-join behavior through this package’s configure() merge step, without needing to fork or reimplement the core serializer.
What You Get
- A single
toMarkdown(tree, options)function that serializes any mdastRootor node into markdown with sensible CommonMark defaults and no required configuration - Default handlers (
defaultHandlers) for every core mdast construct — headings, lists, links, emphasis, code, blockquotes, and more - An extension system (
extensions,handlers,unsafe,join) that lets other packages register new node types and escaping/joining rules without modifying this package - Built-in safe-character escaping so arbitrary text content never accidentally produces unintended markdown syntax
- Exhaustive TypeScript types (
Handle,Info,Tracker,Unsafe,SafeConfig,ConstructNameMap) for authoring your own syntax extensions
Common Use Cases
- Programmatically generating or rewriting markdown files from structured data or a modified AST
- Building a markdown formatter/linter (e.g. as part of a remark plugin pipeline)
- Round-tripping markdown through
mdast-util-from-markdownand back after AST transformations - Adding support for custom or non-standard markdown syntax (MDX, GFM tables, directives, math) via the extension API
- Powering higher-level tools like
remark-stringifythat expose a simpler, plugin-driven markdown-generation interface
Under The Hood
Architecture
The library is organized as a layered compiler: toMarkdown() in lib/index.js builds a State object (handlers, unsafe patterns, join rules, position tracker) and passes it through configure() in lib/configure.js, which merges any user-supplied Options/extensions — custom handlers, unsafe patterns, join rules — into that base state before dispatch begins. Node-type dispatch itself is delegated to zwitch, a tiny type-based visitor keyed on each mdast node’s type field, which invokes one handler module per construct under lib/handle/ (link.js, heading.js, code.js, and so on), each composing lower-level lib/util/ helpers (safe.js for escaping, track.js for line/column bookkeeping, container-flow.js/container-phrasing.js for recursing into children, check-*.js for stylistic decisions like quote or bullet characters). The design is cleanly modular: the core dispatcher has zero knowledge of individual constructs, so registering a new node type — as mdast-util-gfm or mdast-util-mdx do — is purely additive through configure(), with no changes required to the dispatcher itself.
Tech Stack
Pure ESM (type: module), with no bundler or build step beyond TypeScript type-checking of hand-authored JSDoc types (tsc --build plus a hand-written index.d.ts), targeting Node.js 16+ and browsers via esm.sh. Runtime dependencies are entirely from the unified/syntax-tree ecosystem: zwitch for dispatch, longest-streak for fence-length calculation, unist-util-visit for tree traversal, mdast-util-phrasing/mdast-util-to-string for node classification, and micromark-util-classify-character/micromark-util-decode-string shared with the micromark tokenizer. Dev tooling pairs xo (an ESLint preset) and prettier with remark-cli/remark-preset-wooorm to self-lint its own README, plus c8 for coverage and type-coverage to enforce full JSDoc type coverage — there’s no framework or runtime beyond Node/the browser, since this is a leaf utility consumed by higher-level packages like remark-stringify.
Code Quality
Testing is thorough: a single test/index.js file of roughly 5,000 lines and 370+ assertions runs via Node’s built-in test runner (node --conditions development test/index.js), enforced at 100% statement coverage via c8 --100. Error handling is minimal by design given the small, pure-function nature of most modules, but the core dispatcher’s invalid/unknown handlers throw clear, descriptive errors for malformed or unrecognized node types rather than failing silently. Naming is consistent and semantic throughout (checkQuote, formatLinkAsAutolink, containerPhrasing), type safety is enforced via strict JSDoc annotations checked against a hand-maintained .d.ts file with type-coverage --at-least 100 --strict, and formatting/linting is automated via xo and prettier.
API Design
The public surface is deliberately tiny: two named exports (toMarkdown, defaultHandlers), no required configuration, and toMarkdown(tree) works immediately with sensible CommonMark defaults. Extensibility is achieved without subclassing or inheritance — consumers pass a plain options object with extensions/handlers/unsafe/join fields that configure() merges into internal state, which is exactly how the wider mdast-util ecosystem (GFM, MDX, frontmatter, math, directives) plugs in new syntax without forking this package. Documentation is unusually exhaustive for a utility this size — every exported TypeScript type is documented with parameter and field tables — though authoring a new extension handler requires understanding the Tracker/Info/safe() escaping contract, which carries a real learning curve beyond simple consumption.
Used by 6 apps in this directory
Kuku
Note Taking
A local-first, open-source Markdown knowledge workspace for macOS — plain files, personal wiki and Second Brain workflows, AI-assisted diffs, and encrypted sync, built as an Obsidian alternative.
LobeHub
AI Assistants · Productivity · Automation
Your Chief Agent Operator — build, schedule, and collaborate with an entire AI team in one self-hostable workspace.
OneUptime
Monitoring
The complete open-source observability platform that replaces PagerDuty, Datadog, Sentry, and StatusPage with a single self-hostable system.
Supabase
Developer Tools · Databases · Search
The open-source Postgres development platform that replaces Firebase with authentication, real-time APIs, edge functions, storage, and vector embeddings — all built on PostgreSQL.
TinaCMS
CMS
An open-source, Git-backed headless CMS that gives editors a live visual editing UI over Markdown, MDX, JSON, and YAML content while developers keep everything in version control.
Zulip
Team Chat
Topic-based team chat that brings the structure of email threads to real-time messaging, so distributed teams never lose context across hundreds of concurrent conversations.