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.
From Crates.io (Recommended)
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:
-
Add to book.toml:
[preprocessor.mdbook-lint]
-
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:
- Initial setup: Add configuration file and run first lint
- Fix major issues: Address structural problems first
- Customize rules: Disable rules that don't fit your style
- Integrate with build: Add to mdBook or CI pipeline
- 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
- Learn about Configuration options
- Explore CLI Usage in detail
- Set up mdBook Integration
- Browse the Rules Reference
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
Setting | Type | Default | Description |
---|---|---|---|
fail-on-warnings | boolean | false | Exit with error code when warnings are found |
disabled-rules | array | [] | 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):
- Built-in defaults
.mdbook-lint.toml
in project root- 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
- Learn about CLI Usage
- Browse the Configuration Reference for all options
- See Rules Reference for rule-specific settings
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 found2
: Invalid arguments or configuration
Next Steps
- Learn about mdBook Integration
- See Configuration Reference for all options
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:
- Check all markdown files in your book
- Report any linting issues
- Optionally fail the build if issues are found
- 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:
- Ensure mdbook-lint is installed and in your PATH
- Check that
[preprocessor.mdbook-lint]
is in yourbook.toml
- Try running with verbose output:
mdbook build -v
Configuration Not Applied
Configuration precedence for the preprocessor:
- Built-in defaults
.mdbook-lint.toml
filebook.toml
preprocessor configuration- Command-line arguments (when running CLI directly)
Next Steps
- Learn about Rules Reference
- See Configuration Reference for all options
- Check out Contributing to help improve mdBook integration
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.
Link Rules
MD034 - Bare URL used
Requires proper link formatting for URLs.
MD039 - Spaces inside link text
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.
MDBOOK002 - Internal link validation
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
- Learn about Configuration Reference
- Explore CLI Usage for rule-specific commands
- Check Contributing to request new rules
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):
- Built-in defaults
.mdbook-lint.toml
in project rootbook.toml
preprocessor configuration (when used as preprocessor)- Command-line arguments
Environment Variables
MDBOOK_LINT_CONFIG
: Path to custom configuration fileMDBOOK_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
- Learn about specific rules in Rules Reference
- See CLI Usage for command-line configuration options
- Check mdBook Integration for preprocessor setup
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 issuesDocumentError
: Document creation and parsing problemsRuleError
: Rule execution failuresIoError
: File system access problems
Next Steps
- See Configuration for detailed configuration options
- Check Rules Reference for available rules
- Review Architecture for internal design details
- Browse the source code on GitHub
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
- Create a branch:
git checkout -b feature/your-change
- Make changes: Follow the guidelines below
- Test thoroughly: Ensure all tests pass
- 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
- Add module: Include your rule in
src/rules/standard/mod.rs
- Register rule: Add to
StandardRuleProvider::register_rules()
- 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
- Automated checks run on all PRs
- Maintainer review for code quality
- Testing to ensure functionality
- 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
andAstRule
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###
orMDBOOK###
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
- Changes merged to main via PR
- Release-please creates release PR automatically
- Merge release PR to trigger release
- GitHub Actions publishes to crates.io
Getting Help
Resources
- Documentation: joshrotenberg.github.io/mdbook-lint
- Repository: github.com/joshrotenberg/mdbook-lint
- Issues: Report bugs and request features
- Discussions: Ask questions and get help
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
ormdbook###.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
-
Input Processing
- Command-line arguments parsed
- Configuration files loaded and merged
- File paths resolved and validated
-
Document Processing
- Markdown files parsed into AST
- Source position tracking maintained
- Document metadata extracted
-
Rule Application
- Enabled rules identified
- Rules applied to document AST
- Violations collected with positions
-
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 parsingserde
: Configuration serializationclap
: Command-line interfaceanyhow
: Error handling
Development Dependencies
criterion
: Performance benchmarkingtempfile
: Test file managementassert_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:
- Maintain backward compatibility
- Document design decisions
- Consider performance implications
- Ensure testability
- Follow Rust best practices
Next Steps
- See Contributing for development guidelines
- Check Rules Reference for rule implementation details
- Review source code for implementation specifics