mdast-util-to-markdown

Serializes an mdast syntax tree back into CommonMark-compatible markdown text, with pluggable handlers for custom syntax extensions.

Library
npm
v2.1.2
140stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
48/100Fair
Development Activity8
Maintenance44
Community60
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
81/100Excellent
Architecture90
Code Quality90
Innovation85
Learning Curve60

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 mdast Root or 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-markdown and 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-stringify that 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

Rust
47%
MIT

Kuku

Note Taking

219

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.

View details
81
Repo Health
67
Technical
67
Dependency
Built with
Rust47%
TypeScript40%
Updated yesterday
TypeScript
99%
Other

LobeHub

AI Assistants · Productivity · Automation

82,273

Your Chief Agent Operator — build, schedule, and collaborate with an entire AI team in one self-hostable workspace.

View details
92
Repo Health
81
Technical
69
Dependency
Built with
TypeScript99%
Updated today
TypeScript
94%
Apache 2.0

OneUptime

Monitoring

7,563

The complete open-source observability platform that replaces PagerDuty, Datadog, Sentry, and StatusPage with a single self-hostable system.

View details
90
Repo Health
81
Technical
65
Dependency
Built with
TypeScript94%
Updated today
TypeScript
71%
Apache 2.0

Supabase

Developer Tools · Databases · Search

108,912

The open-source Postgres development platform that replaces Firebase with authentication, real-time APIs, edge functions, storage, and vector embeddings — all built on PostgreSQL.

View details
90
Repo Health
91
Technical
62
Dependency
Built with
TypeScript71%
MDX26%
Updated today
TypeScript
98%
Apache 2.0

TinaCMS

CMS

13,780

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.

View details
92
Repo Health
74
Technical
59
Dependency
Built with
TypeScript98%
Updated today
Python
57%
Apache 2.0

Zulip

Team Chat

25,855

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.

View details
96
Repo Health
86
Technical
61
Dependency
Built with
Python57%
TypeScript19%
JavaScript10%
Updated yesterday

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search