mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-01 15:03:11 +02:00
Support nested functions
This commit is contained in:
@@ -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
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user