rumdl 0.1.51

A fast Markdown linter written in Rust (Ru(st) MarkDown Linter)
Documentation
//! Anchor generation styles for different Markdown platforms
//!
//! This module provides different anchor generation implementations that match
//! the behavior of various Markdown platforms:
//!
//! - **GitHub**: GitHub.com's official anchor generation algorithm
//! - **KramdownGfm**: Kramdown with GFM input (used by Jekyll/GitHub Pages)
//! - **Kramdown**: Pure kramdown without GFM extensions
//!
//! Each style is implemented in a separate module with comprehensive tests
//! verified against the official tools/platforms.
//!
//! Common utilities are shared via the `common` module to avoid duplication.

pub mod common;
pub mod github;
pub mod kramdown;
pub mod kramdown_gfm; // Renamed from jekyll for clarity
pub mod python_markdown;

use serde::{Deserialize, Serialize};

/// Anchor generation style for heading fragments
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[derive(Default)]
pub enum AnchorStyle {
    /// GitHub/GFM style (default): preserves underscores, removes punctuation
    #[default]
    #[serde(rename = "github")]
    GitHub,
    /// Kramdown with GFM input: matches Jekyll/GitHub Pages behavior
    /// Accepts "kramdown-gfm", "kramdown_gfm", and "jekyll" (for backward compatibility)
    #[serde(rename = "kramdown-gfm", alias = "kramdown_gfm", alias = "jekyll")]
    KramdownGfm,
    /// Pure kramdown style: removes underscores and punctuation
    #[serde(rename = "kramdown")]
    Kramdown,
    /// Python-Markdown style: used by MkDocs (NFKD → ASCII, collapse separators)
    #[serde(rename = "python-markdown", alias = "python_markdown", alias = "mkdocs")]
    PythonMarkdown,
}

impl AnchorStyle {
    /// Generate an anchor fragment using the specified style
    pub fn generate_fragment(&self, heading: &str) -> String {
        match self {
            AnchorStyle::GitHub => github::heading_to_fragment(heading),
            AnchorStyle::KramdownGfm => kramdown_gfm::heading_to_fragment(heading),
            AnchorStyle::Kramdown => kramdown::heading_to_fragment(heading),
            AnchorStyle::PythonMarkdown => python_markdown::heading_to_fragment(heading),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_anchor_style_serde() {
        // Test serialization (uses primary names)
        assert_eq!(serde_json::to_string(&AnchorStyle::GitHub).unwrap(), "\"github\"");
        assert_eq!(
            serde_json::to_string(&AnchorStyle::KramdownGfm).unwrap(),
            "\"kramdown-gfm\""
        );
        assert_eq!(serde_json::to_string(&AnchorStyle::Kramdown).unwrap(), "\"kramdown\"");
        assert_eq!(
            serde_json::to_string(&AnchorStyle::PythonMarkdown).unwrap(),
            "\"python-markdown\""
        );

        // Test deserialization with primary names (kebab-case)
        assert_eq!(
            serde_json::from_str::<AnchorStyle>("\"github\"").unwrap(),
            AnchorStyle::GitHub
        );
        assert_eq!(
            serde_json::from_str::<AnchorStyle>("\"kramdown-gfm\"").unwrap(),
            AnchorStyle::KramdownGfm
        );
        assert_eq!(
            serde_json::from_str::<AnchorStyle>("\"kramdown\"").unwrap(),
            AnchorStyle::Kramdown
        );
        assert_eq!(
            serde_json::from_str::<AnchorStyle>("\"python-markdown\"").unwrap(),
            AnchorStyle::PythonMarkdown
        );

        // Test snake_case alias
        assert_eq!(
            serde_json::from_str::<AnchorStyle>("\"kramdown_gfm\"").unwrap(),
            AnchorStyle::KramdownGfm
        );
        assert_eq!(
            serde_json::from_str::<AnchorStyle>("\"python_markdown\"").unwrap(),
            AnchorStyle::PythonMarkdown
        );

        // Test backward compatibility aliases
        assert_eq!(
            serde_json::from_str::<AnchorStyle>("\"jekyll\"").unwrap(),
            AnchorStyle::KramdownGfm
        );
        assert_eq!(
            serde_json::from_str::<AnchorStyle>("\"mkdocs\"").unwrap(),
            AnchorStyle::PythonMarkdown
        );
    }

    #[test]
    fn test_anchor_style_differences() {
        let test_cases = [
            "cbrown --> sbrown: --unsafe-paths",
            "Update login_type",
            "Test---with---multiple---hyphens",
            "API::Response > Error--Handling",
        ];

        for case in test_cases {
            let github = AnchorStyle::GitHub.generate_fragment(case);
            let kramdown_gfm = AnchorStyle::KramdownGfm.generate_fragment(case);
            let kramdown = AnchorStyle::Kramdown.generate_fragment(case);
            let python_md = AnchorStyle::PythonMarkdown.generate_fragment(case);

            // Each style should produce a valid non-empty result
            assert!(!github.is_empty(), "GitHub style failed for: {case}");
            assert!(!kramdown_gfm.is_empty(), "KramdownGfm style failed for: {case}");
            assert!(!kramdown.is_empty(), "Kramdown style failed for: {case}");
            assert!(!python_md.is_empty(), "PythonMarkdown style failed for: {case}");
        }
    }
}