Skip to main content

rumdl_lib/utils/anchor_styles/
mod.rs

1//! Anchor generation styles for different Markdown platforms
2//!
3//! This module provides different anchor generation implementations that match
4//! the behavior of various Markdown platforms:
5//!
6//! - **GitHub**: GitHub.com's official anchor generation algorithm
7//! - **KramdownGfm**: Kramdown with GFM input (used by Jekyll/GitHub Pages)
8//! - **Kramdown**: Pure kramdown without GFM extensions
9//!
10//! Each style is implemented in a separate module with comprehensive tests
11//! verified against the official tools/platforms.
12//!
13//! Common utilities are shared via the `common` module to avoid duplication.
14
15pub mod common;
16pub mod github;
17pub mod kramdown;
18pub mod kramdown_gfm; // Renamed from jekyll for clarity
19
20use serde::{Deserialize, Serialize};
21
22/// Anchor generation style for heading fragments
23#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
24#[serde(rename_all = "kebab-case")]
25#[derive(Default)]
26pub enum AnchorStyle {
27    /// GitHub/GFM style (default): preserves underscores, removes punctuation
28    #[default]
29    #[serde(rename = "github")]
30    GitHub,
31    /// Kramdown with GFM input: matches Jekyll/GitHub Pages behavior
32    /// Accepts "kramdown-gfm", "kramdown_gfm", and "jekyll" (for backward compatibility)
33    #[serde(rename = "kramdown-gfm", alias = "kramdown_gfm", alias = "jekyll")]
34    KramdownGfm,
35    /// Pure kramdown style: removes underscores and punctuation
36    #[serde(rename = "kramdown")]
37    Kramdown,
38}
39
40impl AnchorStyle {
41    /// Generate an anchor fragment using the specified style
42    pub fn generate_fragment(&self, heading: &str) -> String {
43        match self {
44            AnchorStyle::GitHub => github::heading_to_fragment(heading),
45            AnchorStyle::KramdownGfm => kramdown_gfm::heading_to_fragment(heading),
46            AnchorStyle::Kramdown => kramdown::heading_to_fragment(heading),
47        }
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn test_anchor_style_serde() {
57        // Test serialization (uses primary names)
58        assert_eq!(serde_json::to_string(&AnchorStyle::GitHub).unwrap(), "\"github\"");
59        assert_eq!(
60            serde_json::to_string(&AnchorStyle::KramdownGfm).unwrap(),
61            "\"kramdown-gfm\""
62        );
63        assert_eq!(serde_json::to_string(&AnchorStyle::Kramdown).unwrap(), "\"kramdown\"");
64
65        // Test deserialization with primary names (kebab-case)
66        assert_eq!(
67            serde_json::from_str::<AnchorStyle>("\"github\"").unwrap(),
68            AnchorStyle::GitHub
69        );
70        assert_eq!(
71            serde_json::from_str::<AnchorStyle>("\"kramdown-gfm\"").unwrap(),
72            AnchorStyle::KramdownGfm
73        );
74        assert_eq!(
75            serde_json::from_str::<AnchorStyle>("\"kramdown\"").unwrap(),
76            AnchorStyle::Kramdown
77        );
78
79        // Test snake_case alias
80        assert_eq!(
81            serde_json::from_str::<AnchorStyle>("\"kramdown_gfm\"").unwrap(),
82            AnchorStyle::KramdownGfm
83        );
84
85        // Test backward compatibility: "jekyll" alias still works
86        assert_eq!(
87            serde_json::from_str::<AnchorStyle>("\"jekyll\"").unwrap(),
88            AnchorStyle::KramdownGfm
89        );
90    }
91
92    #[test]
93    fn test_anchor_style_differences() {
94        let test_cases = [
95            "cbrown --> sbrown: --unsafe-paths",
96            "Update login_type",
97            "Test---with---multiple---hyphens",
98            "API::Response > Error--Handling",
99        ];
100
101        for case in test_cases {
102            let github = AnchorStyle::GitHub.generate_fragment(case);
103            let kramdown_gfm = AnchorStyle::KramdownGfm.generate_fragment(case);
104            let kramdown = AnchorStyle::Kramdown.generate_fragment(case);
105
106            // Each style should produce a valid non-empty result
107            assert!(!github.is_empty(), "GitHub style failed for: {case}");
108            assert!(!kramdown_gfm.is_empty(), "KramdownGfm style failed for: {case}");
109            assert!(!kramdown.is_empty(), "Kramdown style failed for: {case}");
110        }
111    }
112}