mdbook_lint_core/rules/standard/
md013.rs1use crate::error::Result;
2use crate::rule::{Rule, RuleCategory, RuleMetadata};
3use crate::{
4 Document,
5 violation::{Severity, Violation},
6};
7
8pub struct MD013 {
13 pub line_length: usize,
15 pub ignore_code_blocks: bool,
17 pub ignore_tables: bool,
19 pub ignore_headings: bool,
21}
22
23impl MD013 {
24 pub fn new() -> Self {
26 Self {
27 line_length: 80,
28 ignore_code_blocks: true,
29 ignore_tables: true,
30 ignore_headings: true,
31 }
32 }
33
34 #[allow(dead_code)]
36 pub fn with_line_length(line_length: usize) -> Self {
37 Self {
38 line_length,
39 ignore_code_blocks: true,
40 ignore_tables: true,
41 ignore_headings: true,
42 }
43 }
44
45 fn should_ignore_line(&self, line: &str, in_code_block: bool, in_table: bool) -> bool {
47 let trimmed = line.trim_start();
48
49 if in_code_block && self.ignore_code_blocks {
51 return true;
52 }
53
54 if in_table && self.ignore_tables {
56 return true;
57 }
58
59 if self.ignore_headings && trimmed.starts_with('#') {
61 return true;
62 }
63
64 if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
66 return true;
67 }
68
69 false
70 }
71}
72
73impl Default for MD013 {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl Rule for MD013 {
80 fn id(&self) -> &'static str {
81 "MD013"
82 }
83
84 fn name(&self) -> &'static str {
85 "line-length"
86 }
87
88 fn description(&self) -> &'static str {
89 "Line length should not exceed a specified limit"
90 }
91
92 fn metadata(&self) -> RuleMetadata {
93 RuleMetadata::stable(RuleCategory::Formatting).introduced_in("markdownlint v0.1.0")
94 }
95
96 fn check_with_ast<'a>(
97 &self,
98 document: &Document,
99 _ast: Option<&'a comrak::nodes::AstNode<'a>>,
100 ) -> Result<Vec<Violation>> {
101 let mut violations = Vec::new();
103 let mut in_code_block = false;
104 let mut in_table = false;
105
106 for (line_number, line) in document.lines.iter().enumerate() {
107 let line_num = line_number + 1; if line.trim_start().starts_with("```") {
111 in_code_block = !in_code_block;
112 continue;
113 }
114
115 let trimmed = line.trim();
117 if !in_code_block && (trimmed.starts_with('|') || trimmed.contains(" | ")) {
118 in_table = true;
119 } else if in_table && trimmed.is_empty() {
120 in_table = false;
121 }
122
123 if self.should_ignore_line(line, in_code_block, in_table) {
125 continue;
126 }
127
128 if line.len() > self.line_length {
130 let message = format!(
131 "Line length is {} characters, expected no more than {}",
132 line.len(),
133 self.line_length
134 );
135
136 violations.push(self.create_violation(
137 message,
138 line_num,
139 self.line_length + 1, Severity::Warning,
141 ));
142 }
143 }
144
145 Ok(violations)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::path::PathBuf;
153
154 #[test]
155 fn test_md013_short_lines() {
156 let content = "# Short title\n\nThis is a short line.\nAnother short line.";
157 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
158 let rule = MD013::new();
159 let violations = rule.check(&document).unwrap();
160
161 assert_eq!(violations.len(), 0);
162 }
163
164 #[test]
165 fn test_md013_long_line() {
166 let long_line = "a".repeat(100);
167 let content = format!("# Title\n\n{long_line}");
168 let document = Document::new(content, PathBuf::from("test.md")).unwrap();
169 let rule = MD013::new();
170 let violations = rule.check(&document).unwrap();
171
172 assert_eq!(violations.len(), 1);
173 assert_eq!(violations[0].rule_id, "MD013");
174 assert_eq!(violations[0].line, 3);
175 assert_eq!(violations[0].column, 81);
176 assert_eq!(violations[0].severity, Severity::Warning);
177 assert!(violations[0].message.contains("100 characters"));
178 assert!(violations[0].message.contains("no more than 80"));
179 }
180
181 #[test]
182 fn test_md013_custom_line_length() {
183 let content = "This line is exactly fifty characters long here.";
184 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
185 let rule = MD013::with_line_length(40);
186 let violations = rule.check(&document).unwrap();
187
188 assert_eq!(violations.len(), 1);
189 assert!(violations[0].message.contains("48 characters"));
190 assert!(violations[0].message.contains("no more than 40"));
191 }
192
193 #[test]
194 fn test_md013_ignore_headings() {
195 let long_heading = format!("# {}", "a".repeat(100));
196 let document = Document::new(long_heading, PathBuf::from("test.md")).unwrap();
197 let rule = MD013::new(); let violations = rule.check(&document).unwrap();
199
200 assert_eq!(violations.len(), 0);
201 }
202
203 #[test]
204 fn test_md013_ignore_code_blocks() {
205 let content = r#"# Title
206
207```rust
208let very_long_line_of_code_that_exceeds_the_normal_line_length_limit_but_should_be_ignored = "value";
209```
210
211This is a normal line that should be checked."#;
212
213 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
214 let rule = MD013::new(); let violations = rule.check(&document).unwrap();
216
217 assert_eq!(violations.len(), 0);
218 }
219
220 #[test]
221 fn test_md013_ignore_urls() {
222 let content = "https://example.com/very/long/path/that/exceeds/normal/line/length/limits/but/should/be/ignored";
223 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
224 let rule = MD013::new();
225 let violations = rule.check(&document).unwrap();
226
227 assert_eq!(violations.len(), 0);
228 }
229
230 #[test]
231 fn test_md013_ignore_tables() {
232 let content = r#"# Title
233
234| Column 1 with very long content | Column 2 with very long content | Column 3 with very long content |
235|----------------------------------|----------------------------------|----------------------------------|
236| Data 1 with very long content | Data 2 with very long content | Data 3 with very long content |
237
238This is a normal line that should be checked if it's too long."#;
239
240 let document = Document::new(content.to_string(), PathBuf::from("test.md")).unwrap();
241 let rule = MD013::new(); let violations = rule.check(&document).unwrap();
243
244 assert_eq!(violations.len(), 0);
245 }
246
247 #[test]
248 fn test_md013_multiple_violations() {
249 let long_line = "a".repeat(100);
250 let content = format!("Normal line\n{long_line}\nAnother normal line\n{long_line}");
251 let document = Document::new(content, PathBuf::from("test.md")).unwrap();
252 let rule = MD013::new();
253 let violations = rule.check(&document).unwrap();
254
255 assert_eq!(violations.len(), 2);
256 assert_eq!(violations[0].line, 2);
257 assert_eq!(violations[1].line, 4);
258 }
259}