| use std::borrow::{Borrow, Cow}; |
| use std::collections::{BTreeMap, VecDeque}; |
| use std::fmt; |
| use std::rc::Rc; |
| |
| use serde_json::value::Value as Json; |
| |
| use crate::block::BlockContext; |
| use crate::context::Context; |
| use crate::error::RenderError; |
| use crate::helpers::HelperDef; |
| use crate::json::path::Path; |
| use crate::json::value::{JsonRender, PathAndJson, ScopedJson}; |
| use crate::output::{Output, StringOutput}; |
| use crate::registry::Registry; |
| use crate::support; |
| use crate::template::TemplateElement::*; |
| use crate::template::{ |
| BlockParam, DecoratorTemplate, HelperTemplate, Parameter, Template, TemplateElement, |
| TemplateMapping, |
| }; |
| use crate::{partial, RenderErrorReason}; |
| |
| const HELPER_MISSING: &str = "helperMissing"; |
| const BLOCK_HELPER_MISSING: &str = "blockHelperMissing"; |
| |
| /// The context of a render call |
| /// |
| /// This context stores information of a render and a writer where generated |
| /// content is written to. |
| /// |
| #[derive(Clone, Debug)] |
| pub struct RenderContext<'reg, 'rc> { |
| inner: Rc<RenderContextInner<'reg, 'rc>>, |
| blocks: VecDeque<BlockContext<'rc>>, |
| // copy-on-write context |
| modified_context: Option<Rc<Context>>, |
| } |
| |
| #[derive(Clone)] |
| pub struct RenderContextInner<'reg: 'rc, 'rc> { |
| partials: BTreeMap<String, &'rc Template>, |
| partial_block_stack: VecDeque<&'reg Template>, |
| partial_block_depth: isize, |
| local_helpers: BTreeMap<String, Rc<dyn HelperDef + Send + Sync + 'rc>>, |
| /// current template name |
| current_template: Option<&'rc String>, |
| /// root template name |
| root_template: Option<&'reg String>, |
| disable_escape: bool, |
| indent_string: Option<&'reg String>, |
| } |
| |
| impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { |
| /// Create a render context |
| pub fn new(root_template: Option<&'reg String>) -> RenderContext<'reg, 'rc> { |
| let inner = Rc::new(RenderContextInner { |
| partials: BTreeMap::new(), |
| partial_block_stack: VecDeque::new(), |
| partial_block_depth: 0, |
| local_helpers: BTreeMap::new(), |
| current_template: None, |
| root_template, |
| disable_escape: false, |
| indent_string: None, |
| }); |
| |
| let mut blocks = VecDeque::with_capacity(5); |
| blocks.push_front(BlockContext::new()); |
| |
| let modified_context = None; |
| RenderContext { |
| inner, |
| blocks, |
| modified_context, |
| } |
| } |
| |
| pub(crate) fn new_for_block(&self) -> RenderContext<'reg, 'rc> { |
| let inner = self.inner.clone(); |
| |
| let mut blocks = VecDeque::with_capacity(2); |
| blocks.push_front(BlockContext::new()); |
| |
| let modified_context = self.modified_context.clone(); |
| |
| RenderContext { |
| inner, |
| blocks, |
| modified_context, |
| } |
| } |
| |
| /// Push a block context into render context stack. This is typically |
| /// called when you entering a block scope. |
| pub fn push_block(&mut self, block: BlockContext<'rc>) { |
| self.blocks.push_front(block); |
| } |
| |
| /// Pop and drop current block context. |
| /// This is typically called when leaving a block scope. |
| pub fn pop_block(&mut self) { |
| self.blocks.pop_front(); |
| } |
| |
| pub(crate) fn clear_blocks(&mut self) { |
| self.blocks.clear(); |
| } |
| |
| /// Borrow a reference to current block context |
| pub fn block(&self) -> Option<&BlockContext<'rc>> { |
| self.blocks.front() |
| } |
| |
| /// Borrow a mutable reference to current block context in order to |
| /// modify some data. |
| pub fn block_mut(&mut self) -> Option<&mut BlockContext<'rc>> { |
| self.blocks.front_mut() |
| } |
| |
| fn inner(&self) -> &RenderContextInner<'reg, 'rc> { |
| self.inner.borrow() |
| } |
| |
| fn inner_mut(&mut self) -> &mut RenderContextInner<'reg, 'rc> { |
| Rc::make_mut(&mut self.inner) |
| } |
| |
| /// Get the modified context data if any |
| pub fn context(&self) -> Option<Rc<Context>> { |
| self.modified_context.clone() |
| } |
| |
| /// Set new context data into the render process. |
| /// This is typically called in decorators where user can modify |
| /// the data they were rendering. |
| pub fn set_context(&mut self, ctx: Context) { |
| self.modified_context = Some(Rc::new(ctx)) |
| } |
| |
| /// Evaluate a Json path in current scope. |
| /// |
| /// Typically you don't need to evaluate it by yourself. |
| /// The Helper and Decorator API will provide your evaluated value of |
| /// their parameters and hash data. |
| pub fn evaluate( |
| &self, |
| context: &'rc Context, |
| relative_path: &str, |
| ) -> Result<ScopedJson<'rc>, RenderError> { |
| let path = Path::parse(relative_path)?; |
| self.evaluate2(context, &path) |
| } |
| |
| pub(crate) fn evaluate2( |
| &self, |
| context: &'rc Context, |
| path: &Path, |
| ) -> Result<ScopedJson<'rc>, RenderError> { |
| match path { |
| Path::Local((level, name, _)) => Ok(self |
| .get_local_var(*level, name) |
| .map(|v| ScopedJson::Derived(v.clone())) |
| .unwrap_or_else(|| ScopedJson::Missing)), |
| Path::Relative((segs, _)) => context.navigate(segs, &self.blocks), |
| } |
| } |
| |
| /// Get registered partial in this render context |
| pub fn get_partial(&self, name: &str) -> Option<&Template> { |
| if name == partial::PARTIAL_BLOCK { |
| return self |
| .inner() |
| .partial_block_stack |
| .get(self.inner().partial_block_depth as usize) |
| .copied(); |
| } |
| self.inner().partials.get(name).copied() |
| } |
| |
| /// Register a partial for this context |
| pub fn set_partial(&mut self, name: String, partial: &'rc Template) { |
| self.inner_mut().partials.insert(name, partial); |
| } |
| |
| pub(crate) fn push_partial_block(&mut self, partial: &'reg Template) { |
| self.inner_mut().partial_block_stack.push_front(partial); |
| } |
| |
| pub(crate) fn pop_partial_block(&mut self) { |
| self.inner_mut().partial_block_stack.pop_front(); |
| } |
| |
| pub(crate) fn inc_partial_block_depth(&mut self) { |
| self.inner_mut().partial_block_depth += 1; |
| } |
| |
| pub(crate) fn dec_partial_block_depth(&mut self) { |
| let depth = &mut self.inner_mut().partial_block_depth; |
| if *depth > 0 { |
| *depth -= 1; |
| } |
| } |
| |
| pub(crate) fn set_indent_string(&mut self, indent: Option<&'reg String>) { |
| self.inner_mut().indent_string = indent; |
| } |
| |
| #[inline] |
| pub(crate) fn get_indent_string(&self) -> Option<&'reg String> { |
| self.inner.indent_string |
| } |
| |
| /// Remove a registered partial |
| pub fn remove_partial(&mut self, name: &str) { |
| self.inner_mut().partials.remove(name); |
| } |
| |
| fn get_local_var(&self, level: usize, name: &str) -> Option<&Json> { |
| self.blocks |
| .get(level) |
| .and_then(|blk| blk.get_local_var(name)) |
| } |
| |
| /// Test if given template name is current template. |
| pub fn is_current_template(&self, p: &str) -> bool { |
| self.inner() |
| .current_template |
| .map(|s| s == p) |
| .unwrap_or(false) |
| } |
| |
| /// Register a helper in this render context. |
| /// This is a feature provided by Decorator where you can create |
| /// temporary helpers. |
| pub fn register_local_helper( |
| &mut self, |
| name: &str, |
| def: Box<dyn HelperDef + Send + Sync + 'rc>, |
| ) { |
| self.inner_mut() |
| .local_helpers |
| .insert(name.to_string(), def.into()); |
| } |
| |
| /// Remove a helper from render context |
| pub fn unregister_local_helper(&mut self, name: &str) { |
| self.inner_mut().local_helpers.remove(name); |
| } |
| |
| /// Attempt to get a helper from current render context. |
| pub fn get_local_helper(&self, name: &str) -> Option<Rc<dyn HelperDef + Send + Sync + 'rc>> { |
| self.inner().local_helpers.get(name).cloned() |
| } |
| |
| #[inline] |
| fn has_local_helper(&self, name: &str) -> bool { |
| self.inner.local_helpers.contains_key(name) |
| } |
| |
| /// Returns the current template name. |
| /// Note that the name can be vary from root template when you are rendering |
| /// from partials. |
| pub fn get_current_template_name(&self) -> Option<&'rc String> { |
| self.inner().current_template |
| } |
| |
| /// Set the current template name. |
| pub fn set_current_template_name(&mut self, name: Option<&'rc String>) { |
| self.inner_mut().current_template = name; |
| } |
| |
| /// Get root template name if any. |
| /// This is the template name that you call `render` from `Handlebars`. |
| pub fn get_root_template_name(&self) -> Option<&'reg String> { |
| self.inner().root_template |
| } |
| |
| /// Get the escape toggle |
| pub fn is_disable_escape(&self) -> bool { |
| self.inner().disable_escape |
| } |
| |
| /// Set the escape toggle. |
| /// When toggle is on, escape_fn will be called when rendering. |
| pub fn set_disable_escape(&mut self, disable: bool) { |
| self.inner_mut().disable_escape = disable |
| } |
| } |
| |
| impl<'reg, 'rc> fmt::Debug for RenderContextInner<'reg, 'rc> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { |
| f.debug_struct("RenderContextInner") |
| .field("partials", &self.partials) |
| .field("partial_block_stack", &self.partial_block_stack) |
| .field("partial_block_depth", &self.partial_block_depth) |
| .field("root_template", &self.root_template) |
| .field("current_template", &self.current_template) |
| .field("disable_escape", &self.disable_escape) |
| .finish() |
| } |
| } |
| |
| /// Render-time Helper data when using in a helper definition |
| #[derive(Debug, Clone)] |
| pub struct Helper<'rc> { |
| name: Cow<'rc, str>, |
| params: Vec<PathAndJson<'rc>>, |
| hash: BTreeMap<&'rc str, PathAndJson<'rc>>, |
| template: Option<&'rc Template>, |
| inverse: Option<&'rc Template>, |
| block_param: Option<&'rc BlockParam>, |
| block: bool, |
| } |
| |
| impl<'reg: 'rc, 'rc> Helper<'rc> { |
| fn try_from_template( |
| ht: &'rc HelperTemplate, |
| registry: &'reg Registry<'reg>, |
| context: &'rc Context, |
| render_context: &mut RenderContext<'reg, 'rc>, |
| ) -> Result<Helper<'rc>, RenderError> { |
| let name = ht.name.expand_as_name(registry, context, render_context)?; |
| let mut pv = Vec::with_capacity(ht.params.len()); |
| for p in &ht.params { |
| let r = p.expand(registry, context, render_context)?; |
| pv.push(r); |
| } |
| |
| let mut hm = BTreeMap::new(); |
| for (k, p) in &ht.hash { |
| let r = p.expand(registry, context, render_context)?; |
| hm.insert(k.as_ref(), r); |
| } |
| |
| Ok(Helper { |
| name, |
| params: pv, |
| hash: hm, |
| template: ht.template.as_ref(), |
| inverse: ht.inverse.as_ref(), |
| block_param: ht.block_param.as_ref(), |
| block: ht.block, |
| }) |
| } |
| |
| /// Returns helper name |
| pub fn name(&self) -> &str { |
| &self.name |
| } |
| |
| /// Returns all helper params, resolved within the context |
| pub fn params(&self) -> &Vec<PathAndJson<'rc>> { |
| &self.params |
| } |
| |
| /// Returns nth helper param, resolved within the context. |
| /// |
| /// ## Example |
| /// |
| /// To get the first param in `{{my_helper abc}}` or `{{my_helper 2}}`, |
| /// use `h.param(0)` in helper definition. |
| /// Variable `abc` is auto resolved in current context. |
| /// |
| /// ``` |
| /// use handlebars::*; |
| /// |
| /// fn my_helper(h: &Helper, rc: &mut RenderContext) -> Result<(), RenderError> { |
| /// let v = h.param(0).map(|v| v.value()) |
| /// .ok_or(RenderErrorReason::ParamNotFoundForIndex("myhelper", 0)); |
| /// // .. |
| /// Ok(()) |
| /// } |
| /// ``` |
| pub fn param(&self, idx: usize) -> Option<&PathAndJson<'rc>> { |
| self.params.get(idx) |
| } |
| |
| /// Returns hash, resolved within the context |
| pub fn hash(&self) -> &BTreeMap<&'rc str, PathAndJson<'rc>> { |
| &self.hash |
| } |
| |
| /// Return hash value of a given key, resolved within the context |
| /// |
| /// ## Example |
| /// |
| /// To get the first param in `{{my_helper v=abc}}` or `{{my_helper v=2}}`, |
| /// use `h.hash_get("v")` in helper definition. |
| /// Variable `abc` is auto resolved in current context. |
| /// |
| /// ``` |
| /// use handlebars::*; |
| /// |
| /// fn my_helper(h: &Helper, rc: &mut RenderContext) -> Result<(), RenderError> { |
| /// let v = h.hash_get("v").map(|v| v.value()) |
| /// .ok_or(RenderErrorReason::ParamNotFoundForIndex("my_helper", 0)); |
| /// // .. |
| /// Ok(()) |
| /// } |
| /// ``` |
| pub fn hash_get(&self, key: &str) -> Option<&PathAndJson<'rc>> { |
| self.hash.get(key) |
| } |
| |
| /// Returns the default inner template if the helper is a block helper. |
| /// |
| /// Typically you will render the template via: `template.render(registry, render_context)` |
| /// |
| pub fn template(&self) -> Option<&'rc Template> { |
| self.template |
| } |
| |
| /// Returns the template of `else` branch if any |
| pub fn inverse(&self) -> Option<&'rc Template> { |
| self.inverse |
| } |
| |
| /// Returns if the helper is a block one `{{#helper}}{{/helper}}` or not `{{helper 123}}` |
| pub fn is_block(&self) -> bool { |
| self.block |
| } |
| |
| /// Returns if the helper has either a block param or block param pair |
| pub fn has_block_param(&self) -> bool { |
| self.block_param.is_some() |
| } |
| |
| /// Returns block param if any |
| pub fn block_param(&self) -> Option<&'rc str> { |
| if let Some(&BlockParam::Single(Parameter::Name(ref s))) = self.block_param { |
| Some(s) |
| } else { |
| None |
| } |
| } |
| |
| /// Return block param pair (for example |key, val|) if any |
| pub fn block_param_pair(&self) -> Option<(&'rc str, &'rc str)> { |
| if let Some(&BlockParam::Pair((Parameter::Name(ref s1), Parameter::Name(ref s2)))) = |
| self.block_param |
| { |
| Some((s1, s2)) |
| } else { |
| None |
| } |
| } |
| } |
| |
| /// Render-time Decorator data when using in a decorator definition |
| #[derive(Debug)] |
| pub struct Decorator<'rc> { |
| name: Cow<'rc, str>, |
| params: Vec<PathAndJson<'rc>>, |
| hash: BTreeMap<&'rc str, PathAndJson<'rc>>, |
| template: Option<&'rc Template>, |
| indent: Option<&'rc String>, |
| } |
| |
| impl<'reg: 'rc, 'rc> Decorator<'rc> { |
| fn try_from_template( |
| dt: &'rc DecoratorTemplate, |
| registry: &'reg Registry<'reg>, |
| context: &'rc Context, |
| render_context: &mut RenderContext<'reg, 'rc>, |
| ) -> Result<Decorator<'rc>, RenderError> { |
| let name = dt.name.expand_as_name(registry, context, render_context)?; |
| |
| let mut pv = Vec::with_capacity(dt.params.len()); |
| for p in &dt.params { |
| let r = p.expand(registry, context, render_context)?; |
| pv.push(r); |
| } |
| |
| let mut hm = BTreeMap::new(); |
| for (k, p) in &dt.hash { |
| let r = p.expand(registry, context, render_context)?; |
| hm.insert(k.as_ref(), r); |
| } |
| |
| Ok(Decorator { |
| name, |
| params: pv, |
| hash: hm, |
| template: dt.template.as_ref(), |
| indent: dt.indent.as_ref(), |
| }) |
| } |
| |
| /// Returns helper name |
| pub fn name(&self) -> &str { |
| self.name.as_ref() |
| } |
| |
| /// Returns all helper params, resolved within the context |
| pub fn params(&self) -> &Vec<PathAndJson<'rc>> { |
| &self.params |
| } |
| |
| /// Returns nth helper param, resolved within the context |
| pub fn param(&self, idx: usize) -> Option<&PathAndJson<'rc>> { |
| self.params.get(idx) |
| } |
| |
| /// Returns hash, resolved within the context |
| pub fn hash(&self) -> &BTreeMap<&'rc str, PathAndJson<'rc>> { |
| &self.hash |
| } |
| |
| /// Return hash value of a given key, resolved within the context |
| pub fn hash_get(&self, key: &str) -> Option<&PathAndJson<'rc>> { |
| self.hash.get(key) |
| } |
| |
| /// Returns the default inner template if any |
| pub fn template(&self) -> Option<&'rc Template> { |
| self.template |
| } |
| |
| pub fn indent(&self) -> Option<&'rc String> { |
| self.indent |
| } |
| } |
| |
| /// Render trait |
| pub trait Renderable { |
| /// render into RenderContext's `writer` |
| fn render<'reg: 'rc, 'rc>( |
| &'rc self, |
| registry: &'reg Registry<'reg>, |
| context: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError>; |
| |
| /// render into string |
| fn renders<'reg: 'rc, 'rc>( |
| &'rc self, |
| registry: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| ) -> Result<String, RenderError> { |
| let mut so = StringOutput::new(); |
| self.render(registry, ctx, rc, &mut so)?; |
| so.into_string() |
| .map_err(|e| RenderErrorReason::from(e).into()) |
| } |
| } |
| |
| /// Evaluate decorator |
| pub trait Evaluable { |
| fn eval<'reg: 'rc, 'rc>( |
| &'rc self, |
| registry: &'reg Registry<'reg>, |
| context: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| ) -> Result<(), RenderError>; |
| } |
| |
| #[inline] |
| fn call_helper_for_value<'reg: 'rc, 'rc>( |
| hd: &dyn HelperDef, |
| ht: &Helper<'rc>, |
| r: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| ) -> Result<PathAndJson<'rc>, RenderError> { |
| match hd.call_inner(ht, r, ctx, rc) { |
| Ok(result) => Ok(PathAndJson::new(None, result)), |
| Err(e) => { |
| if e.is_unimplemented() { |
| // parse value from output |
| let mut so = StringOutput::new(); |
| |
| // here we don't want subexpression result escaped, |
| // so we temporarily disable it |
| let disable_escape = rc.is_disable_escape(); |
| rc.set_disable_escape(true); |
| |
| hd.call(ht, r, ctx, rc, &mut so)?; |
| rc.set_disable_escape(disable_escape); |
| |
| let string = so.into_string().map_err(RenderError::from)?; |
| Ok(PathAndJson::new( |
| None, |
| ScopedJson::Derived(Json::String(string)), |
| )) |
| } else { |
| Err(e) |
| } |
| } |
| } |
| } |
| |
| impl Parameter { |
| pub fn expand_as_name<'reg: 'rc, 'rc>( |
| &'rc self, |
| registry: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| ) -> Result<Cow<'rc, str>, RenderError> { |
| match self { |
| Parameter::Name(ref name) => Ok(Cow::Borrowed(name)), |
| Parameter::Path(ref p) => Ok(Cow::Borrowed(p.raw())), |
| Parameter::Subexpression(_) => self |
| .expand(registry, ctx, rc) |
| .map(|v| v.value().render()) |
| .map(Cow::Owned), |
| Parameter::Literal(ref j) => Ok(Cow::Owned(j.render())), |
| } |
| } |
| |
| pub fn expand<'reg: 'rc, 'rc>( |
| &'rc self, |
| registry: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| ) -> Result<PathAndJson<'rc>, RenderError> { |
| match self { |
| Parameter::Name(ref name) => { |
| // FIXME: raise error when expanding with name? |
| Ok(PathAndJson::new(Some(name.to_owned()), ScopedJson::Missing)) |
| } |
| Parameter::Path(ref path) => { |
| if let Some(rc_context) = rc.context() { |
| let result = rc.evaluate2(rc_context.borrow(), path)?; |
| Ok(PathAndJson::new( |
| Some(path.raw().to_owned()), |
| ScopedJson::Derived(result.as_json().clone()), |
| )) |
| } else { |
| let result = rc.evaluate2(ctx, path)?; |
| Ok(PathAndJson::new(Some(path.raw().to_owned()), result)) |
| } |
| } |
| Parameter::Literal(ref j) => Ok(PathAndJson::new(None, ScopedJson::Constant(j))), |
| Parameter::Subexpression(ref t) => match *t.as_element() { |
| Expression(ref ht) => { |
| let name = ht.name.expand_as_name(registry, ctx, rc)?; |
| |
| let h = Helper::try_from_template(ht, registry, ctx, rc)?; |
| if let Some(ref d) = rc.get_local_helper(&name) { |
| call_helper_for_value(d.as_ref(), &h, registry, ctx, rc) |
| } else { |
| let mut helper = registry.get_or_load_helper(&name)?; |
| |
| if helper.is_none() { |
| helper = registry.get_or_load_helper(if ht.block { |
| BLOCK_HELPER_MISSING |
| } else { |
| HELPER_MISSING |
| })?; |
| } |
| |
| helper |
| .ok_or_else(|| { |
| RenderErrorReason::HelperNotFound(name.to_string()).into() |
| }) |
| .and_then(|d| call_helper_for_value(d.as_ref(), &h, registry, ctx, rc)) |
| } |
| } |
| _ => unreachable!(), |
| }, |
| } |
| } |
| } |
| |
| impl Renderable for Template { |
| fn render<'reg: 'rc, 'rc>( |
| &'rc self, |
| registry: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| rc.set_current_template_name(self.name.as_ref()); |
| let iter = self.elements.iter(); |
| |
| for (idx, t) in iter.enumerate() { |
| t.render(registry, ctx, rc, out).map_err(|mut e| { |
| // add line/col number if the template has mapping data |
| if e.line_no.is_none() { |
| if let Some(&TemplateMapping(line, col)) = self.mapping.get(idx) { |
| e.line_no = Some(line); |
| e.column_no = Some(col); |
| } |
| } |
| |
| if e.template_name.is_none() { |
| e.template_name.clone_from(&self.name); |
| } |
| |
| e |
| })?; |
| } |
| Ok(()) |
| } |
| } |
| |
| impl Evaluable for Template { |
| fn eval<'reg: 'rc, 'rc>( |
| &'rc self, |
| registry: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| ) -> Result<(), RenderError> { |
| let iter = self.elements.iter(); |
| |
| for (idx, t) in iter.enumerate() { |
| t.eval(registry, ctx, rc).map_err(|mut e| { |
| if e.line_no.is_none() { |
| if let Some(&TemplateMapping(line, col)) = self.mapping.get(idx) { |
| e.line_no = Some(line); |
| e.column_no = Some(col); |
| } |
| } |
| |
| e.template_name.clone_from(&self.name); |
| e |
| })?; |
| } |
| Ok(()) |
| } |
| } |
| |
| fn helper_exists<'reg: 'rc, 'rc>( |
| name: &str, |
| reg: &Registry<'reg>, |
| rc: &RenderContext<'reg, 'rc>, |
| ) -> bool { |
| rc.has_local_helper(name) || reg.has_helper(name) |
| } |
| |
| #[inline] |
| fn render_helper<'reg: 'rc, 'rc>( |
| ht: &'rc HelperTemplate, |
| registry: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| let h = Helper::try_from_template(ht, registry, ctx, rc)?; |
| debug!( |
| "Rendering helper: {:?}, params: {:?}, hash: {:?}", |
| h.name(), |
| h.params(), |
| h.hash() |
| ); |
| if let Some(ref d) = rc.get_local_helper(h.name()) { |
| d.call(&h, registry, ctx, rc, out) |
| } else { |
| let mut helper = registry.get_or_load_helper(h.name())?; |
| |
| if helper.is_none() { |
| helper = registry.get_or_load_helper(if ht.block { |
| BLOCK_HELPER_MISSING |
| } else { |
| HELPER_MISSING |
| })?; |
| } |
| |
| helper |
| .ok_or_else(|| RenderErrorReason::HelperNotFound(h.name().to_owned()).into()) |
| .and_then(|d| d.call(&h, registry, ctx, rc, out)) |
| } |
| } |
| |
| pub(crate) fn do_escape(r: &Registry<'_>, rc: &RenderContext<'_, '_>, content: String) -> String { |
| if !rc.is_disable_escape() { |
| r.get_escape_fn()(&content) |
| } else { |
| content |
| } |
| } |
| |
| #[inline] |
| fn indent_aware_write( |
| v: &str, |
| rc: &RenderContext<'_, '_>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| if let Some(indent) = rc.get_indent_string() { |
| out.write(support::str::with_indent(v, indent).as_ref())?; |
| } else { |
| out.write(v.as_ref())?; |
| } |
| Ok(()) |
| } |
| |
| impl Renderable for TemplateElement { |
| fn render<'reg: 'rc, 'rc>( |
| &'rc self, |
| registry: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| out: &mut dyn Output, |
| ) -> Result<(), RenderError> { |
| match self { |
| RawString(ref v) => indent_aware_write(v.as_ref(), rc, out), |
| Expression(ref ht) | HtmlExpression(ref ht) => { |
| let is_html_expression = matches!(self, HtmlExpression(_)); |
| if is_html_expression { |
| rc.set_disable_escape(true); |
| } |
| |
| // test if the expression is to render some value |
| let result = if ht.is_name_only() { |
| let helper_name = ht.name.expand_as_name(registry, ctx, rc)?; |
| if helper_exists(&helper_name, registry, rc) { |
| render_helper(ht, registry, ctx, rc, out) |
| } else { |
| debug!("Rendering value: {:?}", ht.name); |
| let context_json = ht.name.expand(registry, ctx, rc)?; |
| if context_json.is_value_missing() { |
| if registry.strict_mode() { |
| Err(RenderError::strict_error(context_json.relative_path())) |
| } else { |
| // helper missing |
| if let Some(hook) = registry.get_or_load_helper(HELPER_MISSING)? { |
| let h = Helper::try_from_template(ht, registry, ctx, rc)?; |
| hook.call(&h, registry, ctx, rc, out) |
| } else { |
| Ok(()) |
| } |
| } |
| } else { |
| let rendered = context_json.value().render(); |
| let output = do_escape(registry, rc, rendered); |
| indent_aware_write(output.as_ref(), rc, out) |
| } |
| } |
| } else { |
| // this is a helper expression |
| render_helper(ht, registry, ctx, rc, out) |
| }; |
| |
| if is_html_expression { |
| rc.set_disable_escape(false); |
| } |
| |
| result |
| } |
| HelperBlock(ref ht) => render_helper(ht, registry, ctx, rc, out), |
| DecoratorExpression(_) | DecoratorBlock(_) => self.eval(registry, ctx, rc), |
| PartialExpression(ref dt) | PartialBlock(ref dt) => { |
| let di = Decorator::try_from_template(dt, registry, ctx, rc)?; |
| |
| partial::expand_partial(&di, registry, ctx, rc, out) |
| } |
| _ => Ok(()), |
| } |
| } |
| } |
| |
| impl Evaluable for TemplateElement { |
| fn eval<'reg: 'rc, 'rc>( |
| &'rc self, |
| registry: &'reg Registry<'reg>, |
| ctx: &'rc Context, |
| rc: &mut RenderContext<'reg, 'rc>, |
| ) -> Result<(), RenderError> { |
| match *self { |
| DecoratorExpression(ref dt) | DecoratorBlock(ref dt) => { |
| let di = Decorator::try_from_template(dt, registry, ctx, rc)?; |
| match registry.get_decorator(di.name()) { |
| Some(d) => d.call(&di, registry, ctx, rc), |
| None => Err(RenderErrorReason::DecoratorNotFound(di.name().to_owned()).into()), |
| } |
| } |
| _ => Ok(()), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use std::collections::BTreeMap; |
| |
| use super::{Helper, RenderContext, Renderable}; |
| use crate::block::BlockContext; |
| use crate::context::Context; |
| use crate::error::RenderError; |
| use crate::json::path::Path; |
| use crate::json::value::JsonRender; |
| use crate::output::{Output, StringOutput}; |
| use crate::registry::Registry; |
| use crate::template::TemplateElement::*; |
| use crate::template::{HelperTemplate, Template, TemplateElement}; |
| |
| #[test] |
| fn test_raw_string() { |
| let r = Registry::new(); |
| let raw_string = RawString("<h1>hello world</h1>".to_string()); |
| |
| let mut out = StringOutput::new(); |
| let ctx = Context::null(); |
| { |
| let mut rc = RenderContext::new(None); |
| raw_string.render(&r, &ctx, &mut rc, &mut out).ok().unwrap(); |
| } |
| assert_eq!( |
| out.into_string().unwrap(), |
| "<h1>hello world</h1>".to_string() |
| ); |
| } |
| |
| #[test] |
| fn test_expression() { |
| let r = Registry::new(); |
| let element = Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths( |
| &["hello"], |
| )))); |
| |
| let mut out = StringOutput::new(); |
| let mut m: BTreeMap<String, String> = BTreeMap::new(); |
| let value = "<p></p>".to_string(); |
| m.insert("hello".to_string(), value); |
| let ctx = Context::wraps(&m).unwrap(); |
| { |
| let mut rc = RenderContext::new(None); |
| element.render(&r, &ctx, &mut rc, &mut out).ok().unwrap(); |
| } |
| |
| assert_eq!( |
| out.into_string().unwrap(), |
| "<p></p>".to_string() |
| ); |
| } |
| |
| #[test] |
| fn test_html_expression() { |
| let r = Registry::new(); |
| let element = HtmlExpression(Box::new(HelperTemplate::with_path(Path::with_named_paths( |
| &["hello"], |
| )))); |
| |
| let mut out = StringOutput::new(); |
| let mut m: BTreeMap<String, String> = BTreeMap::new(); |
| let value = "world"; |
| m.insert("hello".to_string(), value.to_string()); |
| let ctx = Context::wraps(&m).unwrap(); |
| { |
| let mut rc = RenderContext::new(None); |
| element.render(&r, &ctx, &mut rc, &mut out).ok().unwrap(); |
| } |
| |
| assert_eq!(out.into_string().unwrap(), value.to_string()); |
| } |
| |
| #[test] |
| fn test_template() { |
| let r = Registry::new(); |
| let mut out = StringOutput::new(); |
| let mut m: BTreeMap<String, String> = BTreeMap::new(); |
| let value = "world".to_string(); |
| m.insert("hello".to_string(), value); |
| let ctx = Context::wraps(&m).unwrap(); |
| |
| let elements: Vec<TemplateElement> = vec![ |
| RawString("<h1>".to_string()), |
| Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths( |
| &["hello"], |
| )))), |
| RawString("</h1>".to_string()), |
| Comment("".to_string()), |
| ]; |
| |
| let template = Template { |
| elements, |
| name: None, |
| mapping: Vec::new(), |
| }; |
| |
| { |
| let mut rc = RenderContext::new(None); |
| template.render(&r, &ctx, &mut rc, &mut out).ok().unwrap(); |
| } |
| |
| assert_eq!(out.into_string().unwrap(), "<h1>world</h1>".to_string()); |
| } |
| |
| #[test] |
| fn test_render_context_promotion_and_demotion() { |
| use crate::json::value::to_json; |
| let mut render_context = RenderContext::new(None); |
| let mut block = BlockContext::new(); |
| |
| block.set_local_var("index", to_json(0)); |
| render_context.push_block(block); |
| |
| render_context.push_block(BlockContext::new()); |
| assert_eq!( |
| render_context.get_local_var(1, "index").unwrap(), |
| &to_json(0) |
| ); |
| |
| render_context.pop_block(); |
| |
| assert_eq!( |
| render_context.get_local_var(0, "index").unwrap(), |
| &to_json(0) |
| ); |
| } |
| |
| #[test] |
| fn test_render_subexpression_issue_115() { |
| use crate::support::str::StringWriter; |
| |
| let mut r = Registry::new(); |
| r.register_helper( |
| "format", |
| Box::new( |
| |h: &Helper<'_>, |
| _: &Registry<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output| |
| -> Result<(), RenderError> { |
| out.write(&h.param(0).unwrap().value().render()) |
| .map(|_| ()) |
| .map_err(RenderError::from) |
| }, |
| ), |
| ); |
| |
| let mut sw = StringWriter::new(); |
| let mut m: BTreeMap<String, String> = BTreeMap::new(); |
| m.insert("a".to_string(), "123".to_string()); |
| |
| { |
| if let Err(e) = r.render_template_to_write("{{format (format a)}}", &m, &mut sw) { |
| panic!("{}", e); |
| } |
| } |
| |
| assert_eq!(sw.into_string(), "123".to_string()); |
| } |
| |
| #[test] |
| fn test_render_error_line_no() { |
| let mut r = Registry::new(); |
| let m: BTreeMap<String, String> = BTreeMap::new(); |
| |
| let name = "invalid_template"; |
| assert!(r |
| .register_template_string(name, "<h1>\n{{#if true}}\n {{#each}}{{/each}}\n{{/if}}") |
| .is_ok()); |
| |
| if let Err(e) = r.render(name, &m) { |
| assert_eq!(e.line_no.unwrap(), 3); |
| assert_eq!(e.column_no.unwrap(), 3); |
| assert_eq!(e.template_name, Some(name.to_owned())); |
| } else { |
| panic!("Error expected"); |
| } |
| } |
| |
| #[test] |
| fn test_partial_failback_render() { |
| let mut r = Registry::new(); |
| |
| assert!(r |
| .register_template_string("parent", "<html>{{> layout}}</html>") |
| .is_ok()); |
| assert!(r |
| .register_template_string( |
| "child", |
| "{{#*inline \"layout\"}}content{{/inline}}{{#> parent}}{{> seg}}{{/parent}}", |
| ) |
| .is_ok()); |
| assert!(r.register_template_string("seg", "1234").is_ok()); |
| |
| let r = r.render("child", &true).expect("should work"); |
| assert_eq!(r, "<html>content</html>"); |
| } |
| |
| #[test] |
| fn test_key_with_slash() { |
| let mut r = Registry::new(); |
| |
| assert!(r |
| .register_template_string("t", "{{#each this}}{{@key}}: {{this}}\n{{/each}}") |
| .is_ok()); |
| |
| let r = r.render("t", &json!({"/foo": "bar"})).unwrap(); |
| |
| assert_eq!(r, "/foo: bar\n"); |
| } |
| |
| #[test] |
| fn test_comment() { |
| let r = Registry::new(); |
| |
| assert_eq!( |
| r.render_template("Hello {{this}} {{! test me }}", &0) |
| .unwrap(), |
| "Hello 0 " |
| ); |
| } |
| |
| #[test] |
| fn test_zero_args_heler() { |
| let mut r = Registry::new(); |
| |
| r.register_helper( |
| "name", |
| Box::new( |
| |_: &Helper<'_>, |
| _: &Registry<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output| |
| -> Result<(), RenderError> { |
| out.write("N/A").map_err(Into::into) |
| }, |
| ), |
| ); |
| |
| r.register_template_string("t0", "Output name: {{name}}") |
| .unwrap(); |
| r.register_template_string("t1", "Output name: {{first_name}}") |
| .unwrap(); |
| r.register_template_string("t2", "Output name: {{./name}}") |
| .unwrap(); |
| |
| // when "name" is available in context, use context first |
| assert_eq!( |
| r.render("t0", &json!({"name": "Alex"})).unwrap(), |
| "Output name: N/A" |
| ); |
| |
| // when "name" is unavailable, call helper with same name |
| assert_eq!( |
| r.render("t2", &json!({"name": "Alex"})).unwrap(), |
| "Output name: Alex" |
| ); |
| |
| // output nothing when neither context nor helper available |
| assert_eq!( |
| r.render("t1", &json!({"name": "Alex"})).unwrap(), |
| "Output name: " |
| ); |
| |
| // generate error in strict mode for above case |
| r.set_strict_mode(true); |
| assert!(r.render("t1", &json!({"name": "Alex"})).is_err()); |
| |
| // output nothing when helperMissing was defined |
| r.set_strict_mode(false); |
| r.register_helper( |
| "helperMissing", |
| Box::new( |
| |h: &Helper<'_>, |
| _: &Registry<'_>, |
| _: &Context, |
| _: &mut RenderContext<'_, '_>, |
| out: &mut dyn Output| |
| -> Result<(), RenderError> { |
| let name = h.name(); |
| write!(out, "{} not resolved", name)?; |
| Ok(()) |
| }, |
| ), |
| ); |
| assert_eq!( |
| r.render("t1", &json!({"name": "Alex"})).unwrap(), |
| "Output name: first_name not resolved" |
| ); |
| } |
| |
| #[test] |
| fn test_identifiers_starting_with_numbers() { |
| let mut r = Registry::new(); |
| |
| assert!(r |
| .register_template_string("r1", "{{#if 0a}}true{{/if}}") |
| .is_ok()); |
| let r1 = r.render("r1", &json!({"0a": true})).unwrap(); |
| assert_eq!(r1, "true"); |
| |
| assert!(r.register_template_string("r2", "{{eq 1a 1}}").is_ok()); |
| let r2 = r.render("r2", &json!({"1a": 2, "a": 1})).unwrap(); |
| assert_eq!(r2, "false"); |
| |
| assert!(r |
| .register_template_string("r3", "0: {{0}} {{#if (eq 0 true)}}resolved from context{{/if}}\n1a: {{1a}} {{#if (eq 1a true)}}resolved from context{{/if}}\n2_2: {{2_2}} {{#if (eq 2_2 true)}}resolved from context{{/if}}") // YUP it is just eq that barfs! is if handled specially? maybe this test should go nearer to specific helpers that fail? |
| .is_ok()); |
| let r3 = r |
| .render("r3", &json!({"0": true, "1a": true, "2_2": true})) |
| .unwrap(); |
| assert_eq!( |
| r3, |
| "0: true \n1a: true resolved from context\n2_2: true resolved from context" |
| ); |
| |
| // these should all be errors: |
| assert!(r.register_template_string("r4", "{{eq 1}}").is_ok()); |
| assert!(r.register_template_string("r5", "{{eq a1}}").is_ok()); |
| assert!(r.register_template_string("r6", "{{eq 1a}}").is_ok()); |
| assert!(r.render("r4", &()).is_err()); |
| assert!(r.render("r5", &()).is_err()); |
| assert!(r.render("r6", &()).is_err()); |
| } |
| } |