Hongdown ======== [![crates.io][crates.io badge]][crates.io] [![npm][npm badge]][npm] [![@hongdown/wasm][@hongdown/wasm badge]][@hongdown/wasm] [![GitHub Actions][GitHub Actions badge]][GitHub Actions] Hongdown is a Markdown formatter that enforces [Hong Minhee's Markdown style conventions](./STYLE.md). The formatter is implemented in Rust using the [Comrak] library for parsing. It produces consistently formatted Markdown output following a distinctive style used across multiple projects including [Fedify], [LogTape], and [Optique]. [crates.io badge]: https://img.shields.io/crates/v/hongdown?logo=rust [crates.io]: https://crates.io/crates/hongdown [npm badge]: https://img.shields.io/npm/v/hongdown?logo=npm [npm]: https://www.npmjs.com/package/hongdown [@hongdown/wasm badge]: https://img.shields.io/npm/v/@hongdown/wasm?logo=webassembly&label=%40hongdown%2Fwasm [@hongdown/wasm]: https://www.npmjs.com/package/@hongdown/wasm [GitHub Actions badge]: https://github.com/dahlia/hongdown/actions/workflows/main.yaml/badge.svg [GitHub Actions]: https://github.com/dahlia/hongdown/actions/workflows/main.yaml [Comrak]: https://comrak.ee/ [Fedify]: https://fedify.dev/ [LogTape]: https://logtape.org/ [Optique]: https://optique.dev/ Installation ------------ ### Windows Package Manager ~~~~ powershell winget install HongMinhee.Hongdown ~~~~ ### Scoop ~~~~ powershell scoop bucket add extras scoop install hongdown ~~~~ ### npm ~~~~ bash npm install -g hongdown ~~~~ ### mise ~~~~ bash mise use -g aqua:dahlia/hongdown ~~~~ ### Nix ~~~~ bash nix run github:dahlia/hongdown ~~~~ ### Cargo ~~~~ bash cargo install hongdown ~~~~ ### Pre-built binaries Pre-built binaries for Linux, macOS, and Windows are available on the [GitHub Releases] page. [GitHub Releases]: https://github.com/dahlia/hongdown/releases Usage ----- ### Basic usage ~~~~ bash # Format a file and print to stdout hongdown input.md # Format a file in place hongdown --write input.md hongdown -w input.md # Format multiple files hongdown -w *.md # Format all Markdown files in a directory (recursive) hongdown -w . hongdown -w docs/ # Check if files are formatted (exit 1 if not) hongdown --check input.md hongdown -c input.md # Show diff of formatting changes hongdown --diff input.md hongdown -d input.md # Read from stdin (use --stdin flag or - as filename) echo "# Hello" | hongdown --stdin echo "# Hello" | hongdown - hongdown --stdin < input.md hongdown - < input.md # Custom line width hongdown --line-width 100 input.md # Disable word wrapping entirely hongdown --no-line-width input.md # Enable MDX mode for non-.mdx input (.mdx files enable it automatically) hongdown --mdx input.md ~~~~ ### HTML comment directives Hongdown supports special HTML comment directives to control formatting behavior within documents. #### Disable formatting You can disable formatting for specific sections: ~~~~ markdown This entire file will not be formatted. ~~~~ ~~~~ markdown This line preserves its spacing. The next line will be formatted normally. ~~~~ ~~~~ markdown Everything here is preserved as-is until the next heading (h1 or h2). Next heading ------------ This section will be formatted normally. ~~~~ ~~~~ markdown This section is not formatted. This section is formatted again. ~~~~ #### Sentence case customization When sentence case is enabled, you can define document-specific proper nouns and common nouns: ~~~~ markdown # Using Swift And Go For Python Development This heading will be formatted as: "Using Swift and Go for python development" ~~~~ - `` – Defines proper nouns to preserve (case-sensitive) - `` – Overrides built-in proper nouns by treating them as common nouns These directives are merged with configuration file settings. ### Configuration file Hongdown supports cascading configuration files from multiple locations. Configuration files are loaded and merged in the following order (lowest to highest priority): 1. **System-wide**: */etc/hongdown/config.toml* (Linux/Unix only) 2. **User (legacy)**: *~/.hongdown.toml* (all platforms) 3. **User (platform-specific)**: - Linux: *$XDG\_CONFIG\_HOME/hongdown/config.toml* or *~/.config/hongdown/config.toml* - macOS: *~/Library/Application Support/hongdown/config.toml* - Windows: *%APPDATA%\\hongdown\\config.toml* 4. **Project**: *.hongdown.toml* in the current directory or any parent directory Settings from higher-priority configurations override those from lower-priority ones. This allows you to set global defaults at the user or system level while overriding them for specific projects. You can also specify a configuration file explicitly with the `--config` option, which bypasses the cascading system and uses only that file. #### Disabling configuration inheritance To ignore all system and user configurations and use only your project config: ~~~~ toml no_inherit = true # Your project-specific settings line_width = 100 ~~~~ When `no_inherit = true`, only the project config and Hongdown's defaults are used. This is useful for projects that need strict formatting control regardless of user preferences. #### Configuration options Below is an example configuration with all available options and their default values: ~~~~ toml # File patterns (glob syntax) include = [] # Files to format (default: none, specify on CLI) exclude = [] # Files to skip (default: none) git_aware = true # Respect .gitignore and skip .git directory (default: true) # Formatting options line_width = 80 # Maximum line width (min: 8, default: 80); set to false to disable wrapping math = true # Preserve TeX math ($…$ and $$…$$) verbatim (default: true) mdx = false # MDX mode: preserve embedded JS/JSX verbatim (default: false) [heading] setext_h1 = true # Use === underline for h1 (default: true) setext_h2 = true # Use --- underline for h2 (default: true) sentence_case = false # Convert headings to sentence case (default: false) proper_nouns = [] # Additional proper nouns to preserve (default: []) common_nouns = [] # Exclude built-in proper nouns (default: []) anchor_align = 0 # Spaces before anchor {#id}: positive = exact count, # 0/negative = right-align to line_width+anchor_align (default: 0) [unordered_list] unordered_marker = "-" # "-", "*", or "+" (default: "-") leading_spaces = 1 # Spaces before marker (0–3, default: 1) trailing_spaces = 2 # Spaces after marker (0–3, default: 2) indent_width = 4 # Indentation for nested items (min: 1, default: 4) [ordered_list] odd_level_marker = "." # "." or ")" at odd nesting levels (default: ".") even_level_marker = ")" # "." or ")" at even nesting levels (default: ")") pad = "start" # "start" or "end" for number alignment (default: "start") indent_width = 4 # Indentation for nested items (min: 1, default: 4) [code_block] fence_char = "~" # "~" or "`" (default: "~") min_fence_length = 4 # Minimum fence length (min: 3, default: 4) space_after_fence = true # Space between fence and language (default: true) default_language = "" # Default language for code blocks (default: "") # External code formatters (see "External code formatters" section) [code_block.formatters] # javascript = ["deno", "fmt", "--ext=js", "-"] [thematic_break] # Must be valid CommonMark: at least 3 of *, -, or _ (with optional spaces) style = "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" leading_spaces = 3 # Leading spaces (0–3, default: 3) [punctuation] curly_double_quotes = true # "text" to "text" (default: true) curly_single_quotes = true # 'text' to 'text' (default: true) curly_apostrophes = false # it's to it's (default: false) ellipsis = true # ... to ... (default: true) en_dash = false # Disabled by default (use "--" to enable) em_dash = "--" # -- to --- (default: "--", use false to disable) ~~~~ Configuration values are validated at parse time. Invalid values will produce descriptive error messages: ~~~~ bash $ hongdown --config invalid.toml input.md Error: failed to parse configuration file: line_width must be at least 8, got 5. ~~~~ When `include` patterns are configured, you can run Hongdown without specifying files: ~~~~ bash # Format all files matching include patterns hongdown --write # Check all files matching include patterns hongdown --check ~~~~ CLI options override configuration file settings: ~~~~ bash # Use config file but override line width hongdown --line-width 100 input.md # Disable word wrapping (overrides config file) hongdown --no-line-width input.md # Use specific config file hongdown --config /path/to/.hongdown.toml input.md ~~~~ Style rules ----------- Hongdown enforces the following conventions: ### Headings - Level 1 and 2 use Setext-style (underlined with `=` or `-`) - Level 3+ use ATX-style (`###`, `####`, etc.) - Optional sentence case conversion (disabled by default) ~~~~ markdown Document Title ============== Section ------- ### Subsection ~~~~ #### Sentence case (optional) When `sentence_case = true` is set in the configuration, Hongdown automatically converts headings to sentence case using intelligent heuristics: ~~~~ markdown # Development Commands → Development commands # Working With JSON APIs → Working with JSON APIs # Using `MyClass` In Code → Using `MyClass` in code ~~~~ The converter: - Capitalizes only the first word - Preserves code spans (text in backticks) - Preserves acronyms (2+ consecutive uppercase letters, e.g., `API`, `HTTP`) - Preserves proper nouns (built-in list + user-configured) - Handles hyphenated words (e.g., `JSON-RPC`) - Respects quoted text capitalization - Preserves non-Latin scripts (CJK, etc.) You can add custom proper nouns to preserve: ~~~~ toml [heading] sentence_case = true proper_nouns = ["MyCompany", "MyProduct", "MyAPI"] ~~~~ You can also exclude built-in proper nouns by treating them as common nouns. This is useful for words like “Go” which can be either a programming language or a common verb: ~~~~ toml [heading] sentence_case = true common_nouns = ["Go", "Swift"] # Treat these as common nouns, not proper nouns ~~~~ Built-in proper nouns include ~450 entries: programming languages (JavaScript, TypeScript, Python, Rust, Go), technologies (GitHub, Docker, React, Node.js), databases (PostgreSQL, MySQL, MongoDB), countries (United States, Republic of Korea), natural languages (English, Korean, Japanese), and more. You can also use HTML comment directives to define document-specific proper nouns and common nouns. See the “HTML comment directives” section for details. ### Lists - Unordered lists use ` - ` (space-hyphen-two spaces) - Ordered lists use `1.` format - 4-space indentation for nested items ~~~~ markdown - First item - Second item - Nested item ~~~~ ### Code blocks - Fenced with four tildes (`~~~~`) - Language identifier on the opening fence ~~~~~ text ~~~~ rust fn main() { println!("Hello, world!"); } ~~~~ ~~~~~ ### External code formatters You can configure external formatters for code blocks in your *.hongdown.toml*. This allows automatic formatting of embedded code using language-specific tools. ~~~~ toml [code_block.formatters] # Simple format: command as array javascript = ["deno", "fmt", "--ext=js", "-"] typescript = ["deno", "fmt", "--ext=ts", "-"] rust = ["rustfmt"] # With custom timeout (default is 5 seconds) [code_block.formatters.python] command = ["black", "-"] timeout = 10 ~~~~ Behavior: - Language matching is exact only (`javascript` matches `javascript`, not `js`) - Code is passed to the formatter via stdin, formatted output read from stdout - If the formatter fails (non-zero exit, timeout, etc.), the original code is preserved and a warning is emitted - External formatters are only available in CLI mode (not in WASM) To skip formatting for a specific code block, add `hongdown-no-format` after the language identifier: ~~~~~ markdown ~~~~ python hongdown-no-format def hello(): print("Hello, World!") ~~~~ ~~~~~ The `hongdown-no-format` keyword is preserved in the output, ensuring the code block remains unformatted on subsequent runs. For WASM builds, use the `formatWithCodeFormatter` function with a callback: ~~~~ typescript import { formatWithCodeFormatter } from "@hongdown/wasm"; import * as prettier from "prettier"; const { output, warnings } = await formatWithCodeFormatter(markdown, { codeFormatter: (language, code) => { if (language === "javascript" || language === "typescript") { return prettier.format(code, { parser: "babel" }); } return null; // Keep original for other languages }, }); ~~~~ ### Line wrapping - Lines wrap at approximately 80 display columns - East Asian wide characters are counted as 2 columns - Long words that cannot be broken are preserved ### Math - Inline (`$…$`) and display (`$$…$$`) TeX/LaTeX math is preserved verbatim, never escaped or punctuation-transformed (like code spans) - Inline math is kept on a single line when wrapping - Dollar-math parsing follows GitHub's rules, so a lone `$`, `$ command`, or `$5` stays literal text - Disable with `math = false` in *.hongdown.toml* to treat every `$` as text ~~~~ markdown The complexity is $O(n \log n)$. $$ x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$ ~~~~ ### MDX MDX documents mix Markdown with JavaScript and JSX. Because the underlying CommonMark parser does not understand those constructs, formatting an MDX document normally corrupts the embedded code. *MDX mode* (“first, do no harm”) detects the JS/JSX constructs and passes them through verbatim, while the surrounding Markdown prose is still formatted. Within paragraph text, the constructs Hongdown would otherwise corrupt are preserved verbatim — never escaped or punctuation-transformed: - ESM `import`/`export` statements, JSX elements and fragments (``, ``, `<>…`), and `{…}` expressions (including `{/* … */}` comments) - The embedded JavaScript/JSX is preserved, not reformatted - Files with the *.mdx* extension enable MDX mode automatically; for stdin or *.md* input, enable it with `--mdx` or `mdx = true` in *.hongdown.toml* ~~~~ mdx import { Chart } from "./chart.js"; export const meta = { author: "Hong Minhee" }; A paragraph of prose is still wrapped and punctuation-transformed, while the JSX and imports above are left exactly as written. ~~~~ A JSX tag that is already valid inline HTML (for example a single `{value}` attribute) is left to the underlying parser, which keeps the tag while still formatting the element's Markdown children; only tags that break the HTML grammar (a `{{…}}` attribute, embedded quotes or spaces, or a multi-line opener) are protected as a whole. Some constructs are intentionally left unprotected: anything inside a heading (an explicit `{#id}` anchor stays a heading anchor), braces inside inline code (`` `{x}` ``), math (`$\frac{1}{2}$`), or link/image syntax, an expression containing a regex literal whose context is ambiguous (a `/` immediately after `}`), and an ESM object whose body contains a blank line. ### Links - External URLs are converted to reference-style links - References are placed at the end of each section - Relative/local URLs remain inline ~~~~ markdown See the [documentation] for more details. [documentation]: https://example.com/docs ~~~~ ### Tables - Pipes are aligned accounting for East Asian wide characters - Minimum column width is maintained See *[STYLE.md](./STYLE.md)* for the complete style specification, including the philosophy behind these conventions and detailed formatting rules. Editor integrations ------------------- ### Visual Studio Code Install the *Hongdown* extension from the [Visual Studio Marketplace] or [Open VSX Registry], then make it the default formatter for Markdown and MDX: ~~~~ json { "[markdown]": { "editor.defaultFormatter": "hongminhee.hongdown" }, "[mdx]": { "editor.defaultFormatter": "hongminhee.hongdown" } } ~~~~ The extension uses a bundled WebAssembly build by default. To use a system Hongdown CLI instead: ~~~~ json { "hongdown.backend": "cli", "hongdown.cli.path": "hongdown" } ~~~~ By default, the extension looks for *.hongdown.toml* in the workspace root. For a standalone file outside any workspace, it falls back to the file's containing directory. Set `hongdown.config.path` to use a different configuration file. [Visual Studio Marketplace]: https://marketplace.visualstudio.com/items?itemName=hongminhee.hongdown [Open VSX Registry]: https://open-vsx.org/extension/hongminhee/hongdown ### Zed Add the following to your Zed settings to use Hongdown as the Markdown formatter (contributed by [Lee Dogeon][moreal zed]): ~~~~ json { "languages": { "Markdown": { "formatter": { "external": { "command": "hongdown", "arguments": ["-"] } } } } } ~~~~ [moreal zed]: https://hackers.pub/@moreal/019bb141-dc94-7103-ab3d-779941125430 ### Neovim If you use [none-ls.nvim] (a community-maintained fork of *null-ls.nvim*), you can register Hongdown as a formatter (requires [none-ls-extras.nvim], contributed by [Vladimir Rubin]): ~~~~ lua local hongdown = require('none-ls.formatting.hongdown') null_ls.register(hongdown) ~~~~ [none-ls.nvim]: https://github.com/nvimtools/none-ls.nvim [none-ls-extras.nvim]: https://github.com/nvimtools/none-ls-extras.nvim [Vladimir Rubin]: https://github.com/dahlia/hongdown/issues/4 ### Helix If you use [Helix], you can register Hongdown as a formatter (contributed by [Jean Simard]): ~~~~ toml [[language]] name = "markdown" auto-format = true formatter = { command = "hongdown", args = ["--stdin"] } ~~~~ [Helix]: https://helix-editor.com/ [Jean Simard]: https://github.com/dahlia/hongdown/pull/12 Library usage ------------- ### Rust Hongdown can also be used as a Rust library: ~~~~ rust use hongdown::{format, Options}; let input = "# Hello World\nThis is a paragraph."; let options = Options::default(); let output = format(input, &options).unwrap(); println!("{}", output); ~~~~ ### JavaScript/TypeScript Hongdown is available as a WebAssembly-based library for JavaScript and TypeScript: ~~~~ bash npm install @hongdown/wasm ~~~~ ~~~~ typescript import { format, formatWithWarnings, loadConfigFromToml } from "@hongdown/wasm"; // Basic usage const markdown = "# Hello\nWorld"; const formatted = await format(markdown); // With options const result = await format(markdown, { lineWidth: 100, setextH1: false, fenceChar: "`", }); // Get warnings along with formatted output const { output, warnings } = await formatWithWarnings(markdown); if (warnings.length > 0) { for (const warning of warnings) { console.warn(`Line ${warning.line}: ${warning.message}`); } } // Parse .hongdown.toml for WASM formatting const { options: configOptions } = await loadConfigFromToml(configToml); const configured = await format(markdown, configOptions); ~~~~ External code block formatters configured in `code_block.formatters` are reported as warnings because the WASM runtime cannot execute external commands. Use the CLI for those settings. The library works in Node.js, Bun, Deno, and web browsers. See the [TypeScript type definitions] for all available options. [TypeScript type definitions]: ./packages/wasm/src/types.ts Development ----------- This project uses [mise] for task management. [mise]: https://mise.jdx.dev/ ### Initial setup After cloning the repository, set up the Git pre-commit hook to automatically run quality checks before each commit: ~~~~ bash mise generate git-pre-commit --task=check --write ~~~~ ### Quality checks The following tasks are available: ~~~~ bash # Run all quality checks mise run check # Individual checks mise run check:clippy # Run clippy linter mise run check:fmt # Check code formatting mise run check:type # Run Rust type checking mise run check:markdown # Check Markdown formatting ~~~~ See *[AGENTS.md]* for detailed development guidelines including TDD practices, code style conventions, and commit message guidelines. [AGENTS.md]: ./AGENTS.md Etymology --------- The name *Hongdown* is a portmanteau of *Hong* (from Hong Minhee, the author) and *Markdown*. It also sounds like the Korean word *hongdapda* (홍답다), meaning “befitting of Hong” or “Hong-like.” License ------- Distributed under the [GPL-3.0-or-later]. See *[LICENSE]* for more information. [GPL-3.0-or-later]: https://www.gnu.org/licenses/gpl-3.0.html [LICENSE]: ./LICENSE