pub trait AstRule: Send + Sync {
// Required methods
fn id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn metadata(&self) -> RuleMetadata;
fn check_ast<'a>(
&self,
document: &Document,
ast: &'a AstNode<'a>,
) -> Result<Vec<Violation>>;
// Provided methods
fn can_fix(&self) -> bool { ... }
fn fix(&self, _content: &str, _violation: &Violation) -> Option<String> { ... }
fn create_violation(
&self,
message: String,
line: usize,
column: usize,
severity: Severity,
) -> Violation { ... }
}Expand description
Helper trait for AST-based rules
§When to Use AstRule vs Rule
Use AstRule when your rule needs to:
- Analyze document structure (headings, lists, links, code blocks)
- Navigate parent-child relationships in the markdown tree
- Access precise position information from comrak’s sourcepos
- Understand markdown semantics beyond simple text patterns
Use Rule directly when your rule:
- Only needs line-by-line text analysis
- Checks simple text patterns (trailing spaces, line length)
- Doesn’t need to understand markdown structure
§Implementation Examples
AstRule Examples:
MD001(heading-increment): Needs to traverse heading hierarchyMDBOOK002(link-validation): Needs to find and validate link nodesMD031(blanks-around-fences): Needs to identify fenced code blocks
Rule Examples:
MD013(line-length): Simple line-by-line character countingMD009(no-trailing-spaces): Pattern matching on line endings
§Basic Implementation Pattern
use mdbook_lint_core::rule::{AstRule, RuleMetadata, RuleCategory};
use mdbook_lint_core::{Document, Violation, Result};
use comrak::nodes::{AstNode, NodeValue};
pub struct MyRule;
impl AstRule for MyRule {
fn id(&self) -> &'static str { "MY001" }
fn name(&self) -> &'static str { "my-rule" }
fn description(&self) -> &'static str { "Description of what this rule checks" }
fn metadata(&self) -> RuleMetadata {
RuleMetadata::stable(RuleCategory::Structure)
}
fn check_ast<'a>(&self, document: &Document, ast: &'a AstNode<'a>) -> Result<Vec<Violation>> {
let mut violations = Vec::new();
// Find nodes of interest
for node in ast.descendants() {
if let NodeValue::Heading(heading) = &node.data.borrow().value {
// Get position information
if let Some((line, column)) = document.node_position(node) {
// Check some condition
if heading.level > 3 {
violations.push(self.create_violation(
"Heading too deep".to_string(),
line,
column,
mdbook_lint_core::violation::Severity::Warning,
));
}
}
}
}
Ok(violations)
}
}§Key Methods Available
From Document:
document.node_position(node)- Get (line, column) for any AST nodedocument.node_text(node)- Extract text content from a nodedocument.headings(ast)- Get all heading nodesdocument.code_blocks(ast)- Get all code block nodes
From AstNode:
node.descendants()- Iterate all child nodes recursivelynode.children()- Get direct children onlynode.parent()- Get parent node (if any)node.data.borrow().value- Access the NodeValue enum
Creating Violations:
self.create_violation(message, line, column, severity)- Standard violation creation
Required Methods§
Sourcefn description(&self) -> &'static str
fn description(&self) -> &'static str
Description of what the rule checks
Sourcefn metadata(&self) -> RuleMetadata
fn metadata(&self) -> RuleMetadata
Metadata about this rule’s status and properties