use crate::constants::*;
use crate::regex::{QUOTE_REGEX, REF_REGEX, SIMPLE_QUOTE_REGEX};
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListType {
Buttons,
Links,
Inputs,
Headings,
Images,
Text,
All,
Clickable,
}
#[derive(Debug, Clone, Serialize)]
pub struct Element {
pub ref_id: String,
pub element_type: String,
pub label: String,
pub depth: usize,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub disabled: bool,
}
const CLICKABLE_TYPES: &[&str] = &[
ELEM_BUTTON,
ELEM_LINK,
ELEM_TEXTBOX,
ELEM_CHECKBOX,
ELEM_RADIO,
ELEM_COMBOBOX,
ELEM_SEARCHBOX,
ELEM_MENUITEM,
ELEM_OPTION,
ELEM_TAB,
];
fn is_disabled(line: &str) -> bool {
line.contains("[disabled]")
}
pub fn extract_elements(text: &str, list_type: ListType) -> Vec<Element> {
let mut elements = Vec::new();
let type_patterns: &[&str] = match list_type {
ListType::Buttons => &[ELEM_BUTTON],
ListType::Links => &[ELEM_LINK],
ListType::Inputs => &[
ELEM_TEXTBOX,
ELEM_CHECKBOX,
ELEM_RADIO,
ELEM_COMBOBOX,
ELEM_SEARCHBOX,
],
ListType::Headings => &[ELEM_HEADING],
ListType::Images => &[ELEM_IMG],
ListType::Text => &["__text__"],
ListType::All => &[
ELEM_BUTTON,
ELEM_LINK,
ELEM_TEXTBOX,
ELEM_CHECKBOX,
ELEM_RADIO,
ELEM_COMBOBOX,
ELEM_SEARCHBOX,
ELEM_MENUITEM,
ELEM_TAB,
ELEM_OPTION,
ELEM_LISTBOX,
],
ListType::Clickable => CLICKABLE_TYPES,
};
let extract_text_only = matches!(list_type, ListType::Text);
let filter_disabled = matches!(list_type, ListType::Clickable);
for line in text.lines() {
let trimmed = line.trim().trim_start_matches("- ");
if extract_text_only {
if trimmed.starts_with("text:") || trimmed.starts_with("- text:") {
let content = trimmed
.trim_start_matches("- ")
.trim_start_matches("text:")
.trim()
.trim_matches('"')
.to_string();
if !content.is_empty() {
let indent = line.len() - line.trim_start().len();
elements.push(Element {
ref_id: String::new(),
element_type: "text".to_string(),
label: content,
depth: indent / 2,
disabled: false,
});
}
}
continue;
}
let matched_type = type_patterns
.iter()
.find(|&&p| trimmed.starts_with(p) || trimmed.starts_with(&format!("'{}", p)));
if let Some(&elem_type) = matched_type {
if let Some(caps) = REF_REGEX.captures(line) {
let disabled = is_disabled(line);
if filter_disabled && disabled {
continue;
}
let ref_id = caps.get(1).map(|m| m.as_str().to_string()).unwrap_or_default();
let label = extract_label(trimmed);
let indent = line.len() - line.trim_start().len();
let depth = indent / 2;
elements.push(Element {
ref_id,
element_type: elem_type.to_string(),
label,
depth,
disabled,
});
}
}
}
elements
}
#[derive(Debug, Clone, Serialize)]
pub struct ClickableStats {
pub total: usize,
pub disabled_count: usize,
}
pub fn count_clickable(text: &str) -> ClickableStats {
let mut total = 0;
let mut disabled_count = 0;
for line in text.lines() {
let trimmed = line.trim().trim_start_matches("- ");
let is_clickable_type = CLICKABLE_TYPES
.iter()
.any(|&p| trimmed.starts_with(p) || trimmed.starts_with(&format!("'{}", p)));
if is_clickable_type {
if crate::regex::REF_REGEX.captures(line).is_some() {
if is_disabled(line) {
disabled_count += 1;
} else {
total += 1;
}
}
}
}
ClickableStats {
total,
disabled_count,
}
}
fn extract_label(line: &str) -> String {
if let Some(caps) = QUOTE_REGEX.captures(line) {
return caps
.get(1)
.map(|m| m.as_str().to_string())
.unwrap_or_default();
}
if let Some(caps) = SIMPLE_QUOTE_REGEX.captures(line) {
return caps
.get(1)
.map(|m| m.as_str().to_string())
.unwrap_or_default();
}
String::new()
}