diff --git a/crates/simplexpr/src/ast.rs b/crates/simplexpr/src/ast.rs index 57769f8..88bd320 100644 --- a/crates/simplexpr/src/ast.rs +++ b/crates/simplexpr/src/ast.rs @@ -32,6 +32,8 @@ pub enum UnaryOp { #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum SimplExpr { Literal(DynVal), + JsonArray(Span, Vec), + JsonObject(Span, Vec<(SimplExpr, SimplExpr)>), Concat(Span, Vec), VarRef(Span, VarName), BinOp(Span, Box, BinOp, Box), @@ -63,6 +65,10 @@ impl std::fmt::Display for SimplExpr { SimplExpr::FunctionCall(_, function_name, args) => { write!(f, "{}({})", function_name, args.iter().join(", ")) } + SimplExpr::JsonArray(_, values) => write!(f, "[{}]", values.iter().join(", ")), + SimplExpr::JsonObject(_, entries) => { + write!(f, "{{{}}}", entries.iter().map(|(k, v)| format!("{}: {}", k, v)).join(", ")) + } } } } @@ -81,10 +87,13 @@ impl SimplExpr { Self::Literal(s.into()) } } + impl Spanned for SimplExpr { fn span(&self) -> Span { match self { SimplExpr::Literal(x) => x.span(), + SimplExpr::JsonArray(span, _) => *span, + SimplExpr::JsonObject(span, _) => *span, SimplExpr::Concat(span, _) => *span, SimplExpr::VarRef(span, _) => *span, SimplExpr::BinOp(span, ..) => *span, diff --git a/crates/simplexpr/src/dynval.rs b/crates/simplexpr/src/dynval.rs index 9ade8d8..ca88203 100644 --- a/crates/simplexpr/src/dynval.rs +++ b/crates/simplexpr/src/dynval.rs @@ -1,7 +1,7 @@ use eww_shared_util::{Span, Spanned}; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use std::{fmt, iter::FromIterator, str::FromStr}; +use std::{convert::TryFrom, fmt, iter::FromIterator, str::FromStr}; pub type Result = std::result::Result; @@ -94,6 +94,14 @@ macro_rules! impl_dynval_from { impl_dynval_from!(bool, i32, u32, f32, u8, f64, &str); +impl TryFrom for DynVal { + type Error = serde_json::Error; + + fn try_from(value: serde_json::Value) -> std::result::Result { + Ok(DynVal(serde_json::to_string(&value)?, Span::DUMMY)) + } +} + impl From for DynVal { fn from(d: std::time::Duration) -> Self { DynVal(format!("{}ms", d.as_millis()), Span::DUMMY) diff --git a/crates/simplexpr/src/eval.rs b/crates/simplexpr/src/eval.rs index 1b278d2..876602c 100644 --- a/crates/simplexpr/src/eval.rs +++ b/crates/simplexpr/src/eval.rs @@ -5,7 +5,7 @@ use crate::{ dynval::{ConversionError, DynVal}, }; use eww_shared_util::{Span, Spanned, VarName}; -use std::collections::HashMap; +use std::{collections::HashMap, convert::TryFrom}; #[derive(Debug, thiserror::Error)] pub enum EvalError { @@ -30,6 +30,9 @@ pub enum EvalError { #[error("Unable to index into value {0}")] CannotIndex(String), + #[error("Json operation failed: {0}")] + SerdeError(#[from] serde_json::error::Error), + #[error("{1}")] Spanned(Span, Box), } @@ -74,6 +77,16 @@ impl SimplExpr { FunctionCall(span, name, args.into_iter().map(|x| x.try_map_var_refs(f)).collect::>()?) } VarRef(span, name) => f(span, name)?, + JsonArray(span, values) => { + JsonArray(span, values.into_iter().map(|x| x.try_map_var_refs(f)).collect::>()?) + } + JsonObject(span, entries) => JsonObject( + span, + entries + .into_iter() + .map(|(k, v)| Ok((k.try_map_var_refs(f)?, v.try_map_var_refs(f)?))) + .collect::>()?, + ), x @ Literal(..) => x, }) } @@ -121,6 +134,8 @@ impl SimplExpr { refs } FunctionCall(_, _, args) => args.iter().flat_map(|a| a.var_refs()).collect(), + JsonArray(_, values) => values.iter().flat_map(|v| v.var_refs()).collect(), + JsonObject(_, entries) => entries.iter().flat_map(|(k, v)| k.var_refs().into_iter().chain(v.var_refs())).collect(), } } @@ -218,6 +233,20 @@ impl SimplExpr { let args = args.into_iter().map(|a| a.eval(values)).collect::>()?; call_expr_function(&function_name, args).map(|x| x.at(*span)).map_err(|e| e.at(*span)) } + SimplExpr::JsonArray(span, entries) => { + let entries = entries + .into_iter() + .map(|v| Ok(serde_json::Value::String(v.eval(values)?.as_string()?))) + .collect::>()?; + Ok(DynVal::try_from(serde_json::Value::Array(entries))?.at(*span)) + } + SimplExpr::JsonObject(span, entries) => { + let entries = entries + .into_iter() + .map(|(k, v)| Ok((k.eval(values)?.as_string()?, serde_json::Value::String(v.eval(values)?.as_string()?)))) + .collect::>()?; + Ok(DynVal::try_from(serde_json::Value::Object(entries))?.at(*span)) + } }; Ok(value?.at(span)) } diff --git a/crates/simplexpr/src/parser/lexer.rs b/crates/simplexpr/src/parser/lexer.rs index 4df1bde..460aa69 100644 --- a/crates/simplexpr/src/parser/lexer.rs +++ b/crates/simplexpr/src/parser/lexer.rs @@ -35,6 +35,8 @@ pub enum Token { Colon, LPren, RPren, + LCurl, + RCurl, LBrack, RBrack, Dot, @@ -92,6 +94,8 @@ regex_rules! { escape(r")") => |_| Token::RPren, escape(r"[") => |_| Token::LBrack, escape(r"]") => |_| Token::RBrack, + escape(r"{") => |_| Token::LCurl, + escape(r"}") => |_| Token::RCurl, escape(r".") => |_| Token::Dot, escape(r"true") => |_| Token::True, escape(r"false") => |_| Token::False, @@ -222,14 +226,31 @@ impl<'s> Lexer<'s> { } else { let segment_start = self.pos; let mut toks = Vec::new(); - while self.pos < self.source.len() && !self.remaining().starts_with(STR_INTERPOLATION_END) { - match self.next_token()? { - Ok(tok) => toks.push(tok), - Err(err) => return Some(Err(err)), + let mut curly_nesting = 0; + + 'inner: while let Some(tok) = self.next_token() { + if self.pos >= self.source.len() { + break 'inner; + } + + let tok = match tok { + Ok(x) => x, + Err(e) => return Some(Err(e)), + }; + if tok.1 == Token::LCurl { + curly_nesting += 1; + } else if tok.1 == Token::RCurl { + curly_nesting -= 1; + } + + if curly_nesting < 0 { + break 'inner; + } else { + toks.push(tok); } } - elements.push((segment_start + self.offset, StrLitSegment::Interp(toks), self.pos + self.offset)); - self.advance_by(STR_INTERPOLATION_END.len()); + + elements.push((segment_start + self.offset, StrLitSegment::Interp(toks), self.pos + self.offset - 1)); in_string_lit = true; } } @@ -282,9 +303,13 @@ mod test { number_in_ident => v!(r#"foo_1_bar"#), interpolation_1 => v!(r#" "foo ${2 * 2} bar" "#), interpolation_nested => v!(r#" "foo ${(2 * 2) + "${5 + 5}"} bar" "#), + json_in_interpolation => v!(r#" "${ {1: 2} }" "#), escaping => v!(r#" "a\"b\{}" "#), comments => v!("foo ; bar"), weird_char_boundaries => v!(r#"" " + music"#), symbol_spam => v!(r#"(foo + - "()" "a\"b" true false [] 12.2)"#), + weird_nesting => v!(r#" + "${ {"hi": "ho"}.hi }".hi + "#), } } diff --git a/crates/simplexpr/src/parser/mod.rs b/crates/simplexpr/src/parser/mod.rs index f6a2d9b..017eb77 100644 --- a/crates/simplexpr/src/parser/mod.rs +++ b/crates/simplexpr/src/parser/mod.rs @@ -42,6 +42,8 @@ mod tests { "hi[\"ho\"]", "foo.bar.baz", "foo.bar[2 + 2] * asdf[foo.bar]", + r#"[1, 2, 3 + 4, "bla", [blub, blo]]"#, + r#"{ "key": "value", 5: 1+2, true: false }"#, ); } } diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__json_in_interpolation.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__json_in_interpolation.snap new file mode 100644 index 0000000..410c200 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__json_in_interpolation.snap @@ -0,0 +1,6 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\" \"${ {1: 2} }\" \"#)" + +--- +(1, StringLit([(1, Literal(""), 4), (4, Interp([(5, LCurl, 6), (6, NumLit("1"), 7), (7, Colon, 8), (9, NumLit("2"), 10), (10, RCurl, 11)]), 12), (12, Literal(""), 14)]), 14) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__weird_nesting.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__weird_nesting.snap new file mode 100644 index 0000000..504e155 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__lexer__test__weird_nesting.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/lexer.rs +expression: "v!(r#\"\n \"${ {\"hi\": \"ho\"}.hi }\".hi\n \"#)" + +--- +(13, StringLit([(13, Literal(""), 16), (16, Interp([(17, LCurl, 18), (18, StringLit([(18, Literal("hi"), 22)]), 22), (22, Colon, 23), (24, StringLit([(24, Literal("ho"), 28)]), 28), (28, RCurl, 29), (29, Dot, 30), (30, Ident("hi"), 32)]), 33), (33, Literal(""), 35)]), 35) +(35, Dot, 36) +(36, Ident("hi"), 38) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-14.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-14.snap new file mode 100644 index 0000000..64bf682 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-14.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, r#\"[1, 2, 3 + 4, \"bla\", [blub, blo]]\"#))" + +--- +Ok( + ["1", "2", ("3" + "4"), "bla", [blub, blo]], +) diff --git a/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-15.snap b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-15.snap new file mode 100644 index 0000000..f999216 --- /dev/null +++ b/crates/simplexpr/src/parser/snapshots/simplexpr__parser__tests__test-15.snap @@ -0,0 +1,8 @@ +--- +source: crates/simplexpr/src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, 0, r#\"{ \"key\": \"value\", 5: 1+2, true: false }\"#))" + +--- +Ok( + {"key": "value", "5": ("1" + "2"), "true": "false"}, +) diff --git a/crates/simplexpr/src/simplexpr_parser.lalrpop b/crates/simplexpr/src/simplexpr_parser.lalrpop index 10028b7..e0a96c8 100644 --- a/crates/simplexpr/src/simplexpr_parser.lalrpop +++ b/crates/simplexpr/src/simplexpr_parser.lalrpop @@ -34,6 +34,8 @@ extern { ")" => Token::RPren, "[" => Token::LBrack, "]" => Token::RBrack, + "{" => Token::LCurl, + "}" => Token::RCurl, "." => Token::Dot, "true" => Token::True, @@ -59,10 +61,6 @@ Comma: Vec = { pub Expr: SimplExpr = { #[precedence(level="0")] - // "lexer_error" =>? { - // Err(ParseError::User { error: LexicalError(l, r, fid) }) - //}, - =>? parse_stringlit(Span(l, r, fid), x), => SimplExpr::literal(Span(l, r, fid), x), "true" => SimplExpr::literal(Span(l, r, fid), "true".into()), @@ -70,6 +68,8 @@ pub Expr: SimplExpr = { => VarRef(Span(l, r, fid), VarName(ident.to_string())), "(" ")", + "[" > "]" => SimplExpr::JsonArray(Span(l, r, fid), values), + "{" > "}" => SimplExpr::JsonObject(Span(l, r, fid), values), #[precedence(level="1")] #[assoc(side="right")] "(" > ")" => FunctionCall(Span(l, r, fid), ident, args), @@ -110,3 +110,7 @@ pub Expr: SimplExpr = { }; ExprReset = ; + + +JsonKeyValue = ":" ; + diff --git a/crates/yuck/src/config/snapshots/yuck__config__test__config.snap b/crates/yuck/src/config/snapshots/yuck__config__test__config.snap index 0c4fe00..5aa9681 100644 --- a/crates/yuck/src/config/snapshots/yuck__config__test__config.snap +++ b/crates/yuck/src/config/snapshots/yuck__config__test__config.snap @@ -8,8 +8,16 @@ Config( "bar": WidgetDefinition( name: "bar", expected_args: [ - AttrName("arg"), - AttrName("arg2"), + AttrSpec( + name: AttrName("arg"), + optional: false, + span: Span(25, 28, 0), + ), + AttrSpec( + name: AttrName("arg2"), + optional: false, + span: Span(29, 33, 0), + ), ], widget: WidgetUse( name: "foo", @@ -87,6 +95,7 @@ Config( VarName("stuff"): Listen(ListenScriptVar( name: VarName("stuff"), command: "tail -f stuff", + initial_value: DynVal("", Span(18446744073709551615, 18446744073709551615, 18446744073709551615)), command_span: Span(168, 183, 0), name_span: Span(162, 167, 0), )), diff --git a/crates/yuck/src/parser/lexer.rs b/crates/yuck/src/parser/lexer.rs index 8068c7f..336bae1 100644 --- a/crates/yuck/src/parser/lexer.rs +++ b/crates/yuck/src/parser/lexer.rs @@ -96,27 +96,32 @@ impl Lexer { } fn simplexpr(&mut self) -> Option> { + use simplexpr::parser::lexer as simplexpr_lexer; self.pos += 1; - let mut simplexpr_lexer = simplexpr::parser::lexer::Lexer::new(self.file_id, self.pos, &self.source[self.pos..]); - let mut toks = Vec::new(); + let mut simplexpr_lexer = simplexpr_lexer::Lexer::new(self.file_id, self.pos, &self.source[self.pos..]); + let mut toks: Vec<(usize, _, usize)> = Vec::new(); let mut end = self.pos; + let mut curly_nesting = 0; loop { - match simplexpr_lexer.next_token() { - Some(Ok((lo, tok, hi))) => { + match simplexpr_lexer.next_token()? { + Ok((lo, tok, hi)) => { end = hi; + if tok == simplexpr_lexer::Token::LCurl { + curly_nesting += 1; + } else if tok == simplexpr_lexer::Token::RCurl { + curly_nesting -= 1; + if curly_nesting < 0 { + let start = toks.first().map(|(start, ..)| *start).unwrap_or(end); + self.pos = end; + self.advance_until_char_boundary(); + return Some(Ok((start, Token::SimplExpr(toks), end))); + } + } toks.push((lo, tok, hi)); } - Some(Err(err)) => { - if simplexpr_lexer.continues_with('}') { - let start = toks.first().map(|x| x.0).unwrap_or(end); - self.pos = end + 1; - self.advance_until_char_boundary(); - return Some(Ok((start, Token::SimplExpr(toks), end))); - } else { - return Some(Err(parse_error::ParseError::LexicalError(err.span()))); - } + Err(err) => { + return Some(Err(parse_error::ParseError::LexicalError(err.span()))); } - None => return None, } } } @@ -182,7 +187,7 @@ mod test { macro_rules! v { ($x:literal) => { - Lexer::new(0, 0, $x) + Lexer::new(0, $x.to_string()) .map(|x| match x { Ok((l, x, r)) => format!("({}, {:?}, {})", l, x, r), Err(err) => format!("{}", err), @@ -192,10 +197,12 @@ mod test { } snapshot_string! { - basic => r#"(foo + - "text" )"#, - escaped_strings => r#"{ bla "} \" }" " \" "}"#, - escaped_quote => r#""< \" >""#, - char_boundary => r#"{ " " + music}"#, - quotes_in_quotes => r#"{ " } ' }" }"#, + basic => v!(r#"(foo + - "text" )"#), + basic_simplexpr => v!(r#"({2})"#), + escaped_strings => v!(r#"{ bla "} \" }" " \" "}"#), + escaped_quote => v!(r#""< \" >""#), + char_boundary => v!(r#"{ " " + music}"#), + quotes_in_quotes => v!(r#"{ " } ' }" }"#), + end_with_string_interpolation => v!(r#"(box "foo ${1 + 2}")"#), } } diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic.snap index e8d564a..9184cc6 100644 --- a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic.snap +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic.snap @@ -1,6 +1,11 @@ --- source: crates/yuck/src/parser/lexer.rs -expression: "r#\"(foo + - \"text\" )\"#" +expression: "v!(r#\"(foo + - \"text\" )\"#)" --- -(foo + - "text" ) +(0, LPren, 1) +(1, Symbol("foo"), 4) +(5, Symbol("+"), 6) +(7, Symbol("-"), 8) +(9, SimplExpr([(9, StringLit([(9, Literal("text"), 15)]), 15)]), 15) +(16, RPren, 17) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic_simplexpr.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic_simplexpr.snap new file mode 100644 index 0000000..7432045 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__basic_simplexpr.snap @@ -0,0 +1,8 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "v!(r#\"({2})\"#)" + +--- +(0, LPren, 1) +(2, SimplExpr([(2, NumLit("2"), 3)]), 4) +(4, RPren, 5) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__char_boundary.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__char_boundary.snap index 708318b..83afef9 100644 --- a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__char_boundary.snap +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__char_boundary.snap @@ -1,6 +1,6 @@ --- source: crates/yuck/src/parser/lexer.rs -expression: "r#\"{ \" \" + music}\"#" +expression: "v!(r#\"{ \" \" + music}\"#)" --- -{ " " + music} +(2, SimplExpr([(2, StringLit([(2, Literal("\u{f001} "), 10)]), 10), (11, Plus, 12), (13, Ident("music"), 18)]), 19) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__end_with_string_interpolation.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__end_with_string_interpolation.snap new file mode 100644 index 0000000..32b2243 --- /dev/null +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__end_with_string_interpolation.snap @@ -0,0 +1,9 @@ +--- +source: crates/yuck/src/parser/lexer.rs +expression: "v!(r#\"(box \"foo ${1 + 2}\")\"#)" + +--- +(0, LPren, 1) +(1, Symbol("box"), 4) +(5, SimplExpr([(5, StringLit([(5, Literal("foo "), 12), (12, Interp([(12, NumLit("1"), 13), (14, Plus, 15), (16, NumLit("2"), 17)]), 17), (17, Literal(""), 19)]), 19)]), 19) +(19, RPren, 20) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_quote.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_quote.snap index 9ab4524..31abe37 100644 --- a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_quote.snap +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_quote.snap @@ -1,6 +1,6 @@ --- source: crates/yuck/src/parser/lexer.rs -expression: "r#\"\"< \\\" >\"\"#" +expression: "v!(r#\"\"< \\\" >\"\"#)" --- -"< \" >" +(0, SimplExpr([(0, StringLit([(0, Literal("< \" >"), 8)]), 8)]), 8) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_strings.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_strings.snap index eb6ab16..37ede48 100644 --- a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_strings.snap +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__escaped_strings.snap @@ -1,6 +1,6 @@ --- source: crates/yuck/src/parser/lexer.rs -expression: "r#\"{ bla \"} \\\" }\" \" \\\" \"}\"#" +expression: "v!(r#\"{ bla \"} \\\" }\" \" \\\" \"}\"#)" --- -{ bla "} \" }" " \" "} +(2, SimplExpr([(2, Ident("bla"), 5), (6, StringLit([(6, Literal("} \" }"), 14)]), 14), (15, StringLit([(15, Literal(" \" "), 21)]), 21)]), 22) diff --git a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__quotes_in_quotes.snap b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__quotes_in_quotes.snap index 26ead15..05549c2 100644 --- a/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__quotes_in_quotes.snap +++ b/crates/yuck/src/parser/snapshots/yuck__parser__lexer__test__quotes_in_quotes.snap @@ -1,6 +1,6 @@ --- source: crates/yuck/src/parser/lexer.rs -expression: "r#\"{ \" } ' }\" }\"#" +expression: "v!(r#\"{ \" } ' }\" }\"#)" --- -{ " } ' }" } +(2, SimplExpr([(2, StringLit([(2, Literal(" } ' }"), 10)]), 10)]), 12)