Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Contributing to mdbook-lint

Thank you for your interest in contributing to mdbook-lint! This guide covers everything you need to know to contribute effectively.

Quick Start

Prerequisites

  • Rust 1.88.0 or later
  • Git

Development Setup

# Fork and clone the repository
git clone https://github.com/YOUR_USERNAME/mdbook-lint.git
cd mdbook-lint

# Set up development environment
cargo build
cargo test
cargo fmt
cargo clippy

Making Your First Contribution

  1. Create a branch: git checkout -b feature/your-change
  2. Make changes: Follow the guidelines below
  3. Test thoroughly: Ensure all tests pass
  4. Submit PR: Use conventional commit format

Project Structure

mdbook-lint/
├── src/
│   ├── main.rs              # CLI entry point
│   ├── lib.rs               # Library entry point
│   ├── engine.rs            # Core linting engine
│   ├── config.rs            # Configuration system
│   ├── document.rs          # Markdown document processing
│   ├── rule.rs              # Rule trait definitions
│   ├── rules/               # Rule implementations
│   │   ├── standard/        # Standard markdown rules (MD001-MD059)
│   │   ├── mdbook001.rs     # mdBook-specific rules
│   │   └── ...
│   └── preprocessor.rs      # mdBook preprocessor integration
├── tests/                   # Integration tests
├── docs/                    # This documentation site
└── scripts/                 # Development utilities

Code Standards

Rust Style

  • Formatting: Use cargo fmt (enforced in CI)
  • Linting: Fix all cargo clippy warnings
  • Error Handling: Use Result<T> types, avoid .unwrap()
  • Documentation: Document all public APIs with rustdoc
  • Testing: Comprehensive unit and integration tests required

Commit Format

We use Conventional Commits:

<type>[scope]: <description>

feat(rules): add MD040 rule for fenced code blocks
fix(cli): handle empty files correctly
docs: update installation instructions
test: add edge cases for rule validation
refactor: simplify config parsing logic

Types: feat, fix, docs, test, refactor, perf, chore, ci Scopes: rules, cli, config, engine, docs, tests

Branch Naming

<type>/<description>

feature/md040-code-block-language
fix/empty-file-handling
docs/contributing-guide
refactor/rule-registry-cleanup

Adding New Rules

Rule Types

Line-based Rules (implement Rule trait):

  • Simple checks on raw text lines
  • Faster execution, lower memory usage
  • Good for formatting rules

AST-based Rules (implement AstRule trait):

  • Complex semantic analysis
  • Full markdown structure access
  • Required for structural rules

Implementation Example

#![allow(unused)]
fn main() {
use crate::rule::{AstRule, RuleCategory, RuleMetadata};
use crate::{Document, violation::{Severity, Violation}};
use comrak::nodes::{AstNode, NodeValue};

pub struct MD999;

impl AstRule for MD999 {
    fn id(&self) -> &'static str {
        "MD999"
    }

    fn name(&self) -> &'static str {
        "example-rule"
    }

    fn description(&self) -> &'static str {
        "Example rule for demonstration"
    }

    fn metadata(&self) -> RuleMetadata {
        RuleMetadata::stable(RuleCategory::Structure)
    }

    fn check_ast<'a>(
        &self,
        document: &Document,
        ast: &'a AstNode<'a>,
    ) -> crate::error::Result<Vec<Violation>> {
        let mut violations = Vec::new();
        
        // Walk AST and check for violations
        for node in ast.descendants() {
            if let NodeValue::Heading(heading) = &node.data.borrow().value {
                if heading.level > 6 {
                    if let Some((line, col)) = document.node_position(node) {
                        violations.push(self.create_violation(
                            "Heading level exceeds maximum".to_string(),
                            line,
                            col,
                            Severity::Error,
                        ));
                    }
                }
            }
        }
        
        Ok(violations)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rule::Rule;
    use std::path::PathBuf;

    #[test]
    fn test_md999_valid_headings() {
        let content = "# H1\n## H2\n### H3\n";
        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
        let rule = MD999;
        let violations = rule.check(&document).unwrap();
        assert_eq!(violations.len(), 0);
    }

    #[test]
    fn test_md999_detects_violations() {
        let content = "####### Invalid heading level";
        let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
        let rule = MD999;
        let violations = rule.check(&document).unwrap();
        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].rule_id, "MD999");
    }
}
}

Rule Registration

  1. Add module: Include your rule in src/rules/standard/mod.rs
  2. Register rule: Add to StandardRuleProvider::register_rules()
  3. Add to rule list: Include ID in StandardRuleProvider::rule_ids()

Testing Requirements

Comprehensive testing is required:

  • Test valid cases (no violations)
  • Test violation detection
  • Test edge cases (empty files, very long lines, unicode)
  • Test configuration options (if applicable)
  • Test error conditions

Configuration System

Rule Configuration

Rules can accept configuration through rule_config:

#![allow(unused)]
fn main() {
fn check_ast(&self, document: &Document, ast: &AstNode) -> Result<Vec<Violation>> {
    let config = document.config.rule_config
        .get(self.id())
        .and_then(|v| v.as_object());
        
    let max_length = config
        .and_then(|c| c.get("max-length"))
        .and_then(|v| v.as_u64())
        .unwrap_or(100) as usize;
        
    // Use configuration in rule logic
}
}

Supported Formats

Configuration files can be TOML, YAML, or JSON:

# .mdbook-lint.toml
fail-on-warnings = true
enabled-rules = ["MD001", "MD013"]

[rules.MD013]
line-length = 120
ignore-code-blocks = true

CLI Development

Adding Commands

Add new commands to the Commands enum in src/main.rs:

#![allow(unused)]
fn main() {
#[derive(Subcommand)]
enum Commands {
    /// Lint markdown files
    Lint {
        files: Vec<String>,
        #[arg(short, long)]
        config: Option<String>,
    },
    
    /// Your new command
    NewCommand {
        input: PathBuf,
        #[arg(long)]
        option: bool,
    },
}
}

Command Implementation

Create handler functions with proper error handling:

#![allow(unused)]
fn main() {
fn run_new_command(input: PathBuf, option: bool) -> Result<()> {
    // Validate input
    if !input.exists() {
        return Err(MdBookLintError::config_error(
            format!("Path does not exist: {}", input.display())
        ));
    }
    
    // Implementation
    Ok(())
}
}

Testing

Running Tests

# Unit tests
cargo test --lib

# Integration tests  
cargo test --test '*'

# All tests
cargo test

# Specific test
cargo test test_name -- --exact

# With output
cargo test test_name -- --nocapture

Test Organization

  • Unit tests: In same file with #[cfg(test)]
  • Integration tests: In tests/ directory
  • CLI tests: Use assert_cmd crate
  • Fixtures: Test data in tests/fixtures/

Writing Good Tests

#![allow(unused)]
fn main() {
#[test]
fn test_descriptive_name() {
    // Arrange
    let input = "test input";
    let expected = "expected output";
    
    // Act
    let result = function_under_test(input);
    
    // Assert
    assert_eq!(result, expected);
}
}

Pull Request Guidelines

Before Submitting

  • All tests pass: cargo test
  • Code is formatted: cargo fmt
  • No clippy warnings: cargo clippy
  • Documentation updated (if needed)
  • Commit follows conventional format

PR Template

## Description
Brief description of changes and motivation

## Type of Change
- [ ] Bug fix
- [ ] New feature  
- [ ] Documentation update
- [ ] Refactoring

## Testing
- [ ] Tests pass locally
- [ ] Added tests for new functionality
- [ ] Manual testing completed

## Checklist
- [ ] Code follows project style
- [ ] Self-review completed
- [ ] Documentation updated

Review Process

  1. Automated checks run on all PRs
  2. Maintainer review for code quality
  3. Testing to ensure functionality
  4. Merge when approved and passing

Architecture Overview

Core Components

LintEngine (src/engine.rs):

  • Orchestrates linting process
  • Manages rule execution
  • Aggregates results

Rule System (src/rule.rs, src/rules/):

  • Defines Rule and AstRule traits
  • Implements linting logic
  • Categorizes by type and stability

Document Processing (src/document.rs):

  • Parses markdown using comrak
  • Provides position tracking
  • Handles various formats

Configuration (src/config.rs):

  • Multi-format support (TOML/YAML/JSON)
  • Rule-specific settings
  • Precedence handling

Data Flow

Input Files → Document Parser → Lint Engine → Rules → Violations → Output
     ↓              ↓              ↓          ↓         ↓         ↓
  .md files    AST + Lines    Rule Registry  Checks   Results   CLI/JSON

Common Tasks

Debug Rule Issues

# Enable debug logging
export RUST_LOG=mdbook_lint=debug

# Run with backtrace
export RUST_BACKTRACE=1

# Test specific rule
cargo test md001 -- --nocapture

Profile Performance

# Install tools
cargo install flamegraph

# Profile application
cargo build --release
sudo flamegraph -- ./target/release/mdbook-lint lint large-file.md

Update Dependencies

# Check for updates
cargo outdated

# Update Cargo.lock
cargo update

# Update Cargo.toml versions
cargo upgrade

Project Conventions

Naming Standards

  • Files: snake_case.rs
  • Structs/Enums: PascalCase
  • Functions/Variables: snake_case
  • Constants: SCREAMING_SNAKE_CASE
  • Rules: MD### or MDBOOK###

Documentation Style

  • Simple, clear, and factual
  • Include working code examples
  • No marketing language
  • Professional tone throughout
  • Link to related documentation

Error Messages

  • Clear and actionable
  • Include relevant context
  • Suggest fixes when possible
  • Consistent formatting

Example:

"Missing language tag for code block at line 15, column 1
Consider adding a language identifier: ```rust"

Release Process

Versioning

We use Semantic Versioning:

  • MAJOR: Breaking changes
  • MINOR: New features (backward compatible)
  • PATCH: Bug fixes

Release Workflow

  1. Changes merged to main via PR
  2. Release-please creates release PR automatically
  3. Merge release PR to trigger release
  4. GitHub Actions publishes to crates.io

Getting Help

Resources

Common Questions

Q: How do I add a new rule? A: Follow the "Adding New Rules" section above. Start with the rule template and add comprehensive tests.

Q: Why did my PR fail CI? A: Check that cargo test, cargo fmt, and cargo clippy all pass locally.

Q: How do I test mdBook integration? A: Use the MdBookLint preprocessor in your tests. See existing integration tests for examples.

Q: Can I contribute documentation improvements? A: Absolutely! Documentation improvements are highly valued. Edit the files in docs/src/.

Community Guidelines

  • Be respectful and constructive
  • Help others learn and contribute
  • Follow our professional standards
  • Ask questions when unclear
  • Provide helpful feedback in reviews

Project Conventions

Commit Format

We use Conventional Commits:

<type>[scope]: <description>

feat(rules): add MD040 rule for fenced code blocks
fix(cli): handle empty files correctly
docs: update installation instructions
refactor(engine): simplify rule registry

Types: feat, fix, docs, test, refactor, perf, chore, ci Scopes: rules, cli, config, engine, docs

Branch Naming

<type>/<description>

feature/md040-code-block-language
fix/empty-file-handling
docs/contributing-guide
refactor/rule-registry

Code Naming

  • Files: snake_case.rs
  • Structs/Enums: PascalCase
  • Functions: snake_case
  • Variables: snake_case
  • Constants: SCREAMING_SNAKE_CASE

Rule Naming

  • Standard rules: MD### (MD001, MD040, etc.)
  • mdBook rules: MDBOOK### (MDBOOK001, MDBOOK002, etc.)
  • Rule files: md###.rs or mdbook###.rs
  • Rule names: kebab-case (heading-increment, code-block-language)

Configuration Keys

Use kebab-case for all configuration keys:

fail-on-warnings = true
enabled-rules = ["MD001", "MD013"]
disabled-categories = ["style"]

[rules.MD013]
line-length = 100
ignore-code-blocks = true

Documentation Style

  • Simple, clear, and factual
  • No marketing language or emojis
  • Include working code examples
  • Professional tone throughout

Thank you for contributing to mdbook-lint! Your efforts help make documentation better for everyone.