use crate::{Document, error::Result, violation::Violation};
use comrak::{Arena, nodes::AstNode};
#[derive(Debug, Clone, PartialEq)]
pub enum RuleStability {
Stable,
Experimental,
Deprecated,
Reserved,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RuleCategory {
Structure,
Formatting,
Content,
Links,
Accessibility,
MdBook,
}
#[derive(Debug, Clone)]
pub struct RuleMetadata {
pub deprecated: bool,
pub deprecated_reason: Option<&'static str>,
pub replacement: Option<&'static str>,
pub category: RuleCategory,
pub introduced_in: Option<&'static str>,
pub stability: RuleStability,
}
impl RuleMetadata {
pub fn stable(category: RuleCategory) -> Self {
Self {
deprecated: false,
deprecated_reason: None,
replacement: None,
category,
introduced_in: None,
stability: RuleStability::Stable,
}
}
pub fn deprecated(
category: RuleCategory,
reason: &'static str,
replacement: Option<&'static str>,
) -> Self {
Self {
deprecated: true,
deprecated_reason: Some(reason),
replacement,
category,
introduced_in: None,
stability: RuleStability::Deprecated,
}
}
pub fn experimental(category: RuleCategory) -> Self {
Self {
deprecated: false,
deprecated_reason: None,
replacement: None,
category,
introduced_in: None,
stability: RuleStability::Experimental,
}
}
pub fn reserved(reason: &'static str) -> Self {
Self {
deprecated: false,
deprecated_reason: Some(reason),
replacement: None,
category: RuleCategory::Structure,
introduced_in: None,
stability: RuleStability::Reserved,
}
}
pub fn introduced_in(mut self, version: &'static str) -> Self {
self.introduced_in = Some(version);
self
}
}
pub trait Rule: Send + Sync {
fn id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn metadata(&self) -> RuleMetadata;
fn check_with_ast<'a>(
&self,
document: &Document,
ast: Option<&'a AstNode<'a>>,
) -> Result<Vec<Violation>>;
fn check(&self, document: &Document) -> Result<Vec<Violation>> {
self.check_with_ast(document, None)
}
fn can_fix(&self) -> bool {
false
}
fn fix(&self, _content: &str, _violation: &Violation) -> Option<String> {
None
}
fn create_violation(
&self,
message: String,
line: usize,
column: usize,
severity: crate::violation::Severity,
) -> Violation {
Violation {
rule_id: self.id().to_string(),
rule_name: self.name().to_string(),
message,
line,
column,
severity,
}
}
}
pub trait AstRule: Send + Sync {
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>>;
fn can_fix(&self) -> bool {
false
}
fn fix(&self, _content: &str, _violation: &Violation) -> Option<String> {
None
}
fn create_violation(
&self,
message: String,
line: usize,
column: usize,
severity: crate::violation::Severity,
) -> Violation {
Violation {
rule_id: self.id().to_string(),
rule_name: self.name().to_string(),
message,
line,
column,
severity,
}
}
}
impl<T: AstRule> Rule for T {
fn id(&self) -> &'static str {
T::id(self)
}
fn name(&self) -> &'static str {
T::name(self)
}
fn description(&self) -> &'static str {
T::description(self)
}
fn metadata(&self) -> RuleMetadata {
T::metadata(self)
}
fn check_with_ast<'a>(
&self,
document: &Document,
ast: Option<&'a AstNode<'a>>,
) -> Result<Vec<Violation>> {
if let Some(ast) = ast {
self.check_ast(document, ast)
} else {
let arena = Arena::new();
let ast = document.parse_ast(&arena);
self.check_ast(document, ast)
}
}
fn check(&self, document: &Document) -> Result<Vec<Violation>> {
self.check_with_ast(document, None)
}
fn can_fix(&self) -> bool {
T::can_fix(self)
}
fn fix(&self, content: &str, violation: &Violation) -> Option<String> {
T::fix(self, content, violation)
}
fn create_violation(
&self,
message: String,
line: usize,
column: usize,
severity: crate::violation::Severity,
) -> Violation {
T::create_violation(self, message, line, column, severity)
}
}