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

Introduction

Welcome to mdbook-lint, a fast and comprehensive markdown linter designed specifically for mdBook projects.

What is mdbook-lint?

mdbook-lint is a command-line tool and mdBook preprocessor that helps you maintain high-quality markdown documentation by detecting common issues, enforcing consistent style, and providing mdBook-specific linting rules.

Key Features

  • Fast Performance: Built in Rust for speed and efficiency
  • Comprehensive Rule Set: 59 standard markdown rules plus 4 mdBook-specific rules
  • Flexible Integration: Works as a standalone CLI tool or as an mdBook preprocessor
  • Configurable: Customize rules and behavior through configuration files
  • Zero Dependencies: Self-contained binary with no external dependencies

Why Use mdbook-lint?

Documentation quality matters. Consistent, well-formatted markdown makes your documentation:

  • More readable for contributors and users
  • Easier to maintain across large documentation projects
  • More professional in appearance and structure
  • Less prone to rendering issues in mdBook

Getting Started

Ready to improve your documentation quality? Head over to the Installation guide to get started, or jump straight to Getting Started for a quick walkthrough.

Community and Support

mdbook-lint is open source and welcomes contributions. Visit our GitHub repository to:

  • Report issues
  • Request features
  • Contribute code
  • Browse the source

For development information, see our Contributing guide.

Installation

mdbook-lint can be installed through several methods depending on your needs.

The easiest way to install mdbook-lint is through Cargo:

cargo install mdbook-lint

This will install the latest stable version from crates.io.

From Source

To install the latest development version or contribute to the project:

git clone https://github.com/joshrotenberg/mdbook-lint.git
cd mdbook-lint
cargo install --path .

Pre-built Binaries

Pre-built binaries for common platforms are available on the GitHub releases page.

Download the appropriate binary for your platform and add it to your PATH.

Requirements

  • Rust 1.88 or later (if building from source)
  • No runtime dependencies required

Verification

After installation, verify that mdbook-lint is working correctly:

mdbook-lint --version

You should see output similar to:

mdbook-lint 0.1.0

Next Steps

Once installed, head to the Getting Started guide to learn how to use mdbook-lint with your projects.

Getting Started

This guide will walk you through using mdbook-lint for the first time.

Quick Start

The fastest way to get started is to run mdbook-lint on some markdown files:

# Lint a single file
mdbook-lint lint README.md

# Lint multiple files
mdbook-lint lint src/*.md docs/*.md

# Lint all markdown files in a directory
mdbook-lint lint .

Understanding the Output

When mdbook-lint finds issues, it will display them like this:

README.md:15:1: MD013: Line length (line too long, 85 > 80)
src/intro.md:3:1: MD001: Heading levels should only increment by one level at a time

Each line shows:

  • File and location: filename:line:column
  • Rule ID: The specific rule that was violated (e.g., MD013)
  • Description: What the issue is and how to fix it

Your First Configuration

Create a .mdbook-lint.toml file in your project root:

# Fail the build on warnings
fail-on-warnings = true

# Disable rules that don't fit your project
disabled-rules = ["MD013"]  # Allow long lines

# Configure specific rules
[rules.MD007]
indent = 4  # Use 4-space indentation for lists

Using with mdBook

To integrate mdbook-lint with your mdBook project:

  1. Add to book.toml:

    [preprocessor.mdbook-lint]
    
  2. Build your book:

    mdbook build
    

mdbook-lint will now check your markdown files every time you build your book.

Common Workflow

Here's a typical workflow for using mdbook-lint:

  1. Initial setup: Add configuration file and run first lint
  2. Fix major issues: Address structural problems first
  3. Customize rules: Disable rules that don't fit your style
  4. Integrate with build: Add to mdBook or CI pipeline
  5. Maintain quality: Regular linting keeps documentation clean

Exploring Rules

To see all available rules:

# List all rules
mdbook-lint rules

# Show detailed rule descriptions
mdbook-lint rules --detailed

# Show only enabled rules
mdbook-lint rules --enabled

Next Steps

Configuration

mdbook-lint can be configured through configuration files to customize its behavior for your project.

Configuration File

Create a .mdbook-lint.toml file in your project root:

# Basic settings
fail-on-warnings = false
disabled-rules = ["MD013", "MD033"]

# Rule-specific configuration
[rules.MD007]
indent = 2

[rules.MD013]
line-length = 120

Configuration Options

Global Settings

SettingTypeDefaultDescription
fail-on-warningsbooleanfalseExit with error code when warnings are found
disabled-rulesarray[]List of rule IDs to disable

Rule Configuration

Individual rules can be configured in the [rules.<RULE_ID>] sections.

MD007 - Unordered list indentation

[rules.MD007]
indent = 2  # Number of spaces for list indentation

MD013 - Line length

[rules.MD013]
line-length = 80     # Maximum line length
code-blocks = false  # Check line length in code blocks
tables = false       # Check line length in tables

Configuration Precedence

Configuration is loaded in the following order (later sources override earlier ones):

  1. Built-in defaults
  2. .mdbook-lint.toml in project root
  3. Command-line arguments

Examples

Strict Configuration

fail-on-warnings = true
disabled-rules = []  # Enable all rules

[rules.MD013]
line-length = 80

Relaxed Configuration

fail-on-warnings = false
disabled-rules = ["MD013", "MD033", "MD041"]

[rules.MD007]
indent = 4

mdBook-focused Configuration

# Disable rules that conflict with mdBook conventions
disabled-rules = ["MD025"]  # Multiple top-level headers are OK in mdBook

fail-on-warnings = true

Next Steps

CLI Usage

This page documents the command-line interface for mdbook-lint.

Basic Commands

lint

Lint markdown files and directories.

mdbook-lint lint [OPTIONS] [PATHS]...

rules

List available linting rules.

mdbook-lint rules [OPTIONS]

help

Show help information.

mdbook-lint help [COMMAND]

Options

Global Options

  • -h, --help: Print help information
  • -V, --version: Print version information
  • -v, --verbose: Enable verbose output
  • -q, --quiet: Suppress non-error output

Lint Options

  • --config <FILE>: Use specific configuration file
  • --fail-on-warnings: Exit with error code on warnings
  • --disable <RULES>: Disable specific rules (comma-separated)

Rules Options

  • --detailed: Show detailed rule descriptions
  • --enabled: Show only enabled rules
  • --format <FORMAT>: Output format (text, json)

Examples

# Lint current directory
mdbook-lint lint .

# Lint specific files
mdbook-lint lint README.md src/chapter1.md

# Lint with custom config
mdbook-lint lint --config custom-lint.toml src/

# Show all rules with descriptions
mdbook-lint rules --detailed

# Lint and fail on warnings
mdbook-lint lint --fail-on-warnings docs/

Exit Codes

  • 0: Success (no errors)
  • 1: Linting errors found
  • 2: Invalid arguments or configuration

Next Steps

mdBook Integration

mdbook-lint integrates seamlessly with mdBook as a preprocessor, automatically checking your markdown files during the build process.

Setup

Add mdbook-lint to your book.toml:

[preprocessor.mdbook-lint]

That's it! mdbook-lint will now run automatically when you build your book.

Configuration

You can configure the preprocessor behavior in your book.toml:

[preprocessor.mdbook-lint]
# Fail the build if linting issues are found
fail-on-warnings = true

# Disable specific rules
disabled-rules = ["MD013", "MD033"]

# Only run on specific chapters (optional)
# include = ["src/chapter1.md", "src/chapter2.md"]

# Exclude specific files (optional)
# exclude = ["src/draft.md"]

Build Integration

When you run mdbook build, mdbook-lint will:

  1. Check all markdown files in your book
  2. Report any linting issues
  3. Optionally fail the build if issues are found
  4. Continue with normal mdBook processing
# Build with linting
mdbook build

# Watch mode also includes linting
mdbook serve

CI/CD Integration

mdbook-lint works great in continuous integration:

# .github/workflows/docs.yml
name: Documentation

on: [push, pull_request]

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Install mdbook-lint
        run: cargo install mdbook-lint
      - name: Build documentation
        run: mdbook build
      - name: Deploy to GitHub Pages
        if: github.ref == 'refs/heads/main'
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./book

mdBook-Specific Rules

mdbook-lint includes special rules designed for mdBook projects:

  • MDBOOK001: Check for proper SUMMARY.md structure
  • MDBOOK002: Validate internal link references
  • MDBOOK003: Check for missing files referenced in SUMMARY.md
  • MDBOOK004: Validate mdBook-specific syntax

Troubleshooting

Preprocessor Not Running

If mdbook-lint isn't running during builds:

  1. Ensure mdbook-lint is installed and in your PATH
  2. Check that [preprocessor.mdbook-lint] is in your book.toml
  3. Try running with verbose output: mdbook build -v

Configuration Not Applied

Configuration precedence for the preprocessor:

  1. Built-in defaults
  2. .mdbook-lint.toml file
  3. book.toml preprocessor configuration
  4. Command-line arguments (when running CLI directly)

Next Steps

Rules Reference

This page provides a comprehensive reference for all linting rules available in mdbook-lint.

Standard Markdown Rules (MD001-MD059)

mdbook-lint implements 59 standard markdown linting rules based on the widely-used markdownlint specification.

Heading Rules

MD001 - Heading levels should only increment by one level at a time

Ensures proper heading hierarchy in your documents.

# Good
## Good

# Bad
### Bad (skips level 2)

MD003 - Heading style

Enforces consistent heading styles throughout your document.

MD018 - No space after hash on atx style heading

Requires a space after the hash in ATX-style headings.

MD019 - Multiple spaces after hash on atx style heading

Prevents multiple spaces after the hash in ATX-style headings.

List Rules

MD004 - Unordered list style

Enforces consistent marker style for unordered lists.

MD005 - Inconsistent indentation for list items at the same level

Ensures consistent indentation within lists.

MD007 - Unordered list indentation

Controls indentation levels for nested lists.

Line Rules

MD009 - Trailing whitespace

Removes unnecessary whitespace at the end of lines.

MD010 - Hard tabs

Prevents the use of hard tab characters.

MD012 - Multiple consecutive blank lines

Limits consecutive blank lines.

MD013 - Line length

Enforces maximum line length limits.

MD034 - Bare URL used

Requires proper link formatting for URLs.

Prevents spaces around link text.

mdBook-Specific Rules (MDBOOK001-004)

These rules are specifically designed for mdBook projects and check mdBook-specific syntax and conventions.

MDBOOK001 - SUMMARY.md structure

Validates the structure of your SUMMARY.md file according to mdBook conventions.

Checks that internal links reference valid files and sections within your book.

MDBOOK003 - Missing referenced files

Ensures all files referenced in SUMMARY.md actually exist.

MDBOOK004 - mdBook syntax validation

Validates mdBook-specific markdown syntax and features.

Rule Configuration

Many rules can be customized through configuration. See the Configuration Reference for details.

Example Rule Configuration

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

[rules.MD007]
indent = 2

Disabling Rules

Rules can be disabled globally or for specific files:

# Disable globally
disabled-rules = ["MD013", "MD033"]
<!-- mdbook-lint-disable MD013 -->
This line can be very long and won't trigger the line length rule.
<!-- mdbook-lint-enable MD013 -->

Getting Help

To see all available rules with descriptions:

mdbook-lint rules --detailed

To see only enabled rules:

mdbook-lint rules --enabled

Next Steps

Configuration Reference

This page provides a complete reference for all configuration options available in mdbook-lint.

Configuration File Format

mdbook-lint uses TOML format for configuration files. The default configuration file is .mdbook-lint.toml in your project root.

# Global settings
fail-on-warnings = false
disabled-rules = []

# Rule-specific configuration
[rules.MD013]
line-length = 80

[rules.MD007]
indent = 2

Global Configuration

fail-on-warnings

  • Type: boolean
  • Default: false
  • Description: When true, mdbook-lint exits with a non-zero code if any warnings are found.
fail-on-warnings = true

disabled-rules

  • Type: array of strings
  • Default: []
  • Description: List of rule IDs to disable globally.
disabled-rules = ["MD013", "MD033", "MD041"]

Rule-Specific Configuration

MD001 - Heading levels

No additional configuration options.

MD003 - Heading style

  • style: "atx" | "setext" | "consistent"
  • Default: "consistent"
[rules.MD003]
style = "atx"

MD004 - Unordered list style

  • style: "dash" | "asterisk" | "plus" | "consistent"
  • Default: "consistent"
[rules.MD004]
style = "dash"

MD007 - Unordered list indentation

  • indent: integer (number of spaces)
  • Default: 2
[rules.MD007]
indent = 4

MD009 - Trailing whitespace

  • br-spaces: integer (number of trailing spaces allowed for line breaks)
  • Default: 2
[rules.MD009]
br-spaces = 2

MD012 - Multiple consecutive blank lines

  • maximum: integer (maximum consecutive blank lines)
  • Default: 1
[rules.MD012]
maximum = 2

MD013 - Line length

  • line-length: integer (maximum line length)
  • Default: 80
  • code-blocks: boolean (check line length in code blocks)
  • Default: true
  • tables: boolean (check line length in tables)
  • Default: true
  • headings: boolean (check line length in headings)
  • Default: true
[rules.MD013]
line-length = 120
code-blocks = false
tables = false
headings = true

MD025 - Multiple top level headings

  • level: integer (heading level to check)
  • Default: 1
[rules.MD025]
level = 1

MD029 - Ordered list item prefix

  • style: "one" | "ordered" | "zero"
  • Default: "one"
[rules.MD029]
style = "ordered"

MD033 - Inline HTML

  • allowed-elements: array of strings (HTML elements to allow)
  • Default: []
[rules.MD033]
allowed-elements = ["br", "sub", "sup"]

MD035 - Horizontal rule style

  • style: string (horizontal rule style)
  • Default: "consistent"
[rules.MD035]
style = "---"

MD036 - Emphasis used instead of heading

  • punctuation: string (punctuation marks that indicate emphasis)
  • Default: ".,;:!"
[rules.MD036]
punctuation = ".,;:!?"

mdBook Integration Configuration

When using mdbook-lint as an mdBook preprocessor, configuration can be specified in book.toml:

[preprocessor.mdbook-lint]
fail-on-warnings = true
disabled-rules = ["MD025"]

# Rule configuration in book.toml
[preprocessor.mdbook-lint.rules.MD013]
line-length = 100

Configuration Precedence

Configuration is loaded in the following order (later sources override earlier ones):

  1. Built-in defaults
  2. .mdbook-lint.toml in project root
  3. book.toml preprocessor configuration (when used as preprocessor)
  4. Command-line arguments

Environment Variables

  • MDBOOK_LINT_CONFIG: Path to custom configuration file
  • MDBOOK_LINT_LOG: Log level (error, warn, info, debug, trace)
export MDBOOK_LINT_CONFIG=custom-config.toml
export MDBOOK_LINT_LOG=debug
mdbook-lint lint .

Configuration Examples

Strict Configuration

fail-on-warnings = true
disabled-rules = []

[rules.MD013]
line-length = 80
code-blocks = true
tables = true

[rules.MD007]
indent = 2

Relaxed Configuration

fail-on-warnings = false
disabled-rules = ["MD013", "MD033", "MD041"]

[rules.MD012]
maximum = 3

[rules.MD029]
style = "ordered"

mdBook-Optimized Configuration

# Common mdBook adjustments
disabled-rules = ["MD025"]  # Multiple H1s are OK in books
fail-on-warnings = true

[rules.MD013]
line-length = 100  # Slightly longer lines for books

[rules.MD033]
allowed-elements = ["br", "kbd", "sub", "sup"]  # Common HTML in docs

Validation

To validate your configuration file:

mdbook-lint lint --config .mdbook-lint.toml --dry-run

Next Steps

Library API

mdbook-lint is designed as a library-first project, making it easy to integrate markdown linting capabilities into your own Rust applications.

Overview

The core library (mdbook-lint-core) provides a clean, well-documented API for:

  • Creating lint engines with different rule sets
  • Processing markdown documents programmatically
  • Configuring rules and behavior
  • Handling violations and results

Quick Start

Add mdbook-lint to your Cargo.toml:

[dependencies]
mdbook-lint-core = "0.3.0"

Basic usage:

use mdbook_lint_core::{Document, create_engine_with_all_rules};
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a lint engine with all available rules
    let engine = create_engine_with_all_rules();
    
    // Create a document from content and path
    let content = "# My Document\n\nSome content here.";
    let document = Document::new(content.to_string(), PathBuf::from("example.md"))?;
    
    // Lint the document
    let violations = engine.lint_document(&document)?;
    
    // Process results
    for violation in violations {
        println!("{}:{}: {} - {}", 
            violation.line, 
            violation.column,
            violation.rule_id, 
            violation.message
        );
    }
    
    Ok(())
}

Core Types

Document

Represents a markdown document with content and metadata:

#![allow(unused)]
fn main() {
use mdbook_lint_core::Document;
use std::path::PathBuf;

let document = Document::new(
    "# Title\n\nContent".to_string(),
    PathBuf::from("my-file.md")
)?;
}

LintEngine

The main interface for linting operations:

#![allow(unused)]
fn main() {
use mdbook_lint_core::{LintEngine, create_engine_with_all_rules, create_standard_engine};

// Engine with all rules (standard + mdBook-specific)
let all_engine = create_engine_with_all_rules();

// Engine with only standard markdown rules (MD001-MD059)
let standard_engine = create_standard_engine();

// Engine with only mdBook-specific rules
let mdbook_engine = create_mdbook_engine();
}

Configuration

Control which rules are enabled and configure their behavior:

#![allow(unused)]
fn main() {
use mdbook_lint_core::Config;

let mut config = Config::default();

// Enable specific rules only
config.enabled_rules = vec!["MD001".to_string(), "MD013".to_string()];

// Disable specific rules
config.disabled_rules = vec!["MD002".to_string()];

// Enable specific categories
config.enabled_categories = vec!["structure".to_string()];

// Lint with configuration
let violations = engine.lint_document_with_config(&document, &config)?;
}

Violations

Results from linting operations:

#![allow(unused)]
fn main() {
use mdbook_lint_core::{Violation, Severity};

// Violations contain detailed information about issues found
for violation in violations {
    println!("Rule: {}", violation.rule_id);
    println!("Message: {}", violation.message);
    println!("Location: {}:{}", violation.line, violation.column);
    
    match violation.severity {
        Severity::Error => println!("This is an error"),
        Severity::Warning => println!("This is a warning"),
        Severity::Info => println!("This is informational"),
    }
}
}

Advanced Usage

Custom Rule Providers

Create engines with specific rule sets:

#![allow(unused)]
fn main() {
use mdbook_lint_core::{PluginRegistry, StandardRuleProvider};

let mut registry = PluginRegistry::new();
registry.register_provider(Box::new(StandardRuleProvider))?;

let engine = registry.create_engine()?;
}

Error Handling

The library uses anyhow for comprehensive error handling:

#![allow(unused)]
fn main() {
use mdbook_lint_core::error::Result;

fn lint_file(path: &Path) -> Result<Vec<Violation>> {
    let content = std::fs::read_to_string(path)?;
    let document = Document::new(content, path.to_path_buf())?;
    
    let engine = create_engine_with_all_rules();
    engine.lint_document(&document)
}
}

Batch Processing

Process multiple documents efficiently:

#![allow(unused)]
fn main() {
use walkdir::WalkDir;

let engine = create_engine_with_all_rules();
let mut all_violations = Vec::new();

for entry in WalkDir::new("src/") {
    let entry = entry?;
    if entry.path().extension().map_or(false, |ext| ext == "md") {
        let content = std::fs::read_to_string(entry.path())?;
        let document = Document::new(content, entry.path().to_path_buf())?;
        
        let violations = engine.lint_document(&document)?;
        all_violations.extend(violations);
    }
}
}

Rule Categories

Rules are organized into logical categories:

  • Structure: Document structure and hierarchy (MD001, MD003, etc.)
  • Formatting: Code blocks, lists, emphasis (MD004, MD005, etc.)
  • Content: Language, spelling, accessibility (MD044, MD045, etc.)
  • Links: URL validation and formatting (MD034, MD039, etc.)
  • MdBook: mdBook-specific checks (MDBOOK001-004)

Enable categories programmatically:

#![allow(unused)]
fn main() {
let mut config = Config::default();
config.enabled_categories = vec![
    "structure".to_string(),
    "formatting".to_string()
];
}

Integration Examples

mdBook Preprocessor

#![allow(unused)]
fn main() {
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::book::Book;
use mdbook_lint_core::create_engine_with_all_rules;

struct MyLinter {
    engine: LintEngine,
}

impl Preprocessor for MyLinter {
    fn run(&self, ctx: &PreprocessorContext, book: Book) -> mdbook::errors::Result<Book> {
        // Lint each chapter
        // Return book unchanged (linting only)
        Ok(book)
    }
}
}

CI/CD Integration

use mdbook_lint_core::{create_engine_with_all_rules, Severity};

fn main() -> std::process::ExitCode {
    let engine = create_engine_with_all_rules();
    let mut has_errors = false;
    
    // Process all markdown files in repository
    // Set exit code based on results
    
    if has_errors {
        std::process::ExitCode::FAILURE
    } else {
        std::process::ExitCode::SUCCESS
    }
}

API Documentation

For complete API documentation with examples, see the rustdoc documentation:

  • Online: https://docs.rs/mdbook-lint-core/latest/mdbook_lint_core/
  • Local: Run cargo doc --open --no-deps in the repository

The rustdoc includes:

  • Complete API reference with examples
  • Module-level documentation
  • Implementation details and internal architecture
  • Links between related types and functions

Performance Considerations

  • Single-pass parsing: Documents are parsed once and reused across all rules
  • Lazy evaluation: Rules are only applied to relevant document sections
  • Memory efficient: Minimal AST retention, streaming for large files
  • Parallel processing: Use rayon or similar for batch operations

Error Types

The library defines specific error types for different failure modes:

  • ConfigError: Configuration parsing and validation issues
  • DocumentError: Document creation and parsing problems
  • RuleError: Rule execution failures
  • IoError: File system access problems

Next Steps

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.

Architecture

This page provides an overview of mdbook-lint's internal architecture and design decisions.

High-Level Architecture

mdbook-lint is built as a modular Rust application with clear separation of concerns:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   CLI Interface │    │  mdBook Plugin  │    │  Library Core   │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         └───────────────────────┼───────────────────────┘
                                 │
                    ┌─────────────────┐
                    │   Lint Engine   │
                    └─────────────────┘
                             │
          ┌──────────────────┼──────────────────┐
          │                  │                  │
  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐
  │ Rule Registry │  │ Configuration │  │ File Parser   │
  └───────────────┘  └───────────────┘  └───────────────┘
          │
     ┌────┴────┐
     │  Rules  │
     └─────────┘

Core Components

Lint Engine

The central component that orchestrates the linting process:

  • Loads and validates configuration
  • Discovers and parses markdown files
  • Applies rules to parsed content
  • Collects and formats results

Rule System

A plugin-like architecture for linting rules:

  • Each rule implements a common Rule trait
  • Rules are automatically registered at compile time
  • Configurable rule parameters
  • Extensible for new rule types

Parser

Handles markdown parsing and AST generation:

  • Uses comrak for CommonMark compliance
  • Maintains source position information
  • Provides structured access to document elements

Configuration

Flexible configuration system:

  • TOML-based configuration files
  • Command-line argument override
  • Environment variable support
  • Validation and error reporting

Data Flow

  1. Input Processing

    • Command-line arguments parsed
    • Configuration files loaded and merged
    • File paths resolved and validated
  2. Document Processing

    • Markdown files parsed into AST
    • Source position tracking maintained
    • Document metadata extracted
  3. Rule Application

    • Enabled rules identified
    • Rules applied to document AST
    • Violations collected with positions
  4. Output Generation

    • Results formatted for display
    • Exit codes determined
    • Statistics calculated

Rule Implementation

Rules follow a consistent pattern:

#![allow(unused)]
fn main() {
pub struct ExampleRule {
    config: ExampleConfig,
}

impl Rule for ExampleRule {
    fn id(&self) -> &'static str {
        "MD001"
    }

    fn description(&self) -> &'static str {
        "Rule description"
    }

    fn check(&self, document: &Document) -> Vec<Violation> {
        // Rule logic here
    }
}
}

Rule Categories

  • Standard Rules (MD001-MD059): Based on markdownlint
  • mdBook Rules (MDBOOK001-004): mdBook-specific checks
  • Custom Rules: Extensible for project-specific needs

Performance Considerations

Parsing Strategy

  • Single-pass parsing per document
  • AST reuse across multiple rules
  • Lazy evaluation where possible

Memory Management

  • Streaming file processing for large projects
  • Minimal AST retention
  • Efficient string handling

Concurrency

  • Parallel file processing
  • Thread-safe rule application
  • Configurable worker threads

Error Handling

Error Categories

  • Configuration Errors: Invalid settings, missing files
  • Parse Errors: Malformed markdown, encoding issues
  • Rule Errors: Internal rule failures
  • IO Errors: File system access problems

Error Recovery

  • Graceful degradation on individual file failures
  • Detailed error context and suggestions
  • Configurable error tolerance levels

Extension Points

Custom Rules

#![allow(unused)]
fn main() {
// Plugin-style rule loading
pub fn register_custom_rule<R: Rule + 'static>(rule: R) {
    RULE_REGISTRY.register(Box::new(rule));
}
}

Output Formats

  • JSON for machine consumption
  • SARIF for integration tools
  • Custom formatters via traits

Configuration Sources

  • Environment variables
  • External configuration services
  • Runtime configuration updates

Testing Architecture

Test Categories

  • Unit Tests: Individual rule logic
  • Integration Tests: End-to-end CLI testing
  • Corpus Tests: Real-world markdown validation
  • Performance Tests: Benchmarking and profiling

Test Infrastructure

  • Automated test case generation
  • Snapshot testing for output validation
  • Property-based testing for edge cases

Dependencies

Core Dependencies

  • comrak: Markdown parsing
  • serde: Configuration serialization
  • clap: Command-line interface
  • anyhow: Error handling

Development Dependencies

  • criterion: Performance benchmarking
  • tempfile: Test file management
  • assert_cmd: CLI testing

Future Architecture Considerations

Planned Enhancements

  • Plugin system for external rules
  • Language server protocol support
  • Real-time linting capabilities
  • Integration with popular editors

Scalability

  • Distributed linting for large repositories
  • Caching and incremental analysis
  • Cloud-based rule execution

Contributing to Architecture

When making architectural changes:

  1. Maintain backward compatibility
  2. Document design decisions
  3. Consider performance implications
  4. Ensure testability
  5. Follow Rust best practices

Next Steps

  • See Contributing for development guidelines
  • Check Rules Reference for rule implementation details
  • Review source code for implementation specifics