use crate::filtered_lines::FilteredLinesExt;
use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
use crate::rule_config_serde::RuleConfig;
use crate::utils::sentence_utils::is_after_sentence_ending;
use crate::utils::skip_context::is_table_line;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use regex::Regex;
use std::sync::LazyLock;
static MULTIPLE_SPACES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r" {2,}").unwrap()
});
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct MD064Config {
#[serde(
default = "default_allow_sentence_double_space",
alias = "allow_sentence_double_space"
)]
pub allow_sentence_double_space: bool,
}
fn default_allow_sentence_double_space() -> bool {
false
}
impl Default for MD064Config {
fn default() -> Self {
Self {
allow_sentence_double_space: default_allow_sentence_double_space(),
}
}
}
impl RuleConfig for MD064Config {
const RULE_NAME: &'static str = "MD064";
}
#[derive(Debug, Clone)]
pub struct MD064NoMultipleConsecutiveSpaces {
config: MD064Config,
}
impl Default for MD064NoMultipleConsecutiveSpaces {
fn default() -> Self {
Self::new()
}
}
impl MD064NoMultipleConsecutiveSpaces {
pub fn new() -> Self {
Self {
config: MD064Config::default(),
}
}
pub fn from_config_struct(config: MD064Config) -> Self {
Self { config }
}
fn is_in_code_span(&self, code_spans: &[crate::lint_context::CodeSpan], byte_pos: usize) -> bool {
code_spans
.iter()
.any(|span| byte_pos >= span.byte_offset && byte_pos < span.byte_end)
}
fn is_trailing_whitespace(&self, line: &str, match_end: usize) -> bool {
let remaining = &line[match_end..];
remaining.is_empty() || remaining.chars().all(|c| c == '\n' || c == '\r')
}
fn is_leading_indentation(&self, line: &str, match_start: usize) -> bool {
line[..match_start].chars().all(|c| c == ' ' || c == '\t')
}
fn is_after_list_marker(&self, line: &str, match_start: usize) -> bool {
let before = line[..match_start].trim_start();
if before == "*" || before == "-" || before == "+" {
return true;
}
if before.len() >= 2 {
let last_char = before.chars().last().unwrap();
if last_char == '.' || last_char == ')' {
let prefix = &before[..before.len() - 1];
if !prefix.is_empty() && prefix.chars().all(|c| c.is_ascii_digit()) {
return true;
}
}
}
false
}
fn is_after_blockquote_marker(&self, line: &str, match_start: usize) -> bool {
let before = line[..match_start].trim_start();
if before.is_empty() {
return false;
}
let trimmed = before.trim_end();
if trimmed.chars().all(|c| c == '>') {
return true;
}
if trimmed.ends_with('>') {
let inner = trimmed.trim_end_matches('>').trim();
if inner.is_empty() || inner.chars().all(|c| c == '>') {
return true;
}
}
false
}
fn is_tab_replacement_pattern(&self, space_count: usize) -> bool {
space_count >= 4 && space_count.is_multiple_of(4)
}
fn is_reference_link_definition(&self, line: &str, match_start: usize) -> bool {
let trimmed = line.trim_start();
let leading_spaces = line.len() - trimmed.len();
if trimmed.starts_with('[')
&& let Some(bracket_end) = trimmed.find("]:")
{
let colon_pos = leading_spaces + bracket_end + 2;
if match_start >= colon_pos - 1 && match_start <= colon_pos + 1 {
return true;
}
}
false
}
fn is_after_footnote_marker(&self, line: &str, match_start: usize) -> bool {
let trimmed = line.trim_start();
if trimmed.starts_with("[^")
&& let Some(bracket_end) = trimmed.find("]:")
{
let leading_spaces = line.len() - trimmed.len();
let colon_pos = leading_spaces + bracket_end + 2;
if match_start >= colon_pos.saturating_sub(1) && match_start <= colon_pos + 1 {
return true;
}
}
false
}
fn is_after_definition_marker(&self, line: &str, match_start: usize) -> bool {
let before = line[..match_start].trim_start();
before == ":"
}
fn is_after_task_checkbox(&self, line: &str, match_start: usize, flavor: crate::config::MarkdownFlavor) -> bool {
let before = line[..match_start].trim_start();
let mut chars = before.chars();
let pattern = (
chars.next(),
chars.next(),
chars.next(),
chars.next(),
chars.next(),
chars.next(),
);
match pattern {
(Some('*' | '-' | '+'), Some(' '), Some('['), Some(c), Some(']'), None) => {
if flavor == crate::config::MarkdownFlavor::Obsidian {
true
} else {
matches!(c, ' ' | 'x' | 'X')
}
}
_ => false,
}
}
fn is_table_without_outer_pipes(&self, line: &str) -> bool {
let trimmed = line.trim();
if !trimmed.contains('|') {
return false;
}
if trimmed.starts_with('|') || trimmed.ends_with('|') {
return false;
}
let parts: Vec<&str> = trimmed.split('|').collect();
if parts.len() >= 2 {
let first_has_content = !parts.first().unwrap_or(&"").trim().is_empty();
let last_has_content = !parts.last().unwrap_or(&"").trim().is_empty();
if first_has_content || last_has_content {
return true;
}
}
false
}
}
impl Rule for MD064NoMultipleConsecutiveSpaces {
fn name(&self) -> &'static str {
"MD064"
}
fn description(&self) -> &'static str {
"Multiple consecutive spaces"
}
fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
let content = ctx.content;
if !content.contains(" ") {
return Ok(vec![]);
}
let mut warnings = Vec::new();
let code_spans: Arc<Vec<crate::lint_context::CodeSpan>> = ctx.code_spans();
let line_index = &ctx.line_index;
for line in ctx
.filtered_lines()
.skip_front_matter()
.skip_code_blocks()
.skip_html_blocks()
.skip_html_comments()
.skip_mkdocstrings()
.skip_esm_blocks()
.skip_jsx_expressions()
.skip_mdx_comments()
.skip_pymdown_blocks()
.skip_obsidian_comments()
{
if !line.content.contains(" ") {
continue;
}
if is_table_line(line.content) {
continue;
}
if self.is_table_without_outer_pipes(line.content) {
continue;
}
let line_start_byte = line_index.get_line_start_byte(line.line_num).unwrap_or(0);
for mat in MULTIPLE_SPACES_REGEX.find_iter(line.content) {
let match_start = mat.start();
let match_end = mat.end();
let space_count = match_end - match_start;
if self.is_leading_indentation(line.content, match_start) {
continue;
}
if self.is_trailing_whitespace(line.content, match_end) {
continue;
}
if self.is_tab_replacement_pattern(space_count) {
continue;
}
if self.is_after_list_marker(line.content, match_start) {
continue;
}
if self.is_after_blockquote_marker(line.content, match_start) {
continue;
}
if self.is_after_footnote_marker(line.content, match_start) {
continue;
}
if self.is_reference_link_definition(line.content, match_start) {
continue;
}
if self.is_after_definition_marker(line.content, match_start) {
continue;
}
if self.is_after_task_checkbox(line.content, match_start, ctx.flavor) {
continue;
}
if self.config.allow_sentence_double_space
&& space_count == 2
&& is_after_sentence_ending(line.content, match_start)
{
continue;
}
let abs_byte_start = line_start_byte + match_start;
if self.is_in_code_span(&code_spans, abs_byte_start) {
continue;
}
let abs_byte_end = line_start_byte + match_end;
let replacement =
if self.config.allow_sentence_double_space && is_after_sentence_ending(line.content, match_start) {
" ".to_string() } else {
" ".to_string() };
warnings.push(LintWarning {
rule_name: Some(self.name().to_string()),
message: format!("Multiple consecutive spaces ({space_count}) found"),
line: line.line_num,
column: match_start + 1, end_line: line.line_num,
end_column: match_end + 1, severity: Severity::Warning,
fix: Some(Fix {
range: abs_byte_start..abs_byte_end,
replacement,
}),
});
}
}
Ok(warnings)
}
fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
let content = ctx.content;
if !content.contains(" ") {
return Ok(content.to_string());
}
let warnings = self.check(ctx)?;
let warnings =
crate::utils::fix_utils::filter_warnings_by_inline_config(warnings, ctx.inline_config(), self.name());
if warnings.is_empty() {
return Ok(content.to_string());
}
let mut fixes: Vec<(std::ops::Range<usize>, String)> = warnings
.into_iter()
.filter_map(|w| w.fix.map(|f| (f.range, f.replacement)))
.collect();
fixes.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));
let mut result = content.to_string();
for (range, replacement) in fixes {
if range.start < result.len() && range.end <= result.len() {
result.replace_range(range, &replacement);
}
}
Ok(result)
}
fn category(&self) -> RuleCategory {
RuleCategory::Whitespace
}
fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
ctx.content.is_empty() || !ctx.content.contains(" ")
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn default_config_section(&self) -> Option<(String, toml::Value)> {
let default_config = MD064Config::default();
let json_value = serde_json::to_value(&default_config).ok()?;
let toml_value = crate::rule_config_serde::json_to_toml_value(&json_value)?;
if let toml::Value::Table(table) = toml_value {
if !table.is_empty() {
Some((MD064Config::RULE_NAME.to_string(), toml::Value::Table(table)))
} else {
None
}
} else {
None
}
}
fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
where
Self: Sized,
{
let rule_config = crate::rule_config_serde::load_rule_config::<MD064Config>(config);
Box::new(MD064NoMultipleConsecutiveSpaces::from_config_struct(rule_config))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lint_context::LintContext;
#[test]
fn test_basic_multiple_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "This is a sentence with extra spaces.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 1);
assert_eq!(result[0].column, 8); }
#[test]
fn test_no_issues_single_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "This is a normal sentence with single spaces.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_inline_code() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "Use `code with spaces` for formatting.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_code_blocks() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "# Heading\n\n```\ncode with spaces\n```\n\nNormal text.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_leading_indentation() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = " This is indented text.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_trailing_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "Line with trailing spaces \nNext line.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_all_trailing_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "Two spaces \nThree spaces \nFour spaces \n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_front_matter() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "---\ntitle: Test Title\n---\n\nContent here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_html_comments() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "<!-- comment with spaces -->\n\nContent here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_multiple_issues_one_line() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "This has multiple issues.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3, "Should flag all 3 occurrences");
}
#[test]
fn test_fix_collapses_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "This is a sentence with extra spaces.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "This is a sentence with extra spaces.");
}
#[test]
fn test_fix_preserves_inline_code() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "Text here `code inside` and more.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Text here `code inside` and more.");
}
#[test]
fn test_fix_preserves_trailing_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "Line with extra and trailing \nNext line.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Line with extra and trailing \nNext line.");
}
#[test]
fn test_list_items_with_extra_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "- Item one\n- Item two\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Should flag spaces in list items");
}
#[test]
fn test_blockquote_with_extra_spaces_in_content() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "> Quote with extra spaces\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 2, "Should flag spaces in blockquote content");
}
#[test]
fn test_skip_blockquote_marker_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "> Text with extra space after marker\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "> Text with three spaces after marker\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = ">> Nested blockquote\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_mixed_content() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = r#"# Heading
This has extra spaces.
```
code here is fine
```
- List item
> Quote text
Normal paragraph.
"#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 3, "Should flag only content outside code blocks");
}
#[test]
fn test_multibyte_utf8() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "日本語 テスト æ–‡å—列";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx);
assert!(result.is_ok(), "Should handle multi-byte UTF-8 characters");
let warnings = result.unwrap();
assert_eq!(warnings.len(), 2, "Should find 2 occurrences of multiple spaces");
}
#[test]
fn test_table_rows_skipped() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_link_text_with_extra_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "[Link text](https://example.com)";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag extra spaces in link text");
}
#[test]
fn test_image_alt_with_extra_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag extra spaces in image alt text");
}
#[test]
fn test_skip_list_marker_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "* Item with extra spaces after marker\n- Another item\n+ Third item\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "1. Item one\n2. Item two\n10. Item ten\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = " * Indented item\n 1. Nested numbered item\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_flag_spaces_in_list_content() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "* Item with extra spaces in content\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag extra spaces in list content");
}
#[test]
fn test_skip_reference_link_definition_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "[ref]: https://example.com\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "[reference-link]: https://example.com \"Title\"\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_footnote_marker_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "[^1]: Footnote with extra space\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "[^footnote-label]: This is the footnote text.\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_definition_list_marker_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "Term\n: Definition with extra spaces\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = ": Another definition\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_task_list_checkbox_spaces() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "- [ ] Task with extra space\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "- [x] Completed task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "* [ ] Task with asterisk marker\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_skip_extended_task_checkbox_spaces_obsidian() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "- [/] In progress task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip [/] checkbox in Obsidian");
let content = "- [-] Cancelled task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip [-] checkbox in Obsidian");
let content = "- [>] Deferred task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip [>] checkbox in Obsidian");
let content = "- [<] Scheduled task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip [<] checkbox in Obsidian");
let content = "- [?] Question task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip [?] checkbox in Obsidian");
let content = "- [!] Important task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip [!] checkbox in Obsidian");
let content = "- [*] Starred task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip [*] checkbox in Obsidian");
let content = "* [/] In progress with asterisk\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip extended checkbox with * marker");
let content = "+ [-] Cancelled with plus\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip extended checkbox with + marker");
let content = "- [✓] Completed with checkmark\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip Unicode checkmark [✓]");
let content = "- [✗] Failed with X mark\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip Unicode X mark [✗]");
let content = "- [→] Forwarded with arrow\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should skip Unicode arrow [→]");
}
#[test]
fn test_flag_extended_checkboxes_in_standard_flavor() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "- [/] In progress task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag [/] in Standard flavor");
let content = "- [-] Cancelled task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag [-] in Standard flavor");
let content = "- [✓] Unicode checkbox\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag [✓] in Standard flavor");
}
#[test]
fn test_extended_checkboxes_with_indentation() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = " - [/] In progress task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should skip space-indented extended checkbox in Obsidian"
);
let content = " - [-] Cancelled task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should skip 3-space indented extended checkbox in Obsidian"
);
let content = "- Parent item\n\t- [/] In progress task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Obsidian, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should skip tab-indented nested extended checkbox in Obsidian"
);
let content = " - [/] In progress task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag indented [/] in Standard flavor");
let content = " - [-] Cancelled task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag 3-space indented [-] in Standard flavor");
let content = "- Parent item\n\t- [-] Cancelled task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(
result.len(),
1,
"Should flag tab-indented nested [-] in Standard flavor"
);
let content = " - [x] Completed task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should skip indented standard [x] checkbox in Standard flavor"
);
let content = "- Parent\n\t- [ ] Pending task\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should skip tab-indented nested standard [ ] checkbox"
);
}
#[test]
fn test_skip_table_without_outer_pipes() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "Col1 | Col2 | Col3\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "--------- | --------- | ---------\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
let content = "Data1 | Data2 | Data3\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_flag_spaces_in_footnote_content() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "[^1]: Footnote with extra spaces in content.\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag extra spaces in footnote content");
}
#[test]
fn test_flag_spaces_in_reference_content() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "[ref]: https://example.com \"Title with extra spaces\"\n";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag extra spaces in reference link title");
}
#[test]
fn test_sentence_double_space_disabled_by_default() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "First sentence. Second sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Default should flag 2 spaces after period");
}
#[test]
fn test_sentence_double_space_enabled_allows_period() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "First sentence. Second sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after period");
}
#[test]
fn test_sentence_double_space_enabled_allows_exclamation() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "Wow! That was great.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after exclamation");
}
#[test]
fn test_sentence_double_space_enabled_allows_question() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "Is this OK? Yes it is.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after question mark");
}
#[test]
fn test_sentence_double_space_flags_mid_sentence() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "Word word in the middle.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag 2 spaces mid-sentence");
}
#[test]
fn test_sentence_double_space_flags_triple_after_period() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "First sentence. Three spaces here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag 3 spaces even after period");
}
#[test]
fn test_sentence_double_space_with_closing_quote() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = r#"He said "Hello." Then he left."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after .\" ");
let content = "She said 'Goodbye.' And she was gone.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after .' ");
}
#[test]
fn test_sentence_double_space_with_curly_quotes() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = format!(
"He said {}Hello.{} Then left.",
'\u{201C}', '\u{201D}' );
let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after curly double quote");
let content = format!(
"She said {}Hi.{} And left.",
'\u{2018}', '\u{2019}' );
let ctx = LintContext::new(&content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after curly single quote");
}
#[test]
fn test_sentence_double_space_with_closing_paren() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "(See reference.) The next point is.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after .) ");
}
#[test]
fn test_sentence_double_space_with_closing_bracket() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "[Citation needed.] More text here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after .] ");
}
#[test]
fn test_sentence_double_space_with_ellipsis() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "He paused... Then continued.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after ellipsis");
}
#[test]
fn test_sentence_double_space_complex_ending() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = r#"(He said "Yes.") Then they agreed."#;
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after .\") ");
}
#[test]
fn test_sentence_double_space_mixed_content() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "Good sentence. Bad mid-sentence. Another good one! OK? Yes.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should only flag mid-sentence double space");
assert!(
result[0].column > 15 && result[0].column < 25,
"Should flag the 'Bad mid' double space"
);
}
#[test]
fn test_sentence_double_space_fix_collapses_to_two() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "Sentence. Three spaces here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(
fixed, "Sentence. Three spaces here.",
"Should collapse to 2 spaces after sentence"
);
}
#[test]
fn test_sentence_double_space_fix_collapses_mid_sentence_to_one() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "Word word here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Word word here.", "Should collapse to 1 space mid-sentence");
}
#[test]
fn test_sentence_double_space_config_kebab_case() {
let toml_str = r#"
allow-sentence-double-space = true
"#;
let config: MD064Config = toml::from_str(toml_str).unwrap();
assert!(config.allow_sentence_double_space);
}
#[test]
fn test_sentence_double_space_config_snake_case() {
let toml_str = r#"
allow_sentence_double_space = true
"#;
let config: MD064Config = toml::from_str(toml_str).unwrap();
assert!(config.allow_sentence_double_space);
}
#[test]
fn test_sentence_double_space_at_line_start() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = ". Text after period at start.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let _result = rule.check(&ctx).unwrap();
}
#[test]
fn test_sentence_double_space_guillemets() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "Il a dit «Oui.» Puis il est parti.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after .» (guillemet)");
}
#[test]
fn test_sentence_double_space_multiple_sentences() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "First. Second. Third. Fourth.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow all sentence-ending double spaces");
}
#[test]
fn test_sentence_double_space_abbreviation_detection() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "Dr. Smith arrived.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag Dr. as abbreviation, not sentence ending");
let content = "Prof. Williams teaches.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag Prof. as abbreviation");
let content = "Use e.g. this example.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag e.g. as abbreviation");
let content = "Acme Inc. Next company.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Inc. not in abbreviation list, treated as sentence end"
);
}
#[test]
fn test_sentence_double_space_default_config_has_correct_defaults() {
let config = MD064Config::default();
assert!(
!config.allow_sentence_double_space,
"Default allow_sentence_double_space should be false"
);
}
#[test]
fn test_sentence_double_space_from_config_integration() {
use crate::config::Config;
use std::collections::BTreeMap;
let mut config = Config::default();
let mut values = BTreeMap::new();
values.insert("allow-sentence-double-space".to_string(), toml::Value::Boolean(true));
config.rules.insert(
"MD064".to_string(),
crate::config::RuleConfig { severity: None, values },
);
let rule = MD064NoMultipleConsecutiveSpaces::from_config(&config);
let content = "Sentence. Two spaces OK. But three is not.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should only flag the triple spaces");
}
#[test]
fn test_sentence_double_space_after_inline_code() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "Hello from `backticks`. How's it going?";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should allow 2 spaces after inline code ending with period"
);
let content = "Use `foo` and `bar`. Next sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after code at end of sentence");
let content = "The `code` worked! Celebrate.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after code with exclamation");
let content = "Is `null` falsy? Yes.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after code with question mark");
let content = "The `code` is here.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Should flag 2 spaces after code mid-sentence");
}
#[test]
fn test_sentence_double_space_code_with_closing_punctuation() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "(see `example`). Next sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after code in parentheses");
let content = "He said \"use `code`\". Then left.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after code in quotes");
}
#[test]
fn test_sentence_double_space_after_emphasis() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "The word is *important*. Next sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after emphasis");
let content = "The word is _important_. Next sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after underscore emphasis");
let content = "The word is **critical**. Next sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after bold");
let content = "The word is __critical__. Next sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after underscore bold");
}
#[test]
fn test_sentence_double_space_after_strikethrough() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "This is ~~wrong~~. Next sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after strikethrough");
let content = "That was ~~bad~~! Learn from it.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Should allow 2 spaces after strikethrough with exclamation"
);
}
#[test]
fn test_sentence_double_space_after_extended_markdown() {
let config = MD064Config {
allow_sentence_double_space: true,
};
let rule = MD064NoMultipleConsecutiveSpaces::from_config_struct(config);
let content = "This is ==highlighted==. Next sentence.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after highlight");
let content = "E equals mc^2^. Einstein said.";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Should allow 2 spaces after superscript");
}
#[test]
fn test_inline_config_allow_sentence_double_space() {
let rule = MD064NoMultipleConsecutiveSpaces::new();
let content = "`<svg>`. Fortunately";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Default config should flag double spaces");
let content = r#"<!-- rumdl-configure-file { "MD064": { "allow-sentence-double-space": true } } -->
`<svg>`. Fortunately"#;
let inline_config = crate::inline_config::InlineConfig::from_content(content);
let base_config = crate::config::Config::default();
let merged_config = base_config.merge_with_inline_config(&inline_config);
let effective_rule = MD064NoMultipleConsecutiveSpaces::from_config(&merged_config);
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = effective_rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Inline config should allow double spaces after sentence"
);
let content = r#"<!-- markdownlint-configure-file { "MD064": { "allow-sentence-double-space": true } } -->
**scalable**. Pick"#;
let inline_config = crate::inline_config::InlineConfig::from_content(content);
let merged_config = base_config.merge_with_inline_config(&inline_config);
let effective_rule = MD064NoMultipleConsecutiveSpaces::from_config(&merged_config);
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = effective_rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Inline config with markdownlint prefix should work");
}
#[test]
fn test_inline_config_allow_sentence_double_space_issue_364() {
let content = r#"<!-- rumdl-configure-file { "MD064": { "allow-sentence-double-space": true } } -->
# Title
what the font size is for the toplevel `<svg>`. Fortunately, librsvg
And here is where I want to say, SVG documents are **scalable**. Pick
That's right, no `width`, no `height`, no `viewBox`. There is no easy
**SVG documents are scalable**. That's their whole reason for being!"#;
let inline_config = crate::inline_config::InlineConfig::from_content(content);
let base_config = crate::config::Config::default();
let merged_config = base_config.merge_with_inline_config(&inline_config);
let effective_rule = MD064NoMultipleConsecutiveSpaces::from_config(&merged_config);
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = effective_rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Issue #364: All sentence-ending double spaces should be allowed with inline config. Found {} warnings",
result.len()
);
}
#[test]
fn test_indented_reference_link_not_flagged() {
let rule = MD064NoMultipleConsecutiveSpaces::default();
let content = " [label]: https://example.com";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Indented reference link definitions should not be flagged, got: {:?}",
result
.iter()
.map(|w| format!("col={}: {}", w.column, &w.message))
.collect::<Vec<_>>()
);
let content = "[label]: https://example.com";
let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Reference link definitions should not be flagged");
}
}