Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional support for lax rendering and parsing #492

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 36 additions & 1 deletion crates/core/src/runtime/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}
}
36 changes: 36 additions & 0 deletions crates/core/src/runtime/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should have Copy, Clone, Debug, PartialEq, Eq, and Default

/// Returns an error when a variable is not defined.
Strict,
/// Replaces missing variables with an empty string.
Lax,
}
Comment on lines +10 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comments about ParseMode, I feel like this might be too general


/// State for rendering a template
pub trait Runtime {
/// Partial templates for inclusion.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -78,12 +89,17 @@ impl<'r, R: Runtime + ?Sized> Runtime for &'r R {
fn registers(&self) -> &super::Registers {
<R as Runtime>::registers(self)
}

fn render_mode(&self) -> &RenderingMode {
<R as Runtime>::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> {
Expand All @@ -92,6 +108,7 @@ impl<'c, 'g: 'c, 'p: 'c> RuntimeBuilder<'g, 'p> {
Self {
globals: None,
partials: None,
render_mode: RenderingMode::Strict,
}
}

Expand All @@ -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,
}
}

Expand All @@ -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,
}
}

Expand All @@ -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);
Expand Down Expand Up @@ -208,6 +237,8 @@ pub struct RuntimeCore<'g> {
partials: &'g dyn PartialStore,

registers: Registers,

render_mode: RenderingMode,
}

impl<'g> RuntimeCore<'g> {
Expand Down Expand Up @@ -268,13 +299,18 @@ 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> {
fn default() -> Self {
Self {
partials: &NullPartials,
registers: Default::default(),
render_mode: RenderingMode::Strict,
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions crates/core/src/runtime/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ impl<P: super::Runtime, O: ObjectView> super::Runtime for StackFrame<P, O> {
fn registers(&self) -> &super::Registers {
self.parent.registers()
}

fn render_mode(&self) -> &super::RenderingMode {
self.parent.render_mode()
}
}

pub(crate) struct GlobalFrame<P> {
Expand Down Expand Up @@ -162,6 +166,10 @@ impl<P: super::Runtime> super::Runtime for GlobalFrame<P> {
fn registers(&self) -> &super::Registers {
self.parent.registers()
}

fn render_mode(&self) -> &super::RenderingMode {
self.parent.render_mode()
}
}

pub(crate) struct IndexFrame<P> {
Expand Down Expand Up @@ -237,4 +245,8 @@ impl<P: super::Runtime> super::Runtime for IndexFrame<P> {
fn registers(&self) -> &super::Registers {
self.parent.registers()
}

fn render_mode(&self) -> &super::RenderingMode {
self.parent.render_mode()
}
}
44 changes: 40 additions & 4 deletions src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<String> {
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<String> {
self.render_with_mode(globals, RenderingMode::Lax)
}

Comment on lines +22 to +30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having a bunch of different render funtions, should this be stored with the Parser and passed in when this is created?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually originally started that route but decided against it.

When stored on the parser that means you're unable to determine which mode you'd want to use after the parser is created. Every template that's parsed would be stuck in that rendering mode. People would in theory, need to then create two separate parsers to work around that limitation. Admittedly, I'm not sure how frequent people would ever need to change the rendering mode of a parser. I don't personally have a use case to argue that.

Another option is we could expose a function to set the rendering mode on the Template instead. I'm not convinced that's a better UX, but probably a bit less confusing to most users who may use this solely in "strict" mode.

/// 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<String> {
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,
Expand Down