From 4469b84ad659cf5b41e35c0e18e24f7df474fb0b Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 12 Jun 2024 23:13:36 -0700 Subject: [PATCH] Support nested functions --- src-tauri/templates/src/parser.rs | 137 ++++++++++++++++++---------- src-tauri/templates/src/renderer.rs | 80 +++++++++++----- 2 files changed, 145 insertions(+), 72 deletions(-) diff --git a/src-tauri/templates/src/parser.rs b/src-tauri/templates/src/parser.rs index e90cad8d..98b3b303 100644 --- a/src-tauri/templates/src/parser.rs +++ b/src-tauri/templates/src/parser.rs @@ -1,14 +1,14 @@ #[derive(Clone, PartialEq, Debug)] pub enum Val { Str(String), - Ident(String), + Var(String), + Fn { name: String, args: Vec }, } #[derive(Clone, PartialEq, Debug)] pub enum Token { Raw(String), - Var { name: String }, - Fn { name: String, args: Vec }, + Tag(Val), Eof, } @@ -66,17 +66,10 @@ impl Parser { // Parse up to first identifier // ${[ my_var... self.skip_whitespace(); - let name = match self.parse_ident() { - None => return None, - Some(v) => v, - }; - // Parse fn args if they exist - // ${[ my_var(a, b, c) - let args = if self.match_str("(") { - self.parse_fn_args() - } else { - None + let val = match self.parse_value() { + Some(v) => v, + None => return None, }; // Parse to closing tag @@ -86,29 +79,65 @@ impl Parser { return None; } - Some(match args { - Some(a) => Token::Fn { args: a, name }, - None => Token::Var { name }, - }) + Some(Token::Tag(val)) } #[allow(dead_code)] fn debug_pos(&self, x: &str) { println!( - r#"Position: {x} -- [{}] = {} --> "{}"#, + r#"Position: {x} -- [{}] = {} --> "{}" --> {:?}"#, self.pos, self.chars[self.pos], - self.chars.iter().collect::() + self.chars.iter().collect::(), + self.tokens, ); } + fn parse_value(&mut self) -> Option { + if let Some((name, args)) = self.parse_fn() { + Some(Val::Fn { name, args }) + } else if let Some(v) = self.parse_ident() { + Some(Val::Var(v)) + } else if let Some(v) = self.parse_string() { + Some(Val::Str(v)) + } else { + None + } + } + + fn parse_fn(&mut self) -> Option<(String, Vec)> { + let start_pos = self.pos; + + let name = match self.parse_ident() { + Some(v) => v, + None => { + self.pos = start_pos; + return None; + } + }; + + let args = match self.parse_fn_args() { + Some(args) => args, + None => { + self.pos = start_pos; + return None; + } + }; + + Some((name, args)) + } + fn parse_fn_args(&mut self) -> Option> { + if !self.match_str("(") { + return None; + } + let start_pos = self.pos; let mut args: Vec = Vec::new(); while self.pos < self.chars.len() { self.skip_whitespace(); - if let Some(v) = self.parse_ident_or_string() { + if let Some(v) = self.parse_value() { args.push(v); } @@ -121,6 +150,7 @@ impl Parser { // If we don't find a comma, that's bad if !args.is_empty() && !self.match_str(",") { + self.pos = start_pos; return None; } @@ -132,16 +162,6 @@ impl Parser { return Some(args); } - fn parse_ident_or_string(&mut self) -> Option { - if let Some(i) = self.parse_ident() { - Some(Val::Ident(i)) - } else if let Some(s) = self.parse_string() { - Some(Val::Str(s)) - } else { - None - } - } - fn parse_ident(&mut self) -> Option { let start_pos = self.pos; @@ -161,6 +181,7 @@ impl Parser { } if text.is_empty() { + self.pos = start_pos; return None; } @@ -169,6 +190,7 @@ impl Parser { fn parse_string(&mut self) -> Option { let start_pos = self.pos; + let mut text = String::new(); if !self.match_str("\"") { return None; @@ -264,7 +286,7 @@ mod tests { let mut p = Parser::new("${[ foo ]}"); assert_eq!( p.parse(), - vec![Token::Var { name: "foo".into() }, Token::Eof] + vec![Token::Tag(Val::Var("foo".into())), Token::Eof] ); } @@ -282,7 +304,7 @@ mod tests { let mut p = Parser::new(r#"${[ "foo \"bar\" baz" ]}"#); assert_eq!( p.parse(), - vec![Token::Raw(r#"${[ "foo \"bar\" baz" ]}"#.into()), Token::Eof] + vec![Token::Tag(Val::Str(r#"foo "bar" baz"#.into())), Token::Eof] ); } @@ -293,7 +315,7 @@ mod tests { p.parse(), vec![ Token::Raw("Hello ".to_string()), - Token::Var { name: "foo".into() }, + Token::Tag(Val::Var("foo".into())), Token::Raw("!".to_string()), Token::Eof, ] @@ -306,10 +328,10 @@ mod tests { assert_eq!( p.parse(), vec![ - Token::Fn { + Token::Tag(Val::Fn { name: "foo".into(), args: Vec::new(), - }, + }), Token::Eof ] ); @@ -321,10 +343,10 @@ mod tests { assert_eq!( p.parse(), vec![ - Token::Fn { + Token::Tag(Val::Fn { name: "foo".into(), - args: vec![Val::Ident("bar".into())], - }, + args: vec![Val::Var("bar".into())], + }), Token::Eof ] ); @@ -336,14 +358,14 @@ mod tests { assert_eq!( p.parse(), vec![ - Token::Fn { + Token::Tag(Val::Fn { name: "foo".into(), args: vec![ - Val::Ident("bar".into()), - Val::Ident("baz".into()), - Val::Ident("qux".into()), + Val::Var("bar".into()), + Val::Var("baz".into()), + Val::Var("qux".into()), ], - }, + }), Token::Eof ] ); @@ -355,14 +377,35 @@ mod tests { assert_eq!( p.parse(), vec![ - Token::Fn { + Token::Tag(Val::Fn { name: "foo".into(), args: vec![ - Val::Ident("bar".into()), + Val::Var("bar".into()), Val::Str(r#"baz "hi""#.into()), - Val::Ident("qux".into()), + Val::Var("qux".into()), ], - }, + }), + Token::Eof + ] + ); + } + + #[test] + fn fn_nested() { + let mut p = Parser::new(r#"${[ outer(inner(foo, "i"), "o") ]}"#); + assert_eq!( + p.parse(), + vec![ + Token::Tag(Val::Fn { + name: "outer".into(), + args: vec![ + Val::Fn { + name: "inner".into(), + args: vec![Val::Var("foo".into()), Val::Str("i".into()),], + }, + Val::Str("o".into()) + ], + }), Token::Eof ] ); diff --git a/src-tauri/templates/src/renderer.rs b/src-tauri/templates/src/renderer.rs index 42514a3f..9eb89d13 100644 --- a/src-tauri/templates/src/renderer.rs +++ b/src-tauri/templates/src/renderer.rs @@ -1,7 +1,8 @@ -use crate::{Parser, Token, Val}; use std::collections::HashMap; -type TemplateCallback = fn(name: &str, args: Vec<&str>) -> String; +use crate::{Parser, Token, Val}; + +type TemplateCallback = fn(name: &str, args: Vec) -> String; pub fn parse_and_render( template: &str, @@ -23,26 +24,7 @@ pub fn render( for t in tokens { match t { Token::Raw(s) => doc_str.push(s), - Token::Var { name } => { - if let Some(v) = vars.get(name.as_str()) { - doc_str.push(v.to_string()); - } - } - Token::Fn { name, args } => { - let empty = &""; - let resolved_args = args - .iter() - .map(|a| match a { - Val::Str(s) => s.as_str(), - Val::Ident(i) => vars.get(i.as_str()).unwrap_or(empty), - }) - .collect(); - let val = match cb { - Some(cb) => cb(name.as_str(), resolved_args), - None => "".into(), - }; - doc_str.push(val); - } + Token::Tag(val) => doc_str.push(render_tag(val, vars.clone(), cb)), Token::Eof => {} } } @@ -50,11 +32,41 @@ pub fn render( return doc_str.join(""); } +fn render_tag<'s>( + val: Val, + vars: HashMap<&'s str, &'s str>, + cb: Option, +) -> String { + match val { + Val::Str(s) => s.into(), + Val::Var(name) => match vars.get(name.as_str()) { + Some(v) => v.to_string(), + None => "".into(), + }, + Val::Fn { name, args } => { + let empty = &""; + let resolved_args = args + .iter() + .map(|a| match a { + Val::Str(s) => s.to_string(), + Val::Var(i) => vars.get(i.as_str()).unwrap_or(empty).to_string(), + val => render_tag(val.clone(), vars.clone(), cb), + }) + .collect::>(); + match cb { + Some(cb) => cb(name.as_str(), resolved_args), + None => "".into(), + } + } + } +} + #[cfg(test)] mod tests { - use crate::*; use std::collections::HashMap; + use crate::*; + #[test] fn render_empty() { let template = ""; @@ -92,8 +104,26 @@ mod tests { let vars = HashMap::new(); let template = r#"${[ say_hello("John", "Kate") ]}"#; let result = r#"say_hello: ["John", "Kate"]"#; - let cb: fn(&str, Vec<&str>) -> String = - |name: &str, args: Vec<&str>| format!("{name}: {:?}", args); + + fn cb(name: &str, args: Vec) -> String { + format!("{name}: {:?}", args) + } + assert_eq!(parse_and_render(template, vars, Some(cb)), result); + } + + #[test] + fn render_nested_fn() { + let vars = HashMap::new(); + let template = r#"${[ upper(secret()) ]}"#; + let result = r#"ABC"#; + fn cb(name: &str, args: Vec) -> String { + match name { + "secret" => "abc".to_string(), + "upper" => args[0].to_string().to_uppercase(), + _ => "".to_string(), + } + } + assert_eq!( parse_and_render(template, vars, Some(cb)), result.to_string()