Support nested functions

This commit is contained in:
Gregory Schier
2024-06-12 23:13:36 -07:00
parent f9cd2fa7fa
commit 4469b84ad6
2 changed files with 145 additions and 72 deletions

View File

@@ -1,14 +1,14 @@
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum Val { pub enum Val {
Str(String), Str(String),
Ident(String), Var(String),
Fn { name: String, args: Vec<Val> },
} }
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum Token { pub enum Token {
Raw(String), Raw(String),
Var { name: String }, Tag(Val),
Fn { name: String, args: Vec<Val> },
Eof, Eof,
} }
@@ -66,17 +66,10 @@ impl Parser {
// Parse up to first identifier // Parse up to first identifier
// ${[ my_var... // ${[ my_var...
self.skip_whitespace(); self.skip_whitespace();
let name = match self.parse_ident() {
None => return None,
Some(v) => v,
};
// Parse fn args if they exist let val = match self.parse_value() {
// ${[ my_var(a, b, c) Some(v) => v,
let args = if self.match_str("(") { None => return None,
self.parse_fn_args()
} else {
None
}; };
// Parse to closing tag // Parse to closing tag
@@ -86,29 +79,65 @@ impl Parser {
return None; return None;
} }
Some(match args { Some(Token::Tag(val))
Some(a) => Token::Fn { args: a, name },
None => Token::Var { name },
})
} }
#[allow(dead_code)] #[allow(dead_code)]
fn debug_pos(&self, x: &str) { fn debug_pos(&self, x: &str) {
println!( println!(
r#"Position: {x} -- [{}] = {} --> "{}"#, r#"Position: {x} -- [{}] = {} --> "{}" --> {:?}"#,
self.pos, self.pos,
self.chars[self.pos], self.chars[self.pos],
self.chars.iter().collect::<String>() self.chars.iter().collect::<String>(),
self.tokens,
); );
} }
fn parse_value(&mut self) -> Option<Val> {
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<Val>)> {
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<Vec<Val>> { fn parse_fn_args(&mut self) -> Option<Vec<Val>> {
if !self.match_str("(") {
return None;
}
let start_pos = self.pos; let start_pos = self.pos;
let mut args: Vec<Val> = Vec::new(); let mut args: Vec<Val> = Vec::new();
while self.pos < self.chars.len() { while self.pos < self.chars.len() {
self.skip_whitespace(); self.skip_whitespace();
if let Some(v) = self.parse_ident_or_string() { if let Some(v) = self.parse_value() {
args.push(v); args.push(v);
} }
@@ -121,6 +150,7 @@ impl Parser {
// If we don't find a comma, that's bad // If we don't find a comma, that's bad
if !args.is_empty() && !self.match_str(",") { if !args.is_empty() && !self.match_str(",") {
self.pos = start_pos;
return None; return None;
} }
@@ -132,16 +162,6 @@ impl Parser {
return Some(args); return Some(args);
} }
fn parse_ident_or_string(&mut self) -> Option<Val> {
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<String> { fn parse_ident(&mut self) -> Option<String> {
let start_pos = self.pos; let start_pos = self.pos;
@@ -161,6 +181,7 @@ impl Parser {
} }
if text.is_empty() { if text.is_empty() {
self.pos = start_pos;
return None; return None;
} }
@@ -169,6 +190,7 @@ impl Parser {
fn parse_string(&mut self) -> Option<String> { fn parse_string(&mut self) -> Option<String> {
let start_pos = self.pos; let start_pos = self.pos;
let mut text = String::new(); let mut text = String::new();
if !self.match_str("\"") { if !self.match_str("\"") {
return None; return None;
@@ -264,7 +286,7 @@ mod tests {
let mut p = Parser::new("${[ foo ]}"); let mut p = Parser::new("${[ foo ]}");
assert_eq!( assert_eq!(
p.parse(), 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" ]}"#); let mut p = Parser::new(r#"${[ "foo \"bar\" baz" ]}"#);
assert_eq!( assert_eq!(
p.parse(), 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(), p.parse(),
vec![ vec![
Token::Raw("Hello ".to_string()), Token::Raw("Hello ".to_string()),
Token::Var { name: "foo".into() }, Token::Tag(Val::Var("foo".into())),
Token::Raw("!".to_string()), Token::Raw("!".to_string()),
Token::Eof, Token::Eof,
] ]
@@ -306,10 +328,10 @@ mod tests {
assert_eq!( assert_eq!(
p.parse(), p.parse(),
vec![ vec![
Token::Fn { Token::Tag(Val::Fn {
name: "foo".into(), name: "foo".into(),
args: Vec::new(), args: Vec::new(),
}, }),
Token::Eof Token::Eof
] ]
); );
@@ -321,10 +343,10 @@ mod tests {
assert_eq!( assert_eq!(
p.parse(), p.parse(),
vec![ vec![
Token::Fn { Token::Tag(Val::Fn {
name: "foo".into(), name: "foo".into(),
args: vec![Val::Ident("bar".into())], args: vec![Val::Var("bar".into())],
}, }),
Token::Eof Token::Eof
] ]
); );
@@ -336,14 +358,14 @@ mod tests {
assert_eq!( assert_eq!(
p.parse(), p.parse(),
vec![ vec![
Token::Fn { Token::Tag(Val::Fn {
name: "foo".into(), name: "foo".into(),
args: vec![ args: vec![
Val::Ident("bar".into()), Val::Var("bar".into()),
Val::Ident("baz".into()), Val::Var("baz".into()),
Val::Ident("qux".into()), Val::Var("qux".into()),
], ],
}, }),
Token::Eof Token::Eof
] ]
); );
@@ -355,14 +377,35 @@ mod tests {
assert_eq!( assert_eq!(
p.parse(), p.parse(),
vec![ vec![
Token::Fn { Token::Tag(Val::Fn {
name: "foo".into(), name: "foo".into(),
args: vec![ args: vec![
Val::Ident("bar".into()), Val::Var("bar".into()),
Val::Str(r#"baz "hi""#.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 Token::Eof
] ]
); );

View File

@@ -1,7 +1,8 @@
use crate::{Parser, Token, Val};
use std::collections::HashMap; 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>) -> String;
pub fn parse_and_render( pub fn parse_and_render(
template: &str, template: &str,
@@ -23,26 +24,7 @@ pub fn render(
for t in tokens { for t in tokens {
match t { match t {
Token::Raw(s) => doc_str.push(s), Token::Raw(s) => doc_str.push(s),
Token::Var { name } => { Token::Tag(val) => doc_str.push(render_tag(val, vars.clone(), cb)),
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::Eof => {} Token::Eof => {}
} }
} }
@@ -50,11 +32,41 @@ pub fn render(
return doc_str.join(""); return doc_str.join("");
} }
fn render_tag<'s>(
val: Val,
vars: HashMap<&'s str, &'s str>,
cb: Option<TemplateCallback>,
) -> 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::<Vec<String>>();
match cb {
Some(cb) => cb(name.as_str(), resolved_args),
None => "".into(),
}
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::*;
use std::collections::HashMap; use std::collections::HashMap;
use crate::*;
#[test] #[test]
fn render_empty() { fn render_empty() {
let template = ""; let template = "";
@@ -92,8 +104,26 @@ mod tests {
let vars = HashMap::new(); let vars = HashMap::new();
let template = r#"${[ say_hello("John", "Kate") ]}"#; let template = r#"${[ say_hello("John", "Kate") ]}"#;
let result = 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>) -> 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>) -> String {
match name {
"secret" => "abc".to_string(),
"upper" => args[0].to_string().to_uppercase(),
_ => "".to_string(),
}
}
assert_eq!( assert_eq!(
parse_and_render(template, vars, Some(cb)), parse_and_render(template, vars, Some(cb)),
result.to_string() result.to_string()