Development setup and guidelines for contributing to agent-md.
- Rust 1.94.0 or later (specified in
rust-toolchain.toml) - Git
- Make (optional, for convenience commands)
agent-md/
src/
main.rs - CLI entrypoint and clap parsers
types.rs - Common types, structures and utilities
config.rs - Configuration types, resolution and init
config_discovery.rs - Config file lookup, parsing and target resolution
datetime.rs - Native zero-dependency date and time utilities and parsing
json.rs - Native zero-dependency JSON parser, serializer, and value representation
jsonc.rs - JSONC comment stripping for configuration files
yaml.rs - Native zero-dependency YAML configuration parser
ignore.rs - Ignore file (.markdownlintignore, .gitignore) resolution and matching
linter.rs - Lint rules orchestration
commands.rs - CLI subcommand handlers and parsing logic
sections.rs - Section locate, extract, replace and insert helpers
parser.rs - Structured Markdown parser (block-based)
simd.rs - Hardware SIMD vectorization primitives (ARM NEON, x86_64 AVX2/SSE2)
format/ - Formatting modules
mod.rs - Structured formatter orchestration
tables.rs - Table formatting (separator compaction, row formatting)
bold_tables.rs - Bold stripping from table cells
blockquotes.rs
code_blocks.rs
frontmatter.rs
html.rs - HTML minification and autolink preservation
rules/ - Validation rule modules
tests.rs - Core unit tests
html_tests.rs - HTML rendering tests
docs/ - Documentation
samples/ - Sample configuration files
test-md/ - Test markdown files
Makefile - Convenience commands
agent-md follows a Parse-then-Format architecture:
- Parsing: The
src/parser.rsmodule decomposes the raw Markdown text into a sequence ofMarkdownBlockelements (e.g.,Heading,CodeBlock,List,Table,Html). This stage also extracts YAML frontmatter. Heading blocks retain level, text, raw slice, and 1-based line numbers. - CLI Document and JSONL Parsing: The
src/commands.rsparsing functions (parse_markdownandparse_markdown_to_jsonl) use the nativeparser.rsblock parser directly, keeping agent-md lightweight and eliminating external parser dependencies such aspulldown-cmark. - Formatting: The
format_markdown_structuredfunction insrc/format/mod.rsiterates over these blocks and applies formatting rules based on the block type and user configuration (from.agent-md.json,agent-md.json, ormarkdownlintrc.*fallbacks, includingremove_bold,compact_blank_lines,collapse_spaces,remove_horizontal_rules,remove_emphasis, andminify_html). - Inline Line Processing: The
src/format/lines.rsmodule processes individual markdown lines, handling tasks such as collapsing redundant spaces while preserving spaces and content inside inline code spans (find_code_span_end), stripping bold markers (remove_bold) and emphasis markers (remove_emphasis) outside code, and standardizing list item indentations. Whenremove_boldis disabled, double asterisks and underscores are preserved and protected from single emphasis removal. Thesrc/format/html.rsmodule minifies HTML tags and blocks while strictly preserving Markdown autolinks and pure URLs (<https://...>,<user@example.com>,<example.dev/path/>). Inline code blocks throughout paragraphs, lists, and table cells (src/format/bold_tables.rs) are preserved unchanged. - SIMD Vectorization: The
src/simd.rsmodule provides hardware-accelerated vectorization for core hot paths, including newline counting, delimiter scanning, consecutive space checks, and ASCII case-insensitive search. It targets ARM NEON on AArch64 and AVX2/SSE2 on x86_64 with runtime feature detection and scalar fallbacks.
This approach is more robust than simple line-based processing, especially for complex structures like nested lists or tables.
agent-md supports ignoring files and directories using .markdownlintignore and .gitignore:
- Ignore Pattern Discovery: The
src/ignore.rsmodule checks the current directory (or target path) for.markdownlintignoreand.gitignore. If a.gitignoreis not present in the target directory, it traverses up parent directories to locate the root.gitignore. - List Merging and Deduplication:
merge_ignore_listsmerges patterns from both ignore files into a single collection, removing duplicate entries while preserving order. - Path Matching:
is_ignoredmatches paths against ignore rules with support for directory-only patterns ending with/(dist/), root-anchored patterns starting with/(/target), glob wildcards with*and?(*.log), and negation rules starting with!(!important.md). - Directory Traversal Integration: Both
agent-md listandcollect_markdown_files(agent-md fmt <dir>) useis_ignoredduring directory walking to immediately prune ignored directories before reading entries, saving disk I/O and execution time. - CLI Command:
agent-md ignore [path]exposes the merged and deduplicated list as JSON (or human-formatted JSON with--human).
agent-md resolves configuration files using an ancestor-walking hierarchy:
- Candidate Priority: In any directory, configuration files are resolved in order:
.agent-md.json,agent-md.json, thenmarkdownlintrc.*fallbacks (.markdownlint.json,.markdownlint.jsonc,.markdownlint.yaml,.markdownlint.yml,.markdownlintrc,.markdownlintrc.json). - Target File Ancestor Search: When executing commands against a target Markdown file (such as
agent-md fmt path/to/doc.mdoragent-md lint path/to/doc.md),find_config_for_targetstarts in the target file's parent directory and searches upwards through parent directories. - Fallback: If no configuration file is located in the target directory tree, the resolver falls back to the current working directory.
- Explicit Override: When
--config <PATH>is supplied on the CLI, it bypasses ancestor discovery and uses the specified configuration file or directory directly. - Ignore Markdownlint: When global
--ignore-markdownlintrcis set (defaultfalse), automatic discovery skipsmarkdownlintrc.*files and uses only native agent-md configuration. The same option can be set via theignore-markdownlintrc(orignore_markdownlintrc) key inagent-md.json; the CLI flag always wins, and only native files are consulted for this key (markdownlintrc.*files never set it). An explicit--configfile path is still respected.
agent-md uses internal, zero-dependency parser and serializer modules for configuration and CLI output:
- Native JSON Data Representation (
src/json.rs): DefinesJsonValue(Null,Bool,Number,String,Array,Object) andJsonObject(order-preserving key-value mapping). ImplementsToJsonfor all output structures and provides both compact (to_string) and 2-space indented (to_string_pretty) serialization. - Native JSON Parser (
src/json.rs): A recursive descent parser that parses JSON configuration files (.agent-md.json,.markdownlint.json,.markdownlint.jsoncafter comment stripping). - Native YAML Parser (
src/yaml.rs): An indentation-based YAML parser for.markdownlint.yamland.markdownlint.ymlconfiguration files that handles#comments, scalar types (booleans, numbers, strings), and nested mapping objects without relying on deprecated crates or unsafe C code. - Dependency Elimination: Replaced
serde,serde_json, andserde_yaml, pruning transitive dependencies (syn,quote,proc-macro2,unsafe-libyaml,itoa,ryu,indexmap,hashbrown,equivalent) and accelerating compilation.
# Development build
cargo build
# Release build (optimized)
cargo build --releaseThe project uses inline tests within each module:
# Run all tests
cargo test
# Run specific module tests
cargo test format::tables::tests
cargo test format::bold_tables::tests
cargo test rules::code_blocks::tests
# Run specific test
cargo test test_validate_space_indentation
# Run tests with output
cargo test -- --nocaptureThe project enforces strict code quality standards:
# Run clippy lints
cargo clippy --all-targets --all-features -- -D warnings
# Check code formatting
cargo fmt --check
# Format code
cargo fmt
# Run security audit
cargo audit- Clippy rules: Cognitive complexity ≤ 30, function arguments ≤ 7, type complexity ≤ 250
- Documentation: Required for all public items
- Error handling: No
unwrap()/expect()in production code - Formatting: 100-character line width, 4-space indentation
- Validation Rules: Add logic to
src/rules/and run them insidesrc/linter.rs'svalidate_markdownfunction - CLI Commands: Extend
CommandsandCliinsrc/cli.rsand add handler functions insrc/commands.rs - Global Options: Add global CLI options to the
Clistruct insrc/cli.rsand update command handlers to use them - Tests: Add comprehensive tests to
src/tests.rs - Documentation: Update relevant sections in
docs/
agent-md uses a ResolvedConfig struct to aggregate all configuration options:
// In src/config.rs
pub struct ResolvedConfig {
pub blanks_around_headings: bool,
pub blanks_around_lists: bool,
pub blanks_around_fences: bool,
pub blanks_around_tables: bool,
pub first_line_heading: bool,
pub no_duplicate_heading: bool,
pub no_duplicate_headings: bool,
pub line_length: bool,
pub max_line_length: u64,
pub ol_prefix: bool,
pub table_column_style: bool,
pub no_hard_tabs: bool,
pub no_inline_html: bool,
}Config values are extracted using typed helpers: get_bool_config, get_u64_config, get_string_config. Invalid or missing values fall back to defaults. Configuration files can be created using init_config or the agent-md init subcommand.
// In src/linter.rs
// Use validate_markdown_with_config for config-aware rules
pub fn validate_markdown_with_config(content: &str, config: &ResolvedConfig) -> LintResult {
// Access config options:
if config.no_hard_tabs {
// Check for hard tabs
}
// ...
}
// In src/tests.rs
fn test_validate_new_rule() {
let mut config = ResolvedConfig::default();
config.no_hard_tabs = false; // Disable rule
let result = validate_markdown_with_config("content", &config);
assert!(result.errors.iter().all(|e| e.rule != "no-hard-tabs"));
}# 1. Create feature branch
git checkout -b feature/new-validation-rule
# 2. Implement changes
# - Add validation logic
# - Write comprehensive tests
# - Update documentation
# 3. Verify quality
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
# 4. Test manually
./target/release/agent-md lint test-file.md
# 5. Commit and push
git add .
git commit -m "feat: add new validation rule"
git push origin feature/new-validation-ruleFor debugging validation issues:
# Create test file
echo "Test content" > debug.md
# Run with detailed output
./target/debug/agent-md lint debug.md | jq .
# Run specific test
cargo test test_validate_space_indentation -- --nocapture- Large documents (>10,000 words) should complete validation within 1 second
- Memory usage scales linearly with document size
- Test performance with:
cargo test test_validate_markdown_large_document_performance
- Follow existing code style and patterns
- Add tests for all new functionality
- Update documentation for user-facing changes
- Ensure all tests pass before submitting PR
- Use descriptive commit messages
- Keep PRs focused on single features/fixes