quickmark_core/rules/
md058.rs1use std::rc::Rc;
2
3use tree_sitter::Node;
4
5use crate::{
6 linter::{range_from_tree_sitter, Context, RuleLinter, RuleViolation},
7 rules::{Rule, RuleType},
8};
9
10pub(crate) struct MD058Linter {
15 context: Rc<Context>,
16 violations: Vec<RuleViolation>,
17}
18
19impl MD058Linter {
20 pub fn new(context: Rc<Context>) -> Self {
21 Self {
22 context,
23 violations: Vec::new(),
24 }
25 }
26
27 fn check_table_blanks(&mut self, table_node: &Node) {
28 let start_line = table_node.start_position().row;
29 let lines = self.context.lines.borrow();
30
31 let mut cursor = table_node.walk();
36 let Some(last_row) = table_node
37 .children(&mut cursor)
38 .filter(|child| {
39 matches!(
40 child.kind(),
41 "pipe_table_header" | "pipe_table_row" | "pipe_table_delimiter_row"
42 )
43 })
44 .filter(|row| {
45 let row_line = row.start_position().row;
46 lines.get(row_line).is_some_and(|l| l.contains('|'))
47 })
48 .last()
49 else {
50 return; };
52
53 let actual_end_line = last_row.end_position().row;
54
55 if start_line > 0 {
57 let has_content_above = (0..start_line).any(|i| !lines[i].trim().is_empty());
59
60 if has_content_above && !lines[start_line - 1].trim().is_empty() {
61 self.violations.push(RuleViolation::new(
62 &MD058,
63 format!("{} [Above]", MD058.description),
64 self.context.file_path.clone(),
65 range_from_tree_sitter(&table_node.range()),
66 ));
67 }
68 }
69
70 if actual_end_line + 1 < lines.len() {
72 let has_content_below =
74 ((actual_end_line + 1)..lines.len()).any(|i| !lines[i].trim().is_empty());
75
76 if has_content_below && !lines[actual_end_line + 1].trim().is_empty() {
77 self.violations.push(RuleViolation::new(
78 &MD058,
79 format!("{} [Below]", MD058.description),
80 self.context.file_path.clone(),
81 range_from_tree_sitter(&table_node.range()),
82 ));
83 }
84 }
85 }
86}
87
88impl RuleLinter for MD058Linter {
89 fn feed(&mut self, node: &Node) {
90 if node.kind() == "pipe_table" {
91 self.check_table_blanks(node);
92 }
93 }
94
95 fn finalize(&mut self) -> Vec<RuleViolation> {
96 std::mem::take(&mut self.violations)
97 }
98}
99
100pub const MD058: Rule = Rule {
101 id: "MD058",
102 alias: "blanks-around-tables",
103 tags: &["table", "blank_lines"],
104 description: "Tables should be surrounded by blank lines",
105 rule_type: RuleType::Token,
106 required_nodes: &["pipe_table"],
107 new_linter: |context| Box::new(MD058Linter::new(context)),
108};
109
110#[cfg(test)]
111mod test {
112 use std::path::PathBuf;
113
114 use crate::{
115 config::RuleSeverity, linter::MultiRuleLinter,
116 test_utils::test_helpers::test_config_with_rules,
117 };
118
119 fn test_config() -> crate::config::QuickmarkConfig {
120 test_config_with_rules(vec![("blanks-around-tables", RuleSeverity::Error)])
121 }
122
123 #[test]
124 fn test_table_with_proper_blank_lines() {
125 let input = r#"Some text
126
127| Header 1 | Header 2 |
128| -------- | -------- |
129| Cell 1 | Cell 2 |
130
131More text"#;
132 let config = test_config();
133 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
134 let violations = linter.analyze();
135 assert_eq!(0, violations.len());
136 }
137
138 #[test]
139 fn test_table_missing_blank_line_above() {
140 let input = r#"Some text
141| Header 1 | Header 2 |
142| -------- | -------- |
143| Cell 1 | Cell 2 |
144
145More text"#;
146 let config = test_config();
147 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
148 let violations = linter.analyze();
149 assert_eq!(1, violations.len());
150 assert!(violations[0].message().contains("[Above]"));
151 }
152
153 #[test]
154 fn test_table_missing_blank_line_below() {
155 let input = r#"Some text
156
157| Header 1 | Header 2 |
158| -------- | -------- |
159| Cell 1 | Cell 2 |
160More text"#;
161 let config = test_config();
162 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
163 let violations = linter.analyze();
164 assert_eq!(1, violations.len());
165 assert!(violations[0].message().contains("[Below]"));
166 }
167
168 #[test]
169 fn test_table_missing_both_blank_lines() {
170 let input = r#"Some text
171| Header 1 | Header 2 |
172| -------- | -------- |
173| Cell 1 | Cell 2 |
174More text"#;
175 let config = test_config();
176 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
177 let violations = linter.analyze();
178 assert_eq!(2, violations.len());
179 assert!(violations[0].message().contains("[Above]"));
180 assert!(violations[1].message().contains("[Below]"));
181 }
182
183 #[test]
184 fn test_table_at_start_of_document() {
185 let input = r#"| Header 1 | Header 2 |
186| -------- | -------- |
187| Cell 1 | Cell 2 |
188
189More text"#;
190 let config = test_config();
191 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
192 let violations = linter.analyze();
193 assert_eq!(0, violations.len());
195 }
196
197 #[test]
198 fn test_table_at_end_of_document() {
199 let input = r#"Some text
200
201| Header 1 | Header 2 |
202| -------- | -------- |
203| Cell 1 | Cell 2 |"#;
204 let config = test_config();
205 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
206 let violations = linter.analyze();
207 assert_eq!(0, violations.len());
209 }
210
211 #[test]
212 fn test_table_alone_in_document() {
213 let input = r#"| Header 1 | Header 2 |
214| -------- | -------- |
215| Cell 1 | Cell 2 |"#;
216 let config = test_config();
217 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
218 let violations = linter.analyze();
219 assert_eq!(0, violations.len());
221 }
222
223 #[test]
224 fn test_multiple_tables_proper_spacing() {
225 let input = r#"Some text
226
227| Table 1 | Header |
228| ------- | ------ |
229| Cell | Value |
230
231Text between tables
232
233| Table 2 | Header |
234| ------- | ------ |
235| Cell | Value |
236
237Final text"#;
238 let config = test_config();
239 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
240 let violations = linter.analyze();
241 assert_eq!(0, violations.len());
242 }
243
244 #[test]
245 fn test_multiple_tables_improper_spacing() {
246 let input = r#"Some text
247| Table 1 | Header |
248| ------- | ------ |
249| Cell | Value |
250Text between tables
251| Table 2 | Header |
252| ------- | ------ |
253| Cell | Value |
254Final text"#;
255 let config = test_config();
256 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
257 let violations = linter.analyze();
258 assert_eq!(4, violations.len()); }
260
261 #[test]
262 fn test_table_with_only_blank_lines_above_and_below() {
263 let input = r#"
264
265
266| Header 1 | Header 2 |
267| -------- | -------- |
268| Cell 1 | Cell 2 |
269
270
271"#;
272 let config = test_config();
273 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
274 let violations = linter.analyze();
275 assert_eq!(0, violations.len());
277 }
278}