use crate::constants::{SECTION_TREE_END, SECTION_TREE_START};
use crate::summary::{extract_section, ContentItem, DEFAULT_LABEL_CHAR_LIMIT};
use regex::Regex;
use std::sync::LazyLock;
static REF_EXTRACT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\[ref=([a-zA-Z0-9-]+)\]").unwrap());
pub fn get_tree(text: &str, depth: usize, from_ref: Option<&str>) -> String {
let tree_text = extract_section(text, SECTION_TREE_START, SECTION_TREE_END);
let mut output_lines = Vec::new();
let mut found_from = from_ref.is_none();
let mut base_indent = 0;
for line in tree_text.lines() {
let indent = line.len() - line.trim_start().len();
let line_depth = indent / 2;
if let Some(ref_id) = from_ref {
if !found_from {
if line.contains(&format!("[ref={}]", ref_id)) {
found_from = true;
base_indent = indent;
} else {
continue;
}
} else {
let is_new_element = line.trim().starts_with('-') || line.contains("[ref=");
if indent <= base_indent && is_new_element && !output_lines.is_empty() {
break;
}
}
}
let effective_depth = if from_ref.is_some() {
(indent.saturating_sub(base_indent)) / 2
} else {
line_depth
};
if effective_depth < depth {
output_lines.push(line.to_string());
}
}
output_lines.join("\n")
}
#[derive(Debug)]
pub struct ViewResult {
pub path: Vec<String>,
pub content: Vec<ContentItem>,
}
pub fn view_ref(text: &str, target_ref: &str) -> Option<ViewResult> {
let tree_text = extract_section(text, SECTION_TREE_START, SECTION_TREE_END);
let target_pattern = format!("[ref={}]", target_ref);
let mut target_line_idx = None;
let mut target_indent = 0;
let lines: Vec<&str> = tree_text.lines().collect();
for (idx, line) in lines.iter().enumerate() {
if line.contains(&target_pattern) {
target_line_idx = Some(idx);
target_indent = line.len() - line.trim_start().len();
break;
}
}
let target_idx = target_line_idx?;
let mut path: Vec<String> = Vec::new();
let mut current_indent = target_indent;
for idx in (0..=target_idx).rev() {
let line = lines[idx];
let line_indent = line.len() - line.trim_start().len();
if line_indent < current_indent || idx == target_idx {
let trimmed = line.trim();
if trimmed.starts_with('-') || line.contains("[ref=") {
path.push(line.to_string());
current_indent = line_indent;
}
}
}
path.reverse();
let mut content = Vec::new();
let mut in_subtree = false;
for (idx, line) in lines.iter().enumerate() {
if idx == target_idx {
in_subtree = true;
if let Some(item) = extract_content_item(line) {
content.push(item);
}
continue;
}
if in_subtree {
let indent = line.len() - line.trim_start().len();
if indent <= target_indent && line.trim().starts_with('-') {
break;
}
if let Some(item) = extract_content_item(line) {
content.push(item);
}
}
}
Some(ViewResult { path, content })
}
fn extract_content_item(line: &str) -> Option<ContentItem> {
let trimmed = line.trim().trim_start_matches("- ");
let ref_id = REF_EXTRACT_REGEX
.captures(line)
.and_then(|c| c.get(1).map(|m| m.as_str().to_string()))
.unwrap_or_default();
if trimmed.starts_with("heading") {
let label = extract_quoted_label(trimmed);
if !label.is_empty() {
return Some(ContentItem::Heading {
ref_id,
label: truncate_label(&label, DEFAULT_LABEL_CHAR_LIMIT),
});
}
}
else if trimmed.starts_with("button") {
let label = extract_quoted_label(trimmed);
return Some(ContentItem::Button {
ref_id,
label: truncate_label(&label, DEFAULT_LABEL_CHAR_LIMIT),
});
}
else if trimmed.starts_with("link") {
let label = extract_quoted_label(trimmed);
return Some(ContentItem::Link {
ref_id,
label: truncate_label(&label, DEFAULT_LABEL_CHAR_LIMIT),
});
}
else if trimmed.starts_with("textbox")
|| trimmed.starts_with("searchbox")
|| trimmed.starts_with("combobox")
|| trimmed.starts_with("checkbox")
|| trimmed.starts_with("radio")
{
let label = extract_quoted_label(trimmed);
return Some(ContentItem::Input {
ref_id,
label: truncate_label(&label, DEFAULT_LABEL_CHAR_LIMIT),
});
}
else if trimmed.starts_with("text:") {
let label = trimmed.trim_start_matches("text:").trim().trim_matches('"');
if !label.is_empty() {
return Some(ContentItem::Text {
ref_id,
label: label.to_string(),
});
}
}
else if trimmed.starts_with("generic") && trimmed.contains(':') {
if let Some(colon_pos) = trimmed.find(':') {
let after_colon = trimmed[colon_pos + 1..].trim();
if !after_colon.is_empty() && !after_colon.starts_with('-') {
let label = after_colon.trim_matches('"');
if !label.is_empty() {
return Some(ContentItem::Text {
ref_id,
label: label.to_string(),
});
}
}
}
}
else if trimmed.starts_with("paragraph") && trimmed.contains(':') {
if let Some(colon_pos) = trimmed.find(':') {
let after_colon = trimmed[colon_pos + 1..].trim();
if !after_colon.is_empty() && !after_colon.starts_with('-') {
return Some(ContentItem::Text {
ref_id,
label: after_colon.to_string(),
});
}
}
}
None
}
fn extract_quoted_label(content: &str) -> String {
if let Some(start) = content.find('"') {
if let Some(end) = content.rfind('"') {
if start < end {
return content[start + 1..end].to_string();
}
}
}
String::new()
}
fn truncate_label(s: &str, max_chars: usize) -> String {
let char_count = s.chars().count();
if char_count <= max_chars {
return s.to_string();
}
let truncate_at = max_chars.saturating_sub(3);
let end_pos = s
.char_indices()
.nth(truncate_at)
.map(|(pos, _)| pos)
.unwrap_or(s.len());
format!("{}...", &s[..end_pos])
}