use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum Sel {
Any,
Or(Vec<Sel>),
OfType { prev: Box<Sel>, name: String },
Is { prev: Box<Sel>, name: String },
Class { prev: Box<Sel>, name: String },
Name { prev: Box<Sel>, name: String },
Property {
prev: Box<Sel>,
owner: Option<String>,
name: String,
value: String,
},
NthChild {
prev: Box<Sel>,
step: i32,
offset: i32,
from_end: bool,
},
Not { prev: Box<Sel>, inner: Box<Sel> },
Nesting(Box<Sel>),
Child(Box<Sel>),
Descendant(Box<Sel>),
Template(Box<Sel>),
}
impl Sel {
pub fn has_activator(&self) -> bool {
match self {
Sel::Any => false,
Sel::Or(branches) => branches.iter().any(Sel::has_activator),
Sel::Class { .. } | Sel::Property { .. } => true,
Sel::Not { prev, inner } => prev.has_activator() || inner.has_activator(),
Sel::OfType { prev, .. }
| Sel::Is { prev, .. }
| Sel::Name { prev, .. }
| Sel::NthChild { prev, .. } => prev.has_activator(),
Sel::Nesting(prev) | Sel::Child(prev) | Sel::Descendant(prev) | Sel::Template(prev) => {
prev.has_activator()
}
}
}
pub fn substitute_nesting(&self, parent: &Sel) -> Sel {
let sub = |s: &Sel| Box::new(s.substitute_nesting(parent));
match self {
Sel::Nesting(prev) => match &**prev {
Sel::Any => parent.clone(),
other => other.substitute_nesting(parent),
},
Sel::Any => Sel::Any,
Sel::Or(branches) => Sel::Or(
branches
.iter()
.map(|b| b.substitute_nesting(parent))
.collect(),
),
Sel::OfType { prev, name } => Sel::OfType {
prev: sub(prev),
name: name.clone(),
},
Sel::Is { prev, name } => Sel::Is {
prev: sub(prev),
name: name.clone(),
},
Sel::Class { prev, name } => Sel::Class {
prev: sub(prev),
name: name.clone(),
},
Sel::Name { prev, name } => Sel::Name {
prev: sub(prev),
name: name.clone(),
},
Sel::Property {
prev,
owner,
name,
value,
} => Sel::Property {
prev: sub(prev),
owner: owner.clone(),
name: name.clone(),
value: value.clone(),
},
Sel::NthChild {
prev,
step,
offset,
from_end,
} => Sel::NthChild {
prev: sub(prev),
step: *step,
offset: *offset,
from_end: *from_end,
},
Sel::Not { prev, inner } => Sel::Not {
prev: sub(prev),
inner: Box::new(inner.substitute_nesting(parent)),
},
Sel::Child(prev) => Sel::Child(sub(prev)),
Sel::Descendant(prev) => Sel::Descendant(sub(prev)),
Sel::Template(prev) => Sel::Template(sub(prev)),
}
}
pub fn classes(&self, out: &mut Vec<String>) {
match self {
Sel::Any => {}
Sel::Or(branches) => branches.iter().for_each(|b| b.classes(out)),
Sel::Class { prev, name } => {
out.push(name.clone());
prev.classes(out);
}
Sel::Not { prev, inner } => {
prev.classes(out);
inner.classes(out);
}
Sel::OfType { prev, .. }
| Sel::Is { prev, .. }
| Sel::Name { prev, .. }
| Sel::Property { prev, .. }
| Sel::NthChild { prev, .. } => prev.classes(out),
Sel::Nesting(prev) | Sel::Child(prev) | Sel::Descendant(prev) | Sel::Template(prev) => {
prev.classes(out)
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ElementInfo {
pub type_name: String,
pub name: Option<String>,
pub classes: Vec<String>,
pub child_index: usize,
pub sibling_count: usize,
}
pub fn classes_of(node: &bevy_pf_xaml::XamlNode) -> Vec<String> {
match node.attribute("Classes") {
Some(bevy_pf_xaml::XamlValue::Str(s)) => s.split_whitespace().map(str::to_string).collect(),
_ => Vec::new(),
}
}
pub fn matches_static(sel: &Sel, stack: &[ElementInfo]) -> bool {
match sel {
Sel::Class { prev, .. } => matches_static(prev, stack),
Sel::Or(branches) => branches.iter().any(|b| matches_static(b, stack)),
Sel::Not { prev, inner } => {
matches_static(prev, stack) && (inner.has_activator() || !matches(inner, stack))
}
Sel::OfType { prev, name } | Sel::Is { prev, name } => {
stack.last().is_some_and(|e| e.type_name == *name) && matches_static(prev, stack)
}
Sel::Name { prev, name } => {
stack
.last()
.is_some_and(|e| e.name.as_deref() == Some(name.as_str()))
&& matches_static(prev, stack)
}
Sel::Child(prev) => stack.len() >= 2 && matches_static(prev, &stack[..stack.len() - 1]),
Sel::Descendant(prev) => (1..stack.len())
.rev()
.any(|k| matches_static(prev, &stack[..k])),
_ => matches(sel, stack),
}
}
pub fn matches(sel: &Sel, stack: &[ElementInfo]) -> bool {
let Some(element) = stack.last() else {
return false;
};
match sel {
Sel::Any => true,
Sel::Or(branches) => branches.iter().any(|b| matches(b, stack)),
Sel::OfType { prev, name } | Sel::Is { prev, name } => {
element.type_name == *name && matches(prev, stack)
}
Sel::Class { prev, name } => {
element.classes.iter().any(|c| c == name) && matches(prev, stack)
}
Sel::Name { prev, name } => {
element.name.as_deref() == Some(name.as_str()) && matches(prev, stack)
}
Sel::NthChild {
prev,
step,
offset,
from_end,
} => {
let position = if *from_end {
element.sibling_count.saturating_sub(element.child_index)
} else {
element.child_index + 1
} as i32;
let hit = if *step == 0 {
position == *offset
} else {
let delta = position - *offset;
delta % *step == 0 && delta / *step >= 0
};
hit && matches(prev, stack)
}
Sel::Not { prev, inner } => matches(prev, stack) && !matches(inner, stack),
Sel::Child(prev) => stack.len() >= 2 && matches(prev, &stack[..stack.len() - 1]),
Sel::Descendant(prev) => (1..stack.len()).rev().any(|k| matches(prev, &stack[..k])),
Sel::Template(_) | Sel::Nesting(_) | Sel::Property { .. } => false,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectorError(pub String);
impl fmt::Display for SelectorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
pub fn parse(input: &str) -> Result<Sel, SelectorError> {
let mut p = Parser {
chars: input.char_indices().peekable(),
input,
};
let sel = p.parse_or()?;
p.skip_ws();
if let Some(&(i, c)) = p.chars.peek() {
return Err(SelectorError(format!(
"unexpected `{c}` at byte {i} in selector `{input}`"
)));
}
Ok(sel)
}
struct Parser<'a> {
chars: std::iter::Peekable<std::str::CharIndices<'a>>,
input: &'a str,
}
impl Parser<'_> {
fn skip_ws(&mut self) {
while let Some(&(_, c)) = self.chars.peek() {
if c.is_whitespace() {
self.chars.next();
} else {
break;
}
}
}
fn eat_ws(&mut self) -> bool {
let mut seen = false;
while let Some(&(_, c)) = self.chars.peek() {
if c.is_whitespace() {
seen = true;
self.chars.next();
} else {
break;
}
}
seen
}
fn parse_or(&mut self) -> Result<Sel, SelectorError> {
let mut branches = vec![self.parse_chain()?];
loop {
self.skip_ws();
match self.chars.peek() {
Some(&(_, ',')) => {
self.chars.next();
self.skip_ws();
branches.push(self.parse_chain()?);
}
_ => break,
}
}
Ok(if branches.len() == 1 {
branches.pop().expect("just checked")
} else {
Sel::Or(branches)
})
}
fn parse_chain(&mut self) -> Result<Sel, SelectorError> {
self.skip_ws();
let mut sel = self.parse_fragment(Sel::Any)?;
loop {
let had_ws = self.eat_ws();
match self.chars.peek().copied() {
Some((_, '>')) => {
self.chars.next();
self.skip_ws();
sel = self.parse_fragment(Sel::Child(Box::new(sel)))?;
}
Some((_, '/')) => {
self.expect_word("/template/")?;
self.skip_ws();
sel = self.parse_fragment(Sel::Template(Box::new(sel)))?;
}
Some((_, c)) if had_ws && c != ',' => {
sel = self.parse_fragment(Sel::Descendant(Box::new(sel)))?;
}
_ => break,
}
}
Ok(sel)
}
fn expect_word(&mut self, word: &str) -> Result<(), SelectorError> {
for expected in word.chars() {
match self.chars.next() {
Some((_, c)) if c == expected => {}
_ => {
return Err(SelectorError(format!(
"expected `{word}` in selector `{}`",
self.input
)));
}
}
}
Ok(())
}
fn parse_fragment(&mut self, base: Sel) -> Result<Sel, SelectorError> {
let mut sel = base;
let mut any = false;
loop {
match self.chars.peek().copied() {
Some((_, '.')) => {
self.chars.next();
let name = self.ident("a class name")?;
sel = Sel::Class {
prev: Box::new(sel),
name,
};
}
Some((_, '#')) => {
self.chars.next();
let name = self.ident("a name")?;
sel = Sel::Name {
prev: Box::new(sel),
name,
};
}
Some((_, '^')) => {
self.chars.next();
sel = Sel::Nesting(Box::new(sel));
}
Some((_, ':')) => {
self.chars.next();
sel = self.parse_colon(sel)?;
}
Some((_, '[')) => {
self.chars.next();
sel = self.parse_property(sel)?;
}
Some((_, c)) if c.is_alphanumeric() || c == '_' || c == '|' => {
let name = self.ident("a type name")?;
sel = Sel::OfType {
prev: Box::new(sel),
name,
};
}
_ => break,
}
any = true;
}
if !any {
return Err(SelectorError(format!(
"empty selector fragment in `{}`",
self.input
)));
}
Ok(sel)
}
fn parse_colon(&mut self, sel: Sel) -> Result<Sel, SelectorError> {
let word = self.ident("a pseudo-class")?;
match word.as_str() {
"is" | "not" => {
self.expect_word("(")?;
let inner = self.parse_or()?;
self.skip_ws();
self.expect_word(")")?;
if word == "not" {
Ok(Sel::Not {
prev: Box::new(sel),
inner: Box::new(inner),
})
} else {
match inner {
Sel::OfType { name, .. } => Ok(Sel::Is {
prev: Box::new(sel),
name,
}),
_ => Err(SelectorError(format!(
":is() takes a type name in `{}`",
self.input
))),
}
}
}
"nth-child" | "nth-last-child" => {
self.expect_word("(")?;
let (step, offset) = self.parse_nth()?;
self.expect_word(")")?;
Ok(Sel::NthChild {
prev: Box::new(sel),
step,
offset,
from_end: word == "nth-last-child",
})
}
other => Ok(Sel::Class {
prev: Box::new(sel),
name: format!(":{other}"),
}),
}
}
fn parse_nth(&mut self) -> Result<(i32, i32), SelectorError> {
self.skip_ws();
let mut raw = String::new();
while let Some(&(_, c)) = self.chars.peek() {
if c == ')' {
break;
}
raw.push(c);
self.chars.next();
}
let raw = raw.trim().to_ascii_lowercase();
match raw.as_str() {
"odd" => return Ok((2, 1)),
"even" => return Ok((2, 0)),
_ => {}
}
let bad = || SelectorError(format!("bad nth-child `{raw}` in `{}`", self.input));
let Some(n) = raw.find('n') else {
return Ok((0, raw.parse::<i32>().map_err(|_| bad())?));
};
let (step_src, rest) = raw.split_at(n);
let step = match step_src.trim() {
"" | "+" => 1,
"-" => -1,
s => s.parse::<i32>().map_err(|_| bad())?,
};
let rest = rest[1..].trim().replace(' ', "");
let offset = if rest.is_empty() {
0
} else {
rest.parse::<i32>().map_err(|_| bad())?
};
Ok((step, offset))
}
fn parse_property(&mut self, sel: Sel) -> Result<Sel, SelectorError> {
self.skip_ws();
let parenthesised = matches!(self.chars.peek(), Some(&(_, '(')));
if parenthesised {
self.chars.next();
}
let mut name = String::new();
while let Some(&(_, c)) = self.chars.peek() {
if c == '=' || c == ')' {
break;
}
name.push(c);
self.chars.next();
}
if parenthesised {
self.expect_word(")")?;
}
self.expect_word("=")?;
let mut value = String::new();
while let Some(&(_, c)) = self.chars.peek() {
if c == ']' {
break;
}
value.push(c);
self.chars.next();
}
self.expect_word("]")?;
let name = name.trim().to_string();
let (owner, name) = match name.split_once('.') {
Some((o, p)) => (Some(o.to_string()), p.to_string()),
None => (None, name),
};
Ok(Sel::Property {
prev: Box::new(sel),
owner,
name,
value: value.trim().to_string(),
})
}
fn ident(&mut self, what: &str) -> Result<String, SelectorError> {
let mut out = String::new();
while let Some(&(_, c)) = self.chars.peek() {
if c.is_alphanumeric() || c == '_' || c == '-' || c == '|' {
out.push(c);
self.chars.next();
} else {
break;
}
}
if out.is_empty() {
return Err(SelectorError(format!(
"expected {what} in selector `{}`",
self.input
)));
}
Ok(out.rsplit('|').next().unwrap_or(&out).to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn of_type(name: &str) -> Sel {
Sel::OfType {
prev: Box::new(Sel::Any),
name: name.into(),
}
}
#[test]
fn a_bare_type_is_the_common_case() {
assert_eq!(parse("Button").unwrap(), of_type("Button"));
}
#[test]
fn classes_names_and_pseudo_classes_chain_onto_a_type() {
let sel = parse("Button.danger#go:pointerover").unwrap();
let Sel::Class { name, prev } = &sel else {
panic!("expected the pseudo-class outermost, got {sel:?}");
};
assert_eq!(name, ":pointerover", "a pseudo-class is a class named `:x`");
let Sel::Name { name, .. } = &**prev else {
panic!("expected #go beneath it");
};
assert_eq!(name, "go");
}
#[test]
fn combinators_wrap_what_came_before() {
let sel = parse("Button > TextBlock").unwrap();
let Sel::OfType { name, prev } = &sel else {
panic!("outermost should be the TextBlock, got {sel:?}");
};
assert_eq!(name, "TextBlock");
assert!(
matches!(&**prev, Sel::Child(_)),
"joined by a child combinator"
);
let sel = parse("Button TextBlock").unwrap();
let Sel::OfType { prev, .. } = &sel else {
panic!("got {sel:?}");
};
assert!(
matches!(&**prev, Sel::Descendant(_)),
"space is a descendant"
);
}
#[test]
fn whitespace_around_a_comma_is_not_a_descendant() {
let sel = parse("Button.toolBar, ToggleButton.toolBar").unwrap();
let Sel::Or(branches) = sel else {
panic!("expected two branches");
};
assert_eq!(branches.len(), 2);
}
#[test]
fn template_and_nesting_parse() {
let sel = parse("CheckBox:pointerover /template/ Grid#RootGrid").unwrap();
let mut classes = Vec::new();
sel.classes(&mut classes);
assert!(classes.contains(&":pointerover".to_string()));
let sel = parse("^:disabled /template/ ContentPresenter#PART_ContentPresenter").unwrap();
assert!(sel.has_activator(), ":disabled makes it activated");
}
#[test]
fn nth_child_accepts_every_spelling() {
let cases = [
("Button:nth-child(1)", (0, 1, false)),
("Button:nth-child(2n)", (2, 0, false)),
("Button:nth-child(2n+1)", (2, 1, false)),
("Button:nth-child(-n+2)", (-1, 2, false)),
("Button:nth-child(odd)", (2, 1, false)),
("Button:nth-child(even)", (2, 0, false)),
("Button:nth-last-child(1)", (0, 1, true)),
];
for (src, (step, offset, from_end)) in cases {
let sel = parse(src).unwrap_or_else(|e| panic!("{src}: {e}"));
let Sel::NthChild {
step: s,
offset: o,
from_end: f,
..
} = sel
else {
panic!("{src} did not parse to nth-child: {sel:?}");
};
assert_eq!((s, o, f), (step, offset, from_end), "{src}");
}
}
#[test]
fn property_selectors_parse_plain_and_attached() {
let sel = parse("Button[IsVisible=True]").unwrap();
let Sel::Property {
owner, name, value, ..
} = &sel
else {
panic!("got {sel:?}");
};
assert_eq!(
(owner.as_deref(), name.as_str(), value.as_str()),
(None, "IsVisible", "True")
);
let sel = parse("Button[(Grid.Row)=1]").unwrap();
let Sel::Property { owner, name, .. } = &sel else {
panic!("got {sel:?}");
};
assert_eq!(owner.as_deref(), Some("Grid"));
assert_eq!(name, "Row");
}
#[test]
fn is_and_not_parse() {
assert!(matches!(parse(":is(Button)").unwrap(), Sel::Is { .. }));
assert!(matches!(
parse("Button:not(.danger)").unwrap(),
Sel::Not { .. }
));
}
#[test]
fn the_activator_bucket_is_about_state_not_length() {
assert!(
!parse("Window > Grid > StackPanel Button")
.unwrap()
.has_activator()
);
assert!(!parse("Button#go").unwrap().has_activator());
assert!(!parse("Button:nth-child(2)").unwrap().has_activator());
assert!(parse(".x").unwrap().has_activator());
assert!(parse("Button:pointerover").unwrap().has_activator());
assert!(parse("Button[IsVisible=True]").unwrap().has_activator());
assert!(
parse("Button, .x").unwrap().has_activator(),
"a branch with an activator activates the whole selector"
);
}
#[test]
fn a_namespace_prefix_is_reduced_to_the_local_name() {
assert_eq!(parse("local|Gauge").unwrap(), of_type("Gauge"));
}
#[test]
fn malformed_selectors_are_errors_not_silent_mismatches() {
for bad in ["", ".", "#", "Button >", "Button:nth-child(q)", "Button["] {
assert!(parse(bad).is_err(), "`{bad}` should be rejected");
}
}
}
#[cfg(test)]
mod match_tests {
use super::*;
fn el(type_name: &str) -> ElementInfo {
ElementInfo {
type_name: type_name.into(),
sibling_count: 1,
..Default::default()
}
}
fn named(type_name: &str, name: &str) -> ElementInfo {
ElementInfo {
name: Some(name.into()),
..el(type_name)
}
}
fn classed(type_name: &str, classes: &[&str]) -> ElementInfo {
ElementInfo {
classes: classes.iter().map(|c| c.to_string()).collect(),
..el(type_name)
}
}
fn nth(type_name: &str, index: usize, count: usize) -> ElementInfo {
ElementInfo {
child_index: index,
sibling_count: count,
..el(type_name)
}
}
fn hits(selector: &str, stack: &[ElementInfo]) -> bool {
matches(&parse(selector).expect("selector parses"), stack)
}
#[test]
fn a_type_selector_matches_only_that_type() {
assert!(hits("Button", &[el("Button")]));
assert!(!hits("Button", &[el("TextBlock")]));
}
#[test]
fn a_child_combinator_looks_exactly_one_level_up() {
let direct = [el("UniformGrid"), el("Button")];
let nested = [el("UniformGrid"), el("StackPanel"), el("Button")];
assert!(hits("UniformGrid > Button", &direct));
assert!(
!hits("UniformGrid > Button", &nested),
"a grandchild is not a child"
);
}
#[test]
fn a_descendant_combinator_looks_all_the_way_up() {
let nested = [el("Window"), el("StackPanel"), el("Border"), el("Button")];
assert!(hits("Window Button", &nested));
assert!(hits("StackPanel Button", &nested));
assert!(!hits("ListBox Button", &nested));
}
#[test]
fn chains_must_match_in_order() {
let stack = [el("Window"), el("Border"), el("Button")];
assert!(hits("Window Border > Button", &stack));
assert!(
!hits("Border Window > Button", &stack),
"the ancestor order is part of the selector"
);
}
#[test]
fn names_and_classes_narrow_a_type() {
assert!(hits("Button#go", &[named("Button", "go")]));
assert!(!hits("Button#go", &[named("Button", "stop")]));
assert!(hits("TextBlock.h1", &[classed("TextBlock", &["h1"])]));
assert!(!hits("TextBlock.h1", &[classed("TextBlock", &["h2"])]));
assert!(
hits(".h1", &[classed("TextBlock", &["a", "h1"])]),
"one of several classes is enough"
);
}
#[test]
fn comma_is_alternation() {
assert!(hits(
"Button.toolBar, ToggleButton.toolBar",
&[classed("ToggleButton", &["toolBar"])]
));
assert!(!hits(
"Button.toolBar, ToggleButton.toolBar",
&[classed("CheckBox", &["toolBar"])]
));
}
#[test]
fn nth_child_counts_from_one_like_css() {
assert!(hits("Button:nth-child(1)", &[nth("Button", 0, 3)]));
assert!(!hits("Button:nth-child(1)", &[nth("Button", 1, 3)]));
assert!(hits("Button:nth-child(odd)", &[nth("Button", 0, 4)]));
assert!(!hits("Button:nth-child(odd)", &[nth("Button", 1, 4)]));
assert!(hits("Button:nth-child(even)", &[nth("Button", 1, 4)]));
assert!(hits("Button:nth-child(-n+2)", &[nth("Button", 0, 5)]));
assert!(hits("Button:nth-child(-n+2)", &[nth("Button", 1, 5)]));
assert!(!hits("Button:nth-child(-n+2)", &[nth("Button", 2, 5)]));
assert!(hits("Button:nth-last-child(1)", &[nth("Button", 3, 4)]));
assert!(!hits("Button:nth-last-child(1)", &[nth("Button", 2, 4)]));
}
#[test]
fn not_inverts_only_its_inner_selector() {
assert!(hits("Button:not(.danger)", &[classed("Button", &["safe"])]));
assert!(!hits(
"Button:not(.danger)",
&[classed("Button", &["danger"])]
));
assert!(
!hits("Button:not(.danger)", &[classed("TextBlock", &["safe"])]),
"the outer type must still match"
);
}
#[test]
fn a_selector_needing_state_we_do_not_have_yet_does_not_match() {
assert!(!hits(
"Button /template/ ContentPresenter",
&[el("Button"), el("ContentPresenter")]
));
assert!(!hits("Button[IsVisible=True]", &[el("Button")]));
}
}
pub fn pseudo_class_condition(name: &str) -> crate::triggers::ResolvedCondition {
use crate::triggers::ResolvedCondition as C;
match name {
":pointerover" | ":hover" => C::MouseOver(true),
":pressed" => C::Pressed(true),
":checked" => C::Checked(true),
":unchecked" => C::Checked(false),
":disabled" => C::Enabled(false),
":enabled" => C::Enabled(true),
":focus" | ":focus-within" | ":focused" => C::Focused(true),
":selected" => C::Selected(true),
other => C::HasClass(other.to_string()),
}
}