use crate::{AutomataError, Element};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectorPath {
steps: Vec<PathStep>,
}
impl<'de> serde::Deserialize<'de> for SelectorPath {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
SelectorPath::parse(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PathStep {
combinator: Combinator,
predicates: Vec<Predicate>,
nth: Option<usize>, ascend: usize, }
#[derive(Debug, Clone, PartialEq, Eq)]
enum Combinator {
Root,
Child,
Descendant,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Predicate {
attr: Attr,
op: Op,
values: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Attr {
Role,
Name,
Title, AutomationId,
Url, }
#[derive(Debug, Clone, PartialEq, Eq)]
enum Op {
Exact, Contains, StartsWith, EndsWith, }
impl SelectorPath {
pub fn is_wildcard(&self) -> bool {
matches!(self.steps.as_slice(), [step] if step.predicates.is_empty())
}
pub fn parse(input: &str) -> Result<Self, AutomataError> {
let input = input.trim();
if input.is_empty() {
return Err(AutomataError::Internal("empty selector".into()));
}
let segments = split_segments(input)?;
if segments.is_empty() {
return Err(AutomataError::Internal("empty selector".into()));
}
let mut steps = Vec::with_capacity(segments.len());
for (combinator, seg) in segments {
steps.push(parse_step(combinator, seg)?);
}
Ok(SelectorPath { steps })
}
pub fn find_one<E: Element>(&self, root: &E) -> Option<E> {
if self.steps.is_empty() {
return None;
}
match_steps(root, &self.steps, Some(1)).into_iter().next()
}
pub fn find_all<E: Element>(&self, root: &E) -> Vec<E> {
if self.steps.is_empty() {
return vec![];
}
match_steps(root, &self.steps, None)
}
pub fn matches<E: Element>(&self, element: &E) -> bool {
match self.steps.first() {
Some(step) => step_matches(step, element),
None => false,
}
}
pub fn find_one_with_parent<E: Element>(&self, root: &E) -> Option<(E, Option<E>)> {
if self.steps.is_empty() {
return None;
}
find_first_with_step_parent(root, &self.steps)
}
pub fn matches_tab_info(&self, title: &str, url: &str) -> bool {
let Some(step) = self.steps.first() else {
return false;
};
step.predicates.iter().all(|p| {
let actual = match p.attr {
Attr::Name | Attr::Title => title,
Attr::Url => url,
Attr::Role | Attr::AutomationId => return true,
};
p.values.iter().any(|v| match p.op {
Op::Exact => actual == v.as_str(),
Op::Contains => actual
.to_ascii_lowercase()
.contains(v.to_ascii_lowercase().as_str()),
Op::StartsWith => actual
.to_ascii_lowercase()
.starts_with(v.to_ascii_lowercase().as_str()),
Op::EndsWith => actual
.to_ascii_lowercase()
.ends_with(v.to_ascii_lowercase().as_str()),
})
})
}
pub fn find_one_from_step_parent<E: Element>(&self, step_parent: &E) -> Option<E> {
let step = self.steps.last()?;
match &step.combinator {
Combinator::Root | Combinator::Child => {
let children = step_parent.children().ok()?;
apply_nth(
children
.into_iter()
.filter(|c| step_matches(step, c))
.collect(),
step.nth,
)
.into_iter()
.next()
.and_then(|el| {
if step.ascend > 0 {
ascend_n(el, step.ascend)
} else {
Some(el)
}
})
}
Combinator::Descendant => {
let mut acc = vec![];
let limit = if step.nth.is_none() { Some(1) } else { None };
collect_descendants(step_parent, step, &mut acc, limit);
apply_nth(acc, step.nth).into_iter().next().and_then(|el| {
if step.ascend > 0 {
ascend_n(el, step.ascend)
} else {
Some(el)
}
})
}
}
}
}
fn ascend_n<E: Element>(el: E, n: usize) -> Option<E> {
let mut cur = el;
for _ in 0..n {
cur = cur.parent()?;
}
Some(cur)
}
fn match_steps<E: Element>(origin: &E, steps: &[PathStep], limit: Option<usize>) -> Vec<E> {
let step = &steps[0];
let rest = &steps[1..];
let candidates: Vec<E> = match &step.combinator {
Combinator::Root => {
if step_matches(step, origin) {
vec![origin.clone()]
} else {
vec![]
}
}
Combinator::Child => {
match origin.children() {
Ok(children) => children
.into_iter()
.filter(|c| step_matches(step, c))
.collect(),
Err(e) => {
log::debug!(
"selector: children() failed on '{}' ({}): {e}",
origin.name().unwrap_or_default(),
origin.role()
);
vec![]
}
}
}
Combinator::Descendant => {
let mut acc = vec![];
let step_limit = if rest.is_empty() && step.nth.is_none() {
limit
} else {
None
};
collect_descendants(origin, step, &mut acc, step_limit);
acc
}
};
let candidates = apply_nth(candidates, step.nth);
let candidates: Vec<E> = if step.ascend > 0 {
candidates
.into_iter()
.filter_map(|c| ascend_n(c, step.ascend))
.collect()
} else {
candidates
};
if rest.is_empty() {
candidates
} else {
let mut results = Vec::new();
for c in candidates {
let remaining = limit.map(|l| l.saturating_sub(results.len()));
results.extend(match_steps(&c, rest, remaining));
if limit.is_some_and(|l| results.len() >= l) {
break;
}
}
results
}
}
fn collect_descendants<E: Element>(
parent: &E,
step: &PathStep,
acc: &mut Vec<E>,
limit: Option<usize>,
) {
if limit.is_some_and(|l| acc.len() >= l) {
return;
}
let children = match parent.children() {
Ok(c) => c,
Err(e) => {
log::debug!(
"selector: children() failed on '{}' ({}): {e}",
parent.name().unwrap_or_default(),
parent.role()
);
return;
}
};
for child in children {
if step_matches(step, &child) {
acc.push(child.clone());
if limit.is_some_and(|l| acc.len() >= l) {
return;
}
}
collect_descendants(&child, step, acc, limit);
if limit.is_some_and(|l| acc.len() >= l) {
return;
}
}
}
fn collect_first_with_parent<E: Element>(parent: &E, step: &PathStep) -> Option<(E, E)> {
let children = match parent.children() {
Ok(c) => c,
Err(_) => return None,
};
for child in children {
if step_matches(step, &child) {
return Some((child, parent.clone()));
}
if let Some(found) = collect_first_with_parent(&child, step) {
return Some(found);
}
}
None
}
fn find_first_with_step_parent<E: Element>(
origin: &E,
steps: &[PathStep],
) -> Option<(E, Option<E>)> {
let step = &steps[0];
let rest = &steps[1..];
if rest.is_empty() {
return match &step.combinator {
Combinator::Root => {
if step_matches(step, origin) {
let el = origin.clone();
if step.ascend > 0 {
ascend_n(el, step.ascend).map(|a| (a, None))
} else {
Some((el, None))
}
} else {
None
}
}
Combinator::Child => {
let children = origin.children().ok()?;
let matched = apply_nth(
children
.into_iter()
.filter(|c| step_matches(step, c))
.collect(),
step.nth,
)
.into_iter()
.next();
match matched {
None => None,
Some(el) if step.ascend > 0 => ascend_n(el, step.ascend).map(|a| (a, None)),
Some(el) => Some((el, Some(origin.clone()))),
}
}
Combinator::Descendant => {
if step.nth.is_some() {
let mut acc = vec![];
collect_descendants(origin, step, &mut acc, None);
apply_nth(acc, step.nth).into_iter().next().and_then(|el| {
if step.ascend > 0 {
ascend_n(el, step.ascend).map(|a| (a, None))
} else {
Some((el, Some(origin.clone())))
}
})
} else if step.ascend > 0 {
collect_first_with_parent(origin, step)
.and_then(|(el, _)| ascend_n(el, step.ascend).map(|a| (a, None)))
} else {
collect_first_with_parent(origin, step).map(|(el, parent)| (el, Some(parent)))
}
}
};
}
let candidates: Vec<E> = match &step.combinator {
Combinator::Root => {
if step_matches(step, origin) {
vec![origin.clone()]
} else {
vec![]
}
}
Combinator::Child => match origin.children() {
Ok(c) => c.into_iter().filter(|c| step_matches(step, c)).collect(),
Err(_) => vec![],
},
Combinator::Descendant => {
let mut acc = vec![];
collect_descendants(origin, step, &mut acc, None);
acc
}
};
let candidates = apply_nth(candidates, step.nth);
let candidates: Vec<E> = if step.ascend > 0 {
candidates
.into_iter()
.filter_map(|c| ascend_n(c, step.ascend))
.collect()
} else {
candidates
};
for candidate in candidates {
if let Some(result) = find_first_with_step_parent(&candidate, rest) {
return Some(result);
}
}
None
}
fn apply_nth<E>(candidates: Vec<E>, nth: Option<usize>) -> Vec<E> {
match nth {
None => candidates,
Some(n) => candidates.into_iter().nth(n).into_iter().collect(),
}
}
fn step_matches<E: Element>(step: &PathStep, element: &E) -> bool {
step.predicates
.iter()
.all(|p| predicate_matches(p, element))
}
fn predicate_matches<E: Element>(pred: &Predicate, element: &E) -> bool {
let actual = match pred.attr {
Attr::Role => element.role(),
Attr::Name | Attr::Title => element.name().unwrap_or_default(),
Attr::AutomationId => element.automation_id().unwrap_or_default(),
Attr::Url => String::new(),
};
pred.values.iter().any(|v| match pred.op {
Op::Exact => actual == v.as_str(),
Op::Contains => actual
.to_ascii_lowercase()
.contains(v.to_ascii_lowercase().as_str()),
Op::StartsWith => actual
.to_ascii_lowercase()
.starts_with(v.to_ascii_lowercase().as_str()),
Op::EndsWith => actual
.to_ascii_lowercase()
.ends_with(v.to_ascii_lowercase().as_str()),
})
}
fn split_segments(input: &str) -> Result<Vec<(Combinator, &str)>, AutomataError> {
let bytes = input.as_bytes();
let mut segments: Vec<(Combinator, &str)> = vec![];
let mut depth = 0usize;
let mut seg_start = 0;
let mut i = 0;
let mut pending: Option<Combinator> = None;
while i < bytes.len() {
match bytes[i] {
b'[' => depth += 1,
b']' => depth = depth.saturating_sub(1),
b'>' if depth == 0 => {
let seg = input[seg_start..i].trim();
if !seg.is_empty() {
let combinator = pending.take().unwrap_or(if segments.is_empty() {
Combinator::Root
} else {
Combinator::Child
});
segments.push((combinator, seg));
}
let is_descendant = bytes.get(i + 1) == Some(&b'>');
pending = Some(if is_descendant {
i += 1; Combinator::Descendant
} else {
Combinator::Child
});
seg_start = i + 1;
}
_ => {}
}
i += 1;
}
let tail = input[seg_start..].trim();
if !tail.is_empty() {
let combinator = pending.take().unwrap_or(if segments.is_empty() {
Combinator::Root
} else {
Combinator::Child
});
segments.push((combinator, tail));
}
if depth != 0 {
return Err(AutomataError::Internal("unclosed '[' in selector".into()));
}
if segments.is_empty() {
return Err(AutomataError::Internal(
"selector produced no segments".into(),
));
}
Ok(segments)
}
fn parse_step(combinator: Combinator, seg: &str) -> Result<PathStep, AutomataError> {
let seg = seg.trim();
let (seg, nth, ascend) = extract_pseudos(seg)?;
if seg == "*" {
return Ok(PathStep {
combinator,
predicates: vec![],
nth,
ascend,
});
}
let (bare_role, rest) = split_bare_role(seg);
let mut predicates: Vec<Predicate> = vec![];
if !bare_role.is_empty() {
predicates.push(Predicate {
attr: Attr::Role,
op: Op::Exact,
values: vec![bare_role.to_string()],
});
}
let mut s = rest.trim();
while s.starts_with('[') {
let close = s.find(']').ok_or_else(|| {
AutomataError::Internal(format!("unclosed '[' in selector segment: {seg}"))
})?;
let inner = &s[1..close];
predicates.push(parse_predicate(inner)?);
s = s[close + 1..].trim();
}
if predicates.is_empty() {
return Err(AutomataError::Internal(format!(
"selector step has no predicates: '{seg}'"
)));
}
Ok(PathStep {
combinator,
predicates,
nth,
ascend,
})
}
fn extract_pseudos(seg: &str) -> Result<(&str, Option<usize>, usize), AutomataError> {
let mut s = seg;
let mut nth: Option<usize> = None;
let mut ascend: usize = 0;
loop {
let t = s.trim_end();
if t.ends_with(":parent") {
s = &t[..t.len() - ":parent".len()];
ascend = 1;
continue;
}
if t.ends_with(')') {
if let Some(open) = t.rfind('(') {
let before = &t[..open];
let inner = &t[open + 1..t.len() - 1];
if before.ends_with(":ancestor") {
let n = inner.trim().parse::<usize>().map_err(|_| {
AutomataError::Internal(format!("invalid :ancestor index in '{seg}'"))
})?;
s = &before[..before.len() - ":ancestor".len()];
ascend = n;
continue;
}
if before.ends_with(":nth") {
let n = inner.trim().parse::<usize>().map_err(|_| {
AutomataError::Internal(format!("invalid :nth index in '{seg}'"))
})?;
s = &before[..before.len() - ":nth".len()];
nth = Some(n);
continue;
}
}
}
break;
}
Ok((s, nth, ascend))
}
fn split_bare_role(seg: &str) -> (&str, &str) {
if let Some(pos) = seg.find('[') {
(&seg[..pos], &seg[pos..])
} else {
(seg, "")
}
}
fn parse_predicate(inner: &str) -> Result<Predicate, AutomataError> {
let inner = inner.trim();
let (attr_str, op, value) = if let Some(pos) = inner.find("~=") {
(&inner[..pos], Op::Contains, inner[pos + 2..].trim())
} else if let Some(pos) = inner.find("^=") {
(&inner[..pos], Op::StartsWith, inner[pos + 2..].trim())
} else if let Some(pos) = inner.find("$=") {
(&inner[..pos], Op::EndsWith, inner[pos + 2..].trim())
} else if let Some(pos) = inner.find('=') {
(&inner[..pos], Op::Exact, inner[pos + 1..].trim())
} else {
return Err(AutomataError::Internal(format!(
"no operator found in predicate: '{inner}'"
)));
};
let attr = match attr_str.trim() {
"role" => Attr::Role,
"name" => Attr::Name,
"title" => Attr::Title,
"id" | "automation_id" => Attr::AutomationId,
"url" => Attr::Url,
other => {
return Err(AutomataError::Internal(format!(
"unknown attribute '{other}' in selector"
)));
}
};
let values: Vec<String> = value
.split('|')
.map(|v| v.trim().trim_matches(|c| c == '\'' || c == '"').to_string())
.collect();
Ok(Predicate { attr, op, values })
}
impl std::fmt::Display for SelectorPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, step) in self.steps.iter().enumerate() {
if i > 0 {
match step.combinator {
Combinator::Child => write!(f, " > ")?,
Combinator::Descendant => write!(f, " >> ")?,
Combinator::Root => {}
}
}
if step.predicates.is_empty() {
write!(f, "*")?;
}
for pred in &step.predicates {
let op = match pred.op {
Op::Exact => "=",
Op::Contains => "~=",
Op::StartsWith => "^=",
Op::EndsWith => "$=",
};
let attr = match pred.attr {
Attr::Role => "role",
Attr::Name | Attr::Title => "name",
Attr::AutomationId => "id",
Attr::Url => "url",
};
write!(f, "[{attr}{op}{}]", pred.values.join("|"))?;
}
if let Some(n) = step.nth {
write!(f, ":nth({n})")?;
}
match step.ascend {
0 => {}
1 => write!(f, ":parent")?,
n => write!(f, ":ancestor({n})")?,
}
}
Ok(())
}
}