mds fmt is an opinionated, safety-gated auto-formatter for MDS source. It
makes small, mechanical fixes to directive lines and the file's trailing edge —
and nothing else. Every rewrite is re-compiled and refused if it would change
the compiled output, so formatting a file can never alter what it produces.
Upgrading a legacy project? Run
mds lint --fixfirst to auto-migrate{x}→{{x}}interpolation syntax (thelegacy-interpolationrule), then runmds fmtto normalize formatting.mds fmtitself performs no legacy detection — the migration step is alwaysmds lint --fixfirst.
demo.mds is a complete, already-formatted template. It is checked
in fmt-clean, so mds fmt --check examples/formatting/demo.mds exits 0.
needs-formatting/ holds a deliberately-unformatted
fixture. mds fmt --check examples/formatting/needs-formatting/ exits 1
by design — that is the point. The fixture compiles cleanly but has trailing
whitespace on two directive lines and extra blank lines at EOF, which the
formatter removes. See needs-formatting/ (exits 1 by design).
- Line endings —
CRLF/CRnormalized toLF(everywhere except the verbatim body of@message/@define). - Directive lines — trailing whitespace stripped (e.g.
@if ready:→@if ready:). Only directive lines; see below. - Final newline — the file ends with exactly one
\n(missing one is added; extra trailing blank lines are trimmed). An empty or whitespace-only source formats to 0 bytes (empty output file).
- Body text — including Markdown hard breaks (two trailing spaces on a prose line survive) and whitespace-only lines.
- Interior blank lines — blank runs anywhere in the document are never collapsed (the old blank-collapse rule was removed in v0.4.0).
- Code fences and frontmatter internals — blank-line structure inside them
is untouched (only
CRLF→LFapplies). @message/@definebodies — copied completely verbatim, including trailing whitespace and evenCRLF.
All commands below are run from the repository root.
# Show the already-formatted demo is clean (exit 0, writes nothing)
mds fmt --check examples/formatting/demo.mds
# Show the deliberately-unformatted fixture would change (exit 1)
mds fmt --check examples/formatting/needs-formatting/
# Preview pending changes as a unified diff (writes nothing)
mds fmt --diff examples/formatting/needs-formatting/unformatted.mds
# Format every .mds under a directory, INCLUDING _partials
mds fmt examples/formatting/
# Read-only gate for CI: exit 1 if anything under the tree would change
mds fmt --check examples/
# Filter mode: format from stdin to stdout (creates no file)
printf '@if ready: \nGo\n@end\n' | mds fmt -needs-formatting/unformatted.mds is a template that compiles cleanly
(mds build examples/formatting/ exits 0) but has two formatting issues
that mds fmt fixes automatically:
-
Trailing whitespace on directive lines.
@if audience == "technical":and@endeach have three trailing spaces. The formatter strips them (R4) because the parser discards trailing whitespace on directive lines before matching — removing it is provably output-preserving. -
Extra trailing blank lines at EOF. Two blank lines follow
@end. The formatter trims the file's trailing edge to exactly one\n(R2), matching the compiler's own trailing-edge normalisation.
Running mds fmt --diff on the file produces this output (trailing spaces on
the -@if and -@end lines are present but invisible in most renderers):
$ mds fmt --diff examples/formatting/needs-formatting/unformatted.mds
--- examples/formatting/needs-formatting/unformatted.mds
+++ examples/formatting/needs-formatting/unformatted.mds
@@ -5,8 +5,6 @@
# {{role}} checklist
-@if audience == "technical":
+@if audience == "technical":
Include code samples and API references.
-@end
-
-
+@end--diff writes to stdout and exits 0 (preview only, no files written).
Running the formatter a second time produces no changes. Verify using the stdin path so no file on disk is modified:
$ mds fmt - < examples/formatting/needs-formatting/unformatted.mds | mds fmt --check -Exit 0, no output — the formatted result is already clean, so the second pass
finds nothing to change.
$ mds build examples/formatting/needs-formatting/
Compiled to examples/formatting/needs-formatting/unformatted.md
1 built, 0 failedExit 0 — formatting issues are invisible to the compiler. The fixture is valid
MDS; only mds fmt cares about trailing whitespace or extra blank lines.
mds fmt - formats standard input and writes the result to standard output. No
file is created or modified:
$ printf '@if ready: \nGo\n@end\n' | mds fmt -
@if ready:
Go
@endTrailing whitespace is stripped from the directive line; exactly one final newline is emitted. The idempotence check above uses this path to verify a second pass finds nothing to change.
Every change the formatter applies is re-compiled and compared against the
original compiled output. If they ever diverged, the write would be refused
with mds::formatter_invariant — naming the file — rather than silently
writing a file that compiles differently.
Because every rule the formatter applies is provably output-preserving, valid MDS source can never trip the gate. The gate exists to catch formatter bugs: if a future change accidentally altered compiled output, the write would be refused. Triggering it requires a bug in the formatter, not a bug in the template — no fixture demonstrates it.
| Situation | Exit |
|---|---|
| Formatted OK / already clean / diff preview | 0 |
--check found a file that would change, or a syntax / safety-gate error |
1 |
File not found / not .mds / I/O / invalid UTF-8 |
2 |
| Source exceeds the size cap | 3 |
mds fmt --check examples/formatting/needs-formatting/ exits 1 by design —
unformatted.mds is intentionally left unformatted to demonstrate the formatter.
mds fmt --check examples/formatting/ and mds fmt --check examples/ also exit
1 for the same reason.
Formatted content (stdin filter mode) and --diff output go to stdout; all
status lines, summaries, and errors go to stderr. -q / --quiet suppresses
status and summaries but never errors, and never changes exit codes.