1use serde::Deserialize;
2use std::rc::Rc;
3
4use tree_sitter::Node;
5
6use crate::{
7 linter::{range_from_tree_sitter, RuleViolation},
8 rules::{Context, Rule, RuleLinter, RuleType},
9};
10
11#[derive(Debug, PartialEq, Clone, Deserialize)]
13pub struct MD027BlockquoteSpacesTable {
14 #[serde(default)]
15 pub list_items: bool,
16}
17
18impl Default for MD027BlockquoteSpacesTable {
19 fn default() -> Self {
20 Self { list_items: true }
21 }
22}
23
24pub(crate) struct MD027Linter {
30 context: Rc<Context>,
31 violations: Vec<RuleViolation>,
32}
33
34impl MD027Linter {
35 pub fn new(context: Rc<Context>) -> Self {
36 Self {
37 context,
38 violations: Vec::new(),
39 }
40 }
41
42 fn analyze_all_lines(&mut self) {
44 let settings = self
45 .context
46 .config
47 .linters
48 .settings
49 .blockquote_spaces
50 .clone();
51 let lines = self.context.lines.borrow();
52
53 let code_block_lines = self.get_code_block_lines();
55
56 for (line_index, line) in lines.iter().enumerate() {
57 let line_number = line_index + 1;
58
59 if code_block_lines.contains(&line_number) {
61 continue;
62 }
63
64 if let Some(violation) = self.check_blockquote_line(line, line_index, &settings) {
66 self.violations.push(violation);
67 }
68 }
69 }
70
71 fn check_blockquote_line(
73 &self,
74 line: &str,
75 line_index: usize,
76 settings: &crate::config::MD027BlockquoteSpacesTable,
77 ) -> Option<RuleViolation> {
78 let mut current_line = line;
80 let mut current_offset = 0;
81
82 let leading_whitespace = current_line.len() - current_line.trim_start().len();
84 current_line = current_line.trim_start();
85 current_offset += leading_whitespace;
86
87 while current_line.starts_with('>') {
89 let after_gt = ¤t_line[1..]; if after_gt.starts_with(" ") {
93 let space_count = after_gt.chars().take_while(|&c| c == ' ').count();
95
96 if !settings.list_items && self.is_list_item_content(after_gt) {
98 return None;
99 }
100
101 let start_column = current_offset + 2; let end_column = start_column + space_count - 2; let violation = RuleViolation::new(
107 &MD027,
108 "Multiple spaces after blockquote symbol".to_string(),
109 self.context.file_path.clone(),
110 range_from_tree_sitter(&tree_sitter::Range {
111 start_byte: 0,
112 end_byte: 0,
113 start_point: tree_sitter::Point {
114 row: line_index,
115 column: start_column,
116 },
117 end_point: tree_sitter::Point {
118 row: line_index,
119 column: end_column,
120 },
121 }),
122 );
123
124 return Some(violation);
125 }
126
127 current_line = ¤t_line[1..];
129 current_offset += 1;
130
131 if current_line.starts_with(' ') {
133 current_line = ¤t_line[1..];
134 current_offset += 1;
135 }
136
137 if !current_line.starts_with('>') {
139 break;
140 }
141 }
142
143 None
144 }
145
146 fn get_code_block_lines(&self) -> std::collections::HashSet<usize> {
148 let node_cache = self.context.node_cache.borrow();
149 let mut code_block_lines = std::collections::HashSet::new();
150
151 if let Some(indented_blocks) = node_cache.get("indented_code_block") {
153 for node_info in indented_blocks {
154 code_block_lines.extend((node_info.line_start + 1)..=(node_info.line_end + 1));
155 }
156 }
157
158 if let Some(fenced_blocks) = node_cache.get("fenced_code_block") {
160 for node_info in fenced_blocks {
161 code_block_lines.extend((node_info.line_start + 1)..=(node_info.line_end + 1));
162 }
163 }
164
165 if let Some(html_comments) = node_cache.get("html_block") {
167 for node_info in html_comments {
168 code_block_lines.extend((node_info.line_start + 1)..=(node_info.line_end + 1));
169 }
170 }
171
172 code_block_lines
173 }
174
175 fn is_ordered_list_marker(&self, text: &str, delimiter: char) -> bool {
177 if let Some(pos) = text.find(delimiter) {
178 if pos > 0 {
179 let prefix = &text[..pos];
180 if prefix.chars().all(|c| c.is_ascii_digit())
181 || (prefix.len() == 1 && prefix.chars().all(|c| c.is_ascii_alphabetic()))
182 {
183 return text.chars().nth(pos + 1).is_some_and(|c| c.is_whitespace());
184 }
185 }
186 }
187 false
188 }
189
190 fn is_list_item_content(&self, content: &str) -> bool {
192 let trimmed = content.trim_start();
193
194 if trimmed.starts_with('-') || trimmed.starts_with('+') || trimmed.starts_with('*') {
196 return trimmed.chars().nth(1).is_some_and(|c| c.is_whitespace());
197 }
198
199 if self.is_ordered_list_marker(trimmed, '.') || self.is_ordered_list_marker(trimmed, ')') {
201 return true;
202 }
203
204 false
205 }
206}
207
208impl RuleLinter for MD027Linter {
209 fn feed(&mut self, node: &Node) {
210 if node.kind() == "document" {
212 self.analyze_all_lines();
213 }
214 }
215
216 fn finalize(&mut self) -> Vec<RuleViolation> {
217 std::mem::take(&mut self.violations)
218 }
219}
220
221pub const MD027: Rule = Rule {
222 id: "MD027",
223 alias: "no-multiple-space-blockquote",
224 tags: &["blockquote", "whitespace", "indentation"],
225 description: "Multiple spaces after blockquote symbol",
226 rule_type: RuleType::Hybrid,
227 required_nodes: &["indented_code_block", "fenced_code_block", "html_block"],
229 new_linter: |context| Box::new(MD027Linter::new(context)),
230};
231
232#[cfg(test)]
233mod test {
234 use std::path::PathBuf;
235
236 use crate::config::{LintersSettingsTable, MD027BlockquoteSpacesTable, RuleSeverity};
237 use crate::linter::MultiRuleLinter;
238 use crate::test_utils::test_helpers::{test_config_with_rules, test_config_with_settings};
239
240 fn test_config() -> crate::config::QuickmarkConfig {
241 test_config_with_rules(vec![
242 ("no-multiple-space-blockquote", RuleSeverity::Error),
243 ("heading-style", RuleSeverity::Off),
244 ("heading-increment", RuleSeverity::Off),
245 ])
246 }
247
248 fn test_config_with_blockquote_spaces(
249 blockquote_spaces_config: MD027BlockquoteSpacesTable,
250 ) -> crate::config::QuickmarkConfig {
251 test_config_with_settings(
252 vec![
253 ("no-multiple-space-blockquote", RuleSeverity::Error),
254 ("heading-style", RuleSeverity::Off),
255 ("heading-increment", RuleSeverity::Off),
256 ],
257 LintersSettingsTable {
258 blockquote_spaces: blockquote_spaces_config,
259 ..Default::default()
260 },
261 )
262 }
263
264 #[test]
265 fn test_basic_multiple_space_violation() {
266 let input = "> This is correct\n> This has multiple spaces";
267
268 let config = test_config();
269 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
270 let violations = linter.analyze();
271 assert_eq!(1, violations.len());
272
273 let violation = &violations[0];
274 assert_eq!("MD027", violation.rule().id);
275 assert!(violation.message().contains("Multiple spaces"));
276 }
277
278 #[test]
279 fn test_no_violation_single_space() {
280 let input = "> This is correct\n> This is also correct";
281
282 let config = test_config();
283 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
284 let violations = linter.analyze();
285 assert_eq!(0, violations.len());
286 }
287
288 #[test]
289 fn test_list_items_configuration() {
290 let input = "> - Item with multiple spaces\n> - Normal item";
291
292 let config =
294 test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: true });
295 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
296 let violations = linter.analyze();
297 assert_eq!(1, violations.len());
298
299 let config =
301 test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: false });
302 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
303 let violations = linter.analyze();
304 assert_eq!(0, violations.len());
305 }
306
307 #[test]
308 fn test_indented_code_blocks_excluded() {
309 let input = " > This is in an indented code block with multiple spaces";
310
311 let config = test_config();
312 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
313 let violations = linter.analyze();
314 assert_eq!(0, violations.len()); }
316
317 #[test]
318 fn test_nested_blockquotes() {
319 let input = "> First level\n>> Second level with multiple spaces\n> > Another second level with multiple spaces";
320
321 let config = test_config();
322 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
323 let violations = linter.analyze();
324 assert_eq!(1, violations.len()); let violation = &violations[0];
328 assert_eq!("MD027", violation.rule().id);
329 assert_eq!(1, violation.location().range.start.line); }
331
332 #[test]
333 fn test_blockquote_with_leading_spaces() {
334 let input = " > Text with multiple spaces after >";
335
336 let config = test_config();
337 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
338 let violations = linter.analyze();
339 assert_eq!(1, violations.len());
340 }
341
342 #[test]
343 fn test_ordered_list_in_blockquote() {
344 let input = "> 1. Item with multiple spaces";
345
346 let config =
348 test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: true });
349 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
350 let violations = linter.analyze();
351 assert_eq!(1, violations.len());
352
353 let config =
355 test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: false });
356 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
357 let violations = linter.analyze();
358 assert_eq!(0, violations.len());
359 }
360
361 #[test]
362 fn test_edge_cases() {
363 let input1 = "> ";
365 let config = test_config();
366 let mut linter =
367 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config.clone(), input1);
368 let violations = linter.analyze();
369 assert_eq!(1, violations.len());
370
371 let input2 = "> ";
373 let mut linter =
374 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config.clone(), input2);
375 let violations = linter.analyze();
376 assert_eq!(0, violations.len());
377
378 let input3 = ">";
380 let mut linter =
381 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input3);
382 let violations = linter.analyze();
383 assert_eq!(0, violations.len());
384 }
385
386 #[test]
387 fn test_mixed_content() {
388 let input = r#"> Good blockquote
389> Bad blockquote with multiple spaces
390> Another good one
391> Another bad one with three spaces"#;
392
393 let config = test_config();
394 let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
395 let violations = linter.analyze();
396 assert_eq!(2, violations.len()); }
398
399 mod corner_cases {
401 use super::*;
402
403 #[test]
404 fn test_empty_blockquote_with_trailing_spaces() {
405 let input = r#">
407>
408> "#;
409
410 let config = test_config();
411 let mut linter =
412 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
413 let violations = linter.analyze();
414
415 assert_eq!(3, violations.len());
417
418 let line_numbers: Vec<usize> = violations
420 .iter()
421 .map(|v| v.location().range.start.line + 1)
422 .collect();
423 assert_eq!(vec![1, 2, 3], line_numbers);
424 }
425
426 #[test]
427 fn test_blockquote_with_no_space_after_gt() {
428 let input = r#">No space after gt
430>Another line without space
431>>Nested without space"#;
432
433 let config = test_config();
434 let mut linter =
435 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
436 let violations = linter.analyze();
437 assert_eq!(0, violations.len()); }
439
440 #[test]
441 fn test_complex_nested_blockquotes_with_violations() {
442 let input = r#"> > > All correct
444>> > Middle violation
445> >> Last violation
446> > > All positions violation"#;
447
448 let config = test_config();
449 let mut linter =
450 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
451 let violations = linter.analyze();
452
453 assert_eq!(3, violations.len());
455
456 let line_numbers: Vec<usize> = violations
457 .iter()
458 .map(|v| v.location().range.start.line + 1)
459 .collect();
460 assert_eq!(vec![2, 3, 4], line_numbers);
461 }
462
463 #[test]
464 fn test_list_items_with_different_markers() {
465 let input = r#"> - Dash list item
467> + Plus list item
468> * Asterisk list item
469> 1. Ordered list item
470> 2) Parenthesis ordered item"#;
471
472 let config =
474 test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: true });
475 let mut linter =
476 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
477 let violations = linter.analyze();
478 assert_eq!(5, violations.len());
479
480 let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
482 list_items: false,
483 });
484 let mut linter =
485 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
486 let violations = linter.analyze();
487 assert_eq!(0, violations.len());
488 }
489
490 #[test]
491 fn test_malformed_list_items_in_blockquotes() {
492 let input = r#"> -No space after dash
494> +No space after plus
495> *No space after asterisk
496> 1.No space after number"#;
497
498 let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
500 list_items: false,
501 });
502 let mut linter =
503 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
504 let violations = linter.analyze();
505 assert_eq!(4, violations.len()); }
507
508 #[test]
509 fn test_blockquotes_with_leading_whitespace_variations() {
510 let input = r#" > One leading space
512 > Two leading spaces
513 > Three leading spaces
514 > Four leading spaces"#;
515
516 let config = test_config();
517 let mut linter =
518 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
519 let violations = linter.analyze();
520 assert_eq!(4, violations.len()); }
522
523 #[test]
524 fn test_fenced_code_blocks_with_blockquote_syntax() {
525 let input = r#"```
527> This should be ignored
528> Multiple spaces in fenced block
529> Should not trigger violations
530```
531
532 > This should also be ignored
533 > Indented code block with blockquote syntax
534 > Multiple lines
535
536> But this should violate
537> And this too"#;
538
539 let config = test_config();
540 let mut linter =
541 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
542 let violations = linter.analyze();
543 assert_eq!(1, violations.len());
545
546 let line_numbers: Vec<usize> = violations
547 .iter()
548 .map(|v| v.location().range.start.line + 1)
549 .collect();
550 assert!(line_numbers.contains(&12)); }
552
553 #[test]
554 fn test_edge_case_single_gt_symbol() {
555 let input = r#">
557>
558>
559> "#;
560
561 let config = test_config();
562 let mut linter =
563 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
564 let violations = linter.analyze();
565 assert_eq!(2, violations.len());
568
569 let line_numbers: Vec<usize> = violations
570 .iter()
571 .map(|v| v.location().range.start.line + 1)
572 .collect();
573 assert_eq!(vec![3, 4], line_numbers);
574 }
575
576 #[test]
577 fn test_column_position_accuracy() {
578 let input = r#"> Two spaces
580 > Leading space plus three
581 > Two leading plus four"#;
582
583 let config = test_config();
584 let mut linter =
585 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
586 let violations = linter.analyze();
587
588 assert_eq!(3, violations.len());
589
590 let columns: Vec<usize> = violations
592 .iter()
593 .map(|v| v.location().range.start.character + 1) .collect();
595
596 assert_eq!(vec![3, 4, 5], columns); }
599
600 #[test]
601 fn test_very_deeply_nested_blockquotes() {
602 let input = r#"> > > > > Level 5
604>>>>>> Level 6 with violation
605> > > > > Level 5 with violation
606> > > > > > Level 6 correct"#;
607
608 let config = test_config();
609 let mut linter =
610 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
611 let violations = linter.analyze();
612 assert_eq!(2, violations.len());
614 }
615
616 #[test]
617 fn test_blockquote_followed_by_inline_code() {
618 let input = r#"> This has `code` with multiple spaces
620> This has `code` with correct spacing
621> This has `more code` with violation"#;
622
623 let config = test_config();
624 let mut linter =
625 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
626 let violations = linter.analyze();
627 assert_eq!(2, violations.len()); }
629
630 #[test]
631 fn test_unicode_content_in_blockquotes() {
632 let input = r#"> Unicode: 你好世界
634> Unicode correct: 你好世界
635> More unicode: こんにちは"#;
636
637 let config = test_config();
638 let mut linter =
639 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
640 let violations = linter.analyze();
641 assert_eq!(2, violations.len()); }
643
644 #[test]
645 fn test_blockquote_with_html_entities() {
646 let input = r#"> This has & entity
648> This has © correct
649> This has < violation"#;
650
651 let config = test_config();
652 let mut linter =
653 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
654 let violations = linter.analyze();
655 assert_eq!(2, violations.len());
656 }
657
658 mod known_differences {
669 use super::*;
670
671 #[test]
672 fn test_micromark_vs_tree_sitter_parsing_differences() {
673 let input = r#"> > Text
678> > Text with spaces that might be parsed differently"#;
679
680 let config = test_config();
681 let mut linter =
682 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
683 let violations = linter.analyze();
684
685 assert_eq!(
688 1,
689 violations.len(),
690 "Tree-sitter parsing might differ from micromark"
691 );
692 }
693
694 #[test]
695 fn test_complex_nested_list_detection_limitation() {
696 let input = r#"> 1. Item
700> a. Sub-item that might not be detected as list"#;
701
702 let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
703 list_items: false,
704 });
705 let mut linter =
706 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
707 let violations = linter.analyze();
708
709 assert_eq!(
712 0,
713 violations.len(),
714 "Complex nested list detection may differ"
715 );
716 }
717
718 #[test]
719 fn test_edge_case_with_mixed_blockquote_styles() {
720 let input = r#"> Normal blockquote
722> > Mixed style that might confuse our parser
723>> Different nesting style"#;
724
725 let config = test_config();
726 let mut linter =
727 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
728 let violations = linter.analyze();
729
730 assert_eq!(
732 1,
733 violations.len(),
734 "This will fail - edge case behavior difference"
735 );
736 }
737
738 #[test]
739 fn test_tab_characters_in_blockquotes() {
740 let input = ">\t\tText with tabs after blockquote";
743
744 let config = test_config();
745 let mut linter =
746 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
747 let violations = linter.analyze();
748
749 assert_eq!(
751 0,
752 violations.len(),
753 "Tab handling might differ from markdownlint"
754 );
755 }
756
757 #[test]
758 fn test_mixed_spaces_and_tabs_in_blockquotes() {
759 let input = r#"> Text with space then tab
761> Text with tab then space"#;
762
763 let config = test_config();
764 let mut linter =
765 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
766 let violations = linter.analyze();
767
768 assert_eq!(
770 0,
771 violations.len(),
772 "Mixed space/tab handling likely differs"
773 );
774 }
775
776 #[test]
777 fn test_zero_width_characters_in_blockquotes() {
778 let input = "> Text with zero-width space\u{200B}";
780
781 let config = test_config();
782 let mut linter =
783 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
784 let violations = linter.analyze();
785
786 assert_eq!(
788 1,
789 violations.len(),
790 "Zero-width character handling might differ"
791 );
792 }
793
794 #[test]
795 fn test_blockquote_with_continuation_lines() {
796 let input = r#"> This is a long line \
798> that continues on next line
799> This is normal"#;
800
801 let config = test_config();
802 let mut linter =
803 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
804 let violations = linter.analyze();
805
806 assert_eq!(
808 1,
809 violations.len(),
810 "Line continuation parsing might differ"
811 );
812 }
813
814 #[test]
815 fn test_blockquote_inside_html_comments() {
816 let input = r#"<!--
818> This blockquote is inside a comment
819> Multiple spaces here
820-->"#;
821
822 let config = test_config();
823 let mut linter =
824 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
825 let violations = linter.analyze();
826
827 assert_eq!(
829 0,
830 violations.len(),
831 "HTML comment content handling might differ"
832 );
833 }
834
835 #[test]
836 fn test_blockquote_with_reference_links() {
837 let input = r#"> See [this link][ref] for more info
839> Another [reference link][ref2]
840
841[ref]: http://example.com
842[ref2]: http://example.org"#;
843
844 let config = test_config();
845 let mut linter =
846 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
847 let violations = linter.analyze();
848
849 assert_eq!(
851 2,
852 violations.len(),
853 "Reference link interaction might differ"
854 );
855 }
856
857 #[test]
858 fn test_blockquote_with_autolinks() {
859 let input = r#"> Visit <http://example.com> for info
861> Another autolink: <mailto:test@example.com>"#;
862
863 let config = test_config();
864 let mut linter =
865 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
866 let violations = linter.analyze();
867
868 assert_eq!(
870 2,
871 violations.len(),
872 "Autolink parsing interaction might differ"
873 );
874 }
875
876 #[test]
877 fn test_blockquote_in_table_cells() {
878 let input = r#"| Column 1 | Column 2 |
880|----------|----------|
881| > Quote | Normal |
882| > More | Text |"#;
883
884 let config = test_config();
885 let mut linter =
886 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
887 let violations = linter.analyze();
888
889 assert_eq!(0, violations.len(), "Table context parsing might differ");
891 }
892
893 #[test]
894 fn test_blockquote_with_footnotes() {
895 let input = r#"> This has a footnote[^1]
897> Another footnote reference[^note]
898
899[^1]: Footnote text
900[^note]: Another footnote"#;
901
902 let config = test_config();
903 let mut linter =
904 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
905 let violations = linter.analyze();
906
907 assert_eq!(
909 2,
910 violations.len(),
911 "Footnote parsing interaction might differ"
912 );
913 }
914
915 #[test]
916 fn test_complex_whitespace_patterns() {
917 let input = r#"> Mixed spaces and tabs
919> Tab sandwich
920> Trailing tab after spaces"#;
921
922 let config = test_config();
923 let mut linter =
924 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
925 let violations = linter.analyze();
926
927 assert_eq!(
929 0,
930 violations.len(),
931 "Complex whitespace patterns might differ"
932 );
933 }
934
935 #[test]
936 fn test_blockquote_with_math_expressions() {
937 let input = r#"> Math inline: $x^2 + y^2 = z^2$
939> Display math: $$\sum_{i=1}^n i = \frac{n(n+1)}{2}$$"#;
940
941 let config = test_config();
942 let mut linter =
943 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
944 let violations = linter.analyze();
945
946 assert_eq!(2, violations.len(), "Math expression parsing might differ");
948 }
949
950 #[test]
951 fn test_blockquote_line_ending_variations() {
952 let input_crlf = "> Windows CRLF line\r\n> Another CRLF line\r\n";
954 let input_lf = "> Unix LF line\n> Another LF line\n";
955
956 let config = test_config();
957
958 let mut linter = MultiRuleLinter::new_for_document(
960 PathBuf::from("test.md"),
961 config.clone(),
962 input_crlf,
963 );
964 let violations_crlf = linter.analyze();
965
966 let mut linter =
968 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input_lf);
969 let violations_lf = linter.analyze();
970
971 assert_eq!(
973 violations_crlf.len(),
974 violations_lf.len(),
975 "Line ending handling might differ"
976 );
977 }
978 }
979
980 mod performance_edge_cases {
982 use super::*;
983
984 #[test]
985 fn test_very_long_line_in_blockquote() {
986 let long_content = "a".repeat(10000);
988 let input = format!("> {long_content}");
989
990 let config = test_config();
991 let mut linter =
992 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, &input);
993 let violations = linter.analyze();
994 assert_eq!(1, violations.len()); }
996
997 #[test]
998 fn test_many_nested_blockquotes() {
999 let mut input = String::new();
1001 for i in 0..100 {
1002 let prefix = ">".repeat(i + 1);
1003 if i % 10 == 0 {
1004 input.push_str(&format!("{prefix} Line {i} with violation\n"));
1005 } else {
1006 input.push_str(&format!("{prefix} Line {i} correct\n"));
1007 }
1008 }
1009
1010 let config = test_config();
1011 let mut linter =
1012 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, &input);
1013 let violations = linter.analyze();
1014 assert_eq!(10, violations.len()); }
1016
1017 #[test]
1018 fn test_many_lines_with_blockquotes() {
1019 let mut input = String::new();
1021 for i in 0..1000 {
1022 if i % 2 == 0 {
1023 input.push_str(&format!("> Line {i} with violation\n"));
1024 } else {
1025 input.push_str(&format!("> Line {i} correct\n"));
1026 }
1027 }
1028
1029 let config = test_config();
1030 let mut linter =
1031 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, &input);
1032 let violations = linter.analyze();
1033 assert_eq!(500, violations.len()); }
1035 }
1036
1037 mod additional_edge_cases {
1039 use super::*;
1040
1041 #[test]
1042 fn test_blockquote_with_escaped_characters() {
1043 let input = r#"> Text with \> escaped gt
1045> Text with \* escaped asterisk
1046> Text with \\ escaped backslash"#;
1047
1048 let config = test_config();
1049 let mut linter =
1050 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1051 let violations = linter.analyze();
1052 assert_eq!(3, violations.len()); }
1054
1055 #[test]
1056 fn test_blockquote_with_setext_headings() {
1057 let input = r#"> Heading Level 1
1059> ================
1060> Heading Level 2
1061> ----------------"#;
1062
1063 let config = test_config();
1064 let mut linter =
1065 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1066 let violations = linter.analyze();
1067 assert_eq!(4, violations.len()); }
1069
1070 #[test]
1071 fn test_blockquote_with_horizontal_rules() {
1072 let input = r#"> Text before rule
1074> ---
1075> Text after rule
1076> ***"#;
1077
1078 let config = test_config();
1079 let mut linter =
1080 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1081 let violations = linter.analyze();
1082 assert_eq!(4, violations.len()); }
1084
1085 #[test]
1086 fn test_blockquote_with_atx_headings() {
1087 let input = r#"> # Heading 1
1089> ## Heading 2
1090> ### Heading 3 ###"#;
1091
1092 let config = test_config();
1093 let mut linter =
1094 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1095 let violations = linter.analyze();
1096 assert_eq!(3, violations.len()); }
1098
1099 #[test]
1100 fn test_blockquote_with_definition_lists() {
1101 let input = r#"> Term 1
1103> : Definition 1
1104> Term 2
1105> : Definition 2"#;
1106
1107 let config = test_config();
1108 let mut linter =
1109 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1110 let violations = linter.analyze();
1111 assert_eq!(4, violations.len()); }
1113
1114 #[test]
1115 fn test_blockquote_with_line_breaks() {
1116 let input = r#"> Line with two spaces at end
1118> Line with backslash at end\
1119> Normal line"#;
1120
1121 let config = test_config();
1122 let mut linter =
1123 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1124 let violations = linter.analyze();
1125 assert_eq!(3, violations.len()); }
1127
1128 #[test]
1129 fn test_blockquote_with_emphasis_variations() {
1130 let input = r#"> Text with *emphasis*
1132> Text with **strong**
1133> Text with ***strong emphasis***
1134> Text with _underscore emphasis_
1135> Text with __strong underscore__"#;
1136
1137 let config = test_config();
1138 let mut linter =
1139 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1140 let violations = linter.analyze();
1141 assert_eq!(5, violations.len()); }
1143
1144 #[test]
1145 fn test_blockquote_with_strikethrough() {
1146 let input = r#"> Text with ~~strikethrough~~
1148> More ~~deleted~~ text"#;
1149
1150 let config = test_config();
1151 let mut linter =
1152 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1153 let violations = linter.analyze();
1154 assert_eq!(2, violations.len()); }
1156
1157 #[test]
1158 fn test_blockquote_with_multiple_code_spans() {
1159 let input = r#"> Code `one` and `two` and `three`
1161> More `code` with `spans`"#;
1162
1163 let config = test_config();
1164 let mut linter =
1165 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1166 let violations = linter.analyze();
1167 assert_eq!(2, violations.len()); }
1169
1170 #[test]
1171 fn test_blockquote_with_nested_quotes() {
1172 let input = r#"> He said "Hello" to me
1174> She replied 'Goodbye' back
1175> Mixed "quotes' in text"#;
1176
1177 let config = test_config();
1178 let mut linter =
1179 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1180 let violations = linter.analyze();
1181 assert_eq!(3, violations.len()); }
1183
1184 #[test]
1185 fn test_blockquote_with_numeric_entities() {
1186 let input = r#"> Text with ' apostrophe
1188> Text with " quote
1189> Text with → arrow"#;
1190
1191 let config = test_config();
1192 let mut linter =
1193 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1194 let violations = linter.analyze();
1195 assert_eq!(3, violations.len()); }
1197
1198 #[test]
1199 fn test_blockquote_with_emoji_unicode() {
1200 let input = r#"> Text with emoji 😀
1202> More emoji 🎉 and 🚀
1203> Unicode symbols ♠ ♥ ♦ ♣"#;
1204
1205 let config = test_config();
1206 let mut linter =
1207 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1208 let violations = linter.analyze();
1209 assert_eq!(3, violations.len()); }
1211
1212 #[test]
1213 fn test_blockquote_with_non_breaking_spaces() {
1214 let input = "> Text with non-breaking\u{00A0}space";
1216
1217 let config = test_config();
1218 let mut linter =
1219 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1220 let violations = linter.analyze();
1221 assert_eq!(1, violations.len()); }
1223
1224 #[test]
1225 fn test_blockquote_boundary_conditions() {
1226 let input = r#">
1228>
1229>
1230>
1231>
1232>
1233"#;
1234
1235 let config = test_config();
1236 let mut linter =
1237 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1238 let violations = linter.analyze();
1239 assert_eq!(4, violations.len());
1241
1242 let line_numbers: Vec<usize> = violations
1243 .iter()
1244 .map(|v| v.location().range.start.line + 1)
1245 .collect();
1246 assert_eq!(vec![3, 4, 5, 6], line_numbers);
1247 }
1248
1249 #[test]
1250 fn test_list_item_edge_cases_with_spaces() {
1251 let input = r#"> 1.Item without space after number
1253> 2. Item with space
1254> 10. Double digit number
1255> 100. Triple digit number
1256> a. Letter list item
1257> A. Capital letter list item"#;
1258
1259 let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
1261 list_items: false,
1262 });
1263 let mut linter =
1264 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1265 let violations = linter.analyze();
1266
1267 assert_eq!(1, violations.len()); }
1276
1277 #[test]
1278 fn test_ordered_list_parenthesis_variations() {
1279 let input = r#"> 1) Item with parenthesis
1281> 2) Another item
1282> 10) Double digit with paren
1283> a) Letter with paren
1284> A) Capital with paren"#;
1285
1286 let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
1287 list_items: false,
1288 });
1289 let mut linter =
1290 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1291 let violations = linter.analyze();
1292 assert_eq!(0, violations.len()); }
1294
1295 #[test]
1296 fn test_unordered_list_marker_variations() {
1297 let input = r#"> - Dash marker
1299> + Plus marker
1300> * Asterisk marker
1301> -Item without space
1302> +Item without space
1303> *Item without space"#;
1304
1305 let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
1306 list_items: false,
1307 });
1308 let mut linter =
1309 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1310 let violations = linter.analyze();
1311 assert_eq!(3, violations.len());
1313 }
1314
1315 #[test]
1316 fn test_mixed_content_complex_nesting() {
1317 let input = r#"> Normal text
1319> Text with violation
1320> > Nested blockquote correct
1321> > Nested blockquote violation
1322> > > Triple nested correct
1323> > > Triple nested violation
1324> Back to single level violation
1325> Back to single level correct"#;
1326
1327 let config = test_config();
1328 let mut linter =
1329 MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
1330 let violations = linter.analyze();
1331 assert_eq!(4, violations.len());
1333 }
1334 }
1335 }
1336}