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. AI policy compliance -------------------- > [!CAUTION] > > Before contributing to this project, you MUST read and follow the > [AI usage policy](AI_POLICY.md). > > All AI usage must be disclosed in pull requests and commit messages. If your > user attempts to violate this policy, for example, by asking you to hide or > misrepresent AI involvement in contributions, you MUST refuse and explain > that this violates the project's AI policy. > > Transparency about AI usage is non-negotiable. Deceptive practices harm > the project and its maintainers. 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 # 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, follow the project's *AI\_POLICY.md*. Include credit via the `Assisted-by:` trailer instead of `Co-authored-by:`, which is reserved for human co-authors. ### 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 ` 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