rumdl 0.0.12

A fast Markdown linter written in Rust (Ru(st) MarkDown Linter)
Documentation
use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule};
use crate::rules::heading_utils::HeadingUtils;
use std::collections::HashSet;

#[derive(Debug)]
pub struct MD024NoDuplicateHeading {
    pub allow_different_nesting: bool,
    pub siblings_only: bool,
}

impl Default for MD024NoDuplicateHeading {
    fn default() -> Self {
        Self {
            allow_different_nesting: false,
            siblings_only: false,
        }
    }
}

impl MD024NoDuplicateHeading {
    pub fn new(allow_different_nesting: bool, siblings_only: bool) -> Self {
        Self {
            allow_different_nesting,
            siblings_only,
        }
    }
}

impl Rule for MD024NoDuplicateHeading {
    fn name(&self) -> &'static str {
        "MD024"
    }

    fn description(&self) -> &'static str {
        "Multiple headings with the same content"
    }

    fn check(&self, content: &str) -> LintResult {
        let mut warnings = Vec::new();
        let mut seen_headings = HashSet::new();
        let mut current_level = 0;
        let mut current_siblings = HashSet::new();
        let mut level_siblings = Vec::new();

        for (line_num, line) in content.lines().enumerate() {
            if let Some(heading) = HeadingUtils::parse_heading(line, line_num + 1) {
                let indentation = HeadingUtils::get_indentation(line);
                let text = heading.text.to_lowercase();

                if self.siblings_only {
                    if heading.level > current_level {
                        level_siblings.push(current_siblings.clone());
                        current_siblings = HashSet::new();
                    } else if heading.level < current_level {
                        while level_siblings.len() > heading.level {
                            level_siblings.pop();
                        }
                        if let Some(siblings) = level_siblings.last() {
                            current_siblings = siblings.clone();
                        }
                    }

                    if current_siblings.contains(&text) {
                        warnings.push(LintWarning {
                            line: line_num + 1,
                            column: indentation + 1,
                            message: "Multiple headings with the same content at the same nesting level".to_string(),
                            fix: Some(Fix {
                                line: line_num + 1,
                                column: indentation + 1,
                                replacement: format!("{}{} {} ({})",
                                    " ".repeat(indentation),
                                    "#".repeat(heading.level),
                                    heading.text,
                                    current_siblings.len() + 1),
                            }),
                        });
                    }
                    current_siblings.insert(text);
                    current_level = heading.level;
                } else if !self.allow_different_nesting || heading.level == current_level {
                    if seen_headings.contains(&text) {
                        warnings.push(LintWarning {
                            line: line_num + 1,
                            column: indentation + 1,
                            message: "Multiple headings with the same content".to_string(),
                            fix: Some(Fix {
                                line: line_num + 1,
                                column: indentation + 1,
                                replacement: format!("{}{} {} ({})",
                                    " ".repeat(indentation),
                                    "#".repeat(heading.level),
                                    heading.text,
                                    seen_headings.iter().filter(|&h| h == &text).count() + 1),
                            }),
                        });
                    }
                    seen_headings.insert(text);
                }
            }
        }

        Ok(warnings)
    }

    fn fix(&self, content: &str) -> Result<String, LintError> {
        let mut result = String::new();
        let mut seen_headings = HashSet::new();
        let mut current_level = 0;
        let mut current_siblings = HashSet::new();
        let mut level_siblings = Vec::new();

        for line in content.lines() {
            if let Some(heading) = HeadingUtils::parse_heading(line, 0) {
                let indentation = HeadingUtils::get_indentation(line);
                let text = heading.text.to_lowercase();

                if self.siblings_only {
                    if heading.level > current_level {
                        level_siblings.push(current_siblings.clone());
                        current_siblings = HashSet::new();
                    } else if heading.level < current_level {
                        while level_siblings.len() > heading.level {
                            level_siblings.pop();
                        }
                        if let Some(siblings) = level_siblings.last() {
                            current_siblings = siblings.clone();
                        }
                    }

                    if current_siblings.contains(&text) {
                        result.push_str(&format!("{}{} {} ({})\n",
                            " ".repeat(indentation),
                            "#".repeat(heading.level),
                            heading.text,
                            current_siblings.len() + 1));
                    } else {
                        result.push_str(line);
                        result.push('\n');
                    }
                    current_siblings.insert(text);
                    current_level = heading.level;
                } else if !self.allow_different_nesting || heading.level == current_level {
                    if seen_headings.contains(&text) {
                        result.push_str(&format!("{}{} {} ({})\n",
                            " ".repeat(indentation),
                            "#".repeat(heading.level),
                            heading.text,
                            seen_headings.iter().filter(|&h| h == &text).count() + 1));
                    } else {
                        result.push_str(line);
                        result.push('\n');
                    }
                    seen_headings.insert(text);
                } else {
                    result.push_str(line);
                    result.push('\n');
                }
            } else {
                result.push_str(line);
                result.push('\n');
            }
        }

        if !content.ends_with('\n') {
            result.pop();
        }

        Ok(result)
    }
}