use super::mkdocs_common::{BytePositionTracker, ContextStateMachine, MKDOCS_CONTENT_INDENT, get_line_indent};
use regex::Regex;
use std::sync::LazyLock;
static TAB_MARKER: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"^(\s*)===\s+.*$", )
.unwrap()
});
static TAB_START: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\s*)===\s+").unwrap());
pub fn is_tab_marker(line: &str) -> bool {
let trimmed_start = line.trim_start();
if !trimmed_start.starts_with("===") {
return false;
}
let after_marker = &trimmed_start[3..];
if after_marker.trim_start().starts_with("===") {
return false; }
let trimmed = line.trim();
if trimmed.len() <= 3 || !trimmed.chars().nth(3).is_some_and(|c| c.is_whitespace()) {
return false;
}
TAB_MARKER.is_match(line)
}
pub fn is_tab_start(line: &str) -> bool {
TAB_START.is_match(line)
}
pub fn get_tab_indent(line: &str) -> Option<usize> {
if TAB_MARKER.is_match(line) {
return Some(get_line_indent(line));
}
None
}
pub fn is_tab_content(line: &str, base_indent: usize) -> bool {
if line.trim().is_empty() {
return false;
}
get_line_indent(line) >= base_indent + MKDOCS_CONTENT_INDENT
}
pub fn is_within_tab_content(content: &str, position: usize) -> bool {
let tracker = BytePositionTracker::new(content);
let mut state = ContextStateMachine::new();
let mut in_tab_group = false;
for (_idx, line, start, end) in tracker.iter_with_positions() {
if is_tab_marker(line) {
if !in_tab_group {
in_tab_group = true;
}
let indent = get_tab_indent(line).unwrap_or(0);
state.enter_context(indent, "tab".to_string());
} else if state.is_in_context() {
if !line.trim().is_empty() && !is_tab_content(line, state.context_indent()) {
if is_tab_marker(line) && get_tab_indent(line).unwrap_or(0) == state.context_indent() {
let indent = get_tab_indent(line).unwrap_or(0);
state.enter_context(indent, "tab".to_string());
} else {
state.exit_context();
in_tab_group = false;
}
}
}
if start <= position && position <= end && state.is_in_context() {
return true;
}
}
false
}
pub fn get_tab_group_range(lines: &[&str], start_line_idx: usize) -> Option<(usize, usize)> {
if start_line_idx >= lines.len() {
return None;
}
let start_line = lines[start_line_idx];
if !is_tab_marker(start_line) {
return None;
}
let base_indent = get_tab_indent(start_line).unwrap_or(0);
let mut end_line_idx = start_line_idx;
for (idx, line) in lines.iter().enumerate().skip(start_line_idx + 1) {
if is_tab_marker(line) && get_tab_indent(line).unwrap_or(0) == base_indent {
end_line_idx = idx;
} else if is_tab_content(line, base_indent) {
end_line_idx = idx;
} else {
break;
}
}
Some((start_line_idx, end_line_idx))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tab_marker_detection() {
assert!(is_tab_marker("=== \"Tab 1\""));
assert!(is_tab_marker("=== \"Complex Tab Label\""));
assert!(is_tab_marker("=== SimpleTab"));
assert!(is_tab_marker(" === \"Indented Tab\""));
assert!(!is_tab_marker("== \"Not a tab\""));
assert!(!is_tab_marker("==== \"Too many equals\""));
assert!(!is_tab_marker("Regular text"));
}
#[test]
fn test_tab_indent() {
assert_eq!(get_tab_indent("=== \"Tab\""), Some(0));
assert_eq!(get_tab_indent(" === \"Tab\""), Some(2));
assert_eq!(get_tab_indent(" === \"Tab\""), Some(4));
assert_eq!(get_tab_indent("Not a tab"), None);
}
#[test]
fn test_tab_content() {
assert!(is_tab_content(" Content", 0));
assert!(is_tab_content(" More indented", 0));
assert!(!is_tab_content("", 0)); assert!(!is_tab_content("Not indented", 0));
assert!(!is_tab_content(" Only 2 spaces", 0));
}
#[test]
fn test_within_tab_content() {
let content = r#"# Document
=== "Python"
```python
def hello():
print("Hello")
```
=== "JavaScript"
```javascript
function hello() {
console.log("Hello");
}
```
Regular text outside tabs."#;
let python_code_pos = content.find("def hello").unwrap();
let js_code_pos = content.find("function hello").unwrap();
let outside_pos = content.find("Regular text").unwrap();
assert!(is_within_tab_content(content, python_code_pos));
assert!(is_within_tab_content(content, js_code_pos));
assert!(!is_within_tab_content(content, outside_pos));
}
#[test]
fn test_tab_group_range() {
let content = "=== \"Tab 1\"\n Content 1\n=== \"Tab 2\"\n Content 2\n\nOutside";
let lines: Vec<&str> = content.lines().collect();
let range = get_tab_group_range(&lines, 0);
assert_eq!(range, Some((0, 3))); }
}