rumdl_lib/utils/
mkdocs_tabs.rs1use super::mkdocs_common::{BytePositionTracker, ContextStateMachine, MKDOCS_CONTENT_INDENT, get_line_indent};
2use regex::Regex;
3use std::sync::LazyLock;
13
14static TAB_MARKER: LazyLock<Regex> = LazyLock::new(|| {
18 Regex::new(
19 r"^(\s*)===\s+.*$", )
21 .unwrap()
22});
23
24static TAB_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)===\s+").unwrap());
26
27pub fn is_tab_marker(line: &str) -> bool {
29 let trimmed_start = line.trim_start();
31 if !trimmed_start.starts_with("===") {
32 return false;
33 }
34
35 let after_marker = &trimmed_start[3..];
38 if after_marker.trim_start().starts_with("===") {
39 return false; }
41
42 let trimmed = line.trim();
43
44 if trimmed.len() <= 3 || !trimmed.chars().nth(3).is_some_and(char::is_whitespace) {
46 return false;
47 }
48
49 TAB_MARKER.is_match(line)
55}
56
57pub fn is_tab_start(line: &str) -> bool {
59 TAB_START.is_match(line)
60}
61
62pub fn get_tab_indent(line: &str) -> Option<usize> {
64 if TAB_MARKER.is_match(line) {
65 return Some(get_line_indent(line));
67 }
68 None
69}
70
71pub fn is_tab_content(line: &str, base_indent: usize) -> bool {
73 if line.trim().is_empty() {
76 return false;
77 }
78
79 get_line_indent(line) >= base_indent + MKDOCS_CONTENT_INDENT
81}
82
83pub fn is_within_tab_content(content: &str, position: usize) -> bool {
85 let tracker = BytePositionTracker::new(content);
86 let mut state = ContextStateMachine::new();
87 let mut in_tab_group = false;
88
89 for (_idx, line, start, end) in tracker.iter_with_positions() {
90 if is_tab_marker(line) {
92 if !in_tab_group {
94 in_tab_group = true;
95 }
96 let indent = get_tab_indent(line).unwrap_or(0);
97 state.enter_context(indent, "tab".to_string());
98 } else if state.is_in_context() {
99 if !line.trim().is_empty() && !is_tab_content(line, state.context_indent()) {
101 if is_tab_marker(line) && get_tab_indent(line).unwrap_or(0) == state.context_indent() {
103 let indent = get_tab_indent(line).unwrap_or(0);
105 state.enter_context(indent, "tab".to_string());
106 } else {
107 state.exit_context();
109 in_tab_group = false;
110 }
111 }
112 }
113
114 if start <= position && position <= end && state.is_in_context() {
116 return true;
117 }
118 }
119
120 false
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_tab_marker_detection() {
129 assert!(is_tab_marker("=== \"Tab 1\""));
130 assert!(is_tab_marker("=== \"Complex Tab Label\""));
131 assert!(is_tab_marker("=== SimpleTab"));
132 assert!(is_tab_marker(" === \"Indented Tab\""));
133 assert!(!is_tab_marker("== \"Not a tab\""));
134 assert!(!is_tab_marker("==== \"Too many equals\""));
135 assert!(!is_tab_marker("Regular text"));
136 }
137
138 #[test]
139 fn test_tab_indent() {
140 assert_eq!(get_tab_indent("=== \"Tab\""), Some(0));
141 assert_eq!(get_tab_indent(" === \"Tab\""), Some(2));
142 assert_eq!(get_tab_indent(" === \"Tab\""), Some(4));
143 assert_eq!(get_tab_indent("Not a tab"), None);
144 }
145
146 #[test]
147 fn test_tab_content() {
148 assert!(is_tab_content(" Content", 0));
150 assert!(is_tab_content(" More indented", 0));
151 assert!(!is_tab_content("", 0)); assert!(!is_tab_content("Not indented", 0));
153 assert!(!is_tab_content(" Only 2 spaces", 0));
154 }
155}