@@ -10,8 +10,7 @@
1010//! or := and ("or" and)*
1111//! and := not ("and" not)*
1212//! not := "not" not | compare
13−//! compare := sum (("==" | "!=" | "<" | ">" | "<=" | ">=" | "contains"
14−//! | "starts-with" | "ends-with") sum)?
13+//! compare := sum (("==" | "!=" | "<" | ">" | "<=" | ">=") sum)?
1514//! sum := product (("+" | "-") product)*
1615//! product := unary (("*" | "/" | "%") unary)*
1716//! unary := "-" unary | primary
@@ -22,7 +21,8 @@
2221//! A bare word (`dark`, `red`, `#3b82f6`, `10px`) is a string. A variable
2322//! loaded from a JSON array is a list: `length` counts its items and
2423//! `contains` tests membership. Functions:
25−//! `if(cond, a, b)`, text (`uppercase`, `lowercase`, `capitalize`, `trim`,
24+//! `if(cond, a, b)`, tests (`contains(s, x)`, `starts-with(s, x)`,
25+//! `ends-with(s, x)`), text (`uppercase`, `lowercase`, `capitalize`, `trim`,
2626//! `length`, `reverse`, `truncate(s, n)`, `replace(s, old, new)`,
2727//! `default(s, fallback)`) and colors (`lighten(c, pct)`, `darken(c, pct)`,
2828//! `alpha(c, a)`, `mix(c1, c2, pct)`). In text, `${expr}` interpolates an
@@ -255,15 +255,7 @@ impl Parser<'_> {
255255
256256 fn compare(&mut self) -> Result<Value, String> {
257257 let left = self.sum()?;
258− let op = if let Some(op) = self.eat_op(&["==", "!=", "<=", ">=", "<", ">"]) {
259− op
260− } else if self.eat_word("contains") {
261− "contains"
262− } else if self.eat_word("starts-with") {
263− "starts-with"
264− } else if self.eat_word("ends-with") {
265− "ends-with"
266− } else {
258+ let Some(op) = self.eat_op(&["==", "!=", "<=", ">=", "<", ">"]) else {
267259 return Ok(left);
268260 };
269261 let right = self.sum()?;
@@ -282,12 +274,6 @@ impl Parser<'_> {
282274 (">", None) => l > r,
283275 ("<=", None) => l <= r,
284276 (">=", None) => l >= r,
285− ("contains", _) => match &left {
286− Value::List(items) => items.contains(&r),
287− _ => l.contains(&r),
288− },
289− ("starts-with", _) => l.starts_with(&r),
290− ("ends-with", _) => l.ends_with(&r),
291277 _ => unreachable!(),
292278 }))
293279 }
@@ -401,6 +387,15 @@ fn call(name: &str, args: Vec<Value>) -> Result<Value, String> {
401387 let then = args.pop().unwrap();
402388 if args[0].truthy() { then } else { otherwise }
403389 }
390+ "contains" => {
391+ arity(2)?;
392+ Value::Bool(match &args[0] {
393+ Value::List(items) => items.contains(&text(1)),
394+ other => other.to_string().contains(&text(1)),
395+ })
396+ }
397+ "starts-with" => { arity(2)?; Value::Bool(text(0).starts_with(&text(1))) }
398+ "ends-with" => { arity(2)?; Value::Bool(text(0).ends_with(&text(1))) }
404399 "uppercase" => { arity(1)?; Value::Str(text(0).to_uppercase()) }
405400 "lowercase" => { arity(1)?; Value::Str(text(0).to_lowercase()) }
406401 "capitalize" => {
@@ -561,7 +556,8 @@ mod tests {
561556 assert!(ev("$theme == dark").truthy());
562557 assert!(ev("$theme != light and not $empty").truthy());
563558 assert!(!ev("$count < 2 or $empty").truthy());
564− assert!(ev("$name contains or").truthy());
559+ assert!(ev("contains($name, or)").truthy());
560+ assert!(ev("starts-with($name, Wo) and ends-with($name, ld)").truthy());
565561 assert!(ev("uppercase($name) == WORLD").truthy());
566562 assert!(ev("#3b82f6 != red").truthy());
567563 }
@@ -595,8 +591,8 @@ mod tests {
595591 #[test]
596592 fn lists() {
597593 assert_eq!(ev("length($tags)").to_string(), "2");
598− assert!(ev("$tags contains \"web dev\"").truthy());
599− assert!(!ev("$tags contains web").truthy());
594+ assert!(ev("contains($tags, \"web dev\")").truthy());
595+ assert!(!ev("contains($tags, web)").truthy());
600596 assert_eq!(ev("$tags").to_string(), "rust, web dev");
601597 }
602598