rumdl_lib/utils/
mkdocs_tabs.rs1use super::mkdocs_common::{BytePositionTracker, ContextStateMachine, MKDOCS_CONTENT_INDENT, get_line_indent};
2use lazy_static::lazy_static;
12use regex::Regex;
13
14lazy_static! {
15 static ref TAB_MARKER: Regex = Regex::new(
19 r"^(\s*)===\s+.*$" ).unwrap();
21
22 static ref TAB_START: Regex = Regex::new(
24 r"^(\s*)===\s+"
25 ).unwrap();
26}
27
28pub fn is_tab_marker(line: &str) -> bool {
30 let trimmed_start = line.trim_start();
32 if !trimmed_start.starts_with("===") {
33 return false;
34 }
35
36 let after_marker = &trimmed_start[3..];
39 if after_marker.trim_start().starts_with("===") {
40 return false; }
42
43 let trimmed = line.trim();
44
45 if trimmed.len() <= 3 || !trimmed.chars().nth(3).is_some_and(|c| c.is_whitespace()) {
47 return false;
48 }
49
50 TAB_MARKER.is_match(line)
56}
57
58pub fn is_tab_start(line: &str) -> bool {
60 TAB_START.is_match(line)
61}
62
63pub fn get_tab_indent(line: &str) -> Option<usize> {
65 if TAB_MARKER.is_match(line) {
66 return Some(get_line_indent(line));
68 }
69 None
70}
71
72pub fn is_tab_content(line: &str, base_indent: usize) -> bool {
74 if line.trim().is_empty() {
77 return false;
78 }
79
80 get_line_indent(line) >= base_indent + MKDOCS_CONTENT_INDENT
82}
83
84pub fn is_within_tab_content(content: &str, position: usize) -> bool {
86 let tracker = BytePositionTracker::new(content);
87 let mut state = ContextStateMachine::new();
88 let mut in_tab_group = false;
89
90 for (_idx, line, start, end) in tracker.iter_with_positions() {
91 if is_tab_marker(line) {
93 if !in_tab_group {
95 in_tab_group = true;
96 }
97 let indent = get_tab_indent(line).unwrap_or(0);
98 state.enter_context(indent, "tab".to_string());
99 } else if state.is_in_context() {
100 if !line.trim().is_empty() && !is_tab_content(line, state.context_indent()) {
102 if is_tab_marker(line) && get_tab_indent(line).unwrap_or(0) == state.context_indent() {
104 let indent = get_tab_indent(line).unwrap_or(0);
106 state.enter_context(indent, "tab".to_string());
107 } else {
108 state.exit_context();
110 in_tab_group = false;
111 }
112 }
113 }
114
115 if start <= position && position <= end && state.is_in_context() {
117 return true;
118 }
119 }
120
121 false
122}
123
124pub fn get_tab_group_range(lines: &[&str], start_line_idx: usize) -> Option<(usize, usize)> {
126 if start_line_idx >= lines.len() {
127 return None;
128 }
129
130 let start_line = lines[start_line_idx];
131 if !is_tab_marker(start_line) {
132 return None;
133 }
134
135 let base_indent = get_tab_indent(start_line).unwrap_or(0);
136 let mut end_line_idx = start_line_idx;
137
138 for (idx, line) in lines.iter().enumerate().skip(start_line_idx + 1) {
140 if is_tab_marker(line) && get_tab_indent(line).unwrap_or(0) == base_indent {
141 end_line_idx = idx;
143 } else if is_tab_content(line, base_indent) {
144 end_line_idx = idx;
146 } else {
147 break;
150 }
151 }
152
153 Some((start_line_idx, end_line_idx))
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_tab_marker_detection() {
162 assert!(is_tab_marker("=== \"Tab 1\""));
163 assert!(is_tab_marker("=== \"Complex Tab Label\""));
164 assert!(is_tab_marker("=== SimpleTab"));
165 assert!(is_tab_marker(" === \"Indented Tab\""));
166 assert!(!is_tab_marker("== \"Not a tab\""));
167 assert!(!is_tab_marker("==== \"Too many equals\""));
168 assert!(!is_tab_marker("Regular text"));
169 }
170
171 #[test]
172 fn test_tab_indent() {
173 assert_eq!(get_tab_indent("=== \"Tab\""), Some(0));
174 assert_eq!(get_tab_indent(" === \"Tab\""), Some(2));
175 assert_eq!(get_tab_indent(" === \"Tab\""), Some(4));
176 assert_eq!(get_tab_indent("Not a tab"), None);
177 }
178
179 #[test]
180 fn test_tab_content() {
181 assert!(is_tab_content(" Content", 0));
183 assert!(is_tab_content(" More indented", 0));
184 assert!(!is_tab_content("", 0)); assert!(!is_tab_content("Not indented", 0));
186 assert!(!is_tab_content(" Only 2 spaces", 0));
187 }
188
189 #[test]
190 fn test_within_tab_content() {
191 let content = r#"# Document
192
193=== "Python"
194
195 ```python
196 def hello():
197 print("Hello")
198 ```
199
200=== "JavaScript"
201
202 ```javascript
203 function hello() {
204 console.log("Hello");
205 }
206 ```
207
208Regular text outside tabs."#;
209
210 let python_code_pos = content.find("def hello").unwrap();
211 let js_code_pos = content.find("function hello").unwrap();
212 let outside_pos = content.find("Regular text").unwrap();
213
214 assert!(is_within_tab_content(content, python_code_pos));
215 assert!(is_within_tab_content(content, js_code_pos));
216 assert!(!is_within_tab_content(content, outside_pos));
217 }
218
219 #[test]
220 fn test_tab_group_range() {
221 let content = "=== \"Tab 1\"\n Content 1\n=== \"Tab 2\"\n Content 2\n\nOutside";
222 let lines: Vec<&str> = content.lines().collect();
223
224 let range = get_tab_group_range(&lines, 0);
225 assert_eq!(range, Some((0, 3))); }
227}