diff --git a/crates/core/src/parser/filter.rs b/crates/core/src/parser/filter.rs index 795e5930a..8ef5a53be 100644 --- a/crates/core/src/parser/filter.rs +++ b/crates/core/src/parser/filter.rs @@ -254,3 +254,19 @@ where Box::new(filter) } } + +/// A filter used internally when parsing is done in lax mode. +#[derive(Debug, Default)] +pub struct NoopFilter; + +impl Filter for NoopFilter { + fn evaluate(&self, input: &dyn ValueView, _runtime: &dyn Runtime) -> Result { + Ok(input.to_value()) + } +} + +impl Display for NoopFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + ::std::write!(f, "{}", "noop") + } +} diff --git a/crates/core/src/parser/lang.rs b/crates/core/src/parser/lang.rs index f6a9abf7f..8bf2b5ad0 100644 --- a/crates/core/src/parser/lang.rs +++ b/crates/core/src/parser/lang.rs @@ -3,12 +3,25 @@ use super::ParseFilter; use super::ParseTag; use super::PluginRegistry; +#[derive(Clone)] +pub enum ParseMode { + Strict, + Lax, +} + +impl Default for ParseMode { + fn default() -> Self { + Self::Strict + } +} + #[derive(Clone, Default)] #[non_exhaustive] pub struct Language { pub blocks: PluginRegistry>, pub tags: PluginRegistry>, pub filters: PluginRegistry>, + pub mode: ParseMode, } impl Language { diff --git a/crates/core/src/parser/parser.rs b/crates/core/src/parser/parser.rs index 92335d8cb..b7c51ff2b 100644 --- a/crates/core/src/parser/parser.rs +++ b/crates/core/src/parser/parser.rs @@ -9,9 +9,9 @@ use crate::runtime::Expression; use crate::runtime::Renderable; use crate::runtime::Variable; -use super::Language; use super::Text; use super::{Filter, FilterArguments, FilterChain}; +use super::{Language, ParseMode}; use pest::Parser; @@ -205,22 +205,38 @@ fn parse_filter(filter: Pair, options: &Language) -> Result> { keyword: Box::new(keyword_args.into_iter()), }; - let f = options.filters.get(name).ok_or_else(|| { - let mut available: Vec<_> = options.filters.plugin_names().collect(); - available.sort_unstable(); - let available = itertools::join(available, ", "); - Error::with_msg("Unknown filter") - .context("requested filter", name.to_owned()) - .context("available filters", available) - })?; - - let f = f - .parse(args) - .trace("Filter parsing error") - .context_key("filter") - .value_with(|| filter_str.to_string().into())?; - - Ok(f) + match options.mode { + ParseMode::Strict => { + let f = options.filters.get(name).ok_or_else(|| { + let mut available: Vec<_> = options.filters.plugin_names().collect(); + available.sort_unstable(); + let available = itertools::join(available, ", "); + Error::with_msg("Unknown filter") + .context("requested filter", name.to_owned()) + .context("available filters", available) + })?; + + let f = f + .parse(args) + .trace("Filter parsing error") + .context_key("filter") + .value_with(|| filter_str.to_string().into())?; + + Ok(f) + } + ParseMode::Lax => match options.filters.get(name) { + Some(f) => { + let f = f + .parse(args) + .trace("Filter parsing error") + .context_key("filter") + .value_with(|| filter_str.to_string().into())?; + + Ok(f) + } + None => Ok(Box::new(super::NoopFilter {})), + }, + } } /// Parses a `FilterChain` from a `Pair` with a filter chain. @@ -1172,6 +1188,20 @@ mod test { assert_eq!(output, "5"); } + #[test] + fn test_parse_mode_filters() { + let mut options = Language::default(); + let text = "{{ exp | undefined }}"; + + options.mode = ParseMode::Strict; + let result = parse(text, &options); + assert_eq!(result.is_err(), true); + + options.mode = ParseMode::Lax; + let result = parse(text, &options); + assert_eq!(result.is_err(), false); + } + /// Macro implementation of custom block test. macro_rules! test_custom_block_tags_impl { ($start_tag:expr, $end_tag:expr) => {{ diff --git a/crates/core/src/runtime/expression.rs b/crates/core/src/runtime/expression.rs index af0ab76ae..b8cca0e6d 100644 --- a/crates/core/src/runtime/expression.rs +++ b/crates/core/src/runtime/expression.rs @@ -57,7 +57,13 @@ impl Expression { Expression::Literal(ref x) => ValueCow::Borrowed(x), Expression::Variable(ref x) => { let path = x.evaluate(runtime)?; - runtime.get(&path)? + + match runtime.render_mode() { + super::RenderingMode::Lax => { + runtime.try_get(&path).unwrap_or_else(|| Value::Nil.into()) + } + _ => runtime.get(&path)?, + } } }; Ok(val) @@ -72,3 +78,32 @@ impl fmt::Display for Expression { } } } + +#[cfg(test)] +mod test { + use super::*; + + use crate::model::Object; + use crate::model::Value; + use crate::runtime::RenderingMode; + use crate::runtime::RuntimeBuilder; + use crate::runtime::StackFrame; + + #[test] + fn test_rendering_mode() { + let globals = Object::new(); + let expression = Expression::Variable(Variable::with_literal("test")); + + let runtime = RuntimeBuilder::new() + .set_render_mode(RenderingMode::Strict) + .build(); + let runtime = StackFrame::new(&runtime, &globals); + assert_eq!(expression.evaluate(&runtime).is_err(), true); + + let runtime = RuntimeBuilder::new() + .set_render_mode(RenderingMode::Lax) + .build(); + let runtime = StackFrame::new(&runtime, &globals); + assert_eq!(expression.evaluate(&runtime).unwrap(), Value::Nil); + } +} diff --git a/crates/core/src/runtime/runtime.rs b/crates/core/src/runtime/runtime.rs index 15fb5694e..7d491467d 100644 --- a/crates/core/src/runtime/runtime.rs +++ b/crates/core/src/runtime/runtime.rs @@ -7,6 +7,14 @@ use crate::model::{Object, ObjectView, Scalar, ScalarCow, Value, ValueCow, Value use super::PartialStore; use super::Renderable; +/// What mode to use when rendering. +pub enum RenderingMode { + /// Returns an error when a variable is not defined. + Strict, + /// Replaces missing variables with an empty string. + Lax, +} + /// State for rendering a template pub trait Runtime { /// Partial templates for inclusion. @@ -36,6 +44,9 @@ pub trait Runtime { /// Unnamed state for plugins during rendering fn registers(&self) -> &Registers; + + /// Used to set the mode when rendering + fn render_mode(&self) -> &RenderingMode; } impl<'r, R: Runtime + ?Sized> Runtime for &'r R { @@ -78,12 +89,17 @@ impl<'r, R: Runtime + ?Sized> Runtime for &'r R { fn registers(&self) -> &super::Registers { ::registers(self) } + + fn render_mode(&self) -> &RenderingMode { + ::render_mode(self) + } } /// Create processing runtime for a template. pub struct RuntimeBuilder<'g, 'p> { globals: Option<&'g dyn ObjectView>, partials: Option<&'p dyn PartialStore>, + render_mode: RenderingMode, } impl<'c, 'g: 'c, 'p: 'c> RuntimeBuilder<'g, 'p> { @@ -92,6 +108,7 @@ impl<'c, 'g: 'c, 'p: 'c> RuntimeBuilder<'g, 'p> { Self { globals: None, partials: None, + render_mode: RenderingMode::Strict, } } @@ -100,6 +117,7 @@ impl<'c, 'g: 'c, 'p: 'c> RuntimeBuilder<'g, 'p> { RuntimeBuilder { globals: Some(values), partials: self.partials, + render_mode: self.render_mode, } } @@ -108,6 +126,16 @@ impl<'c, 'g: 'c, 'p: 'c> RuntimeBuilder<'g, 'p> { RuntimeBuilder { globals: self.globals, partials: Some(values), + render_mode: self.render_mode, + } + } + + /// Initialize with the provided rendering mode. + pub fn set_render_mode(self, mode: RenderingMode) -> RuntimeBuilder<'g, 'p> { + RuntimeBuilder { + globals: self.globals, + partials: self.partials, + render_mode: mode, } } @@ -116,6 +144,7 @@ impl<'c, 'g: 'c, 'p: 'c> RuntimeBuilder<'g, 'p> { let partials = self.partials.unwrap_or(&NullPartials); let runtime = RuntimeCore { partials, + render_mode: self.render_mode, ..Default::default() }; let runtime = super::IndexFrame::new(runtime); @@ -208,6 +237,8 @@ pub struct RuntimeCore<'g> { partials: &'g dyn PartialStore, registers: Registers, + + render_mode: RenderingMode, } impl<'g> RuntimeCore<'g> { @@ -268,6 +299,10 @@ impl<'g> Runtime for RuntimeCore<'g> { fn registers(&self) -> &Registers { &self.registers } + + fn render_mode(&self) -> &RenderingMode { + &self.render_mode + } } impl<'g> Default for RuntimeCore<'g> { @@ -275,6 +310,7 @@ impl<'g> Default for RuntimeCore<'g> { Self { partials: &NullPartials, registers: Default::default(), + render_mode: RenderingMode::Strict, } } } diff --git a/crates/core/src/runtime/stack.rs b/crates/core/src/runtime/stack.rs index e04601159..e6e876252 100644 --- a/crates/core/src/runtime/stack.rs +++ b/crates/core/src/runtime/stack.rs @@ -87,6 +87,10 @@ impl super::Runtime for StackFrame { fn registers(&self) -> &super::Registers { self.parent.registers() } + + fn render_mode(&self) -> &super::RenderingMode { + self.parent.render_mode() + } } pub(crate) struct GlobalFrame

{ @@ -162,6 +166,10 @@ impl super::Runtime for GlobalFrame

{ fn registers(&self) -> &super::Registers { self.parent.registers() } + + fn render_mode(&self) -> &super::RenderingMode { + self.parent.render_mode() + } } pub(crate) struct IndexFrame

{ @@ -237,4 +245,8 @@ impl super::Runtime for IndexFrame

{ fn registers(&self) -> &super::Registers { self.parent.registers() } + + fn render_mode(&self) -> &super::RenderingMode { + self.parent.render_mode() + } } diff --git a/src/parser.rs b/src/parser.rs index 980dd2833..2075f35e9 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -5,6 +5,7 @@ use std::sync; use liquid_core::error::{Result, ResultLiquidExt, ResultLiquidReplaceExt}; use liquid_core::parser; +use liquid_core::parser::ParseMode; use liquid_core::runtime; use super::Template; @@ -19,6 +20,7 @@ pub struct ParserBuilder

where P: partials::PartialCompiler, { + mode: parser::ParseMode, blocks: parser::PluginRegistry>, tags: parser::PluginRegistry>, filters: parser::PluginRegistry>, @@ -110,6 +112,12 @@ where .filter(stdlib::Where) } + /// Sets the parse mode to lax. + pub fn in_lax_mode(mut self) -> Self { + self.mode = ParseMode::Lax; + self + } + /// Inserts a new custom block into the parser pub fn block>>(mut self, block: B) -> Self { let block = block.into(); @@ -136,12 +144,14 @@ where /// Set which partial-templates will be available. pub fn partials(self, partials: N) -> ParserBuilder { let Self { + mode, blocks, tags, filters, partials: _partials, } = self; ParserBuilder { + mode, blocks, tags, filters, @@ -152,6 +162,7 @@ where /// Create a parser pub fn build(self) -> Result { let Self { + mode, blocks, tags, filters, @@ -159,6 +170,7 @@ where } = self; let mut options = parser::Language::empty(); + options.mode = mode; options.blocks = blocks; options.tags = tags; options.filters = filters; @@ -178,6 +190,7 @@ where { fn default() -> Self { Self { + mode: Default::default(), blocks: Default::default(), tags: Default::default(), filters: Default::default(), diff --git a/src/template.rs b/src/template.rs index 35d080fb9..fa3a0ae6b 100644 --- a/src/template.rs +++ b/src/template.rs @@ -5,6 +5,7 @@ use liquid_core::error::Result; use liquid_core::runtime; use liquid_core::runtime::PartialStore; use liquid_core::runtime::Renderable; +use liquid_core::runtime::RenderingMode; pub struct Template { pub(crate) template: runtime::Template, @@ -14,16 +15,51 @@ pub struct Template { impl Template { /// Renders an instance of the Template, using the given globals. pub fn render(&self, globals: &dyn crate::ObjectView) -> Result { + self.render_with_mode(globals, RenderingMode::Strict) + } + + /// Renders an instance of the Template, using the given globals. + pub fn render_to(&self, writer: &mut dyn Write, globals: &dyn crate::ObjectView) -> Result<()> { + self.render_to_with_mode(writer, globals, RenderingMode::Strict) + } + + /// Renders an instance of the Template, using the given globals in lax mode. + pub fn render_lax(&self, globals: &dyn crate::ObjectView) -> Result { + self.render_with_mode(globals, RenderingMode::Lax) + } + + /// Renders an instance of the Template, using the given globals in lax mode. + pub fn render_to_lax( + &self, + writer: &mut dyn Write, + globals: &dyn crate::ObjectView, + ) -> Result<()> { + self.render_to_with_mode(writer, globals, RenderingMode::Lax) + } + + /// Renders an instance of the Template, using the given globals with the provided rendering mode. + fn render_with_mode( + &self, + globals: &dyn crate::ObjectView, + mode: RenderingMode, + ) -> Result { const BEST_GUESS: usize = 10_000; let mut data = Vec::with_capacity(BEST_GUESS); - self.render_to(&mut data, globals)?; + self.render_to_with_mode(&mut data, globals, mode)?; Ok(convert_buffer(data)) } - /// Renders an instance of the Template, using the given globals. - pub fn render_to(&self, writer: &mut dyn Write, globals: &dyn crate::ObjectView) -> Result<()> { - let runtime = runtime::RuntimeBuilder::new().set_globals(globals); + /// Renders an instance of the Template, using the given globals with the provided rendering mode. + fn render_to_with_mode( + &self, + writer: &mut dyn Write, + globals: &dyn crate::ObjectView, + mode: RenderingMode, + ) -> Result<()> { + let runtime = runtime::RuntimeBuilder::new() + .set_globals(globals) + .set_render_mode(mode); let runtime = match self.partials { Some(ref partials) => runtime.set_partials(partials.as_ref()), None => runtime,