From 1267e966f47893bdaa9ab0f3f15d32110eddfd6e Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Thu, 12 Apr 2018 11:42:36 -0700 Subject: Isolate proc-macro dependency to the proc macro The dependency of askama on askama_shared on syn on libproc_macro used to mean libproc_macro would be dynamically linked into any crate using askama. We want only askama_derive to have a runtime dependency on proc macro. This commit moves all proc macro code from askama_shared into askama_derive so that the askama crate no longer dynamically links libproc_macro. --- askama_derive/Cargo.toml | 2 + askama_derive/src/generator.rs | 918 +++++++++++++++++++++++++++++++++++++++++ askama_derive/src/input.rs | 175 ++++++++ askama_derive/src/lib.rs | 72 +++- askama_derive/src/parser.rs | 694 +++++++++++++++++++++++++++++++ 5 files changed, 1860 insertions(+), 1 deletion(-) create mode 100644 askama_derive/src/generator.rs create mode 100644 askama_derive/src/input.rs create mode 100644 askama_derive/src/parser.rs (limited to 'askama_derive') diff --git a/askama_derive/Cargo.toml b/askama_derive/Cargo.toml index 3291cef..fed1800 100644 --- a/askama_derive/Cargo.toml +++ b/askama_derive/Cargo.toml @@ -18,4 +18,6 @@ rocket = ["askama_shared/rocket"] [dependencies] askama_shared = { version = "0.6.0", path = "../askama_shared" } +nom = "3" +quote = "0.5" syn = "0.13" diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs new file mode 100644 index 0000000..357bd0f --- /dev/null +++ b/askama_derive/src/generator.rs @@ -0,0 +1,918 @@ +use input::TemplateInput; +use parser::{self, Cond, Expr, Macro, MatchParameter, MatchVariant, Node, Target, When, WS}; +use shared::{filters, path}; + +use quote::{ToTokens, Tokens}; + +use std::{cmp, hash, str}; +use std::path::Path; +use std::collections::{HashMap, HashSet}; + +use syn; + + +pub fn generate(input: &TemplateInput, nodes: &[Node], imported: &HashMap<(&str, &str), Macro>) + -> String { + Generator::default().build(&State::new(input, nodes, imported)) +} + +struct State<'a> { + input: &'a TemplateInput<'a>, + nodes: &'a [Node<'a>], + blocks: Vec<&'a Node<'a>>, + macros: MacroMap<'a>, + trait_name: String, + derived: bool, +} + +impl<'a> State<'a> { + fn new<'n>(input: &'n TemplateInput, nodes: &'n [Node], imported: + &'n HashMap<(&'n str, &'n str), Macro<'n>>) -> State<'n> { + let mut base: Option<&Expr> = None; + let mut blocks = Vec::new(); + let mut macros = HashMap::new(); + for n in nodes.iter() { + match *n { + Node::Extends(ref path) => match base { + Some(_) => panic!("multiple extend blocks found"), + None => { + base = Some(path); + }, + }, + ref def @ Node::BlockDef(_, _, _, _) => { + blocks.push(def); + }, + Node::Macro(name, ref m) => { + macros.insert((None, name), m); + }, + _ => {}, + } + } + for (&(scope, name), m) in imported { + macros.insert((Some(scope), name), m); + } + State { + input, + nodes, + blocks, + macros, + trait_name: trait_name_for_path(&base, &input.path), + derived: base.is_some(), + } + } +} + +fn trait_name_for_path(base: &Option<&Expr>, path: &Path) -> String { + let rooted_path = match *base { + Some(&Expr::StrLit(user_path)) => path::find_template_from_path(user_path, Some(path)), + _ => path.to_path_buf(), + }; + + let mut res = String::new(); + res.push_str("TraitFrom"); + for c in rooted_path.to_string_lossy().chars() { + if c.is_alphanumeric() { + res.push(c); + } else { + res.push_str(&format!("{:x}", c as u32)); + } + } + res +} + +fn get_parent_type(ast: &syn::DeriveInput) -> Option<&syn::Type> { + match ast.data { + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(ref fields), + .. + }) => fields.named.iter().filter_map(|f| { + f.ident.as_ref().and_then(|name| { + if name.as_ref() == "_parent" { + Some(&f.ty) + } else { + None + } + }) + }), + _ => panic!("derive(Template) only works for struct items"), + }.next() +} + +struct Generator<'a> { + buf: String, + indent: u8, + start: bool, + locals: SetChain<'a, &'a str>, + next_ws: Option<&'a str>, + skip_ws: bool, +} + +impl<'a> Generator<'a> { + fn new<'n>(locals: SetChain<'n, &'n str>, indent: u8) -> Generator<'n> { + Generator { + buf: String::new(), + indent: indent, + start: true, + locals: locals, + next_ws: None, + skip_ws: false, + } + } + + fn default<'n>() -> Generator<'n> { + Self::new(SetChain::new(), 0) + } + + fn child(&mut self) -> Generator { + let locals = SetChain::with_parent(&self.locals); + Self::new(locals, self.indent) + } + + // Takes a State and generates the relevant implementations. + fn build(mut self, state: &'a State) -> String { + if !state.blocks.is_empty() { + if !state.derived { + self.define_trait(state); + } else { + let parent_type = get_parent_type(state.input.ast) + .expect("expected field '_parent' in extending template struct"); + self.deref_to_parent(state, parent_type); + } + + let trait_nodes = if !state.derived { + Some(&state.nodes[..]) + } else { + None + }; + self.impl_trait(state, trait_nodes); + self.impl_template_for_trait(state); + } else { + self.impl_template(state); + } + self.impl_display(state); + if cfg!(feature = "iron") { + self.impl_modifier_response(state); + } + if cfg!(feature = "rocket") { + self.impl_responder(state); + } + self.buf + } + + // Implement `Template` for the given context struct. + fn impl_template(&mut self, state: &'a State) { + self.write_header(state, "::askama::Template", &[]); + self.writeln("fn render_into(&self, writer: &mut ::std::fmt::Write) -> \ + ::askama::Result<()> {"); + self.writeln("#[allow(unused_imports)] use ::std::ops::Deref as HiddenDerefTrait;"); + self.handle(state, state.nodes, AstLevel::Top); + self.flush_ws(&WS(false, false)); + self.writeln("Ok(())"); + self.writeln("}"); + self.writeln("}"); + } + + // Implement `Display` for the given context struct. + fn impl_display(&mut self, state: &'a State) { + self.write_header(state, "::std::fmt::Display", &[]); + self.writeln("fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {"); + self.writeln("self.render_into(f).map_err(|_| ::std::fmt::Error {})"); + self.writeln("}"); + self.writeln("}"); + } + + // Implement `Deref` for an inheriting context struct. + fn deref_to_parent(&mut self, state: &'a State, parent_type: &syn::Type) { + self.write_header(state, "::std::ops::Deref", &[]); + let mut tokens = Tokens::new(); + parent_type.to_tokens(&mut tokens); + self.writeln(&format!("type Target = {};", tokens)); + self.writeln("fn deref(&self) -> &Self::Target {"); + self.writeln("&self._parent"); + self.writeln("}"); + self.writeln("}"); + } + + // Implement `TraitFromPathName` for the given context struct. + fn impl_trait(&mut self, state: &'a State, nodes: Option<&'a [Node]>) { + self.write_header(state, &state.trait_name, &[]); + self.write_block_defs(state); + + self.writeln("#[allow(unused_variables)]"); + self.writeln(&format!( + "fn render_trait_into(&self, timpl: &{}, writer: &mut ::std::fmt::Write) \ + -> ::askama::Result<()> {{", + state.trait_name + )); + self.writeln("#[allow(unused_imports)] use ::std::ops::Deref as HiddenDerefTrait;"); + + if let Some(nodes) = nodes { + self.handle(state, nodes, AstLevel::Top); + self.flush_ws(&WS(false, false)); + } else { + self.writeln("self._parent.render_trait_into(self, writer)?;"); + } + + self.writeln("Ok(())"); + self.writeln("}"); + self.flush_ws(&WS(false, false)); + self.writeln("}"); + } + + // Implement `Template` for templates that implement a template trait. + fn impl_template_for_trait(&mut self, state: &'a State) { + self.write_header(state, "::askama::Template", &[]); + self.writeln("fn render_into(&self, writer: &mut ::std::fmt::Write) \ + -> ::askama::Result<()> {"); + if state.derived { + self.writeln("self._parent.render_trait_into(self, writer)?;"); + } else { + self.writeln("self.render_trait_into(self, writer)?;"); + } + self.writeln("Ok(())"); + self.writeln("}"); + self.writeln("}"); + } + + // Defines the `TraitFromPathName` trait. + fn define_trait(&mut self, state: &'a State) { + self.writeln(&format!("pub trait {} {{", state.trait_name)); + self.write_block_defs(state); + self.writeln(&format!( + "fn render_trait_into(&self, timpl: &{}, writer: &mut ::std::fmt::Write) \ + -> ::askama::Result<()>;", + state.trait_name)); + self.writeln("}"); + } + + // Implement iron's Modifier if enabled + fn impl_modifier_response(&mut self, state: &'a State) { + self.write_header(state, "::askama::iron::Modifier<::askama::iron::Response>", &[]); + self.writeln("fn modify(self, res: &mut ::askama::iron::Response) {"); + self.writeln("res.body = Some(Box::new(self.render().unwrap().into_bytes()));"); + + let ext = state.input.path.extension().map_or("", |s| s.to_str().unwrap_or("")); + match ext { + "html" | "htm" => { + self.writeln("::askama::iron::ContentType::html().0.modify(res);"); + }, + _ => (), + }; + + self.writeln("}"); + self.writeln("}"); + } + + // Implement Rocket's `Responder`. + fn impl_responder(&mut self, state: &'a State) { + self.write_header(state, "::askama::rocket::Responder<'r>", &[quote!('r)]); + self.writeln("fn respond_to(self, _: &::askama::rocket::Request) \ + -> ::askama::rocket::Result<'r> {"); + + let ext = match state.input.path.extension() { + Some(s) => s.to_str().unwrap(), + None => "txt", + }; + self.writeln(&format!("::askama::rocket::respond(&self, {:?})", ext)); + + self.writeln("}"); + self.writeln("}"); + } + + // Writes header for the `impl` for `TraitFromPathName` or `Template` + // for the given context struct. + fn write_header(&mut self, state: &'a State, target: &str, vars: &[Tokens]) { + let mut generics = state.input.ast.generics.clone(); + for v in vars.iter() { + generics.params.push(parse_quote!(#v)); + } + let (_, orig_ty_generics, _) = state.input.ast.generics.split_for_impl(); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let ident = state.input.ast.ident.as_ref(); + self.writeln( + format!( + "{} {} for {}{} {{", + quote!(impl#impl_generics), + target, + ident, + quote!(#orig_ty_generics #where_clause), + ).as_ref(), + ); + } + + /* Helper methods for handling node types */ + + fn handle(&mut self, state: &'a State, nodes: &'a [Node], level: AstLevel) { + for n in nodes { + match *n { + Node::Lit(lws, val, rws) => { + self.write_lit(lws, val, rws); + }, + Node::Comment() => {}, + Node::Expr(ref ws, ref val) => { + self.write_expr(state, ws, val); + }, + Node::LetDecl(ref ws, ref var) => { + self.write_let_decl(ws, var); + }, + Node::Let(ref ws, ref var, ref val) => { + self.write_let(ws, var, val); + }, + Node::Cond(ref conds, ref ws) => { + self.write_cond(state, conds, ws); + }, + Node::Match(ref ws1, ref expr, inter, ref arms, ref ws2) => { + self.write_match(state, ws1, expr, inter, arms, ws2); + }, + Node::Loop(ref ws1, ref var, ref iter, ref body, ref ws2) => { + self.write_loop(state, ws1, var, iter, body, ws2); + }, + Node::BlockDef(ref ws1, name, _, ref ws2) => { + if let AstLevel::Nested = level { + panic!("blocks ('{}') are only allowed at the top level", name); + } + self.write_block(ws1, name, ws2); + }, + Node::Include(ref ws, path) => { + self.handle_include(state, ws, path); + }, + Node::Call(ref ws, scope, name, ref args) => { + self.write_call(state, ws, scope, name, args); + }, + Node::Macro(_, ref m) => { + if let AstLevel::Nested = level { + panic!("macro blocks only allowed at the top level"); + } + self.flush_ws(&m.ws1); + self.prepare_ws(&m.ws2); + }, + Node::Import(ref ws, _, _) => { + if let AstLevel::Nested = level { + panic!("import blocks only allowed at the top level"); + } + self.handle_ws(ws); + }, + Node::Extends(_) => { + if let AstLevel::Nested = level { + panic!("extend blocks only allowed at the top level"); + } + // No whitespace handling: child template top-level is not used, + // except for the blocks defined in it. + }, + } + } + } + + fn write_block_defs(&mut self, state: &'a State) { + for b in &state.blocks { + if let Node::BlockDef(ref ws1, name, ref nodes, ref ws2) = **b { + self.writeln("#[allow(unused_variables)]"); + self.writeln(&format!( + "fn render_block_{}_into(&self, writer: &mut ::std::fmt::Write) \ + -> ::askama::Result<()> {{", + name + )); + self.prepare_ws(ws1); + + self.locals.push(); + self.handle(state, nodes, AstLevel::Nested); + self.locals.pop(); + + self.flush_ws(ws2); + self.writeln("Ok(())"); + self.writeln("}"); + } else { + panic!("only block definitions allowed here"); + } + } + } + + fn write_cond(&mut self, state: &'a State, conds: &'a [Cond], ws: &WS) { + for (i, &(ref cws, ref cond, ref nodes)) in conds.iter().enumerate() { + self.handle_ws(cws); + match *cond { + Some(ref expr) => { + if i == 0 { + self.write("if "); + } else { + self.dedent(); + self.write("} else if "); + } + self.visit_expr(expr); + }, + None => { + self.dedent(); + self.write("} else"); + }, + } + self.writeln(" {"); + self.locals.push(); + self.handle(state, nodes, AstLevel::Nested); + self.locals.pop(); + } + self.handle_ws(ws); + self.writeln("}"); + } + + fn write_match(&mut self, state: &'a State, ws1: &WS, expr: &Expr, inter: Option<&'a str>, + arms: &'a [When], ws2: &WS) { + self.flush_ws(ws1); + if let Some(inter) = inter { + if !inter.is_empty() { + self.next_ws = Some(inter); + } + } + + self.write("match "); + self.write("(&"); + self.visit_expr(expr); + self.write(").deref()"); + self.writeln(" {"); + + for arm in arms { + let &(ref ws, ref variant, ref params, ref body) = arm; + self.locals.push(); + match *variant { + Some(ref param) => { + self.visit_match_variant(param); + }, + None => self.write("_"), + }; + if !params.is_empty() { + self.write("("); + for (i, param) in params.iter().enumerate() { + if let MatchParameter::Name(p) = *param { + self.locals.insert(p); + } + if i > 0 { + self.write(", "); + } + self.visit_match_param(param); + } + self.write(")"); + } + self.writeln(" => {"); + self.handle_ws(ws); + self.handle(state, body, AstLevel::Nested); + self.writeln("}"); + self.locals.pop(); + } + + self.writeln("}"); + self.handle_ws(ws2); + } + + fn write_loop(&mut self, state: &'a State, ws1: &WS, var: &'a Target, iter: &Expr, + body: &'a [Node], ws2: &WS) { + self.handle_ws(ws1); + self.locals.push(); + self.write("for (_loop_index, "); + let targets = self.visit_target(var); + for name in &targets { + self.locals.insert(name); + self.write(name); + } + self.write(") in (&"); + self.visit_expr(iter); + self.writeln(").into_iter().enumerate() {"); + + self.handle(state, body, AstLevel::Nested); + self.handle_ws(ws2); + self.writeln("}"); + self.locals.pop(); + } + + fn write_call(&mut self, state: &'a State, ws: &WS, scope: Option<&str>, name: &str, + args: &[Expr]) { + let def = state.macros.get(&(scope, name)).unwrap_or_else(|| { + if let Some(s) = scope { + panic!(format!("macro '{}::{}' not found", s, name)); + } else { + panic!(format!("macro '{}' not found", name)); + } + }); + + self.flush_ws(ws); // Cannot handle_ws() here: whitespace from macro definition comes first + self.locals.push(); + self.writeln("{"); + self.prepare_ws(&def.ws1); + + for (i, arg) in def.args.iter().enumerate() { + self.write(&format!("let {} = &", arg)); + self.visit_expr(args.get(i) + .expect(&format!("macro '{}' takes more than {} arguments", name, i))); + self.writeln(";"); + self.locals.insert(arg); + } + self.handle(state, &def.nodes, AstLevel::Nested); + + self.flush_ws(&def.ws2); + self.writeln("}"); + self.locals.pop(); + self.prepare_ws(ws); + } + + fn handle_include(&mut self, state: &'a State, ws: &WS, path: &str) { + self.flush_ws(ws); + let path = path::find_template_from_path(path, Some(&state.input.path)); + let src = path::get_template_source(&path); + let nodes = parser::parse(&src); + let nested = { + let mut gen = self.child(); + gen.handle(state, &nodes, AstLevel::Nested); + gen.buf + }; + self.buf.push_str(&nested); + self.prepare_ws(ws); + } + + fn write_let_decl(&mut self, ws: &WS, var: &'a Target) { + self.handle_ws(ws); + self.write("let "); + match *var { + Target::Name(name) => { + self.locals.insert(name); + self.write(name); + }, + } + self.writeln(";"); + } + + fn write_let(&mut self, ws: &WS, var: &'a Target, val: &Expr) { + self.handle_ws(ws); + match *var { + Target::Name(name) => { + if !self.locals.contains(name) { + self.write("let "); + self.locals.insert(name); + } + self.write(name); + }, + } + self.write(" = "); + self.visit_expr(val); + self.writeln(";"); + } + + fn write_block(&mut self, ws1: &WS, name: &str, ws2: &WS) { + self.flush_ws(ws1); + self.writeln(&format!("timpl.render_block_{}_into(writer)?;", name)); + self.prepare_ws(ws2); + } + + fn write_expr(&mut self, state: &'a State, ws: &WS, s: &Expr) { + self.handle_ws(ws); + self.write("let askama_expr = &"); + let wrapped = self.visit_expr(s); + self.writeln(";"); + + use self::DisplayWrap::*; + use super::input::EscapeMode::*; + self.write("writer.write_fmt(format_args!(\"{}\", "); + self.write(match (wrapped, &state.input.meta.escaping) { + (Wrapped, &Html) | + (Wrapped, &None) | + (Unwrapped, &None) => "askama_expr", + (Unwrapped, &Html) => "&::askama::MarkupDisplay::from(askama_expr)", + }); + self.writeln("))?;"); + } + + fn write_lit(&mut self, lws: &'a str, val: &str, rws: &'a str) { + assert!(self.next_ws.is_none()); + if !lws.is_empty() { + if self.skip_ws { + self.skip_ws = false; + } else if val.is_empty() { + assert!(rws.is_empty()); + self.next_ws = Some(lws); + } else { + self.writeln(&format!("writer.write_str({:#?})?;", lws)); + } + } + if !val.is_empty() { + self.writeln(&format!("writer.write_str({:#?})?;", val)); + } + if !rws.is_empty() { + self.next_ws = Some(rws); + } + } + + /* Visitor methods for expression types */ + + fn visit_expr(&mut self, expr: &Expr) -> DisplayWrap { + match *expr { + Expr::NumLit(s) => self.visit_num_lit(s), + Expr::StrLit(s) => self.visit_str_lit(s), + Expr::Var(s) => self.visit_var(s), + Expr::Path(ref path) => self.visit_path(path), + Expr::Array(ref elements) => self.visit_array(elements), + Expr::Attr(ref obj, name) => self.visit_attr(obj, name), + Expr::Filter(name, ref args) => self.visit_filter(name, args), + Expr::BinOp(op, ref left, ref right) => self.visit_binop(op, left, right), + Expr::Group(ref inner) => self.visit_group(inner), + Expr::MethodCall(ref obj, method, ref args) => { + self.visit_method_call(obj, method, args) + }, + } + } + + fn visit_match_variant(&mut self, param: &MatchVariant) -> DisplayWrap { + match *param { + MatchVariant::StrLit(s) => self.visit_str_lit(s), + MatchVariant::NumLit(s) => { + // Variants need to be references until match-modes land + self.write("&"); + self.visit_num_lit(s) + }, + MatchVariant::Name(s) => { + self.write("&"); + self.write(s); + DisplayWrap::Unwrapped + }, + MatchVariant::Path(ref s) => { + self.write("&"); + self.write(&s.join("::")); + DisplayWrap::Unwrapped + }, + } + } + + fn visit_match_param(&mut self, param: &MatchParameter) -> DisplayWrap { + match *param { + MatchParameter::NumLit(s) => self.visit_num_lit(s), + MatchParameter::StrLit(s) => self.visit_str_lit(s), + MatchParameter::Name(s) => { + self.write("ref "); + self.write(s); + DisplayWrap::Unwrapped + }, + } + } + + fn visit_filter(&mut self, name: &str, args: &[Expr]) -> DisplayWrap { + if name == "format" { + self._visit_format_filter(args); + return DisplayWrap::Unwrapped; + } else if name == "join" { + self._visit_join_filter(args); + return DisplayWrap::Unwrapped; + } + + if filters::BUILT_IN_FILTERS.contains(&name) { + self.write(&format!("::askama::filters::{}(&", name)); + } else { + self.write(&format!("filters::{}(&", name)); + } + + self._visit_filter_args(args); + self.write(")?"); + if name == "safe" || name == "escape" || name == "e" || name == "json" { + DisplayWrap::Wrapped + } else { + DisplayWrap::Unwrapped + } + } + + fn _visit_format_filter(&mut self, args: &[Expr]) { + self.write("format!("); + self._visit_filter_args(args); + self.write(")"); + } + + // Force type coercion on first argument to `join` filter (see #39). + fn _visit_join_filter(&mut self, args: &[Expr]) { + self.write("::askama::filters::join((&"); + for (i, arg) in args.iter().enumerate() { + if i > 0 { + self.write(", &"); + } + self.visit_expr(arg); + if i == 0 { + self.write(").into_iter()"); + } + } + self.write(")?"); + } + + fn _visit_filter_args(&mut self, args: &[Expr]) { + for (i, arg) in args.iter().enumerate() { + if i > 0 { + self.write(", &"); + } + self.visit_expr(arg); + } + } + + fn visit_attr(&mut self, obj: &Expr, attr: &str) -> DisplayWrap { + if let Expr::Var(name) = *obj { + if name == "loop" { + self.write("_loop_index"); + if attr == "index" { + self.write(" + 1"); + return DisplayWrap::Unwrapped; + } else if attr == "index0" { + return DisplayWrap::Unwrapped; + } else { + panic!("unknown loop variable"); + } + } + } + self.visit_expr(obj); + self.write(&format!(".{}", attr)); + DisplayWrap::Unwrapped + } + + fn visit_method_call(&mut self, obj: &Expr, method: &str, args: &[Expr]) -> DisplayWrap { + self.visit_expr(obj); + self.write(&format!(".{}(", method)); + for (i, arg) in args.iter().enumerate() { + if i > 0 { + self.write(", "); + } + self.visit_expr(arg); + } + self.write(")"); + DisplayWrap::Unwrapped + } + + fn visit_binop(&mut self, op: &str, left: &Expr, right: &Expr) -> DisplayWrap { + self.visit_expr(left); + self.write(&format!(" {} ", op)); + self.visit_expr(right); + DisplayWrap::Unwrapped + } + + fn visit_group(&mut self, inner: &Expr) -> DisplayWrap { + self.write("("); + self.visit_expr(inner); + self.write(")"); + DisplayWrap::Unwrapped + } + + fn visit_array(&mut self, elements: &[Expr]) -> DisplayWrap { + self.write("["); + for (i, el) in elements.iter().enumerate() { + if i > 0 { + self.write(", "); + } + self.visit_expr(el); + } + self.write("]"); + DisplayWrap::Unwrapped + } + + fn visit_path(&mut self, path: &[&str]) -> DisplayWrap { + for (i, part) in path.iter().enumerate() { + if i > 0 { + self.write("::"); + } + self.write(part); + } + DisplayWrap::Unwrapped + } + + fn visit_var(&mut self, s: &str) -> DisplayWrap { + if self.locals.contains(s) { + self.write(s); + } else { + self.write(&format!("self.{}", s)); + } + DisplayWrap::Unwrapped + } + + fn visit_str_lit(&mut self, s: &str) -> DisplayWrap { + self.write(&format!("\"{}\"", s)); + DisplayWrap::Unwrapped + } + + fn visit_num_lit(&mut self, s: &str) -> DisplayWrap { + self.write(s); + DisplayWrap::Unwrapped + } + + fn visit_target_single<'t>(&mut self, name: &'t str) -> Vec<&'t str> { + vec![name] + } + + fn visit_target<'t>(&mut self, target: &'t Target) -> Vec<&'t str> { + match *target { + Target::Name(s) => self.visit_target_single(s), + } + } + + /* Helper methods for dealing with whitespace nodes */ + + // Combines `flush_ws()` and `prepare_ws()` to handle both trailing whitespace from the + // preceding literal and leading whitespace from the succeeding literal. + fn handle_ws(&mut self, ws: &WS) { + self.flush_ws(ws); + self.prepare_ws(ws); + } + + // If the previous literal left some trailing whitespace in `next_ws` and the + // prefix whitespace suppressor from the given argument, flush that whitespace. + // In either case, `next_ws` is reset to `None` (no trailing whitespace). + fn flush_ws(&mut self, ws: &WS) { + if self.next_ws.is_some() && !ws.0 { + let val = self.next_ws.unwrap(); + if !val.is_empty() { + self.writeln(&format!("writer.write_str({:#?})?;", val)); + } + } + self.next_ws = None; + } + + // Sets `skip_ws` to match the suffix whitespace suppressor from the given + // argument, to determine whether to suppress leading whitespace from the + // next literal. + fn prepare_ws(&mut self, ws: &WS) { + self.skip_ws = ws.1; + } + + /* Helper methods for writing to internal buffer */ + + fn writeln(&mut self, s: &str) { + if s.is_empty() { + return; + } + if s == "}" { + self.dedent(); + } + self.write(s); + if s.ends_with('{') { + self.indent(); + } + self.buf.push('\n'); + self.start = true; + } + + fn write(&mut self, s: &str) { + if self.start { + for _ in 0..(self.indent * 4) { + self.buf.push(' '); + } + self.start = false; + } + self.buf.push_str(s); + } + + fn indent(&mut self) { + self.indent += 1; + } + + fn dedent(&mut self) { + if self.indent == 0 { + panic!("dedent() called while indentation == 0"); + } + self.indent -= 1; + } +} + +struct SetChain<'a, T: 'a> where T: cmp::Eq + hash::Hash { + parent: Option<&'a SetChain<'a, T>>, + scopes: Vec>, +} + +impl<'a, T: 'a> SetChain<'a, T> where T: cmp::Eq + hash::Hash { + fn new() -> SetChain<'a, T> { + SetChain { parent: None, scopes: vec![HashSet::new()] } + } + fn with_parent<'p>(parent: &'p SetChain) -> SetChain<'p, T> { + SetChain { parent: Some(parent), scopes: vec![HashSet::new()] } + } + fn contains(&self, val: T) -> bool { + self.scopes.iter().rev().any(|set| set.contains(&val)) || match self.parent { + Some(set) => set.contains(val), + None => false, + } + } + fn insert(&mut self, val: T) { + self.scopes.last_mut().unwrap().insert(val); + } + fn push(&mut self) { + self.scopes.push(HashSet::new()); + } + fn pop(&mut self) { + self.scopes.pop().unwrap(); + assert!(!self.scopes.is_empty()); + } +} + +#[derive(Clone)] +enum AstLevel { + Top, + Nested, +} + +impl Copy for AstLevel {} + +#[derive(Clone)] +enum DisplayWrap { + Wrapped, + Unwrapped, +} + +impl Copy for DisplayWrap {} + +type MacroMap<'a> = HashMap<(Option<&'a str>, &'a str), &'a Macro<'a>>; diff --git a/askama_derive/src/input.rs b/askama_derive/src/input.rs new file mode 100644 index 0000000..b2b815b --- /dev/null +++ b/askama_derive/src/input.rs @@ -0,0 +1,175 @@ +use shared::path; + +use std::path::{Path, PathBuf}; + +use syn; + + +pub struct TemplateInput<'a> { + pub ast: &'a syn::DeriveInput, + pub meta: TemplateMeta, + pub path: PathBuf, + pub source: String, +} + +impl<'a> TemplateInput<'a> { + pub fn new(ast: &'a syn::DeriveInput) -> TemplateInput<'a> { + let meta = TemplateMeta::new(ast); + let (path, source) = match meta.source { + Source::Source(ref s) => { + let path = match meta.ext { + Some(ref v) => PathBuf::from(format!("_.{}", v)), + None => PathBuf::new(), + }; + (path, s.clone()) + }, + Source::Path(ref s) => { + let path = path::find_template_from_path(s, None); + let src = path::get_template_source(&path); + (path, src) + }, + }; + TemplateInput { ast, meta, path, source } + } +} + +// Holds metadata for the template, based on the `template()` attribute. +pub struct TemplateMeta { + source: Source, + pub print: Print, + pub escaping: EscapeMode, + pub ext: Option, +} + +impl TemplateMeta { + fn new(ast: &syn::DeriveInput) -> TemplateMeta { + let attr = ast.attrs + .iter() + .find(|a| a.interpret_meta().unwrap().name() == "template"); + if attr.is_none() { + let msg = format!("'template' attribute not found on struct '{}'", + ast.ident.as_ref()); + panic!(msg); + } + + let attr = attr.unwrap(); + let mut source = None; + let mut print = Print::None; + let mut escaping = None; + let mut ext = None; + if let syn::Meta::List(ref inner) = attr.interpret_meta().unwrap() { + for nm_item in inner.nested.iter() { + if let syn::NestedMeta::Meta(ref item) = *nm_item { + if let syn::Meta::NameValue(ref pair) = *item { + match pair.ident.as_ref() { + "path" => if let syn::Lit::Str(ref s) = pair.lit { + if source.is_some() { + panic!("must specify 'source' or 'path', not both"); + } + source = Some(Source::Path(s.value())); + } else { + panic!("template path must be string literal"); + }, + "source" => if let syn::Lit::Str(ref s) = pair.lit { + if source.is_some() { + panic!("must specify 'source' or 'path', not both"); + } + source = Some(Source::Source(s.value())); + } else { + panic!("template source must be string literal"); + }, + "print" => if let syn::Lit::Str(ref s) = pair.lit { + print = s.value().into(); + } else { + panic!("print value must be string literal"); + }, + "escape" => if let syn::Lit::Str(ref s) = pair.lit { + escaping = Some(s.value().into()); + } else { + panic!("escape value must be string literal"); + }, + "ext" => if let syn::Lit::Str(ref s) = pair.lit { + ext = Some(s.value()); + } else { + panic!("ext value must be string literal"); + }, + attr => panic!("unsupported annotation key '{}' found", attr), + } + } + } + } + } + + let source = source.expect("template path or source not found in attributes"); + match (&source, ext.is_some()) { + (&Source::Path(_), true) => { + panic!("'ext' attribute cannot be used with 'path' attribute") + }, + (&Source::Source(_), false) => { + panic!("must include 'ext' attribute when using 'source' attribute") + }, + _ => {}, + } + let escaping = match escaping { + Some(m) => m, + None => { + let ext = match source { + Source::Path(ref p) => + Path::new(p).extension().map(|s| s.to_str().unwrap()).unwrap_or(""), + Source::Source(_) => ext.as_ref().unwrap(), // Already panicked if None + }; + if HTML_EXTENSIONS.contains(&ext) { + EscapeMode::Html + } else { + EscapeMode::None + } + }, + }; + TemplateMeta { source, print, escaping, ext } + } +} + +enum Source { + Path(String), + Source(String), +} + +#[derive(PartialEq)] +pub enum EscapeMode { + Html, + None, +} + +impl From for EscapeMode { + fn from(s: String) -> EscapeMode { + use self::EscapeMode::*; + match s.as_ref() { + "html" => Html, + "none" => None, + v => panic!("invalid value for escape option: {}", v), + } + } +} + +#[derive(PartialEq)] +pub enum Print { + All, + Ast, + Code, + None, +} + +impl From for Print { + fn from(s: String) -> Print { + use self::Print::*; + match s.as_ref() { + "all" => All, + "ast" => Ast, + "code" => Code, + "none" => None, + v => panic!("invalid value for print option: {}", v), + } + } +} + +const HTML_EXTENSIONS: [&str; 3] = ["html", "htm", "xml"]; diff --git a/askama_derive/src/lib.rs b/askama_derive/src/lib.rs index 7c92fdc..df76507 100644 --- a/askama_derive/src/lib.rs +++ b/askama_derive/src/lib.rs @@ -1,8 +1,24 @@ extern crate askama_shared as shared; +#[macro_use] +extern crate nom; extern crate proc_macro; +#[macro_use] +extern crate quote; +#[macro_use(parse_quote)] extern crate syn; +mod input; +mod generator; +mod parser; + +use input::Print; +use parser::{Macro, Node}; use proc_macro::TokenStream; +use shared::path; + +use std::borrow::Cow; +use std::collections::HashMap; +use std::path::Path; #[proc_macro_derive(Template, attributes(template))] pub fn derive_template(input: TokenStream) -> TokenStream { @@ -11,5 +27,59 @@ pub fn derive_template(input: TokenStream) -> TokenStream { syn::Data::Struct(ref data) => data, _ => panic!("#[derive(Template)] can only be used with structs"), }; - shared::build_template(&ast).parse().unwrap() + build_template(&ast).parse().unwrap() +} + +/// Takes a `syn::DeriveInput` and generates source code for it +/// +/// Reads the metadata from the `template()` attribute to get the template +/// metadata, then fetches the source from the filesystem. The source is +/// parsed, and the parse tree is fed to the code generator. Will print +/// the parse tree and/or generated source according to the `print` key's +/// value as passed to the `template()` attribute. +fn build_template(ast: &syn::DeriveInput) -> String { + let data = input::TemplateInput::new(ast); + let nodes = parser::parse(data.source.as_ref()); + let imports = Imports::new(&nodes, &data.path); + if data.meta.print == Print::Ast || data.meta.print == Print::All { + println!("{:?}", nodes); + } + let code = generator::generate(&data, &nodes, &imports.macro_map()); + if data.meta.print == Print::Code || data.meta.print == Print::All { + println!("{}", code); + } + code +} + +struct Imports<'a> { + sources: HashMap<&'a str, Cow<'a, str>>, +} + +impl<'a> Imports<'a> { + fn new(parent_nodes: &'a [Node], parent_path: &'a Path) -> Imports<'a> { + let sources = parent_nodes.iter().filter_map(|n| { + match *n { + Node::Import(_, import_path, scope) => { + let path = path::find_template_from_path(import_path, Some(parent_path)); + let src = path::get_template_source(&path); + Some((scope, Cow::Owned(src))) + }, + _ => None, + } + }).collect(); + Imports { sources } + } + + fn macro_map(&'a self) -> HashMap<(&'a str, &'a str), Macro<'a>> { + let mut macro_map = HashMap::new(); + for (scope, s) in &self.sources { + for n in parser::parse(s.as_ref()) { + match n { + Node::Macro(name, m) => macro_map.insert((*scope, name), m), + _ => None, + }; + } + } + macro_map + } } diff --git a/askama_derive/src/parser.rs b/askama_derive/src/parser.rs new file mode 100644 index 0000000..402801f --- /dev/null +++ b/askama_derive/src/parser.rs @@ -0,0 +1,694 @@ +// rustfmt doesn't do a very good job on nom parser invocations. +#![cfg_attr(rustfmt, rustfmt_skip)] + +use nom::{self, IResult}; +use std::str; + +#[derive(Debug)] +pub enum Expr<'a> { + NumLit(&'a str), + StrLit(&'a str), + Var(&'a str), + Path(Vec<&'a str>), + Array(Vec>), + Attr(Box>, &'a str), + Filter(&'a str, Vec>), + BinOp(&'a str, Box>, Box>), + Group(Box>), + MethodCall(Box>, &'a str, Vec>), +} + +#[derive(Debug)] +pub enum MatchVariant<'a> { + Path(Vec<&'a str>), + Name(&'a str), + NumLit(&'a str), + StrLit(&'a str), +} + +#[derive(Debug)] +pub enum MatchParameter<'a> { + Name(&'a str), + NumLit(&'a str), + StrLit(&'a str), +} + +#[derive(Debug)] +pub enum Target<'a> { + Name(&'a str), +} + +#[derive(Clone, Copy, Debug)] +pub struct WS(pub bool, pub bool); + +#[derive(Debug)] +pub struct Macro<'a> { + pub ws1: WS, + pub args: Vec<&'a str>, + pub nodes: Vec>, + pub ws2: WS, +} + +#[derive(Debug)] +pub enum Node<'a> { + Lit(&'a str, &'a str, &'a str), + Comment(), + Expr(WS, Expr<'a>), + Call(WS, Option<& 'a str>, &'a str, Vec>), + LetDecl(WS, Target<'a>), + Let(WS, Target<'a>, Expr<'a>), + Cond(Vec<(WS, Option>, Vec>)>, WS), + Match(WS, Expr<'a>, Option<&'a str>, Vec>, WS), + Loop(WS, Target<'a>, Expr<'a>, Vec>, WS), + Extends(Expr<'a>), + BlockDef(WS, &'a str, Vec>, WS), + Include(WS, &'a str), + Import(WS, &'a str, &'a str), + Macro(&'a str, Macro<'a>), +} + +pub type Cond<'a> = (WS, Option>, Vec>); +pub type When<'a> = (WS, Option>, Vec>, Vec>); + +fn split_ws_parts(s: &[u8]) -> Node { + if s.is_empty() { + let rs = str::from_utf8(s).unwrap(); + return Node::Lit(rs, rs, rs); + } + let is_ws = |c: &u8| { + *c != b' ' && *c != b'\t' && *c != b'\r' && *c != b'\n' + }; + let start = s.iter().position(&is_ws); + let res = if start.is_none() { + (s, &s[0..0], &s[0..0]) + } else { + let start = start.unwrap(); + let end = s.iter().rposition(&is_ws); + if end.is_none() { + (&s[..start], &s[start..], &s[0..0]) + } else { + let end = end.unwrap(); + (&s[..start], &s[start..end + 1], &s[end + 1..]) + } + }; + Node::Lit(str::from_utf8(res.0).unwrap(), + str::from_utf8(res.1).unwrap(), + str::from_utf8(res.2).unwrap()) +} + +enum ContentState { + Any, + Brace(usize), + End(usize), +} + +fn take_content(i: &[u8]) -> IResult<&[u8], Node> { + use parser::ContentState::*; + let mut state = Any; + for (idx, c) in i.iter().enumerate() { + state = match (state, *c) { + (Any, b'{') => Brace(idx), + (Brace(start), b'{') | + (Brace(start), b'%') | + (Brace(start), b'#') => End(start), + (Any, _) | + (Brace(_), _) => Any, + (End(_), _) => panic!("cannot happen"), + }; + if let End(_) = state { + break; + } + } + match state { + Any | + Brace(_) => IResult::Done(&i[..0], split_ws_parts(i)), + End(0) => IResult::Error(nom::ErrorKind::Custom(0)), + End(start) => IResult::Done(&i[start..], split_ws_parts(&i[..start])), + } +} + +fn identifier(input: &[u8]) -> IResult<&[u8], &str> { + if !nom::is_alphabetic(input[0]) && input[0] != b'_' { + return IResult::Error(nom::ErrorKind::Custom(0)); + } + for (i, ch) in input.iter().enumerate() { + if i == 0 || nom::is_alphanumeric(*ch) || *ch == b'_' { + continue; + } + return IResult::Done(&input[i..], + str::from_utf8(&input[..i]).unwrap()); + } + IResult::Done(&input[1..], str::from_utf8(&input[..1]).unwrap()) +} + +named!(num_lit<&str>, map!(nom::digit, + |s| str::from_utf8(s).unwrap() +)); + +named!(expr_num_lit, map!(num_lit, + |s| Expr::NumLit(s) +)); + +named!(expr_array_lit, do_parse!( + ws!(tag_s!("[")) >> + first: expr_any >> + rest: many0!(do_parse!( + ws!(tag_s!(",")) >> + part: expr_any >> + (part) + )) >> + ws!(tag_s!("]")) >> + ({ + let mut elements = vec![first]; + elements.extend(rest); + Expr::Array(elements) + }) +)); + +named!(variant_num_lit, map!(num_lit, + |s| MatchVariant::NumLit(s) +)); + +named!(param_num_lit, map!(num_lit, + |s| MatchParameter::NumLit(s) +)); + +named!(expr_str_lit, map!( + delimited!(char!('"'), take_until!("\""), char!('"')), + |s| Expr::StrLit(str::from_utf8(s).unwrap()) +)); + +named!(variant_str_lit, map!( + delimited!(char!('"'), is_not!("\""), char!('"')), + |s| MatchVariant::StrLit(str::from_utf8(s).unwrap()) +)); + +named!(param_str_lit, map!( + delimited!(char!('"'), is_not!("\""), char!('"')), + |s| MatchParameter::StrLit(str::from_utf8(s).unwrap()) +)); + +named!(expr_var, map!(identifier, + |s| Expr::Var(s)) +); + +named!(expr_path, do_parse!( + start: call!(identifier) >> + rest: many1!(do_parse!( + tag_s!("::") >> + part: identifier >> + (part) + )) >> + ({ + let mut path = vec![start]; + path.extend(rest); + Expr::Path(path) + }) +)); + +named!(variant_path, do_parse!( + start: call!(identifier) >> + rest: many1!(do_parse!( + tag_s!("::") >> + part: identifier >> + (part) + )) >> + ({ + let mut path = vec![start]; + path.extend(rest); + MatchVariant::Path(path) + }) +)); + +named!(target_single, map!(identifier, + |s| Target::Name(s) +)); + +named!(variant_name, map!(identifier, + |s| MatchVariant::Name(s) +)); + +named!(param_name, map!(identifier, + |s| MatchParameter::Name(s) +)); + +named!(arguments>, do_parse!( + tag_s!("(") >> + args: opt!(do_parse!( + arg0: ws!(expr_any) >> + args: many0!(do_parse!( + tag_s!(",") >> + argn: ws!(expr_any) >> + (argn) + )) >> + ({ + let mut res = vec![arg0]; + res.extend(args); + res + }) + )) >> + tag_s!(")") >> + (args.unwrap_or_default()) +)); + +named!(parameters>, do_parse!( + tag_s!("(") >> + vals: opt!(do_parse!( + arg0: ws!(identifier) >> + args: many0!(do_parse!( + tag_s!(",") >> + argn: ws!(identifier) >> + (argn) + )) >> + ({ + let mut res = vec![arg0]; + res.extend(args); + res + }) + )) >> + tag_s!(")") >> + (vals.unwrap_or_default()) +)); + +named!(with_parameters>, do_parse!( + tag_s!("with") >> + ws!(tag_s!("(")) >> + vals: opt!(do_parse!( + arg0: ws!(match_parameter) >> + args: many0!(do_parse!( + tag_s!(",") >> + argn: ws!(match_parameter) >> + (argn) + )) >> + ({ + let mut res = vec![arg0]; + res.extend(args); + res + }) + )) >> + tag_s!(")") >> + (vals.unwrap_or_default()) +)); + +named!(expr_group, map!( + delimited!(char!('('), expr_any, char!(')')), + |s| Expr::Group(Box::new(s)) +)); + +named!(expr_single, alt!( + expr_num_lit | + expr_str_lit | + expr_path | + expr_array_lit | + expr_var | + expr_group +)); + +named!(match_variant, alt!( + variant_path | + variant_name | + variant_num_lit | + variant_str_lit +)); + +named!(match_parameter, alt!( + param_name | + param_num_lit | + param_str_lit +)); + +named!(attr<(&str, Option>)>, do_parse!( + tag_s!(".") >> + attr: alt!(num_lit | identifier) >> + args: opt!(arguments) >> + (attr, args) +)); + +named!(expr_attr, do_parse!( + obj: expr_single >> + attrs: many0!(attr) >> + ({ + let mut res = obj; + for (aname, args) in attrs { + res = if args.is_some() { + Expr::MethodCall(Box::new(res), aname, args.unwrap()) + } else { + Expr::Attr(Box::new(res), aname) + }; + } + res + }) +)); + +named!(filter<(&str, Option>)>, do_parse!( + tag_s!("|") >> + fname: identifier >> + args: opt!(arguments) >> + (fname, args) +)); + +named!(expr_filtered, do_parse!( + obj: expr_attr >> + filters: many0!(filter) >> + ({ + let mut res = obj; + for (fname, args) in filters { + res = Expr::Filter(fname, { + let mut args = match args { + Some(inner) => inner, + None => Vec::new(), + }; + args.insert(0, res); + args + }); + } + res + }) +)); + +macro_rules! expr_prec_layer { + ( $name:ident, $inner:ident, $( $op:expr ),* ) => { + named!($name, do_parse!( + left: $inner >> + op_and_right: opt!(pair!(ws!(alt!($( tag_s!($op) )|*)), expr_any)) >> + (match op_and_right { + Some((op, right)) => Expr::BinOp( + str::from_utf8(op).unwrap(), Box::new(left), Box::new(right) + ), + None => left, + }) + )); + } +} + +expr_prec_layer!(expr_muldivmod, expr_filtered, "*", "/", "%"); +expr_prec_layer!(expr_addsub, expr_muldivmod, "+", "-"); +expr_prec_layer!(expr_shifts, expr_addsub, ">>", "<<"); +expr_prec_layer!(expr_band, expr_shifts, "&"); +expr_prec_layer!(expr_bxor, expr_band, "^"); +expr_prec_layer!(expr_bor, expr_bxor, "|"); +expr_prec_layer!(expr_compare, expr_bor, + "==", "!=", ">=", ">", "<=", "<" +); +expr_prec_layer!(expr_and, expr_compare, "&&"); +expr_prec_layer!(expr_any, expr_and, "||"); + +named!(expr_node, do_parse!( + tag_s!("{{") >> + pws: opt!(tag_s!("-")) >> + expr: ws!(expr_any) >> + nws: opt!(tag_s!("-")) >> + tag_s!("}}") >> + (Node::Expr(WS(pws.is_some(), nws.is_some()), expr)) +)); + +named!(block_call, do_parse!( + pws: opt!(tag_s!("-")) >> + ws!(tag_s!("call")) >> + scope: opt!(do_parse!( + scope: ws!(identifier) >> + ws!(tag_s!("::")) >> + (scope) + )) >> + name: ws!(identifier) >> + args: ws!(arguments) >> + nws: opt!(tag_s!("-")) >> + (Node::Call(WS(pws.is_some(), nws.is_some()), scope, name, args)) +)); + +named!(cond_if, do_parse!( + ws!(tag_s!("if")) >> + cond: ws!(expr_any) >> + (cond) +)); + +named!(cond_block, do_parse!( + tag_s!("{%") >> + pws: opt!(tag_s!("-")) >> + ws!(tag_s!("else")) >> + cond: opt!(cond_if) >> + nws: opt!(tag_s!("-")) >> + tag_s!("%}") >> + block: parse_template >> + (WS(pws.is_some(), nws.is_some()), cond, block) +)); + +named!(block_if, do_parse!( + pws1: opt!(tag_s!("-")) >> + cond: ws!(cond_if) >> + nws1: opt!(tag_s!("-")) >> + tag_s!("%}") >> + block: parse_template >> + elifs: many0!(cond_block) >> + tag_s!("{%") >> + pws2: opt!(tag_s!("-")) >> + ws!(tag_s!("endif")) >> + nws2: opt!(tag_s!("-")) >> + ({ + let mut res = Vec::new(); + res.push((WS(pws1.is_some(), nws1.is_some()), Some(cond), block)); + res.extend(elifs); + Node::Cond(res, WS(pws2.is_some(), nws2.is_some())) + }) +)); + +named!(match_else_block, do_parse!( + tag_s!("{%") >> + pws: opt!(tag_s!("-")) >> + ws!(tag_s!("else")) >> + nws: opt!(tag_s!("-")) >> + tag_s!("%}") >> + block: parse_template >> + (WS(pws.is_some(), nws.is_some()), None, vec![], block) +)); + +named!(when_block, do_parse!( + tag_s!("{%") >> + pws: opt!(tag_s!("-")) >> + ws!(tag_s!("when")) >> + variant: ws!(match_variant) >> + params: opt!(ws!(with_parameters)) >> + nws: opt!(tag_s!("-")) >> + tag_s!("%}") >> + block: parse_template >> + (WS(pws.is_some(), nws.is_some()), Some(variant), params.unwrap_or_default(), block) +)); + +named!(block_match, do_parse!( + pws1: opt!(tag_s!("-")) >> + ws!(tag_s!("match")) >> + expr: ws!(expr_any) >> + nws1: opt!(tag_s!("-")) >> + tag_s!("%}") >> + inter: opt!(take_content) >> + arms: many1!(when_block) >> + else_arm: opt!(match_else_block) >> + ws!(tag_s!("{%")) >> + pws2: opt!(tag_s!("-")) >> + ws!(tag_s!("endmatch")) >> + nws2: opt!(tag_s!("-")) >> + ({ + let mut arms = arms; + if let Some(arm) = else_arm { + arms.push(arm); + } + let inter = match inter { + Some(Node::Lit(lws, val, rws)) => { + assert!(val.is_empty(), + "only whitespace allowed between match and first when, found {}", val); + assert!(rws.is_empty(), + "only whitespace allowed between match and first when, found {}", rws); + Some(lws) + }, + None => None, + _ => panic!("only literals allowed between match and first when"), + }; + Node::Match( + WS(pws1.is_some(), nws1.is_some()), + expr, + inter, + arms, + WS(pws2.is_some(), nws2.is_some()), + ) + }) +)); + +named!(block_let, do_parse!( + pws: opt!(tag_s!("-")) >> + ws!(tag_s!("let")) >> + var: ws!(target_single) >> + val: opt!(do_parse!( + ws!(tag_s!("=")) >> + val: ws!(expr_any) >> + (val) + )) >> + nws: opt!(tag_s!("-")) >> + (if val.is_some() { + Node::Let(WS(pws.is_some(), nws.is_some()), var, val.unwrap()) + } else { + Node::LetDecl(WS(pws.is_some(), nws.is_some()), var) + }) +)); + +named!(block_for, do_parse!( + pws1: opt!(tag_s!("-")) >> + ws!(tag_s!("for")) >> + var: ws!(target_single) >> + ws!(tag_s!("in")) >> + iter: ws!(expr_any) >> + nws1: opt!(tag_s!("-")) >> + tag_s!("%}") >> + block: parse_template >> + tag_s!("{%") >> + pws2: opt!(tag_s!("-")) >> + ws!(tag_s!("endfor")) >> + nws2: opt!(tag_s!("-")) >> + (Node::Loop(WS(pws1.is_some(), nws1.is_some()), + var, iter, block, + WS(pws2.is_some(), pws2.is_some()))) +)); + +named!(block_extends, do_parse!( + ws!(tag_s!("extends")) >> + name: ws!(expr_str_lit) >> + (Node::Extends(name)) +)); + +named!(block_block, do_parse!( + pws1: opt!(tag_s!("-")) >> + ws!(tag_s!("block")) >> + name: ws!(identifier) >> + nws1: opt!(tag_s!("-")) >> + tag_s!("%}") >> + contents: parse_template >> + tag_s!("{%") >> + pws2: opt!(tag_s!("-")) >> + ws!(tag_s!("endblock")) >> + opt!(ws!(tag_s!(name))) >> + nws2: opt!(tag_s!("-")) >> + (Node::BlockDef(WS(pws1.is_some(), nws1.is_some()), + name, contents, + WS(pws2.is_some(), pws2.is_some()))) +)); + +named!(block_include, do_parse!( + pws: opt!(tag_s!("-")) >> + ws!(tag_s!("include")) >> + name: ws!(expr_str_lit) >> + nws: opt!(tag_s!("-")) >> + (Node::Include(WS(pws.is_some(), nws.is_some()), match name { + Expr::StrLit(s) => s, + _ => panic!("include path must be a string literal"), + })) +)); + +named!(block_import, do_parse!( + pws: opt!(tag_s!("-")) >> + ws!(tag_s!("import")) >> + name: ws!(expr_str_lit) >> + ws!(tag_s!("as")) >> + scope: ws!(identifier) >> + nws: opt!(tag_s!("-")) >> + (Node::Import(WS(pws.is_some(), nws.is_some()), match name { + Expr::StrLit(s) => s, + _ => panic!("import path must be a string literal"), + }, scope)) +)); + +named!(block_macro, do_parse!( + pws1: opt!(tag_s!("-")) >> + ws!(tag_s!("macro")) >> + name: ws!(identifier) >> + params: ws!(parameters) >> + nws1: opt!(tag_s!("-")) >> + tag_s!("%}") >> + contents: parse_template >> + tag_s!("{%") >> + pws2: opt!(tag_s!("-")) >> + ws!(tag_s!("endmacro")) >> + nws2: opt!(tag_s!("-")) >> + (Node::Macro( + name, + Macro { + ws1: WS(pws1.is_some(), nws1.is_some()), + args: params, + nodes: contents, + ws2: WS(pws2.is_some(), nws2.is_some()) + } + )) +)); + +named!(block_node, do_parse!( + tag_s!("{%") >> + contents: alt!( + block_call | + block_let | + block_if | + block_for | + block_match | + block_extends | + block_include | + block_import | + block_block | + block_macro + ) >> + tag_s!("%}") >> + (contents) +)); + +named!(block_comment, do_parse!( + tag_s!("{#") >> + take_until_s!("#}") >> + tag_s!("#}") >> + (Node::Comment()) +)); + +named!(parse_template>>, many0!(alt!( + take_content | + block_comment | + expr_node | + block_node +))); + +pub fn parse(src: &str) -> Vec { + match parse_template(src.as_bytes()) { + IResult::Done(left, res) => { + if !left.is_empty() { + let s = str::from_utf8(left).unwrap(); + panic!("unable to parse template:\n\n{:?}", s); + } else { + res + } + }, + IResult::Error(err) => panic!("problems parsing template source: {}", err), + IResult::Incomplete(_) => panic!("parsing incomplete"), + } +} + +#[cfg(test)] +mod tests { + fn check_ws_split(s: &str, res: &(&str, &str, &str)) { + let node = super::split_ws_parts(s.as_bytes()); + match node { + super::Node::Lit(lws, s, rws) => { + assert_eq!(lws, res.0); + assert_eq!(s, res.1); + assert_eq!(rws, res.2); + }, + _ => { panic!("fail"); }, + } + } + #[test] + fn test_ws_splitter() { + check_ws_split("", &("", "", "")); + check_ws_split("a", &("", "a", "")); + check_ws_split("\ta", &("\t", "a", "")); + check_ws_split("b\n", &("", "b", "\n")); + check_ws_split(" \t\r\n", &(" \t\r\n", "", "")); + } + #[test] + #[should_panic] + fn test_invalid_block() { + super::parse("{% extend \"blah\" %}"); + } +} -- cgit