use super::error::ValidationError;
use crate::token::Token;
fn tokens_to_string(tokens: &[Token]) -> String {
let mut result = String::new();
for token in tokens {
match token {
Token::Number(num) => result.push_str(num),
Token::Unit(unit) => result.push_str(unit),
Token::Operator(op) => {
result.push(' ');
result.push_str(op);
result.push(' ');
},
Token::LeftParen => result.push('('),
Token::RightParen => result.push(')'),
Token::Variable(var) => result.push_str(var),
}
}
result
}
pub(crate) fn validator(t: &[Token]) -> Result<(), ValidationError> {
let parentheses = t.iter().fold(0, |count, current| match current {
Token::LeftParen => count + 1,
Token::RightParen => count - 1,
_ => count,
});
if parentheses != 0 {
return Err(ValidationError::MismatchedParentheses(tokens_to_string(t)));
}
let operators: bool = t.iter().any(|c| matches!(c, Token::Operator(_)));
if !operators {
return Err(ValidationError::NoOperators(tokens_to_string(t)));
}
let mut ctx_stack: Vec<(Option<&str>, Option<&str>, Option<&Token>)> = Vec::new();
ctx_stack.push((None, None, None));
for token in t {
match token {
Token::LeftParen => {
let context = ctx_stack.last_mut().unwrap();
context.2 = Some(token);
ctx_stack.push((None, None, None))
},
Token::RightParen => {
ctx_stack.pop();
let stack_len = ctx_stack.len();
if stack_len == 0 {
return Err(ValidationError::MismatchedParentheses(tokens_to_string(t)))
}
let context = ctx_stack.last_mut().unwrap();
context.2 = Some(token);
},
Token::Operator(op) => {
let context = ctx_stack.last_mut().unwrap();
context.0 = Some(op);
if op != "/" && op != "*" {
context.1 = None;
}
if matches!(context.2, None) {
return Err(ValidationError::InvalidSyntax(tokens_to_string(t)));
}
context.2 = Some(token);
},
Token::Unit(unit) => {
let context = ctx_stack.last_mut().unwrap();
if !matches!(context.2, Some(Token::Number(_))) {
return Err(ValidationError::InvalidSyntax(tokens_to_string(t)))
}
if matches!(context.0, Some("/")) {
return Err(ValidationError::InvalidDivisionRHS(tokens_to_string(t)));
}
if matches!(context.0, Some("*")) && context.1.is_some() {
return Err(ValidationError::MultiplicationWithUnits(tokens_to_string(t)));
}
context.1 = Some(unit);
context.2 = Some(token);
},
Token::Number(num) => {
let context = ctx_stack.last_mut().unwrap();
if !matches!(context.2, None | Some(Token::Operator(_))) {
return Err(ValidationError::InvalidSyntax(tokens_to_string(t)))
}
if matches!(context.0, Some("/")) && num == "0" {
return Err(ValidationError::DivisionByZero(tokens_to_string(t)))
}
if num.ends_with(".") {
return Err(ValidationError::InvalidNumber(tokens_to_string(t)));
}
context.2 = Some(token);
},
Token::Variable(value) => {
let context = ctx_stack.last_mut().unwrap();
if !value.starts_with("var(--") || !value.ends_with(")") {
return Err(ValidationError::InvalidVariable(tokens_to_string(t)))
}
context.2 = Some(token);
}
}
}
if ctx_stack.len() > 1 || matches!(ctx_stack[0].2, Some(Token::Operator(_))) {
return Err(ValidationError::IncompleteExpression(tokens_to_string(t)))
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use test_case::test_case;
#[test_case(&[Token::Number("100".to_string()), Token::Unit("%".to_string()), Token::Operator("-".to_string()), Token::Number("50".to_string()), Token::Unit("px".to_string())]; "subtract px from percent")]
#[test_case(&[Token::Number("100".to_string()), Token::Unit("%".to_string()), Token::Operator("*".to_string()), Token::Number("2".to_string())]; "multiply percent by number")]
#[test_case(&[Token::Number("50".to_string()), Token::Unit("vh".to_string()), Token::Operator("/".to_string()), Token::Number("2".to_string())]; "divide vh by number")]
#[test_case(&[Token::Number("100".to_string()), Token::Unit("px".to_string()), Token::Operator("+".to_string()), Token::Number("2".to_string()), Token::Unit("em".to_string())]; "add px to em")]
#[test_case(&[Token::Number("100".to_string()), Token::Unit("px".to_string()), Token::Operator("-".to_string()), Token::LeftParen, Token::Number("50".to_string()), Token::Unit("px".to_string()), Token::Operator("-".to_string()), Token::Number("30".to_string()), Token::Unit("px".to_string()),Token::RightParen]; "nested operation with subtraction")]
#[test_case(&[Token::LeftParen, Token::Number("100".to_string()), Token::Unit("px".to_string()), Token::Operator("-".to_string()), Token::Number("50".to_string()), Token::Unit("px".to_string()), Token::RightParen, Token::Operator("*".to_string()), Token::Number("3".to_string())]; "nested operation with subtraction and multiplication")]
#[test_case(&[Token::Number("100".to_string()), Token::Unit("%".to_string()), Token::Operator("/".to_string()), Token::Number("2".to_string()), Token::Operator("-".to_string()), Token::Number("30".to_string()), Token::Unit("px".to_string())] ; "Division and subtraction with no parentheses")]
#[test_case(&[Token::Variable("var(--width)".to_string()), Token::Operator("*".to_string()), Token::Number("2".to_string())] ; "multiply variable by number")]
#[test_case(&[Token::Number("100".to_string()), Token::Unit("%".to_string()), Token::Operator("-".to_string()), Token::Variable("var(--padding)".to_string())] ; "subtract variable from percentage")]
#[test_case(&[Token::Number("100".to_string()), Token::Unit("vh".to_string()), Token::Operator("-".to_string()), Token::LeftParen, Token::Number("2".to_string()), Token::Operator("*".to_string()), Token::Variable("var(--margin)".to_string()), Token::RightParen] ; "complex nested operation with variable")]
#[test_case(&[
Token::LeftParen,
Token::LeftParen,
Token::Number("100".to_string()),
Token::Unit("px".to_string()),
Token::Operator("+".to_string()),
Token::Number("50".to_string()),
Token::Unit("px".to_string()),
Token::RightParen,
Token::Operator("-".to_string()),
Token::Number("20".to_string()),
Token::Unit("px".to_string()),
Token::RightParen,
Token::Operator("*".to_string()),
Token::Number("2".to_string())
]; "double nested add and subtract, then multiply")]
#[test_case(&[
Token::Number("100".to_string()),
Token::Unit("vw".to_string()),
Token::Operator("-".to_string()),
Token::LeftParen,
Token::Number("80".to_string()),
Token::Unit("vw".to_string()),
Token::Operator("+".to_string()),
Token::Number("30".to_string()),
Token::Unit("px".to_string()),
Token::RightParen
]; "subtract with nested add")]
#[test_case(&[
Token::LeftParen,
Token::Number("200".to_string()),
Token::Operator("/".to_string()),
Token::Number("2".to_string()),
Token::RightParen,
Token::Operator("*".to_string()),
Token::LeftParen,
Token::Number("50".to_string()),
Token::Operator("+".to_string()),
Token::Number("50".to_string()),
Token::RightParen
]; "nested division and addition")]
fn valid(input: &[Token]) {
let result = validator(input);
assert!(result.is_ok())
}
#[test_case(&[
Token::Number("100".to_string()),
Token::Unit("px".to_string()),
Token::Operator("/".to_string()),
Token::Number("0".to_string())
]; "division by zero")]
#[test_case(&[
Token::Number("100".to_string()),
Token::Unit("px".to_string()),
Token::Operator("/".to_string()),
Token::Number("2".to_string()),
Token::Unit("px".to_string())
]; "divide px by px")]
#[test_case(&[
Token::Number("100".to_string()),
Token::Unit("%".to_string()),
Token::Operator("*".to_string()),
Token::Number("50".to_string()),
Token::Unit("%".to_string())
]; "multiply percent by percent")]
#[test_case(&[
Token::Number("100".to_string()),
Token::Operator("+".to_string())
]; "incomplete expression")]
#[test_case(&[
Token::LeftParen,
Token::Number("100".to_string()),
Token::Unit("px".to_string())
]; "missing closing parenthesis")]
#[test_case(&[
Token::Number("100".to_string()),
Token::Unit("em".to_string()),
Token::Operator("*".to_string()),
Token::Number("5".to_string()),
Token::Unit("vh".to_string())
]; "multiplication with two units")]
#[test_case(&[
Token::Variable("var(--base)".to_string()),
Token::Operator("/".to_string()),
Token::Number("2".to_string()),
Token::Unit("px".to_string())
]; "division of variable by unit")]
#[test_case(&[
Token::Unit("px".to_string()),
Token::Number("100".to_string()),
Token::Operator("+".to_string()),
Token::Number("100".to_string())
]; "unit before number")]
#[test_case(&[
Token::Operator("*".to_string()),
Token::Number("100".to_string()),
Token::Unit("px".to_string())
]; "operator at the start")]
#[test_case(&[
Token::LeftParen,
Token::Operator("*".to_string()),
Token::Number("100".to_string()),
Token::Unit("px".to_string()),
Token::Operator("-".to_string()),
Token::Number("50".to_string()),
Token::Unit("px".to_string()),
Token::RightParen,
Token::Operator("*".to_string()),
Token::Number("3".to_string())
]; "operator at the start of nested expression")]
#[test_case(&[
Token::LeftParen,
Token::Number("100".to_string()),
Token::Unit("px".to_string()),
Token::Operator("*".to_string()),
Token::LeftParen,
Token::Number("50".to_string()),
Token::RightParen,
Token::Operator("+".to_string()),
Token::Number("50".to_string()),
Token::Unit("px".to_string())
]; "nested multiplication without unit in inner expression")]
#[test_case(&[
Token::Number("100".to_string()),
Token::Unit("px".to_string()),
Token::Operator("/".to_string()),
Token::LeftParen,
Token::Number("10".to_string()),
Token::Unit("px".to_string()),
Token::Operator("/".to_string()),
Token::Number("0".to_string()),
Token::RightParen
]; "division by zero in nested expression")]
#[test_case(&[
Token::LeftParen,
Token::Operator("-".to_string()),
Token::Number("100".to_string()),
Token::Unit("px".to_string()),
Token::RightParen,
Token::Operator("*".to_string()),
Token::Number("3".to_string())
]; "invalid operator at the start of nested expression")]
#[test_case(&[
Token::LeftParen,
Token::Number("100".to_string()),
Token::Unit("px".to_string()),
Token::Operator("-".to_string()),
Token::RightParen,
Token::Number("50".to_string())
]; "missing operator before a number")]
#[test_case(&[
Token::LeftParen,
Token::Number("100".to_string()),
Token::Unit("%".to_string()),
Token::Operator("+".to_string()),
Token::Number("50".to_string()),
Token::Unit("px".to_string()),
Token::RightParen,
Token::Operator("/".to_string()),
Token::Number("2".to_string()),
Token::Unit("px".to_string())
]; "unit mismatch in division after nested expression")]
fn invalid(input: &[Token]) {
let result = validator(input);
assert!(result.is_err())
}
}