| Crates.io | md-formatter |
| lib.rs | md-formatter |
| version | 0.4.1 |
| created_at | 2025-11-29 18:30:32.530039+00 |
| updated_at | 2025-12-01 00:56:17.04794+00 |
| description | A fast, opinionated Markdown formatter |
| homepage | https://github.com/rewdy/md-formatter |
| repository | https://github.com/rewdy/md-formatter |
| max_upload_size | |
| id | 1957189 |
| size | 194,322 |

The md-formatter ("mad formatter") is a fast, opinionated Markdown formatter written in Rust.
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.
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
# Install from crates.io
cargo install md-formatter
# Or build from source
cargo build --release
./target/release/mdfmt --help
# npm
npm install @rewdy/md-formatter
# pnpm
pnpm add @rewdy/md-formatter
# bun
bun add @rewdy/md-formatter
# 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 -
# Use glob patterns
mdfmt "**/*.md"
# Format files in a specific directory
mdfmt docs/
# Multiple paths
mdfmt src/ tests/ README.md
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-excludes
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) |
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 |
# Pre-commit hook
mdfmt . --check
# CI pipeline
mdfmt . --check || exit 1
# Format only changed files
git diff --name-only -- '*.md' | xargs mdfmt --write
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.
{
"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"
}
}
- name: Check formatting
run: |
pnpm biome format .
pnpm mdfmt . --check
{
"lint-staged": {
"*.{js,ts,json}": ["biome check --write"],
"*.md": ["mdfmt --write"]
}
}
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);
--wrap mode)# Heading format)-, ordered with --ordered-list mode, with nesting)> prefix per depth)---)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.
| Scenario | Time | Throughput |
|---|---|---|
| 360KB file | 4ms | ~90MB/s |
| Average file (2KB) | <1ms | Instant |
Input Markdown
↓
Extract Frontmatter (if present)
↓
Parse to Event Stream (pulldown-cmark)
↓
Format Events (state machine with hard breaks)
↓
Prepend Frontmatter
↓
Output Markdown
The formatter never parses the output, so idempotence is guaranteed by design.
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 version
Default exclusions: node_modules, target, .git, vendor, dist, build
# Run all tests
cargo test --release --lib
# Run specific test
cargo test --release --lib test_idempotence -- --nocapture
# Build release binary
cargo build --release
Current status: 25 unit tests passing ✓
--width, --wrap, and --ordered-list options supported (by design)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)
Version: 0.1.1
MVP: Complete ✓
Tests: 25/25 passing ✓
Idempotence: Verified ✓
Performance: Excellent ✓
See STATUS.md for detailed feature matrix and quality metrics.
As long as changes are conceptually in-line with the project, I welcome all contributions. 😄
MIT