mdbook_lint_core/rules/standard/
md010.rs1use crate::error::Result;
6use crate::rule::{Rule, RuleCategory, RuleMetadata};
7use crate::{
8 Document,
9 violation::{Severity, Violation},
10};
11
12pub struct MD010 {
14 spaces_per_tab: usize,
16}
17
18impl MD010 {
19 pub fn new() -> Self {
21 Self { spaces_per_tab: 4 }
22 }
23
24 #[allow(dead_code)]
26 pub fn with_spaces_per_tab(spaces_per_tab: usize) -> Self {
27 Self { spaces_per_tab }
28 }
29}
30
31impl Default for MD010 {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl Rule for MD010 {
38 fn id(&self) -> &'static str {
39 "MD010"
40 }
41
42 fn name(&self) -> &'static str {
43 "no-hard-tabs"
44 }
45
46 fn description(&self) -> &'static str {
47 "Hard tabs are not allowed"
48 }
49
50 fn metadata(&self) -> RuleMetadata {
51 RuleMetadata::stable(RuleCategory::Formatting).introduced_in("markdownlint v0.1.0")
52 }
53
54 fn check_with_ast<'a>(
55 &self,
56 document: &Document,
57 _ast: Option<&'a comrak::nodes::AstNode<'a>>,
58 ) -> Result<Vec<Violation>> {
59 let mut violations = Vec::new();
60
61 for (line_number, line) in document.lines.iter().enumerate() {
62 let line_num = line_number + 1; if let Some(tab_pos) = line.find('\t') {
66 let column = tab_pos + 1; violations.push(self.create_violation(
69 format!(
70 "Hard tab character found (consider using {} spaces)",
71 self.spaces_per_tab
72 ),
73 line_num,
74 column,
75 Severity::Warning,
76 ));
77 }
78 }
79
80 Ok(violations)
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::rule::Rule;
88 use std::path::PathBuf;
89
90 fn create_test_document(content: &str) -> Document {
91 Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
92 }
93
94 #[test]
95 fn test_md010_no_tabs() {
96 let content = "# Heading\n\nNo tabs here.\nJust spaces.";
97 let document = create_test_document(content);
98 let rule = MD010::new();
99 let violations = rule.check(&document).unwrap();
100
101 assert_eq!(violations.len(), 0);
102 }
103
104 #[test]
105 fn test_md010_single_tab() {
106 let content = "# Heading\n\nLine with\ttab.\nClean line.";
107 let document = create_test_document(content);
108 let rule = MD010::new();
109 let violations = rule.check(&document).unwrap();
110
111 assert_eq!(violations.len(), 1);
112 assert_eq!(violations[0].rule_id, "MD010");
113 assert_eq!(violations[0].line, 3);
114 assert_eq!(violations[0].column, 10);
115 assert!(violations[0].message.contains("Hard tab character"));
116 assert!(violations[0].message.contains("4 spaces"));
117 }
118
119 #[test]
120 fn test_md010_multiple_tabs() {
121 let content = "# Heading\n\nLine\twith\ttabs.\nAnother\ttab line.";
122 let document = create_test_document(content);
123 let rule = MD010::new();
124 let violations = rule.check(&document).unwrap();
125
126 assert_eq!(violations.len(), 2);
127 assert_eq!(violations[0].line, 3);
128 assert_eq!(violations[0].column, 5); assert_eq!(violations[1].line, 4);
130 assert_eq!(violations[1].column, 8); }
132
133 #[test]
134 fn test_md010_custom_spaces_per_tab() {
135 let content = "Line with\ttab.";
136 let document = create_test_document(content);
137 let rule = MD010::with_spaces_per_tab(2);
138 let violations = rule.check(&document).unwrap();
139
140 assert_eq!(violations.len(), 1);
141 assert!(violations[0].message.contains("2 spaces"));
142 }
143
144 #[test]
145 fn test_md010_tab_at_beginning() {
146 let content = "\tIndented with tab";
147 let document = create_test_document(content);
148 let rule = MD010::new();
149 let violations = rule.check(&document).unwrap();
150
151 assert_eq!(violations.len(), 1);
152 assert_eq!(violations[0].column, 1);
153 }
154
155 #[test]
156 fn test_md010_only_first_tab_reported() {
157 let content = "Line\twith\tmultiple\ttabs";
158 let document = create_test_document(content);
159 let rule = MD010::new();
160 let violations = rule.check(&document).unwrap();
161
162 assert_eq!(violations.len(), 1);
164 assert_eq!(violations[0].column, 5);
165 }
166}