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}