rumdl_lib/rules/
code_fence_utils.rs

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