@@ -10,8 +10,7 @@
10//! or := and ("or" and)*
10//! or := and ("or" and)*
11//! and := not ("and" not)*
11//! and := not ("and" not)*
12//! not := "not" not | compare
12//! not := "not" not | compare
13−//! compare := sum (("==" | "!=" | "<" | ">" | "<=" | ">=" | "contains"
13+//! compare := sum (("==" | "!=" | "<" | ">" | "<=" | ">=") sum)?
14−//! | "starts-with" | "ends-with") sum)?
15//! sum := product (("+" | "-") product)*
14//! sum := product (("+" | "-") product)*
16//! product := unary (("*" | "/" | "%") unary)*
15//! product := unary (("*" | "/" | "%") unary)*
17//! unary := "-" unary | primary
16//! unary := "-" unary | primary
@@ -22,7 +21,8 @@
22//! A bare word (`dark`, `red`, `#3b82f6`, `10px`) is a string. A variable
21//! A bare word (`dark`, `red`, `#3b82f6`, `10px`) is a string. A variable
23//! loaded from a JSON array is a list: `length` counts its items and
22//! loaded from a JSON array is a list: `length` counts its items and
24//! `contains` tests membership. Functions:
23//! `contains` tests membership. Functions:
24+//! `if(cond, a, b)`, tests (`contains(s, x)`, `starts-with(s, x)`,
25−//! `if(cond, a, b)`, text (`uppercase`, `lowercase`, `capitalize`, `trim`,
25+//! `ends-with(s, x)`), text (`uppercase`, `lowercase`, `capitalize`, `trim`,
26//! `length`, `reverse`, `truncate(s, n)`, `replace(s, old, new)`,
26//! `length`, `reverse`, `truncate(s, n)`, `replace(s, old, new)`,
27//! `default(s, fallback)`) and colors (`lighten(c, pct)`, `darken(c, pct)`,
27//! `default(s, fallback)`) and colors (`lighten(c, pct)`, `darken(c, pct)`,
28//! `alpha(c, a)`, `mix(c1, c2, pct)`). In text, `${expr}` interpolates an
28//! `alpha(c, a)`, `mix(c1, c2, pct)`). In text, `${expr}` interpolates an
@@ -255,15 +255,7 @@ impl Parser<'_> {
256 fn compare(&mut self) -> Result<Value, String> {
256 fn compare(&mut self) -> Result<Value, String> {
257 let left = self.sum()?;
257 let left = self.sum()?;
258− let op = if let Some(op) = self.eat_op(&["==", "!=", "<=", ">=", "<", ">"]) {
258+ let Some(op) = self.eat_op(&["==", "!=", "<=", ">=", "<", ">"]) else {
260− } else if self.eat_word("contains") {
262− } else if self.eat_word("starts-with") {
264− } else if self.eat_word("ends-with") {
267 return Ok(left);
259 return Ok(left);
269 let right = self.sum()?;
261 let right = self.sum()?;
@@ -282,12 +274,6 @@ impl Parser<'_> {
282 (">", None) => l > r,
274 (">", None) => l > r,
283 ("<=", None) => l <= r,
275 ("<=", None) => l <= r,
284 (">=", None) => l >= r,
276 (">=", None) => l >= r,
285− ("contains", _) => match &left {
286− Value::List(items) => items.contains(&r),
287− _ => l.contains(&r),
289− ("starts-with", _) => l.starts_with(&r),
290− ("ends-with", _) => l.ends_with(&r),
291 _ => unreachable!(),
277 _ => unreachable!(),
@@ -401,6 +387,15 @@ fn call(name: &str, args: Vec<Value>) -> Result<Value, String> {
401 let then = args.pop().unwrap();
387 let then = args.pop().unwrap();
402 if args[0].truthy() { then } else { otherwise }
388 if args[0].truthy() { then } else { otherwise }
392+ Value::Bool(match &args[0] {
393+ Value::List(items) => items.contains(&text(1)),
394+ other => other.to_string().contains(&text(1)),
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))) }
404 "uppercase" => { arity(1)?; Value::Str(text(0).to_uppercase()) }
399 "uppercase" => { arity(1)?; Value::Str(text(0).to_uppercase()) }
405 "lowercase" => { arity(1)?; Value::Str(text(0).to_lowercase()) }
400 "lowercase" => { arity(1)?; Value::Str(text(0).to_lowercase()) }
406 "capitalize" => {
401 "capitalize" => {
@@ -561,7 +556,8 @@ mod tests {
561 assert!(ev("$theme == dark").truthy());
556 assert!(ev("$theme == dark").truthy());
562 assert!(ev("$theme != light and not $empty").truthy());
557 assert!(ev("$theme != light and not $empty").truthy());
563 assert!(!ev("$count < 2 or $empty").truthy());
558 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());
565 assert!(ev("uppercase($name) == WORLD").truthy());
561 assert!(ev("uppercase($name) == WORLD").truthy());
566 assert!(ev("#3b82f6 != red").truthy());
562 assert!(ev("#3b82f6 != red").truthy());
@@ -595,8 +591,8 @@ mod tests {
596 fn lists() {
592 fn lists() {
597 assert_eq!(ev("length($tags)").to_string(), "2");
593 assert_eq!(ev("length($tags)").to_string(), "2");
598− assert!(ev("$tags contains \"web dev\"").truthy());
594+ assert!(ev("contains($tags, \"web dev\")").truthy());
599− assert!(!ev("$tags contains web").truthy());
595+ assert!(!ev("contains($tags, web)").truthy());
600 assert_eq!(ev("$tags").to_string(), "rust, web dev");
596 assert_eq!(ev("$tags").to_string(), "rust, web dev");