Skip to content

Latest commit

 

History

History
251 lines (192 loc) · 12 KB

DEV.md

File metadata and controls

251 lines (192 loc) · 12 KB

Development

Development setup and guidelines for contributing to agent-md.

Prerequisites

  • Rust 1.94.0 or later (specified in rust-toolchain.toml)
  • Git
  • Make (optional, for convenience commands)

Project Structure

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

Architecture: Structured Parsing and Formatting

agent-md follows a Parse-then-Format architecture:

  1. Parsing: The src/parser.rs module decomposes the raw Markdown text into a sequence of MarkdownBlock elements (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.
  2. CLI Document and JSONL Parsing: The src/commands.rs parsing functions (parse_markdown and parse_markdown_to_jsonl) use the native parser.rs block parser directly, keeping agent-md lightweight and eliminating external parser dependencies such as pulldown-cmark.
  3. Formatting: The format_markdown_structured function in src/format/mod.rs iterates over these blocks and applies formatting rules based on the block type and user configuration (from .agent-md.json, agent-md.json, or markdownlintrc.* fallbacks, including remove_bold, compact_blank_lines, collapse_spaces, remove_horizontal_rules, remove_emphasis, and minify_html).
  4. Inline Line Processing: The src/format/lines.rs module 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. When remove_bold is disabled, double asterisks and underscores are preserved and protected from single emphasis removal. The src/format/html.rs module 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.
  5. SIMD Vectorization: The src/simd.rs module 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.

Architecture: Ignore Rules Resolution and Matching

agent-md supports ignoring files and directories using .markdownlintignore and .gitignore:

  1. Ignore Pattern Discovery: The src/ignore.rs module checks the current directory (or target path) for .markdownlintignore and .gitignore. If a .gitignore is not present in the target directory, it traverses up parent directories to locate the root .gitignore.
  2. List Merging and Deduplication: merge_ignore_lists merges patterns from both ignore files into a single collection, removing duplicate entries while preserving order.
  3. Path Matching: is_ignored matches 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).
  4. Directory Traversal Integration: Both agent-md list and collect_markdown_files (agent-md fmt <dir>) use is_ignored during directory walking to immediately prune ignored directories before reading entries, saving disk I/O and execution time.
  5. CLI Command: agent-md ignore [path] exposes the merged and deduplicated list as JSON (or human-formatted JSON with --human).

Architecture: Configuration Resolution and Hierarchy

agent-md resolves configuration files using an ancestor-walking hierarchy:

  1. Candidate Priority: In any directory, configuration files are resolved in order: .agent-md.json, agent-md.json, then markdownlintrc.* fallbacks (.markdownlint.json, .markdownlint.jsonc, .markdownlint.yaml, .markdownlint.yml, .markdownlintrc, .markdownlintrc.json).
  2. Target File Ancestor Search: When executing commands against a target Markdown file (such as agent-md fmt path/to/doc.md or agent-md lint path/to/doc.md), find_config_for_target starts in the target file's parent directory and searches upwards through parent directories.
  3. Fallback: If no configuration file is located in the target directory tree, the resolver falls back to the current working directory.
  4. Explicit Override: When --config <PATH> is supplied on the CLI, it bypasses ancestor discovery and uses the specified configuration file or directory directly.
  5. Ignore Markdownlint: When global --ignore-markdownlintrc is set (default false), automatic discovery skips markdownlintrc.* files and uses only native agent-md configuration. The same option can be set via the ignore-markdownlintrc (or ignore_markdownlintrc) key in agent-md.json; the CLI flag always wins, and only native files are consulted for this key (markdownlintrc.* files never set it). An explicit --config file path is still respected.

Architecture: Zero-Dependency JSON and YAML Handling

agent-md uses internal, zero-dependency parser and serializer modules for configuration and CLI output:

  1. Native JSON Data Representation (src/json.rs): Defines JsonValue (Null, Bool, Number, String, Array, Object) and JsonObject (order-preserving key-value mapping). Implements ToJson for all output structures and provides both compact (to_string) and 2-space indented (to_string_pretty) serialization.
  2. Native JSON Parser (src/json.rs): A recursive descent parser that parses JSON configuration files (.agent-md.json, .markdownlint.json, .markdownlint.jsonc after comment stripping).
  3. Native YAML Parser (src/yaml.rs): An indentation-based YAML parser for .markdownlint.yaml and .markdownlint.yml configuration files that handles # comments, scalar types (booleans, numbers, strings), and nested mapping objects without relying on deprecated crates or unsafe C code.
  4. Dependency Elimination: Replaced serde, serde_json, and serde_yaml, pruning transitive dependencies (syn, quote, proc-macro2, unsafe-libyaml, itoa, ryu, indexmap, hashbrown, equivalent) and accelerating compilation.

Building

# Development build
cargo build

# Release build (optimized)
cargo build --release

Testing

The 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 -- --nocapture

Code Quality

The 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

Quality Standards

  • 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

Adding New Features

  1. Validation Rules: Add logic to src/rules/ and run them inside src/linter.rs's validate_markdown function
  2. CLI Commands: Extend Commands and Cli in src/cli.rs and add handler functions in src/commands.rs
  3. Global Options: Add global CLI options to the Cli struct in src/cli.rs and update command handlers to use them
  4. Tests: Add comprehensive tests to src/tests.rs
  5. Documentation: Update relevant sections in docs/

Configuration System

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.

Example: Adding a New Validation Rule

// 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"));
}

Development Workflow

# 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-rule

Debugging

For 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

Performance Considerations

  • 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

Contributing Guidelines

  • 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