use crate::error::Result;
use crate::rule::{Rule, RuleCategory, RuleMetadata};
use crate::{
Document,
violation::{Severity, Violation},
};
pub struct MD013 {
pub line_length: usize,
pub ignore_code_blocks: bool,
pub ignore_tables: bool,
pub ignore_headings: bool,
}
impl MD013 {
pub fn new() -> Self {
Self {
line_length: 80,
ignore_code_blocks: true,
ignore_tables: true,
ignore_headings: true,
}
}
#[allow(dead_code)]
pub fn with_line_length(line_length: usize) -> Self {
Self {
line_length,
ignore_code_blocks: true,
ignore_tables: true,
ignore_headings: true,
}
}
fn should_ignore_line(&self, line: &str, in_code_block: bool, in_table: bool) -> bool {
let trimmed = line.trim_start();
if in_code_block && self.ignore_code_blocks {
return true;
}
if in_table && self.ignore_tables {
return true;
}
if self.ignore_headings && trimmed.starts_with('#') {
return true;
}
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
return true;
}
false
}
}
impl Default for MD013 {
fn default() -> Self {
Self::new()
}
}
impl Rule for MD013 {
fn id(&self) -> &'static str {
"MD013"
}
fn name(&self) -> &'static str {
"line-length"
}
fn description(&self) -> &'static str {
"Line length should not exceed a specified limit"
}
fn metadata(&self) -> RuleMetadata {
RuleMetadata::stable(RuleCategory::Formatting).introduced_in("markdownlint v0.1.0")
}
fn check_with_ast<'a>(
&self,
document: &Document,
_ast: Option<&'a comrak::nodes::AstNode<'a>>,
) -> Result<Vec<Violation>> {
let mut violations = Vec::new();
let mut in_code_block = false;
let mut in_table = false;
for (line_number, line) in document.lines.iter().enumerate() {
let line_num = line_number + 1;
if line.trim_start().starts_with("```") {
in_code_block = !in_code_block;
continue;
}
let trimmed = line.trim();
if !in_code_block && (trimmed.starts_with('|') || trimmed.contains(" | ")) {
in_table = true;
} else if in_table && trimmed.is_empty() {
in_table = false;
}
if self.should_ignore_line(line, in_code_block, in_table) {
continue;
}
if line.len() > self.line_length {
let message = format!(
"Line length is {} characters, expected no more than {}",
line.len(),
self.line_length
);
violations.push(self.create_violation(
message,
line_num,
self.line_length + 1, Severity::Warning,
));
}
}
Ok(violations)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_md013_short_lines() {
let content = "# Short title\n\nThis is a short line.\nAnother short line.";
let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
let rule = MD013::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md013_long_line() {
let long_line = "a".repeat(100);
let content = format!("# Title\n\n{long_line}");
let document = Document::new(content, PathBuf::from("test.md")).unwrap();
let rule = MD013::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule_id, "MD013");
assert_eq!(violations[0].line, 3);
assert_eq!(violations[0].column, 81);
assert_eq!(violations[0].severity, Severity::Warning);
assert!(violations[0].message.contains("100 characters"));
assert!(violations[0].message.contains("no more than 80"));
}
#[test]
fn test_md013_custom_line_length() {
let content = "This line is exactly fifty characters long here.";
let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
let rule = MD013::with_line_length(40);
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("48 characters"));
assert!(violations[0].message.contains("no more than 40"));
}
#[test]
fn test_md013_ignore_headings() {
let long_heading = format!("# {}", "a".repeat(100));
let document = Document::new(long_heading, PathBuf::from("test.md")).unwrap();
let rule = MD013::new(); let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md013_ignore_code_blocks() {
let content = r#"# Title
```rust
let very_long_line_of_code_that_exceeds_the_normal_line_length_limit_but_should_be_ignored = "value";
```
This is a normal line that should be checked."#;
let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
let rule = MD013::new(); let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md013_ignore_urls() {
let content = "https://example.com/very/long/path/that/exceeds/normal/line/length/limits/but/should/be/ignored";
let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
let rule = MD013::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md013_ignore_tables() {
let content = r#"# Title
| Column 1 with very long content | Column 2 with very long content | Column 3 with very long content |
|----------------------------------|----------------------------------|----------------------------------|
| Data 1 with very long content | Data 2 with very long content | Data 3 with very long content |
This is a normal line that should be checked if it's too long."#;
let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
let rule = MD013::new(); let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 0);
}
#[test]
fn test_md013_multiple_violations() {
let long_line = "a".repeat(100);
let content = format!("Normal line\n{long_line}\nAnother normal line\n{long_line}");
let document = Document::new(content, PathBuf::from("test.md")).unwrap();
let rule = MD013::new();
let violations = rule.check(&document).unwrap();
assert_eq!(violations.len(), 2);
assert_eq!(violations[0].line, 2);
assert_eq!(violations[1].line, 4);
}
}