aura-gui 0.3.5

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Contextos de teclado y predicados (`Editor && !Picker`, `Workspace > Editor`).

use std::fmt;

/// Identificadores que un elemento declara con `.key_context("Editor")`.
#[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>),
    /// `padre > hijo`: el hijo coincide en este nivel y el padre en un nivel superior.
    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}`"))),
        }
    }

    /// Evalúa contra la pila de contextos `stack[..=depth]` (de la raíz al nodo).
    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
}

/// Precedencia de menor a mayor: `>`, `||`, `&&`, `!`.
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());
    }
}