use std::fmt;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct KeyContext(Vec<String>);
impl KeyContext {
pub fn parse(source: &str) -> Self {
Self(source.split_whitespace().map(str::to_owned).collect())
}
pub fn contains(&self, identifier: &str) -> bool {
self.0.iter().any(|entry| entry == identifier)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ContextPredicate {
Identifier(String),
Not(Box<ContextPredicate>),
And(Box<ContextPredicate>, Box<ContextPredicate>),
Or(Box<ContextPredicate>, Box<ContextPredicate>),
Child(Box<ContextPredicate>, Box<ContextPredicate>),
}
#[derive(Debug, PartialEq, Eq)]
pub struct PredicateError(pub String);
impl fmt::Display for PredicateError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "predicado de contexto inválido: {}", self.0)
}
}
impl std::error::Error for PredicateError {}
impl ContextPredicate {
pub fn parse(source: &str) -> Result<Self, PredicateError> {
let tokens = tokenize(source);
let mut parser = Parser { tokens: &tokens, position: 0 };
let predicate = parser.child()?;
match parser.tokens.get(parser.position) {
None => Ok(predicate),
Some(extra) => Err(PredicateError(format!("sobra `{extra}` en `{source}`"))),
}
}
pub fn matches(&self, stack: &[KeyContext], depth: usize) -> bool {
let Some(context) = stack.get(depth) else { return false };
match self {
Self::Identifier(name) => context.contains(name),
Self::Not(inner) => !inner.matches(stack, depth),
Self::And(left, right) => left.matches(stack, depth) && right.matches(stack, depth),
Self::Or(left, right) => left.matches(stack, depth) || right.matches(stack, depth),
Self::Child(parent, child) => {
child.matches(stack, depth) && (0..depth).any(|ancestor| parent.matches(stack, ancestor))
}
}
}
}
fn tokenize(source: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut characters = source.chars().peekable();
while let Some(character) = characters.next() {
match character {
' ' | '\t' => {}
'(' | ')' | '!' | '>' => tokens.push(character.to_string()),
'&' | '|' => {
if characters.peek() == Some(&character) {
characters.next();
}
tokens.push(format!("{character}{character}"));
}
_ => {
let mut identifier = character.to_string();
while let Some(next) = characters.peek().filter(|next| next.is_alphanumeric() || **next == '_') {
identifier.push(*next);
characters.next();
}
tokens.push(identifier);
}
}
}
tokens
}
struct Parser<'a> {
tokens: &'a [String],
position: usize,
}
impl Parser<'_> {
fn peek(&self) -> Option<&str> {
self.tokens.get(self.position).map(String::as_str)
}
fn child(&mut self) -> Result<ContextPredicate, PredicateError> {
let mut left = self.or()?;
while self.peek() == Some(">") {
self.position += 1;
left = ContextPredicate::Child(Box::new(left), Box::new(self.or()?));
}
Ok(left)
}
fn or(&mut self) -> Result<ContextPredicate, PredicateError> {
let mut left = self.and()?;
while self.peek() == Some("||") {
self.position += 1;
left = ContextPredicate::Or(Box::new(left), Box::new(self.and()?));
}
Ok(left)
}
fn and(&mut self) -> Result<ContextPredicate, PredicateError> {
let mut left = self.unary()?;
while self.peek() == Some("&&") {
self.position += 1;
left = ContextPredicate::And(Box::new(left), Box::new(self.unary()?));
}
Ok(left)
}
fn unary(&mut self) -> Result<ContextPredicate, PredicateError> {
let token = self.peek().map(str::to_owned).ok_or_else(|| PredicateError("expresión incompleta".into()))?;
self.position += 1;
match token.as_str() {
"!" => Ok(ContextPredicate::Not(Box::new(self.unary()?))),
"(" => {
let inner = self.child()?;
if self.peek() != Some(")") {
return Err(PredicateError("falta `)`".into()));
}
self.position += 1;
Ok(inner)
}
"&&" | "||" | ">" | ")" => Err(PredicateError(format!("`{token}` inesperado"))),
_ => Ok(ContextPredicate::Identifier(token)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn stack(levels: &[&str]) -> Vec<KeyContext> {
levels.iter().map(|level| KeyContext::parse(level)).collect()
}
#[test]
fn evaluates_boolean_operators() {
let predicate = ContextPredicate::parse("Editor && !Picker").expect("válido");
assert!(predicate.matches(&stack(&["Workspace", "Editor"]), 1));
assert!(!predicate.matches(&stack(&["Workspace", "Editor Picker"]), 1));
}
#[test]
fn child_operator_requires_an_ancestor() {
let predicate = ContextPredicate::parse("Workspace > Editor").expect("válido");
assert!(predicate.matches(&stack(&["Workspace", "Pane", "Editor"]), 2));
assert!(!predicate.matches(&stack(&["Pane", "Editor"]), 1));
}
#[test]
fn rejects_malformed_expressions() {
assert!(ContextPredicate::parse("Editor &&").is_err());
assert!(ContextPredicate::parse("(Editor").is_err());
}
}