From 0a7e881362c4c9d3a8cafc56595f6c1ad8d4173c Mon Sep 17 00:00:00 2001 From: Ryan Amos Date: Mon, 9 Jan 2023 11:05:26 -0500 Subject: [PATCH 1/2] feat: Add support for lax rendering mode In lax rendering mode, an undefined variable will simply be rendered as a `nil` value which will ultimately result in an empty string. In strict rendering mode, an undefined variable will continue to return an error. --- crates/core/src/runtime/expression.rs | 37 +++++++++++++++++++++- crates/core/src/runtime/runtime.rs | 36 ++++++++++++++++++++++ crates/core/src/runtime/stack.rs | 12 ++++++++ src/template.rs | 44 ++++++++++++++++++++++++--- 4 files changed, 124 insertions(+), 5 deletions(-) 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/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, From 89871ec3d7c1f95847088ac8e54b373aadb095a4 Mon Sep 17 00:00:00 2001 From: Ryan Amos Date: Mon, 9 Jan 2023 15:22:37 -0500 Subject: [PATCH 2/2] feat: Add support for lax parsing mode In lax parsing mode, an undefined filter will simply return an internal noop filter. That filter will simply pass along the string to the next filter, if one is provided. In strict parsing mode, an undefined filter will continue to return an error. --- crates/core/src/parser/filter.rs | 16 ++++++++ crates/core/src/parser/lang.rs | 13 +++++++ crates/core/src/parser/parser.rs | 64 +++++++++++++++++++++++--------- src/parser.rs | 13 +++++++ 4 files changed, 89 insertions(+), 17 deletions(-) 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/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(),