rumdl_lib/rules/
code_fence_utils.rs

1use regex::Regex;
2use std::fmt;
3use std::sync::LazyLock;
4
5/// The style for code fence markers (MD048)
6#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Hash)]
7pub enum CodeFenceStyle {
8    /// Consistent with the first code fence style found
9    #[default]
10    Consistent,
11    /// Backtick style (```)
12    Backtick,
13    /// Tilde style (~~~)
14    Tilde,
15}
16
17impl fmt::Display for CodeFenceStyle {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            CodeFenceStyle::Backtick => write!(f, "backtick"),
21            CodeFenceStyle::Tilde => write!(f, "tilde"),
22            CodeFenceStyle::Consistent => write!(f, "consistent"),
23        }
24    }
25}
26
27/// Get regex pattern for finding code fence markers
28pub fn get_code_fence_pattern() -> &'static Regex {
29    static CODE_FENCE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(```|~~~)").unwrap());
30    &CODE_FENCE_PATTERN
31}
32
33/// Determine the code fence style from a marker
34pub fn get_fence_style(marker: &str) -> Option<CodeFenceStyle> {
35    match marker {
36        "```" => Some(CodeFenceStyle::Backtick),
37        "~~~" => Some(CodeFenceStyle::Tilde),
38        _ => None,
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn test_code_fence_style_default() {
48        let style = CodeFenceStyle::default();
49        assert_eq!(style, CodeFenceStyle::Consistent);
50    }
51
52    #[test]
53    fn test_code_fence_style_equality() {
54        assert_eq!(CodeFenceStyle::Backtick, CodeFenceStyle::Backtick);
55        assert_eq!(CodeFenceStyle::Tilde, CodeFenceStyle::Tilde);
56        assert_eq!(CodeFenceStyle::Consistent, CodeFenceStyle::Consistent);
57
58        assert_ne!(CodeFenceStyle::Backtick, CodeFenceStyle::Tilde);
59        assert_ne!(CodeFenceStyle::Backtick, CodeFenceStyle::Consistent);
60        assert_ne!(CodeFenceStyle::Tilde, CodeFenceStyle::Consistent);
61    }
62
63    #[test]
64    fn test_code_fence_style_display() {
65        assert_eq!(format!("{}", CodeFenceStyle::Backtick), "backtick");
66        assert_eq!(format!("{}", CodeFenceStyle::Tilde), "tilde");
67        assert_eq!(format!("{}", CodeFenceStyle::Consistent), "consistent");
68    }
69
70    #[test]
71    fn test_code_fence_style_debug() {
72        assert_eq!(format!("{:?}", CodeFenceStyle::Backtick), "Backtick");
73        assert_eq!(format!("{:?}", CodeFenceStyle::Tilde), "Tilde");
74        assert_eq!(format!("{:?}", CodeFenceStyle::Consistent), "Consistent");
75    }
76
77    #[test]
78    fn test_code_fence_style_clone() {
79        let style1 = CodeFenceStyle::Backtick;
80        let style2 = style1;
81        assert_eq!(style1, style2);
82    }
83
84    #[test]
85    fn test_get_code_fence_pattern() {
86        let pattern = get_code_fence_pattern();
87
88        // Test matching backtick fences
89        assert!(pattern.is_match("```"));
90        assert!(pattern.is_match("```rust"));
91        assert!(pattern.is_match("```\n"));
92
93        // Test matching tilde fences
94        assert!(pattern.is_match("~~~"));
95        assert!(pattern.is_match("~~~python"));
96        assert!(pattern.is_match("~~~\n"));
97
98        // Test non-matching cases
99        assert!(!pattern.is_match("  ```")); // Indented
100        assert!(!pattern.is_match("text```")); // Not at start
101        assert!(!pattern.is_match("``")); // Too few markers
102        assert!(pattern.is_match("````")); // Four backticks still matches (first 3)
103
104        // Test that it only matches at start of line
105        let captures = pattern.captures("```rust");
106        assert!(captures.is_some());
107        assert_eq!(captures.unwrap().get(1).unwrap().as_str(), "```");
108
109        let captures = pattern.captures("~~~yaml");
110        assert!(captures.is_some());
111        assert_eq!(captures.unwrap().get(1).unwrap().as_str(), "~~~");
112    }
113
114    #[test]
115    fn test_get_fence_style() {
116        // Valid styles
117        assert_eq!(get_fence_style("```"), Some(CodeFenceStyle::Backtick));
118        assert_eq!(get_fence_style("~~~"), Some(CodeFenceStyle::Tilde));
119
120        // Invalid inputs
121        assert_eq!(get_fence_style("``"), None);
122        assert_eq!(get_fence_style("````"), None);
123        assert_eq!(get_fence_style("~~"), None);
124        assert_eq!(get_fence_style("~~~~"), None);
125        assert_eq!(get_fence_style(""), None);
126        assert_eq!(get_fence_style("random"), None);
127        assert_eq!(get_fence_style("```rust"), None); // Full fence line
128        assert_eq!(get_fence_style("~~~yaml"), None); // Full fence line
129    }
130
131    #[test]
132    fn test_pattern_singleton() {
133        // Ensure the lazy_static pattern is the same instance
134        let pattern1 = get_code_fence_pattern();
135        let pattern2 = get_code_fence_pattern();
136
137        // Compare pointers
138        assert_eq!(pattern1 as *const _, pattern2 as *const _);
139    }
140
141    #[test]
142    fn test_edge_cases() {
143        let pattern = get_code_fence_pattern();
144
145        // Empty string
146        assert!(!pattern.is_match(""));
147
148        // Unicode in fence
149        assert!(pattern.is_match("```中文"));
150        assert!(pattern.is_match("~~~émoji🦀"));
151
152        // Tabs and spaces after fence
153        assert!(pattern.is_match("```\t"));
154        assert!(pattern.is_match("~~~   "));
155
156        // Mixed markers (should match first set)
157        let captures = pattern.captures("```~~~");
158        assert!(captures.is_some());
159        assert_eq!(captures.unwrap().get(1).unwrap().as_str(), "```");
160    }
161
162    #[test]
163    fn test_code_fence_style_hash() {
164        use std::collections::HashSet;
165
166        let mut set = HashSet::new();
167        set.insert(CodeFenceStyle::Backtick);
168        set.insert(CodeFenceStyle::Tilde);
169        set.insert(CodeFenceStyle::Consistent);
170
171        assert_eq!(set.len(), 3);
172        assert!(set.contains(&CodeFenceStyle::Backtick));
173        assert!(set.contains(&CodeFenceStyle::Tilde));
174        assert!(set.contains(&CodeFenceStyle::Consistent));
175    }
176
177    #[test]
178    fn test_pattern_usage_examples() {
179        let pattern = get_code_fence_pattern();
180
181        // Typical markdown code fence lines
182        let test_cases = vec![
183            ("```rust", true, "```"),
184            ("```", true, "```"),
185            ("~~~python", true, "~~~"),
186            ("~~~", true, "~~~"),
187            ("```json\n", true, "```"),
188            ("~~~yaml\n", true, "~~~"),
189            ("    ```", false, ""),       // Indented code fence
190            ("Some text ```", false, ""), // Not at start
191        ];
192
193        for (input, should_match, expected_capture) in test_cases {
194            let is_match = pattern.is_match(input);
195            assert_eq!(is_match, should_match, "Failed for input: {input}");
196
197            if should_match {
198                let captures = pattern.captures(input).unwrap();
199                assert_eq!(
200                    captures.get(1).unwrap().as_str(),
201                    expected_capture,
202                    "Failed capture for input: {input}"
203                );
204            }
205        }
206    }
207}