Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ pub enum RenderErrorReason {
MissingVariable(Option<String>),
#[error("Partial not found {0}")]
PartialNotFound(String),
#[error("Partial block cound not be found")]
PartialBlockNotFound,
#[error("Helper not found {0}")]
HelperNotFound(String),
#[error("Helper/Decorator {0} param at index {1} required but not found")]
Expand Down
221 changes: 159 additions & 62 deletions src/partial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::output::Output;
use crate::registry::Registry;
use crate::render::{Decorator, Evaluable, RenderContext, Renderable};
use crate::template::Template;
use crate::{Path, RenderErrorReason};
use crate::{Path, RenderErrorReason, StringOutput};

pub(crate) const PARTIAL_BLOCK: &str = "@partial-block";

Expand Down Expand Up @@ -59,74 +59,90 @@ pub fn expand_partial<'reg: 'rc, 'rc>(
return Err(RenderErrorReason::CannotIncludeSelf.into());
}

let partial = find_partial(rc, r, d, tname)?;

let Some(partial) = partial else {
return Err(RenderErrorReason::PartialNotFound(tname.to_owned()).into());
};

let is_partial_block = tname == PARTIAL_BLOCK;

// add partial block depth there are consecutive partial
// blocks in the stack.
if is_partial_block {
rc.inc_partial_block_depth();
} else {
// depth cannot be lower than 0, which is guaranted in the
// `dec_partial_block_depth` method
rc.dec_partial_block_depth();
}

// hash
let hash_ctx = d
.hash()
.iter()
.map(|(k, v)| (*k, v.value()))
.collect::<HashMap<&str, &Json>>();

let mut partial_include_block = BlockContext::new();
// evaluate context for partial
let merged_context = if let Some(p) = d.param(0) {
if let Some(relative_path) = p.relative_path() {
// path as parameter provided
merge_json(rc.evaluate(ctx, relative_path)?.as_json(), &hash_ctx)
// check if referencing partial_block
if tname == PARTIAL_BLOCK {
if let Some(Some(content)) = rc.peek_partial_block() {
out.write(content.as_str())?;
Ok(())
} else {
// literal provided
merge_json(p.value(), &hash_ctx)
// no partial_block for this scope
Err(RenderErrorReason::PartialBlockNotFound.into())
}
} else {
// use current path
merge_json(rc.evaluate2(ctx, &Path::current())?.as_json(), &hash_ctx)
};
partial_include_block.set_base_value(merged_context);

// replace and hold blocks from current render context
let current_blocks = rc.replace_blocks(VecDeque::with_capacity(1));
rc.push_block(partial_include_block);

// @partial-block
if let Some(pb) = d.template() {
rc.push_partial_block(pb);
}

// indent
rc.set_indent_string(d.indent().cloned());

let result = partial.render(r, ctx, rc, out);

// cleanup
let trailing_newline = rc.get_trailine_newline();
// normal partial
let partial = find_partial(rc, r, d, tname)?;
let Some(partial) = partial else {
return Err(RenderErrorReason::PartialNotFound(tname.to_owned()).into());
};

// check if this inclusion has a block
if let Some(current_parital_block) = d.template() {
let mut tmp_out = StringOutput::new();
current_parital_block.render(r, ctx, rc, &mut tmp_out)?;
rc.push_partial_block(Some(tmp_out.into_string()?));
} else {
rc.push_partial_block(None);
}

if d.template().is_some() {
// hash
let hash_ctx = d
.hash()
.iter()
.map(|(k, v)| (*k, v.value()))
.collect::<HashMap<&str, &Json>>();

let mut partial_include_block = BlockContext::new();
// evaluate context for partial
let merged_context = if let Some(p) = d.param(0) {
if let Some(relative_path) = p.relative_path() {
// path as parameter provided
if let Some(rc_context) = rc.context() {
merge_json(
rc.evaluate(&rc_context, relative_path)?.as_json(),
&hash_ctx,
)
} else {
merge_json(rc.evaluate(ctx, relative_path)?.as_json(), &hash_ctx)
}
} else {
// literal provided
merge_json(p.value(), &hash_ctx)
}
} else {
// use current path
if let Some(rc_context) = rc.context() {
merge_json(
rc.evaluate2(&rc_context, &Path::current())?.as_json(),
&hash_ctx,
)
} else {
merge_json(rc.evaluate2(ctx, &Path::current())?.as_json(), &hash_ctx)
}
};
partial_include_block.set_base_value(merged_context);

// replace and hold blocks from current render context
let current_blocks = rc.replace_blocks(VecDeque::with_capacity(1));
rc.push_block(partial_include_block);

// indent
rc.set_indent_string(d.indent().cloned());

let result = partial.render(r, ctx, rc, out);

// cleanup
let trailing_newline = rc.get_trailine_newline();

// remove current partial_block
rc.pop_partial_block();
}

let _ = rc.replace_blocks(current_blocks);
rc.set_trailing_newline(trailing_newline);
rc.set_current_template_name(current_template_before);
rc.set_indent_string(indent_before);
let _ = rc.replace_blocks(current_blocks);
rc.set_trailing_newline(trailing_newline);
rc.set_current_template_name(current_template_before);
rc.set_indent_string(indent_before);

result
result
}
}

#[cfg(test)]
Expand All @@ -136,6 +152,7 @@ mod test {
use crate::output::Output;
use crate::registry::Registry;
use crate::render::{Helper, RenderContext};
use crate::{Decorator, RenderErrorReason};

#[test]
fn test() {
Expand Down Expand Up @@ -811,4 +828,84 @@ outer third line",

assert_eq!("a:1,", hbs.render("t2", &json!({"b": 2})).unwrap());
}

#[test]
fn test_nested_partial_block_scope_issue() {
let mut hs = Registry::new();
hs.register_template_string("primary", "{{> @partial-block }}")
.unwrap();
hs.register_template_string("secondary", "{{#*inline \"inl\"}}Bug{{/inline}}{{#>primary}}{{> @partial-block }}{{>inl}}{{/primary}}").unwrap();
hs.register_template_string("current", "{{>secondary}}")
.unwrap();

assert!(matches!(
hs.render("current", &()).unwrap_err().reason(),
RenderErrorReason::PartialBlockNotFound
));

let mut hs = Registry::new();
hs.register_template_string("primary", "{{> @partial-block }}")
.unwrap();
hs.register_template_string("secondary", "{{#*inline \"inl\"}}Bug{{/inline}}{{#>primary}}{{> @partial-block }}{{>inl}}{{/primary}}").unwrap();
hs.register_template_string("current", "{{#>secondary}}Not a {{/secondary}}")
.unwrap();

assert_eq!(hs.render("current", &()).unwrap(), "Not a Bug");
}

#[test]
fn test_referencing_data_in_partial() {
fn set_decorator(
d: &Decorator<'_>,
_: &Registry<'_>,
_ctx: &Context,
rc: &mut RenderContext<'_, '_>,
) -> Result<(), RenderError> {
let data_to_set = d.hash();
for (k, v) in data_to_set {
set_in_context(rc, k, v.value().clone());
}
Ok(())
}

/// Sets a variable to a value within the context.
fn set_in_context(rc: &mut RenderContext<'_, '_>, key: &str, value: serde_json::Value) {
let mut gctx = match rc.context() {
Some(c) => (*c).clone(),
None => Context::wraps(serde_json::Value::Object(serde_json::Map::new())).unwrap(),
};
if let serde_json::Value::Object(m) = gctx.data_mut() {
m.insert(key.to_string(), value);
rc.set_context(gctx);
} else {
panic!("expected object in context");
}
}

let mut handlebars = Registry::new();
handlebars.register_decorator("set", Box::new(set_decorator));

handlebars_helper!(lower: |s: str| s.to_lowercase());
handlebars.register_helper("lower", Box::new(lower));
handlebars
.register_template_string(
"an-included-file",
"This file is included.\n\nSee {{lower somevalue}}\n",
)
.unwrap();

let data = serde_json::json!({});
assert_eq!(
handlebars
.render_template(
r#"
{{~*set somevalue="Example"}}
{{> an-included-file }}
"#,
&data
)
.unwrap(),
"This file is included.\n\nSee example\n"
);
}
}
34 changes: 11 additions & 23 deletions src/render.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::borrow::{Borrow, Cow};
use std::borrow::Cow;
use std::collections::{BTreeMap, VecDeque};
use std::fmt;
use std::rc::Rc;
Expand Down Expand Up @@ -43,8 +43,11 @@ pub struct RenderContext<'reg: 'rc, 'rc> {
modified_context: Option<Rc<Context>>,

partials: BTreeMap<String, &'rc Template>,
partial_block_stack: VecDeque<&'rc Template>,
partial_block_depth: isize,
// when rendering partials, store rendered string into partial_block_stack
// for `@partial-block` referencing. If it's a `{{> partial}}`, push `None`
// to stack and report error when child partial try to referencing
// `@partial-block`.
partial_block_stack: VecDeque<Option<String>>,
local_helpers: BTreeMap<String, Rc<dyn HelperDef + Send + Sync + 'rc>>,
/// current template name
current_template: Option<&'rc String>,
Expand Down Expand Up @@ -79,7 +82,6 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> {
RenderContext {
partials: BTreeMap::new(),
partial_block_stack: VecDeque::new(),
partial_block_depth: 0,
local_helpers: BTreeMap::new(),
current_template: None,
root_template,
Expand Down Expand Up @@ -174,12 +176,6 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> {

/// Get registered partial in this render context
pub fn get_partial(&self, name: &str) -> Option<&'rc Template> {
if name == partial::PARTIAL_BLOCK {
return self
.partial_block_stack
.get(self.partial_block_depth as usize)
.copied();
}
self.partials.get(name).copied()
}

Expand All @@ -188,23 +184,16 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> {
self.partials.insert(name, partial);
}

pub(crate) fn push_partial_block(&mut self, partial: &'rc Template) {
self.partial_block_stack.push_front(partial);
pub(crate) fn push_partial_block(&mut self, partial_block: Option<String>) {
self.partial_block_stack.push_front(partial_block);
}

pub(crate) fn pop_partial_block(&mut self) {
self.partial_block_stack.pop_front();
}

pub(crate) fn inc_partial_block_depth(&mut self) {
self.partial_block_depth += 1;
}

pub(crate) fn dec_partial_block_depth(&mut self) {
let depth = &mut self.partial_block_depth;
if *depth > 0 {
*depth -= 1;
}
pub(crate) fn peek_partial_block(&self) -> Option<&Option<String>> {
self.partial_block_stack.front()
}

pub(crate) fn set_indent_string(&mut self, indent: Option<Cow<'rc, str>>) {
Expand Down Expand Up @@ -342,7 +331,6 @@ impl fmt::Debug for RenderContext<'_, '_> {
.field("modified_context", &self.modified_context)
.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)
Expand Down Expand Up @@ -683,7 +671,7 @@ impl Parameter {
}
Parameter::Path(ref path) => {
if let Some(rc_context) = rc.context() {
let result = rc.evaluate2(rc_context.borrow(), path)?;
let result = rc.evaluate2(&rc_context, path)?;
Ok(PathAndJson::new(
Some(path.raw().to_owned()),
ScopedJson::Derived(result.as_json().clone()),
Expand Down
Loading