@rewdy/md-formatter
v0.4.1
Published
A fast, opinionated Markdown formatter written in Rust
Maintainers
Readme
md-formatter

The md-formatter ("mad formatter") is a fast, opinionated Markdown formatter written in Rust.
Why/Approach
Many now use modern tools for linting and formatting node code (biome, oxlint, etc), but these do not support formatting markdown. This tools is meant to provide a rust-based formatter for markdown only. The approach it takes is to parse the markdown with Rust's pulldown-cmark, then pump it back out with opinionated formatting. For simplicity, it explicitly ignores code blocks (for now), tables are handled minimally, and otherwise is pretty rudimentary.
Quick Start
Fast - Formats 360KB of Markdown in 4ms (~90MB/s)
Opinionated - Minimal configuration (--width, --wrap, --ordered-list)
Idempotent - format(format(x)) == format(x) guaranteed
Safe - Uses hard breaks to preserve structure across re-parsing
Complete - All CommonMark + GFM elements supported
Installation
Rust (CLI)
# Install from crates.io
cargo install md-formatter
# Or build from source
cargo build --release
./target/release/mdfmt --helpNode.js
# npm
npm install @rewdy/md-formatter
# pnpm
pnpm add @rewdy/md-formatter
# bun
bun add @rewdy/md-formatterUsage
CLI (Rust)
# Format all markdown files in current directory (prints to stdout)
mdfmt .
# Format all markdown files in-place
mdfmt . --write
# Check if all files are formatted (for CI)
mdfmt . --check
# Format a specific file
mdfmt README.md
# Format multiple files or directories
mdfmt src/ docs/ README.md
# Custom line width
mdfmt . --width 100
# Read from stdin
cat file.md | mdfmt -Glob Patterns
# Use glob patterns
mdfmt "**/*.md"
# Format files in a specific directory
mdfmt docs/
# Multiple paths
mdfmt src/ tests/ README.mdExclusions
By default, mdfmt excludes common directories: node_modules, target, .git, vendor, dist, build.
# Add additional exclusions
mdfmt . --exclude my-vendor --exclude tmp
# Include everything (no default exclusions)
mdfmt . --no-default-excludesProse Wrapping
Control how prose (paragraph text) is wrapped with the --wrap option:
# Reflow prose to fit line width (default: 80)
mdfmt . --wrap always
# Unwrap prose into single lines per paragraph
mdfmt . --wrap never
# Keep existing line breaks (default)
mdfmt . --wrap preserve| Mode | Description |
|------|-------------|
| always | Reflow text to fit within line width |
| never | Unwrap each paragraph to a single long line |
| preserve | Leave existing line breaks unchanged (default) |
Ordered Lists
Control how ordered list items are numbered with the --ordered-list option:
# Renumber items sequentially (default)
mdfmt . --ordered-list ascending
# Use 1. for all items
mdfmt . --ordered-list one| Mode | Description |
|------|-------------|
| ascending | Renumber items sequentially: 1, 2, 3, ... (default) |
| one | Use 1. for all items |
Integration
# Pre-commit hook
mdfmt . --check
# CI pipeline
mdfmt . --check || exit 1
# Format only changed files
git diff --name-only -- '*.md' | xargs mdfmt --writeNode.js Integration
The npm package includes the mdfmt binary, making it easy to add markdown formatting to your existing Node.js toolchain alongside Biome, ESLint, or other tools.
package.json Scripts
{
"scripts": {
"format": "biome format --write . && mdfmt . --write",
"format:check": "biome format . && mdfmt . --check",
"format:md": "mdfmt . --write",
"format:md:check": "mdfmt . --check",
"lint": "biome lint .",
"check": "biome check . && mdfmt . --check"
}
}CI Example (GitHub Actions)
- name: Check formatting
run: |
pnpm biome format .
pnpm mdfmt . --checkWith Husky/lint-staged
{
"lint-staged": {
"*.{js,ts,json}": ["biome check --write"],
"*.md": ["mdfmt --write"]
}
}Programmatic API
For advanced use cases, you can also use the formatter programmatically:
import { formatMarkdown, checkMarkdown } from '@rewdy/md-formatter';
// Format a string
const formatted = formatMarkdown(input, {
width: 80,
wrap: 'preserve',
orderedList: 'ascending'
});
// Check if formatted (returns boolean)
const isFormatted = checkMarkdown(input);Formatting Rules
Supported Elements
- Paragraphs (line breaks controlled by
--wrapmode) - Headings (normalized to
# Headingformat) - Lists (unordered
-, ordered with--ordered-listmode, with nesting) - Blockquotes (with
>prefix per depth) - Code blocks (fenced, language tags preserved)
- Inline code, emphasis, links
- Horizontal rules (normalized to
---) - Frontmatter (YAML blocks preserved)
- GFM strikethrough and autolinks
Design Philosophy
Uses hard breaks (two spaces + newline) instead of soft breaks to ensure
idempotence. This prevents Markdown parsers from reinterpreting wrapped lines as
soft breaks on re-parsing.
Performance
| Scenario | Time | Throughput | | ----------------- | ------- | ---------- | | 360KB file | 4ms | ~90MB/s | | Average file (2KB)| <1ms | Instant |
Architecture
Input Markdown
↓
Extract Frontmatter (if present)
↓
Parse to Event Stream (pulldown-cmark)
↓
Format Events (state machine with hard breaks)
↓
Prepend Frontmatter
↓
Output MarkdownThe formatter never parses the output, so idempotence is guaranteed by design.
CLI Options
Usage: mdfmt [OPTIONS] [PATH]...
Arguments:
[PATH]... Files or directories to format (supports glob patterns, use - for stdin)
Options:
-w, --write Write formatted output to file in-place
--check Check if files are formatted (exit with 1 if not)
--stdin Read from stdin
--width <WIDTH> Line width for wrapping [default: 80]
--wrap <MODE> How to wrap prose: always, never, preserve [default: preserve]
--ordered-list <MODE> How to number ordered lists: ascending, one [default: ascending]
--exclude <DIR> Additional directories to exclude
--no-default-excludes Don't exclude any directories by default
-h, --help Print help
-V, --version Print versionDefault exclusions: node_modules, target, .git, vendor, dist, build
Testing
# Run all tests
cargo test --release --lib
# Run specific test
cargo test --release --lib test_idempotence -- --nocapture
# Build release binary
cargo build --releaseCurrent status: 25 unit tests passing ✓
Known Limitations
- Autolinks - Converted to regular links (parser limitation)
- Configuration - Only
--width,--wrap, and--ordered-listoptions supported (by design) - MDX - Not supported (different language)
Project Structure
src/
├── main.rs - CLI entry point and file I/O
├── cli.rs - Argument parsing (clap)
├── formatter.rs - Core formatting logic (~430 lines)
├── parser.rs - Markdown parsing and frontmatter extraction
└── lib.rs - Public API and unit tests (14 tests)Status
Version: 0.1.1
MVP: Complete ✓
Tests: 25/25 passing ✓
Idempotence: Verified ✓
Performance: Excellent ✓
See STATUS.md for detailed feature matrix and quality metrics.
Contributing
As long as changes are conceptually in-line with the project, I welcome all contributions. 😄
License
MIT
