hongdown 0.3.8

A Markdown formatter that enforces Hong Minhee's Markdown style conventions
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
Guidance for LLM-based code agents
==================================

This file provides guidance to LLM-based code agents (e.g., Claude Code,
OpenCode) when working with code in this repository.


Project overview
----------------

Hongdown is a Markdown formatter that enforces Hong Minhee's Markdown style
conventions.  The formatter is implemented in Rust (Edition 2024) 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.

### Project architecture

~~~~ text
hongdown/
├── src/
│   ├── main.rs           # CLI entry point (clap-based argument parsing)
│   ├── lib.rs            # Library entry point (Options, format functions)
│   ├── config.rs         # Configuration file handling (.hongdown.toml)
│   ├── wasm.rs           # WASM bindings (wasm-bindgen)
│   └── serializer/       # Core formatting logic
│       ├── mod.rs        # Main serializer module
│       ├── state.rs      # Serializer state and types
│       ├── block.rs      # Block quotes and GitHub alerts
│       ├── code.rs       # Code blocks (fenced and indented)
│       ├── document.rs   # Document-level handling
│       ├── escape.rs     # Text escaping utilities
│       ├── inline.rs     # Inline elements (emphasis, code spans)
│       ├── link.rs       # Links and images
│       ├── list.rs       # Ordered and unordered lists
│       ├── table.rs      # Table formatting
│       ├── wrap.rs       # Text wrapping utilities
│       └── tests.rs      # Unit tests for serializer
├── tests/
│   └── integration.rs    # Integration tests
├── packages/
│   ├── hongdown/         # CLI npm package (hongdown)
│   │   ├── package.json
│   │   └── bin/hongdown.js
│   └── wasm/             # WASM library package (@hongdown/wasm)
│       ├── src/          # TypeScript wrapper source
│       └── test/         # Tests for Node.js, Bun, Deno
├── demo/                 # Web demo application (hongdown-demo)
│   ├── src/
│   │   ├── components/   # UI components (TabBar, Options, Inputs)
│   │   ├── App.tsx       # Main application logic
│   │   ├── index.tsx     # Entry point
│   │   ├── sample.ts     # Sample Markdown content
│   │   └── styles.css    # Custom styles (scrollbar, etc.)
│   ├── index.html        # HTML template
│   ├── vite.config.ts    # Vite configuration
│   └── uno.config.ts     # UnoCSS configuration
├── pnpm-workspace.yaml   # pnpm workspace configuration
└── package.json          # Root workspace package.json
~~~~


Development commands
--------------------

This is a Rust project using Cargo as the build system.  We use [mise] for
task management to streamline common development workflows.

[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
~~~~

### Building

~~~~ bash
cargo build            # Debug build
cargo build --release  # Release build
~~~~

### Testing

The recommended way to run all tests is using mise:

~~~~ bash
mise run test              # Run all tests (Rust + WASM)
mise run test:rust         # Run Rust tests only
mise run test:wasm         # Run WASM package tests only
~~~~

You can also run Cargo tests directly:

~~~~ bash
cargo test                        # Run all Rust tests
cargo test <name>                 # Run tests matching a name pattern
cargo test serialize_table        # e.g., runs all table serialization tests
cargo test -- --nocapture         # Show println! output
cargo test -- --test-threads=1    # Run tests sequentially
~~~~

Tests are located in several places:

 -  *Rust unit tests*: *src/serializer/tests.rs* contains serializer tests
 -  *Rust integration tests*: *tests/integration.rs* contains CLI and full
    document tests
 -  *WASM package tests*: *packages/wasm/test/* contains tests that run on
    Node.js, Bun, and Deno

Test helper functions in *src/serializer/tests.rs*:

 -  `parse_and_serialize(input)` - Format with default options
 -  `parse_and_serialize_with_options(input, options)` - Format with custom
    options
 -  `parse_and_serialize_with_source(input)` - Format with source context
    for directive support
 -  `parse_and_serialize_with_warnings(input)` - Format and capture warnings

### Demo application

The *demo/* directory contains a web-based playground for Hongdown.  To run
the demo locally:

~~~~ bash
pnpm --filter hongdown-demo dev     # Start development server
pnpm --filter hongdown-demo build   # Build for production
~~~~

The demo app uses:

 -  *Solid.js* for reactive UI
 -  *UnoCSS* for styling with atomic CSS
 -  *Vite* for bundling and development
 -  *@hongdown/wasm* for real-time Markdown formatting

Design principles:

 -  *Borderless design*: Visual hierarchy is established through background
    color differences (e.g., `bg-neutral-50` vs `bg-white` in light mode)
    rather than borders
 -  *Responsive layout*: Desktop shows split-view (editor/output side-by-side),
    mobile shows tabbed interface (Editor/Output/Warnings tabs)
 -  *Dark mode*: Automatically respects system preferences via
    `@media (prefers-color-scheme: dark)`

When modifying the demo UI, verify changes in both light and dark modes.

### Formatting

To format all files (Rust code and Markdown files):

~~~~ bash
mise run fmt           # Format all files (recommended)
~~~~

This runs:

 -  `mise run fmt:rust` - Format Rust code with `cargo fmt`
 -  `mise run fmt:markdown` - Format Markdown files with Hongdown

### Quality checks

The recommended way to run quality checks is using mise tasks:

~~~~ bash
mise run check         # Run all quality checks (recommended)
~~~~

This runs all checks in parallel:

 -  `mise run check:clippy` - Run clippy linter with warnings as errors
 -  `mise run check:fmt` - Check code formatting
 -  `mise run check:type` - Run Rust type checking
 -  `mise run check:markdown` - Check Markdown formatting

You can also run individual Cargo commands directly:

~~~~ bash
cargo fmt              # Format code
cargo fmt --check      # Check formatting without modifying
cargo clippy           # Run linter
~~~~


Development practices
---------------------

### Test-driven development

This project strictly follows test-driven development (TDD) practices.
All new code must be developed using the TDD cycle:

1.  *Red*: Write a failing test that describes the expected behavior.
    Run the test to confirm it fails.
2.  *Green*: Write the minimum code necessary to make the test pass.
    Run the test to confirm it passes.
3.  *Refactor*: Improve the code while keeping all tests passing.
    Run tests after each refactoring step.

Additional TDD guidelines:

 -  *Write tests first*: Before implementing new functionality, write tests
    that describe the expected behavior.  Confirm that the tests fail before
    proceeding with the implementation.
 -  *Regression tests for bugs*: When fixing bugs, first write a regression
    test that reproduces the bug.  Confirm that the test fails, then fix the
    bug and verify the test passes.
 -  *Small increments*: Implement features in small, testable increments.
    Each increment should have its own test.
 -  *Run tests frequently*: Run `cargo test` after every change to ensure
    existing functionality is not broken.

### Commit messages

 -  Do not use Conventional Commits (no `fix:`, `feat:`, etc. prefixes).
    Keep the first line under 50 characters when possible.

 -  Focus on *why* the change was made, not just *what* changed.

 -  When referencing issues or PRs, use permalink URLs instead of just
    numbers (e.g., `#123`).  This preserves context if the repository
    is moved later.

 -  When listing items after a colon, add a blank line after the colon:

    ~~~~
    This commit includes the following changes:

    - Added foo
    - Fixed bar
    ~~~~

 -  When using LLMs or coding agents, include credit via `Co-Authored-By:`.
    Include a permalink to the agent session if available.

### Changelog (*CHANGES.md*)

This repository uses *CHANGES.md* as a human-readable changelog.  Follow these
conventions:

 -  *Structure*: Keep entries in reverse chronological order (newest version at
    the top).

 -  *Version sections*: Each release is a top-level section:

    ~~~~
    Version 0.1.0
    -------------
    ~~~~

 -  *Unreleased version*: The next version should start with:

    ~~~~
    To be released.
    ~~~~

 -  *Released versions*: Use a release-date line right after the version header:

    ~~~~
    Released on December 30, 2025.
    ~~~~

    If you need to add brief context (e.g., initial release), keep it on the
    same sentence:

    ~~~~
    Released on August 21, 2025.  Initial release.
    ~~~~

 -  *Bullets and wrapping*: Use ` -  ` list items, wrap around ~80 columns, and
    indent continuation lines by 4 spaces so they align with the bullet text.

 -  *Write useful change notes*: Prefer concrete, user-facing descriptions.
    Include what changed, why it changed, and what users should do differently
    (especially for breaking changes, deprecations, and security fixes).

 -  *Multi-paragraph items*: For longer explanations, keep paragraphs inside the
    same bullet item by indenting them by 4 spaces and separating paragraphs
    with a blank line (also indented).

 -  *Code blocks in bullets*: If a bullet includes code, indent the entire code
    fence by 4 spaces so it remains part of that list item.  Use `~~~~` fences
    and specify a language (e.g., `~~~~ rust`).

 -  *Nested lists*: If you need sub-items (e.g., a list of added features), use
    a nested list inside the parent bullet, indented by 4 spaces.

 -  *Issue and PR references*: Use `[[#123]]` markers in the text and add
    reference links at the end of the version section.

    When listing multiple issues/PRs, list them like `[[#123], [#124]]`.

    When the reference is for a PR authored by an external contributor, append
    `by <NAME>` after the last reference marker (e.g., `[[#123] by Hong Minhee]`
    or `[[#123], [#124] by Hong Minhee]`).

    ~~~~
    [#123]: https://github.com/dahlia/hongdown/issues/123
    [#124]: https://github.com/dahlia/hongdown/pull/124
    ~~~~


Development tips
----------------

### Dependency management

 -  *Always use `cargo add`*: When adding dependencies, use `cargo add`
    instead of manually editing *Cargo.toml*.  This ensures you get the
    latest compatible version:

    ~~~~ bash
    cargo add serde --features derive
    cargo add tokio --features full
    ~~~~

 -  *Check before adding*: Before adding a new dependency, consider whether
    the functionality can be achieved with existing dependencies or the
    standard library.

### Configuration changes

 -  *Options struct*: When adding new configuration options, update both
    the `Config` struct in *src/config.rs* and the `Options` struct in
    *src/lib.rs*.  Also update *src/main.rs* to wire them together.

 -  *Update documentation*: When adding configuration options, update the
    Configuration file section in *README.md*.

### Code organization

 -  *Serializer modules*: The serializer is split into multiple modules
    under *src/serializer/*.  Each module handles a specific type of
    Markdown element (e.g., *list.rs*, *table.rs*, *code.rs*).

 -  *Test location*: Unit tests for the serializer are in
    *src/serializer/tests.rs*.  Integration tests are in *tests/*.

### Quality checks

 -  *Run checks frequently*: Run `cargo fmt --check` and `cargo clippy`
    frequently during development, not just before committing.  This
    catches issues early and keeps the codebase clean.

 -  *Format files*: After editing code or Markdown files, format them:

    ~~~~ bash
    mise run fmt       # Format all files
    ~~~~

 -  *Before committing*: Always run the full quality check suite.  This is
    automatically enforced by the Git pre-commit hook:

    ~~~~ bash
    mise run check     # Runs all quality checks
    ~~~~

    The pre-commit hook runs `mise run check` automatically before each
    commit.  If any check fails, the commit will be aborted.

### Performance considerations

 -  *Parallel processing*: The CLI uses `rayon` for parallel file
    processing in `--write` and `--check` modes.  Keep this in mind
    when modifying file processing logic.

 -  *Avoid unnecessary allocations*: Prefer borrowing over cloning when
    possible.  Use `&str` instead of `String` for read-only string data.


Code style
----------

This project uses default `rustfmt` formatting (no *rustfmt.toml*) and default
clippy lints (no *clippy.toml*).  Run `cargo fmt` to format code.

### Imports

Organize imports in this order, with blank lines between groups:

1.  Standard library (`use std::...`)
2.  External crates (`use clap::...`, `use comrak::...`)
3.  Internal modules (`use super::...`, `use crate::...`)

~~~~ rust
use std::fs;
use std::path::PathBuf;

use clap::Parser;
use comrak::{Arena, Options as ComrakOptions};

use crate::config::Config;
~~~~

### Naming conventions

 -  *Modules and files*: `snake_case` (e.g., *code\_block.rs*)
 -  *Types and traits*: `PascalCase` (e.g., `Serializer`, `FormatError`)
 -  *Functions and methods*: `snake_case` (e.g., `serialize_node`)
 -  *Constants*: `SCREAMING_SNAKE_CASE` (e.g., `CONFIG_FILE_NAME`)
 -  *Method prefixes*: Use `serialize_*` for serialization, `is_*` for
    boolean checks, `with_*` for builder patterns

### Type safety

 -  All code must be type-safe.  Avoid using `unsafe` blocks unless
    absolutely necessary.
 -  Prefer immutable data structures unless there is a specific reason to
    use mutable ones.

### Error handling

 -  Define custom error types as `enum` with descriptive variants.
 -  Manually implement `std::fmt::Display` and `std::error::Error` traits.
 -  Include context in error messages (e.g., file paths, pattern names).
 -  Prefer `Result` over `panic!` for recoverable errors.
 -  End error messages with a period.

Example error type pattern:

~~~~ rust
#[derive(Debug)]
pub enum ConfigError {
    Io(PathBuf, std::io::Error),
    Parse(PathBuf, toml::de::Error),
}

impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ConfigError::Io(path, err) => {
                write!(f, "failed to read {}: {}", path.display(), err)
            }
            ConfigError::Parse(path, err) => {
                write!(f, "failed to parse {}: {}", path.display(), err)
            }
        }
    }
}

impl std::error::Error for ConfigError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ConfigError::Io(_, err) => Some(err),
            ConfigError::Parse(_, err) => Some(err),
        }
    }
}
~~~~

### Struct patterns

 -  Use `#[derive(...)]` extensively for common traits.
 -  Common derives: `Debug`, `Clone`, `PartialEq`.
 -  For serde: `#[derive(Deserialize)]`, `#[serde(default)]`,
    `#[serde(rename_all = "lowercase")]`.
 -  Implement `Default` for configuration structs.

### API documentation

 -  All public APIs must have doc comments describing their purpose,
    parameters, and return values.
 -  Use `///` for item documentation and `//!` for module documentation.


Markdown style guide
--------------------

This project follows [Hong Minhee's Markdown style convention], documented
in *[STYLE.md]*.  That document is the authoritative specification for all
formatting rules enforced by Hongdown.

When modifying Hongdown's formatting behavior, you must also update
*STYLE.md* to reflect the changes.  The specification should always match
the actual behavior of the formatter.

[Hong Minhee's Markdown style convention]: ./STYLE.md
[STYLE.md]: ./STYLE.md