on master help sign in
Text tests are functions
ef93e44iko
DESIGN.md+2−1
@@ -346,9 +346,10 @@ Conditions and computed values (`@let x = ...`) are expressions:
346|---|---|
346|---|---|
347| Values | numbers, `"strings"` (with `$var` interpolation), `$variables`, `true`, `false`, and bare words (`dark`, `#fff`) as strings |
347| Values | numbers, `"strings"` (with `$var` interpolation), `$variables`, `true`, `false`, and bare words (`dark`, `#fff`) as strings |
348| Arithmetic | `+ - * / %` with the usual precedence, unary `-`, `( )` |
348| Arithmetic | `+ - * / %` with the usual precedence, unary `-`, `( )` |
349−| Comparison | `== != < > <= >=` (numeric when both sides are numbers), `contains` (in text, or as an item of a list), `starts-with`, `ends-with` |
349+| Comparison | `== != < > <= >=` (numeric when both sides are numbers) |
350| Logic | `and`, `or`, `not`; empty, `false` and `0` are false |
350| Logic | `and`, `or`, `not`; empty, `false` and `0` are false |
351| Choice | `if(CONDITION, A, B)` |
351| Choice | `if(CONDITION, A, B)` |
352+| Tests | `contains(s, x)` (in text, or as an item of a list), `starts-with(s, x)`, `ends-with(s, x)` |
352| Text functions | `uppercase(s)`, `lowercase(s)`, `capitalize(s)`, `trim(s)`, `length(s)` (a list's items, or a text's characters), `reverse(s)`, `truncate(s, n)`, `replace(s, old, new)`, `default(s, fallback)` |
353| Text functions | `uppercase(s)`, `lowercase(s)`, `capitalize(s)`, `trim(s)`, `length(s)` (a list's items, or a text's characters), `reverse(s)`, `truncate(s, n)`, `replace(s, old, new)`, `default(s, fallback)` |
353| Color functions | `lighten(c, pct)`, `darken(c, pct)`, `alpha(c, a)`, `mix(c1, c2, pct)` |
354| Color functions | `lighten(c, pct)`, `darken(c, pct)`, `alpha(c, a)`, `mix(c1, c2, pct)` |
354
355
crates/htmlang-core/src/expr.rs+17−21
@@ -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<'_> {
255
255
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 {
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 {
267 return Ok(left);
259 return Ok(left);
268 };
260 };
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),
288− },
289− ("starts-with", _) => l.starts_with(&r),
290− ("ends-with", _) => l.ends_with(&r),
291 _ => unreachable!(),
277 _ => unreachable!(),
292 }))
278 }))
293 }
279 }
@@ -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 }
403 }
389 }
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))) }
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());
567 }
563 }
@@ -595,8 +591,8 @@ mod tests {
595 #[test]
591 #[test]
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");
601 }
597 }
602
598
tests/snapshots.rs+3−3
@@ -955,14 +955,14 @@ fn comparison_operators_lt() {
955
955
956#[test]
956#[test]
957fn comparison_operators_contains() {
957fn comparison_operators_contains() {
958− let output = compile("@page T\n@let name hello world\n@if $name contains world\n @text found");
958+ let output = compile("@page T\n@let name hello world\n@if contains($name, world)\n @text found");
959 assert!(output.contains("found"));
959 assert!(output.contains("found"));
960}
960}
961
961
962#[test]
962#[test]
963fn comparison_operators_starts_with() {
963fn comparison_operators_starts_with() {
964 let output = compile(
964 let output = compile(
965− "@page T\n@let url https://example.com\n@if $url starts-with https\n @text secure",
965+ "@page T\n@let url https://example.com\n@if starts-with($url, https)\n @text secure",
966 );
966 );
967 assert!(output.contains("secure"));
967 assert!(output.contains("secure"));
968}
968}
@@ -1050,7 +1050,7 @@ fn comparison_gte_lte() {
1050
1050
1051#[test]
1051#[test]
1052fn comparison_ends_with() {
1052fn comparison_ends_with() {
1053− let output = compile("@page T\n@let file photo.jpg\n@if $file ends-with .jpg\n @text image");
1053+ let output = compile("@page T\n@let file photo.jpg\n@if ends-with($file, .jpg)\n @text image");
1054 assert!(output.contains("image"));
1054 assert!(output.contains("image"));
1055}
1055}
1056
1056