rumdl_lib/utils/
kramdown_utils.rs1use regex::Regex;
7use std::sync::LazyLock;
8
9static SPAN_IAL_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{[:\.#][^}]*\}$").unwrap());
11
12static EXTENSION_OPEN_PATTERN: LazyLock<Regex> =
15 LazyLock::new(|| Regex::new(r"^\s*\{::([a-z]+)(?:\s+[^}]*)?\}\s*$").unwrap());
16
17static EXTENSION_SELF_CLOSING_PATTERN: LazyLock<Regex> =
19 LazyLock::new(|| Regex::new(r"^\s*\{::[a-z]+(?:\s+[^}]*)?\s*/\}\s*$").unwrap());
20
21static EXTENSION_CLOSE_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*\{:/([a-z]+)?\}\s*$").unwrap());
23
24static MATH_BLOCK_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\$\$").unwrap());
26
27pub fn is_kramdown_block_attribute(line: &str) -> bool {
50 let trimmed = line.trim();
51
52 if !trimmed.starts_with('{') || !trimmed.ends_with('}') || trimmed.len() < 3 {
54 return false;
55 }
56
57 let second_char = trimmed.chars().nth(1);
60 matches!(second_char, Some(':') | Some('#') | Some('.'))
61}
62
63pub fn has_span_ial(text: &str) -> bool {
74 SPAN_IAL_PATTERN.is_match(text.trim())
75}
76
77pub fn is_kramdown_extension_self_closing(line: &str) -> bool {
79 EXTENSION_SELF_CLOSING_PATTERN.is_match(line)
80}
81
82pub fn is_kramdown_extension_open(line: &str) -> bool {
86 EXTENSION_OPEN_PATTERN.is_match(line) && !is_kramdown_extension_self_closing(line)
87}
88
89pub fn is_kramdown_extension_close(line: &str) -> bool {
91 EXTENSION_CLOSE_PATTERN.is_match(line)
92}
93
94pub fn is_math_block_delimiter(line: &str) -> bool {
96 let trimmed = line.trim();
97 trimmed == "$$" || MATH_BLOCK_PATTERN.is_match(trimmed)
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_kramdown_class_attributes() {
106 assert!(is_kramdown_block_attribute("{:.wrap}"));
107 assert!(is_kramdown_block_attribute("{:.class-name}"));
108 assert!(is_kramdown_block_attribute("{:.multiple .classes}"));
109 }
110
111 #[test]
112 fn test_kramdown_id_attributes() {
113 assert!(is_kramdown_block_attribute("{:#my-id}"));
114 assert!(is_kramdown_block_attribute("{:#section-1}"));
115 }
116
117 #[test]
118 fn test_kramdown_generic_attributes() {
119 assert!(is_kramdown_block_attribute("{:style=\"color: red\"}"));
120 assert!(is_kramdown_block_attribute("{:data-value=\"123\"}"));
121 }
122
123 #[test]
124 fn test_kramdown_combined_attributes() {
125 assert!(is_kramdown_block_attribute("{:.class #id}"));
126 assert!(is_kramdown_block_attribute("{:#id .class style=\"color: blue\"}"));
127 assert!(is_kramdown_block_attribute("{:.wrap #my-code .highlight}"));
128 }
129
130 #[test]
131 fn test_non_kramdown_braces() {
132 assert!(!is_kramdown_block_attribute("{just some text}"));
133 assert!(!is_kramdown_block_attribute("{not kramdown}"));
134 assert!(!is_kramdown_block_attribute("{ spaces }"));
135 }
136
137 #[test]
138 fn test_edge_cases() {
139 assert!(!is_kramdown_block_attribute("{}"));
140 assert!(!is_kramdown_block_attribute("{"));
141 assert!(!is_kramdown_block_attribute("}"));
142 assert!(!is_kramdown_block_attribute(""));
143 assert!(!is_kramdown_block_attribute("not braces"));
144 }
145
146 #[test]
147 fn test_whitespace_handling() {
148 assert!(is_kramdown_block_attribute(" {:.wrap} "));
149 assert!(is_kramdown_block_attribute("\t{:#id}\t"));
150 assert!(is_kramdown_block_attribute(" {:.class #id} "));
151 }
152
153 #[test]
154 fn test_self_closing_extension_blocks() {
155 assert!(is_kramdown_extension_self_closing("{::options toc_levels=\"2..4\" /}"));
157 assert!(is_kramdown_extension_self_closing("{::comment /}"));
158 assert!(is_kramdown_extension_self_closing("{::nomarkdown this='is' .ignore /}"));
159 assert!(is_kramdown_extension_self_closing(" {::options key=\"val\" /} "));
160
161 assert!(!is_kramdown_extension_self_closing("{::comment}"));
163 assert!(!is_kramdown_extension_self_closing("{::nomarkdown}"));
164 assert!(!is_kramdown_extension_self_closing("{::nomarkdown type='html'}"));
165 }
166
167 #[test]
168 fn test_extension_open_excludes_self_closing() {
169 assert!(is_kramdown_extension_open("{::comment}"));
171 assert!(is_kramdown_extension_open("{::nomarkdown}"));
172 assert!(is_kramdown_extension_open("{::nomarkdown type='html'}"));
173
174 assert!(!is_kramdown_extension_open("{::options toc_levels=\"2..4\" /}"));
176 assert!(!is_kramdown_extension_open("{::comment /}"));
177 }
178}