use serde::Deserialize;
use std::rc::Rc;
use tree_sitter::Node;
use crate::{
linter::{range_from_tree_sitter, RuleViolation},
rules::{Context, Rule, RuleLinter, RuleType},
};
#[derive(Debug, PartialEq, Clone, Deserialize)]
pub struct MD027BlockquoteSpacesTable {
#[serde(default)]
pub list_items: bool,
}
impl Default for MD027BlockquoteSpacesTable {
fn default() -> Self {
Self { list_items: true }
}
}
pub(crate) struct MD027Linter {
context: Rc<Context>,
violations: Vec<RuleViolation>,
}
impl MD027Linter {
pub fn new(context: Rc<Context>) -> Self {
Self {
context,
violations: Vec::new(),
}
}
fn analyze_all_lines(&mut self) {
let settings = self
.context
.config
.linters
.settings
.blockquote_spaces
.clone();
let lines = self.context.lines.borrow();
let code_block_lines = self.get_code_block_lines();
for (line_index, line) in lines.iter().enumerate() {
let line_number = line_index + 1;
if code_block_lines.contains(&line_number) {
continue;
}
if let Some(violation) = self.check_blockquote_line(line, line_index, &settings) {
self.violations.push(violation);
}
}
}
fn check_blockquote_line(
&self,
line: &str,
line_index: usize,
settings: &crate::config::MD027BlockquoteSpacesTable,
) -> Option<RuleViolation> {
let mut current_line = line;
let mut current_offset = 0;
let leading_whitespace = current_line.len() - current_line.trim_start().len();
current_line = current_line.trim_start();
current_offset += leading_whitespace;
while current_line.starts_with('>') {
let after_gt = ¤t_line[1..];
if after_gt.starts_with(" ") {
let space_count = after_gt.chars().take_while(|&c| c == ' ').count();
if !settings.list_items && self.is_list_item_content(after_gt) {
return None;
}
let start_column = current_offset + 2; let end_column = start_column + space_count - 2;
let violation = RuleViolation::new(
&MD027,
"Multiple spaces after blockquote symbol".to_string(),
self.context.file_path.clone(),
range_from_tree_sitter(&tree_sitter::Range {
start_byte: 0,
end_byte: 0,
start_point: tree_sitter::Point {
row: line_index,
column: start_column,
},
end_point: tree_sitter::Point {
row: line_index,
column: end_column,
},
}),
);
return Some(violation);
}
current_line = ¤t_line[1..];
current_offset += 1;
if current_line.starts_with(' ') {
current_line = ¤t_line[1..];
current_offset += 1;
}
if !current_line.starts_with('>') {
break;
}
}
None
}
fn get_code_block_lines(&self) -> std::collections::HashSet<usize> {
let node_cache = self.context.node_cache.borrow();
let mut code_block_lines = std::collections::HashSet::new();
if let Some(indented_blocks) = node_cache.get("indented_code_block") {
for node_info in indented_blocks {
code_block_lines.extend((node_info.line_start + 1)..=(node_info.line_end + 1));
}
}
if let Some(fenced_blocks) = node_cache.get("fenced_code_block") {
for node_info in fenced_blocks {
code_block_lines.extend((node_info.line_start + 1)..=(node_info.line_end + 1));
}
}
if let Some(html_comments) = node_cache.get("html_block") {
for node_info in html_comments {
code_block_lines.extend((node_info.line_start + 1)..=(node_info.line_end + 1));
}
}
code_block_lines
}
fn is_ordered_list_marker(&self, text: &str, delimiter: char) -> bool {
if let Some(pos) = text.find(delimiter) {
if pos > 0 {
let prefix = &text[..pos];
if prefix.chars().all(|c| c.is_ascii_digit())
|| (prefix.len() == 1 && prefix.chars().all(|c| c.is_ascii_alphabetic()))
{
return text.chars().nth(pos + 1).is_some_and(|c| c.is_whitespace());
}
}
}
false
}
fn is_list_item_content(&self, content: &str) -> bool {
let trimmed = content.trim_start();
if trimmed.starts_with('-') || trimmed.starts_with('+') || trimmed.starts_with('*') {
return trimmed.chars().nth(1).is_some_and(|c| c.is_whitespace());
}
if self.is_ordered_list_marker(trimmed, '.') || self.is_ordered_list_marker(trimmed, ')') {
return true;
}
false
}
}
impl RuleLinter for MD027Linter {
fn feed(&mut self, node: &Node) {
if node.kind() == "document" {
self.analyze_all_lines();
}
}
fn finalize(&mut self) -> Vec<RuleViolation> {
std::mem::take(&mut self.violations)
}
}
pub const MD027: Rule = Rule {
id: "MD027",
alias: "no-multiple-space-blockquote",
tags: &["blockquote", "whitespace", "indentation"],
description: "Multiple spaces after blockquote symbol",
rule_type: RuleType::Hybrid,
required_nodes: &["indented_code_block", "fenced_code_block", "html_block"],
new_linter: |context| Box::new(MD027Linter::new(context)),
};
#[cfg(test)]
mod test {
use std::path::PathBuf;
use crate::config::{LintersSettingsTable, MD027BlockquoteSpacesTable, RuleSeverity};
use crate::linter::MultiRuleLinter;
use crate::test_utils::test_helpers::{test_config_with_rules, test_config_with_settings};
fn test_config() -> crate::config::QuickmarkConfig {
test_config_with_rules(vec![
("no-multiple-space-blockquote", RuleSeverity::Error),
("heading-style", RuleSeverity::Off),
("heading-increment", RuleSeverity::Off),
])
}
fn test_config_with_blockquote_spaces(
blockquote_spaces_config: MD027BlockquoteSpacesTable,
) -> crate::config::QuickmarkConfig {
test_config_with_settings(
vec![
("no-multiple-space-blockquote", RuleSeverity::Error),
("heading-style", RuleSeverity::Off),
("heading-increment", RuleSeverity::Off),
],
LintersSettingsTable {
blockquote_spaces: blockquote_spaces_config,
..Default::default()
},
)
}
#[test]
fn test_basic_multiple_space_violation() {
let input = "> This is correct\n> This has multiple spaces";
let config = test_config();
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(1, violations.len());
let violation = &violations[0];
assert_eq!("MD027", violation.rule().id);
assert!(violation.message().contains("Multiple spaces"));
}
#[test]
fn test_no_violation_single_space() {
let input = "> This is correct\n> This is also correct";
let config = test_config();
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(0, violations.len());
}
#[test]
fn test_list_items_configuration() {
let input = "> - Item with multiple spaces\n> - Normal item";
let config =
test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: true });
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(1, violations.len());
let config =
test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: false });
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(0, violations.len());
}
#[test]
fn test_indented_code_blocks_excluded() {
let input = " > This is in an indented code block with multiple spaces";
let config = test_config();
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(0, violations.len()); }
#[test]
fn test_nested_blockquotes() {
let input = "> First level\n>> Second level with multiple spaces\n> > Another second level with multiple spaces";
let config = test_config();
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(1, violations.len());
let violation = &violations[0];
assert_eq!("MD027", violation.rule().id);
assert_eq!(1, violation.location().range.start.line); }
#[test]
fn test_blockquote_with_leading_spaces() {
let input = " > Text with multiple spaces after >";
let config = test_config();
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(1, violations.len());
}
#[test]
fn test_ordered_list_in_blockquote() {
let input = "> 1. Item with multiple spaces";
let config =
test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: true });
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(1, violations.len());
let config =
test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: false });
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(0, violations.len());
}
#[test]
fn test_edge_cases() {
let input1 = "> ";
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config.clone(), input1);
let violations = linter.analyze();
assert_eq!(1, violations.len());
let input2 = "> ";
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config.clone(), input2);
let violations = linter.analyze();
assert_eq!(0, violations.len());
let input3 = ">";
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input3);
let violations = linter.analyze();
assert_eq!(0, violations.len());
}
#[test]
fn test_mixed_content() {
let input = r#"> Good blockquote
> Bad blockquote with multiple spaces
> Another good one
> Another bad one with three spaces"#;
let config = test_config();
let mut linter = MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(2, violations.len()); }
mod corner_cases {
use super::*;
#[test]
fn test_empty_blockquote_with_trailing_spaces() {
let input = r#">
>
> "#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len());
let line_numbers: Vec<usize> = violations
.iter()
.map(|v| v.location().range.start.line + 1)
.collect();
assert_eq!(vec![1, 2, 3], line_numbers);
}
#[test]
fn test_blockquote_with_no_space_after_gt() {
let input = r#">No space after gt
>Another line without space
>>Nested without space"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(0, violations.len()); }
#[test]
fn test_complex_nested_blockquotes_with_violations() {
let input = r#"> > > All correct
>> > Middle violation
> >> Last violation
> > > All positions violation"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len());
let line_numbers: Vec<usize> = violations
.iter()
.map(|v| v.location().range.start.line + 1)
.collect();
assert_eq!(vec![2, 3, 4], line_numbers);
}
#[test]
fn test_list_items_with_different_markers() {
let input = r#"> - Dash list item
> + Plus list item
> * Asterisk list item
> 1. Ordered list item
> 2) Parenthesis ordered item"#;
let config =
test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable { list_items: true });
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(5, violations.len());
let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
list_items: false,
});
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(0, violations.len());
}
#[test]
fn test_malformed_list_items_in_blockquotes() {
let input = r#"> -No space after dash
> +No space after plus
> *No space after asterisk
> 1.No space after number"#;
let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
list_items: false,
});
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(4, violations.len()); }
#[test]
fn test_blockquotes_with_leading_whitespace_variations() {
let input = r#" > One leading space
> Two leading spaces
> Three leading spaces
> Four leading spaces"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(4, violations.len()); }
#[test]
fn test_fenced_code_blocks_with_blockquote_syntax() {
let input = r#"```
> This should be ignored
> Multiple spaces in fenced block
> Should not trigger violations
```
> This should also be ignored
> Indented code block with blockquote syntax
> Multiple lines
> But this should violate
> And this too"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(1, violations.len());
let line_numbers: Vec<usize> = violations
.iter()
.map(|v| v.location().range.start.line + 1)
.collect();
assert!(line_numbers.contains(&12)); }
#[test]
fn test_edge_case_single_gt_symbol() {
let input = r#">
>
>
> "#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(2, violations.len());
let line_numbers: Vec<usize> = violations
.iter()
.map(|v| v.location().range.start.line + 1)
.collect();
assert_eq!(vec![3, 4], line_numbers);
}
#[test]
fn test_column_position_accuracy() {
let input = r#"> Two spaces
> Leading space plus three
> Two leading plus four"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len());
let columns: Vec<usize> = violations
.iter()
.map(|v| v.location().range.start.character + 1) .collect();
assert_eq!(vec![3, 4, 5], columns); }
#[test]
fn test_very_deeply_nested_blockquotes() {
let input = r#"> > > > > Level 5
>>>>>> Level 6 with violation
> > > > > Level 5 with violation
> > > > > > Level 6 correct"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(2, violations.len());
}
#[test]
fn test_blockquote_followed_by_inline_code() {
let input = r#"> This has `code` with multiple spaces
> This has `code` with correct spacing
> This has `more code` with violation"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(2, violations.len()); }
#[test]
fn test_unicode_content_in_blockquotes() {
let input = r#"> Unicode: 你好世界
> Unicode correct: 你好世界
> More unicode: こんにちは"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(2, violations.len()); }
#[test]
fn test_blockquote_with_html_entities() {
let input = r#"> This has & entity
> This has © correct
> This has < violation"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(2, violations.len());
}
mod known_differences {
use super::*;
#[test]
fn test_micromark_vs_tree_sitter_parsing_differences() {
let input = r#"> > Text
> > Text with spaces that might be parsed differently"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
1,
violations.len(),
"Tree-sitter parsing might differ from micromark"
);
}
#[test]
fn test_complex_nested_list_detection_limitation() {
let input = r#"> 1. Item
> a. Sub-item that might not be detected as list"#;
let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
list_items: false,
});
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
0,
violations.len(),
"Complex nested list detection may differ"
);
}
#[test]
fn test_edge_case_with_mixed_blockquote_styles() {
let input = r#"> Normal blockquote
> > Mixed style that might confuse our parser
>> Different nesting style"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
1,
violations.len(),
"This will fail - edge case behavior difference"
);
}
#[test]
fn test_tab_characters_in_blockquotes() {
let input = ">\t\tText with tabs after blockquote";
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
0,
violations.len(),
"Tab handling might differ from markdownlint"
);
}
#[test]
fn test_mixed_spaces_and_tabs_in_blockquotes() {
let input = r#"> Text with space then tab
> Text with tab then space"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
0,
violations.len(),
"Mixed space/tab handling likely differs"
);
}
#[test]
fn test_zero_width_characters_in_blockquotes() {
let input = "> Text with zero-width space\u{200B}";
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
1,
violations.len(),
"Zero-width character handling might differ"
);
}
#[test]
fn test_blockquote_with_continuation_lines() {
let input = r#"> This is a long line \
> that continues on next line
> This is normal"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
1,
violations.len(),
"Line continuation parsing might differ"
);
}
#[test]
fn test_blockquote_inside_html_comments() {
let input = r#"<!--
> This blockquote is inside a comment
> Multiple spaces here
-->"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
0,
violations.len(),
"HTML comment content handling might differ"
);
}
#[test]
fn test_blockquote_with_reference_links() {
let input = r#"> See [this link][ref] for more info
> Another [reference link][ref2]
[ref]: http://example.com
[ref2]: http://example.org"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
2,
violations.len(),
"Reference link interaction might differ"
);
}
#[test]
fn test_blockquote_with_autolinks() {
let input = r#"> Visit <http://example.com> for info
> Another autolink: <mailto:test@example.com>"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
2,
violations.len(),
"Autolink parsing interaction might differ"
);
}
#[test]
fn test_blockquote_in_table_cells() {
let input = r#"| Column 1 | Column 2 |
|----------|----------|
| > Quote | Normal |
| > More | Text |"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(0, violations.len(), "Table context parsing might differ");
}
#[test]
fn test_blockquote_with_footnotes() {
let input = r#"> This has a footnote[^1]
> Another footnote reference[^note]
[^1]: Footnote text
[^note]: Another footnote"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
2,
violations.len(),
"Footnote parsing interaction might differ"
);
}
#[test]
fn test_complex_whitespace_patterns() {
let input = r#"> Mixed spaces and tabs
> Tab sandwich
> Trailing tab after spaces"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(
0,
violations.len(),
"Complex whitespace patterns might differ"
);
}
#[test]
fn test_blockquote_with_math_expressions() {
let input = r#"> Math inline: $x^2 + y^2 = z^2$
> Display math: $$\sum_{i=1}^n i = \frac{n(n+1)}{2}$$"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(2, violations.len(), "Math expression parsing might differ");
}
#[test]
fn test_blockquote_line_ending_variations() {
let input_crlf = "> Windows CRLF line\r\n> Another CRLF line\r\n";
let input_lf = "> Unix LF line\n> Another LF line\n";
let config = test_config();
let mut linter = MultiRuleLinter::new_for_document(
PathBuf::from("test.md"),
config.clone(),
input_crlf,
);
let violations_crlf = linter.analyze();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input_lf);
let violations_lf = linter.analyze();
assert_eq!(
violations_crlf.len(),
violations_lf.len(),
"Line ending handling might differ"
);
}
}
mod performance_edge_cases {
use super::*;
#[test]
fn test_very_long_line_in_blockquote() {
let long_content = "a".repeat(10000);
let input = format!("> {long_content}");
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, &input);
let violations = linter.analyze();
assert_eq!(1, violations.len()); }
#[test]
fn test_many_nested_blockquotes() {
let mut input = String::new();
for i in 0..100 {
let prefix = ">".repeat(i + 1);
if i % 10 == 0 {
input.push_str(&format!("{prefix} Line {i} with violation\n"));
} else {
input.push_str(&format!("{prefix} Line {i} correct\n"));
}
}
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, &input);
let violations = linter.analyze();
assert_eq!(10, violations.len()); }
#[test]
fn test_many_lines_with_blockquotes() {
let mut input = String::new();
for i in 0..1000 {
if i % 2 == 0 {
input.push_str(&format!("> Line {i} with violation\n"));
} else {
input.push_str(&format!("> Line {i} correct\n"));
}
}
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, &input);
let violations = linter.analyze();
assert_eq!(500, violations.len()); }
}
mod additional_edge_cases {
use super::*;
#[test]
fn test_blockquote_with_escaped_characters() {
let input = r#"> Text with \> escaped gt
> Text with \* escaped asterisk
> Text with \\ escaped backslash"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len()); }
#[test]
fn test_blockquote_with_setext_headings() {
let input = r#"> Heading Level 1
> ================
> Heading Level 2
> ----------------"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(4, violations.len()); }
#[test]
fn test_blockquote_with_horizontal_rules() {
let input = r#"> Text before rule
> ---
> Text after rule
> ***"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(4, violations.len()); }
#[test]
fn test_blockquote_with_atx_headings() {
let input = r#"> # Heading 1
> ## Heading 2
> ### Heading 3 ###"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len()); }
#[test]
fn test_blockquote_with_definition_lists() {
let input = r#"> Term 1
> : Definition 1
> Term 2
> : Definition 2"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(4, violations.len()); }
#[test]
fn test_blockquote_with_line_breaks() {
let input = r#"> Line with two spaces at end
> Line with backslash at end\
> Normal line"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len()); }
#[test]
fn test_blockquote_with_emphasis_variations() {
let input = r#"> Text with *emphasis*
> Text with **strong**
> Text with ***strong emphasis***
> Text with _underscore emphasis_
> Text with __strong underscore__"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(5, violations.len()); }
#[test]
fn test_blockquote_with_strikethrough() {
let input = r#"> Text with ~~strikethrough~~
> More ~~deleted~~ text"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(2, violations.len()); }
#[test]
fn test_blockquote_with_multiple_code_spans() {
let input = r#"> Code `one` and `two` and `three`
> More `code` with `spans`"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(2, violations.len()); }
#[test]
fn test_blockquote_with_nested_quotes() {
let input = r#"> He said "Hello" to me
> She replied 'Goodbye' back
> Mixed "quotes' in text"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len()); }
#[test]
fn test_blockquote_with_numeric_entities() {
let input = r#"> Text with ' apostrophe
> Text with " quote
> Text with → arrow"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len()); }
#[test]
fn test_blockquote_with_emoji_unicode() {
let input = r#"> Text with emoji 😀
> More emoji 🎉 and 🚀
> Unicode symbols ♠ ♥ ♦ ♣"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len()); }
#[test]
fn test_blockquote_with_non_breaking_spaces() {
let input = "> Text with non-breaking\u{00A0}space";
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(1, violations.len()); }
#[test]
fn test_blockquote_boundary_conditions() {
let input = r#">
>
>
>
>
>
"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(4, violations.len());
let line_numbers: Vec<usize> = violations
.iter()
.map(|v| v.location().range.start.line + 1)
.collect();
assert_eq!(vec![3, 4, 5, 6], line_numbers);
}
#[test]
fn test_list_item_edge_cases_with_spaces() {
let input = r#"> 1.Item without space after number
> 2. Item with space
> 10. Double digit number
> 100. Triple digit number
> a. Letter list item
> A. Capital letter list item"#;
let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
list_items: false,
});
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(1, violations.len()); }
#[test]
fn test_ordered_list_parenthesis_variations() {
let input = r#"> 1) Item with parenthesis
> 2) Another item
> 10) Double digit with paren
> a) Letter with paren
> A) Capital with paren"#;
let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
list_items: false,
});
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(0, violations.len()); }
#[test]
fn test_unordered_list_marker_variations() {
let input = r#"> - Dash marker
> + Plus marker
> * Asterisk marker
> -Item without space
> +Item without space
> *Item without space"#;
let config = test_config_with_blockquote_spaces(MD027BlockquoteSpacesTable {
list_items: false,
});
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(3, violations.len());
}
#[test]
fn test_mixed_content_complex_nesting() {
let input = r#"> Normal text
> Text with violation
> > Nested blockquote correct
> > Nested blockquote violation
> > > Triple nested correct
> > > Triple nested violation
> Back to single level violation
> Back to single level correct"#;
let config = test_config();
let mut linter =
MultiRuleLinter::new_for_document(PathBuf::from("test.md"), config, input);
let violations = linter.analyze();
assert_eq!(4, violations.len());
}
}
}
}