use crate::constants::{SECTION_TREE_END, SECTION_TREE_START};
use crate::summary::{extract_section, ContentItem, DEFAULT_LABEL_CHAR_LIMIT};
use crate::utils::{
calculate_depth, extract_quoted_content, set_item_depth, truncate_label, AncestorTracker,
};
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;
let mut tracker = AncestorTracker::new();
for (idx, line) in lines.iter().enumerate() {
let dom_depth = calculate_depth(line);
if idx == target_idx {
in_subtree = true;
if let Some(mut item) = extract_content_item_raw(line) {
set_item_depth(&mut item, 0);
content.push(item);
tracker.push(dom_depth, true);
} else {
tracker.push(dom_depth, false);
}
continue;
}
if in_subtree {
if dom_depth <= target_indent / 2 && line.trim().starts_with('-') {
break;
}
tracker.pop_non_ancestors(dom_depth);
let visible_depth = tracker.visible_depth();
if let Some(mut item) = extract_content_item_raw(line) {
set_item_depth(&mut item, visible_depth);
content.push(item);
tracker.push(dom_depth, true);
} else {
tracker.push(dom_depth, false);
}
}
}
Some(ViewResult { path, content })
}
fn extract_content_item_raw(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_content(trimmed).unwrap_or_default();
if !label.is_empty() {
return Some(ContentItem::Heading {
ref_id,
label: truncate_label(&label, DEFAULT_LABEL_CHAR_LIMIT),
depth: 0,
});
}
}
else if trimmed.starts_with("button") {
let label = extract_quoted_content(trimmed).unwrap_or_default();
return Some(ContentItem::Button {
ref_id,
label: truncate_label(&label, DEFAULT_LABEL_CHAR_LIMIT),
depth: 0,
});
}
else if trimmed.starts_with("link") {
let label = extract_quoted_content(trimmed).unwrap_or_default();
return Some(ContentItem::Link {
ref_id,
label: truncate_label(&label, DEFAULT_LABEL_CHAR_LIMIT),
depth: 0,
});
}
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_content(trimmed).unwrap_or_default();
return Some(ContentItem::Input {
ref_id,
label: truncate_label(&label, DEFAULT_LABEL_CHAR_LIMIT),
depth: 0,
});
}
else if trimmed.starts_with("text:") {
let label = trimmed.trim_start_matches("text:").trim().trim_matches('"');
let has_content = label.chars().any(|c| c.is_alphanumeric());
if has_content || !ref_id.is_empty() {
return Some(ContentItem::Text {
ref_id,
label: label.to_string(),
depth: 0,
});
}
}
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(),
depth: 0,
});
}
}
}
}
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(),
depth: 0,
});
}
}
}
None
}