1022 lines
42 KiB
Rust
1022 lines
42 KiB
Rust
![]() |
use crate::parser::template;
|
||
|
use crate::parser::Body;
|
||
|
use crate::parser::DustTag;
|
||
|
use crate::parser::KVPair;
|
||
|
use crate::parser::PartialNameElement;
|
||
|
use crate::parser::Path;
|
||
|
use crate::parser::RValue;
|
||
|
use crate::parser::Special;
|
||
|
use crate::parser::Template;
|
||
|
use crate::parser::{Filter, TemplateElement};
|
||
|
use crate::renderer::context_element::ContextElement;
|
||
|
use crate::renderer::context_element::IntoContextElement;
|
||
|
use crate::renderer::errors::CompileError;
|
||
|
use crate::renderer::errors::RenderError;
|
||
|
use crate::renderer::errors::WalkError;
|
||
|
use crate::renderer::inline_partial_tree::extract_inline_partials;
|
||
|
use crate::renderer::inline_partial_tree::InlinePartialTreeElement;
|
||
|
use crate::renderer::iteration_context::IterationContext;
|
||
|
use crate::renderer::parameters_context::ParametersContext;
|
||
|
use crate::renderer::walking::walk_path;
|
||
|
use std::borrow::Borrow;
|
||
|
use std::collections::HashMap;
|
||
|
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub struct CompiledTemplate<'a> {
|
||
|
template: Template<'a>,
|
||
|
pub name: String,
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub struct DustRenderer<'a> {
|
||
|
templates: HashMap<String, &'a Template<'a>>,
|
||
|
}
|
||
|
|
||
|
pub fn compile_template<'a>(
|
||
|
source: &'a str,
|
||
|
name: String,
|
||
|
) -> Result<CompiledTemplate<'a>, CompileError> {
|
||
|
// TODO: This could use better error management
|
||
|
let (_remaining, parsed_template) = template(source).expect("Failed to compile template");
|
||
|
Ok(CompiledTemplate {
|
||
|
template: parsed_template,
|
||
|
name: name,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
impl<'a> DustRenderer<'a> {
|
||
|
pub fn new() -> DustRenderer<'a> {
|
||
|
DustRenderer {
|
||
|
templates: HashMap::new(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn load_source(&mut self, template: &'a CompiledTemplate) {
|
||
|
self.templates
|
||
|
.insert(template.name.clone(), &template.template);
|
||
|
}
|
||
|
|
||
|
pub fn render(
|
||
|
&'a self,
|
||
|
name: &str,
|
||
|
breadcrumbs: &Vec<&'a dyn IntoContextElement>,
|
||
|
) -> Result<String, RenderError> {
|
||
|
self.render_template(name, breadcrumbs, None)
|
||
|
}
|
||
|
|
||
|
fn render_template(
|
||
|
&'a self,
|
||
|
name: &str,
|
||
|
breadcrumbs: &Vec<&'a dyn IntoContextElement>,
|
||
|
blocks: Option<&'a InlinePartialTreeElement<'a>>,
|
||
|
) -> Result<String, RenderError> {
|
||
|
let main_template = match self.templates.get(name) {
|
||
|
Some(tmpl) => tmpl,
|
||
|
None => {
|
||
|
return Err(RenderError::TemplateNotFound(name.to_owned()));
|
||
|
}
|
||
|
};
|
||
|
let extracted_inline_partials = extract_inline_partials(main_template);
|
||
|
let new_blocks = InlinePartialTreeElement::new(blocks, extracted_inline_partials);
|
||
|
let new_block_context = BlockContext {
|
||
|
breadcrumbs: breadcrumbs,
|
||
|
blocks: &new_blocks,
|
||
|
};
|
||
|
self.render_body(&main_template.contents, breadcrumbs, &new_block_context)
|
||
|
}
|
||
|
|
||
|
fn render_maybe_body(
|
||
|
&'a self,
|
||
|
body: &'a Option<Body>,
|
||
|
breadcrumbs: &Vec<&'a dyn IntoContextElement>,
|
||
|
blocks: &'a BlockContext<'a>,
|
||
|
) -> Result<String, RenderError> {
|
||
|
match body {
|
||
|
None => Ok("".to_owned()),
|
||
|
Some(body) => Ok(self.render_body(body, breadcrumbs, blocks)?),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn render_body(
|
||
|
&'a self,
|
||
|
body: &'a Body,
|
||
|
breadcrumbs: &Vec<&'a dyn IntoContextElement>,
|
||
|
blocks: &'a BlockContext<'a>,
|
||
|
) -> Result<String, RenderError> {
|
||
|
let mut output = String::new();
|
||
|
for elem in &body.elements {
|
||
|
match elem {
|
||
|
TemplateElement::TEIgnoredWhitespace(_) => {}
|
||
|
TemplateElement::TESpan(span) => output.push_str(span.contents),
|
||
|
TemplateElement::TETag(dt) => {
|
||
|
output.push_str(&self.render_tag(dt, breadcrumbs, blocks)?);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
Ok(output)
|
||
|
}
|
||
|
|
||
|
/// For rendering a dynamic partial's name or an rvalue template
|
||
|
pub fn render_partial_name(
|
||
|
&'a self,
|
||
|
body: &'a Vec<PartialNameElement>,
|
||
|
breadcrumbs: &Vec<&'a dyn IntoContextElement>,
|
||
|
) -> Result<String, RenderError> {
|
||
|
let converted_to_template_elements: Vec<TemplateElement<'a>> =
|
||
|
body.into_iter().map(|e| e.into()).collect();
|
||
|
// Simple templates like partial names and reference rvalues
|
||
|
// cannot contain blocks or inline partials, so we use a blank
|
||
|
// BlockContext.
|
||
|
let empty_block_context = BlockContext {
|
||
|
breadcrumbs: &Vec::new(),
|
||
|
blocks: &InlinePartialTreeElement::new(None, HashMap::new()),
|
||
|
};
|
||
|
self.render_body(
|
||
|
&Body {
|
||
|
elements: converted_to_template_elements,
|
||
|
},
|
||
|
breadcrumbs,
|
||
|
&empty_block_context,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
fn render_tag(
|
||
|
&'a self,
|
||
|
tag: &'a DustTag,
|
||
|
breadcrumbs: &Vec<&'a dyn IntoContextElement>,
|
||
|
blocks: &'a BlockContext<'a>,
|
||
|
) -> Result<String, RenderError> {
|
||
|
match tag {
|
||
|
DustTag::DTComment(_comment) => (),
|
||
|
DustTag::DTSpecial(special) => {
|
||
|
return Ok(match special {
|
||
|
Special::Space => " ",
|
||
|
Special::NewLine => "\n",
|
||
|
Special::CarriageReturn => "\r",
|
||
|
Special::LeftCurlyBrace => "{",
|
||
|
Special::RightCurlyBrace => "}",
|
||
|
}
|
||
|
.to_owned())
|
||
|
}
|
||
|
DustTag::DTLiteralStringBlock(literal) => return Ok((*literal).to_owned()),
|
||
|
DustTag::DTReference(reference) => {
|
||
|
let val = walk_path(breadcrumbs, &reference.path.keys)
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
match val {
|
||
|
Err(WalkError::CantWalk) => return Ok("".to_owned()),
|
||
|
Ok(final_val) => {
|
||
|
return if final_val.is_truthy() {
|
||
|
final_val.render(&Self::preprocess_filters(&reference.filters))
|
||
|
} else {
|
||
|
Ok("".to_owned())
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
DustTag::DTSection(container) => {
|
||
|
let injected_context = ParametersContext::new(breadcrumbs, &container.params);
|
||
|
let val = walk_path(breadcrumbs, &container.path.keys)
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
match val {
|
||
|
Err(WalkError::CantWalk) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_section(
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
Some(&injected_context),
|
||
|
&container.explicit_context,
|
||
|
None,
|
||
|
);
|
||
|
return self.render_maybe_body(
|
||
|
&container.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
}
|
||
|
Ok(final_val) => {
|
||
|
return if final_val.is_truthy() {
|
||
|
match &container.contents {
|
||
|
// If the body is empty, just shortcut
|
||
|
// to an empty string now rather than
|
||
|
// generating intermediate contexts
|
||
|
// and iterating for nothing.
|
||
|
None => Ok("".to_owned()),
|
||
|
Some(body) => {
|
||
|
let loop_elements: Vec<&dyn ContextElement> =
|
||
|
final_val.get_loop_elements();
|
||
|
if loop_elements.is_empty() {
|
||
|
// Scalar value
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_section(
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
Some(&injected_context),
|
||
|
&container.explicit_context,
|
||
|
Some(final_val),
|
||
|
);
|
||
|
self.render_body(
|
||
|
body,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
} else {
|
||
|
// Array-like value
|
||
|
let total_length = loop_elements.len();
|
||
|
let rendered_results: Result<Vec<String>, RenderError> =
|
||
|
loop_elements
|
||
|
.into_iter()
|
||
|
.enumerate()
|
||
|
.map(|(i, array_elem)| {
|
||
|
let index_context =
|
||
|
IterationContext::new(i, total_length);
|
||
|
let new_breadcrumbs = self
|
||
|
.new_breadcrumbs_section(
|
||
|
breadcrumbs,
|
||
|
Some(&index_context),
|
||
|
Some(&injected_context),
|
||
|
&container.explicit_context,
|
||
|
Some(array_elem),
|
||
|
);
|
||
|
self.render_body(
|
||
|
&body,
|
||
|
new_breadcrumbs
|
||
|
.as_ref()
|
||
|
.unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
})
|
||
|
.collect();
|
||
|
let rendered_slice: &[String] = &rendered_results?;
|
||
|
return Ok(rendered_slice.join(""));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
// Oddly enough if the value is falsey (like
|
||
|
// an empty array or null), Dust uses the
|
||
|
// original context before walking the path as
|
||
|
// the context for rendering the else block
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_section(
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
Some(&injected_context),
|
||
|
&container.explicit_context,
|
||
|
None,
|
||
|
);
|
||
|
return self.render_maybe_body(
|
||
|
&container.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
DustTag::DTExists(container) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
&container.explicit_context,
|
||
|
);
|
||
|
let val = walk_path(breadcrumbs, &container.path.keys)
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
return if val.map(|v| v.is_truthy()).unwrap_or(false) {
|
||
|
self.render_maybe_body(
|
||
|
&container.contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
} else {
|
||
|
self.render_maybe_body(
|
||
|
&container.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
};
|
||
|
}
|
||
|
DustTag::DTNotExists(container) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
&container.explicit_context,
|
||
|
);
|
||
|
let val = walk_path(breadcrumbs, &container.path.keys)
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
return if !val.map(|v| v.is_truthy()).unwrap_or(false) {
|
||
|
self.render_maybe_body(
|
||
|
&container.contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
} else {
|
||
|
self.render_maybe_body(
|
||
|
&container.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
};
|
||
|
}
|
||
|
DustTag::DTPartial(partial) => {
|
||
|
let partial_name = self.render_partial_name(&partial.name, breadcrumbs)?;
|
||
|
if partial.params.is_empty() {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
&partial.explicit_context,
|
||
|
);
|
||
|
let rendered_content = self.render_template(
|
||
|
&partial_name,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
Some(blocks.blocks),
|
||
|
)?;
|
||
|
return Ok(rendered_content);
|
||
|
} else {
|
||
|
let injected_context = ParametersContext::new(breadcrumbs, &partial.params);
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
Some(&injected_context),
|
||
|
&partial.explicit_context,
|
||
|
);
|
||
|
let rendered_content = self.render_template(
|
||
|
&partial_name,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
Some(blocks.blocks),
|
||
|
)?;
|
||
|
return Ok(rendered_content);
|
||
|
}
|
||
|
}
|
||
|
DustTag::DTInlinePartial(_named_block) => {
|
||
|
// Inline partials are blank during rendering (they get injected into blocks)
|
||
|
return Ok("".to_owned());
|
||
|
}
|
||
|
DustTag::DTBlock(named_block) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
blocks.breadcrumbs,
|
||
|
None,
|
||
|
&named_block.explicit_context,
|
||
|
);
|
||
|
return match blocks.blocks.get_block(named_block.path.keys[0]) {
|
||
|
None => self.render_maybe_body(
|
||
|
&named_block.contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
),
|
||
|
Some(inline_partial) => self.render_maybe_body(
|
||
|
inline_partial,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
),
|
||
|
};
|
||
|
}
|
||
|
DustTag::DTHelperEquals(parameterized_block) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
¶meterized_block.explicit_context,
|
||
|
);
|
||
|
let param_map: HashMap<&str, &RValue<'a>> =
|
||
|
Self::get_rval_map(¶meterized_block.params);
|
||
|
// Special case: when comparing two RVPaths, if the
|
||
|
// path is equal then dust assumes the values are
|
||
|
// equal (otherwise, non-scalar values are
|
||
|
// automatically not equal)
|
||
|
if Self::are_paths_identical(¶m_map) {
|
||
|
return match ¶meterized_block.contents {
|
||
|
None => Ok("".to_owned()),
|
||
|
Some(body) => {
|
||
|
let rendered_content = self.render_body(
|
||
|
body,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)?;
|
||
|
Ok(rendered_content)
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let left_side: Result<&dyn ContextElement, WalkError> =
|
||
|
match Self::get_rval(breadcrumbs, ¶m_map, "key") {
|
||
|
None => return Ok("".to_owned()),
|
||
|
Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)),
|
||
|
};
|
||
|
let right_side: Result<&dyn ContextElement, WalkError> =
|
||
|
Self::get_rval(breadcrumbs, ¶m_map, "value")
|
||
|
.unwrap_or(Err(WalkError::CantWalk))
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
|
||
|
if left_side == right_side {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
} else {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
DustTag::DTHelperNotEquals(parameterized_block) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
¶meterized_block.explicit_context,
|
||
|
);
|
||
|
let param_map: HashMap<&str, &RValue<'a>> =
|
||
|
Self::get_rval_map(¶meterized_block.params);
|
||
|
// Special case: when comparing two RVPaths, if the
|
||
|
// path is equal then dust assumes the values are
|
||
|
// equal (otherwise, non-scalar values are
|
||
|
// automatically not equal)
|
||
|
if Self::are_paths_identical(¶m_map) {
|
||
|
return match ¶meterized_block.else_contents {
|
||
|
None => Ok("".to_owned()),
|
||
|
Some(body) => {
|
||
|
let rendered_content = self.render_body(
|
||
|
body,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)?;
|
||
|
Ok(rendered_content)
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
let left_side: Result<&dyn ContextElement, WalkError> =
|
||
|
match Self::get_rval(breadcrumbs, ¶m_map, "key") {
|
||
|
None => return Ok("".to_owned()),
|
||
|
Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)),
|
||
|
};
|
||
|
let right_side: Result<&dyn ContextElement, WalkError> =
|
||
|
Self::get_rval(breadcrumbs, ¶m_map, "value")
|
||
|
.unwrap_or(Err(WalkError::CantWalk))
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
if left_side != right_side {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
} else {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
DustTag::DTHelperGreaterThan(parameterized_block) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
¶meterized_block.explicit_context,
|
||
|
);
|
||
|
let param_map: HashMap<&str, &RValue<'a>> =
|
||
|
Self::get_rval_map(¶meterized_block.params);
|
||
|
let left_side: Result<&dyn ContextElement, WalkError> =
|
||
|
match Self::get_rval(breadcrumbs, ¶m_map, "key") {
|
||
|
None => return Ok("".to_owned()),
|
||
|
Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)),
|
||
|
};
|
||
|
let right_side: Result<&dyn ContextElement, WalkError> =
|
||
|
Self::get_rval(breadcrumbs, ¶m_map, "value")
|
||
|
.unwrap_or(Err(WalkError::CantWalk))
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
match (left_side, right_side) {
|
||
|
(Err(_), _) | (_, Err(_)) => {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
}
|
||
|
(Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => {
|
||
|
if left_side_unwrapped > right_side_unwrapped {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
} else {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
DustTag::DTHelperGreaterThanOrEquals(parameterized_block) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
¶meterized_block.explicit_context,
|
||
|
);
|
||
|
let param_map: HashMap<&str, &RValue<'a>> =
|
||
|
Self::get_rval_map(¶meterized_block.params);
|
||
|
let left_side: Result<&dyn ContextElement, WalkError> =
|
||
|
match Self::get_rval(breadcrumbs, ¶m_map, "key") {
|
||
|
None => return Ok("".to_owned()),
|
||
|
Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)),
|
||
|
};
|
||
|
let right_side: Result<&dyn ContextElement, WalkError> =
|
||
|
Self::get_rval(breadcrumbs, ¶m_map, "value")
|
||
|
.unwrap_or(Err(WalkError::CantWalk))
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
match (left_side, right_side) {
|
||
|
(Err(_), _) | (_, Err(_)) => {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
}
|
||
|
(Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => {
|
||
|
if left_side_unwrapped >= right_side_unwrapped {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
} else {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
DustTag::DTHelperLessThan(parameterized_block) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
¶meterized_block.explicit_context,
|
||
|
);
|
||
|
let param_map: HashMap<&str, &RValue<'a>> =
|
||
|
Self::get_rval_map(¶meterized_block.params);
|
||
|
let left_side: Result<&dyn ContextElement, WalkError> =
|
||
|
match Self::get_rval(breadcrumbs, ¶m_map, "key") {
|
||
|
None => return Ok("".to_owned()),
|
||
|
Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)),
|
||
|
};
|
||
|
let right_side: Result<&dyn ContextElement, WalkError> =
|
||
|
Self::get_rval(breadcrumbs, ¶m_map, "value")
|
||
|
.unwrap_or(Err(WalkError::CantWalk))
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
match (left_side, right_side) {
|
||
|
(Err(_), _) | (_, Err(_)) => {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
}
|
||
|
(Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => {
|
||
|
if left_side_unwrapped < right_side_unwrapped {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
} else {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
DustTag::DTHelperLessThanOrEquals(parameterized_block) => {
|
||
|
let new_breadcrumbs = self.new_breadcrumbs_partial(
|
||
|
breadcrumbs,
|
||
|
breadcrumbs,
|
||
|
None,
|
||
|
¶meterized_block.explicit_context,
|
||
|
);
|
||
|
let param_map: HashMap<&str, &RValue<'a>> =
|
||
|
Self::get_rval_map(¶meterized_block.params);
|
||
|
let left_side: Result<&dyn ContextElement, WalkError> =
|
||
|
match Self::get_rval(breadcrumbs, ¶m_map, "key") {
|
||
|
None => return Ok("".to_owned()),
|
||
|
Some(res) => res.map(|ice| ice.into_context_element(self, breadcrumbs)),
|
||
|
};
|
||
|
let right_side: Result<&dyn ContextElement, WalkError> =
|
||
|
Self::get_rval(breadcrumbs, ¶m_map, "value")
|
||
|
.unwrap_or(Err(WalkError::CantWalk))
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs));
|
||
|
match (left_side, right_side) {
|
||
|
(Err(_), _) | (_, Err(_)) => {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
)
|
||
|
}
|
||
|
(Ok(left_side_unwrapped), Ok(right_side_unwrapped)) => {
|
||
|
if left_side_unwrapped <= right_side_unwrapped {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
} else {
|
||
|
return self.render_maybe_body(
|
||
|
¶meterized_block.else_contents,
|
||
|
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
|
||
|
blocks,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
Ok("".to_owned())
|
||
|
}
|
||
|
|
||
|
/// Gets the elements to loop over for a section.
|
||
|
///
|
||
|
/// If the value is falsey, and therefore should render the else
|
||
|
/// block, this will return an empty vector.
|
||
|
fn get_loop_elements<'b>(
|
||
|
walk_result: Result<&'b dyn ContextElement, WalkError>,
|
||
|
) -> Vec<&'b dyn ContextElement> {
|
||
|
match walk_result {
|
||
|
Err(WalkError::CantWalk) => Vec::new(),
|
||
|
Ok(walk_target) => walk_target.get_loop_elements(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn are_paths_identical<'b>(param_map: &HashMap<&str, &RValue<'b>>) -> bool {
|
||
|
match (param_map.get("key"), param_map.get("value")) {
|
||
|
(None, _) => false,
|
||
|
(_, None) => false,
|
||
|
(Some(key_rval), Some(value_rval)) => match (key_rval, value_rval) {
|
||
|
(RValue::RVPath(key_path), RValue::RVPath(value_path)) => {
|
||
|
key_path.keys == value_path.keys
|
||
|
}
|
||
|
_ => false,
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn get_rval_map<'b>(params: &'b Vec<KVPair<'b>>) -> HashMap<&'b str, &'b RValue<'b>> {
|
||
|
params
|
||
|
.iter()
|
||
|
.map(|pair: &KVPair<'b>| (pair.key, &pair.value))
|
||
|
.collect()
|
||
|
}
|
||
|
|
||
|
fn get_rval<'b>(
|
||
|
breadcrumbs: &'b Vec<&'b dyn IntoContextElement>,
|
||
|
param_map: &HashMap<&str, &'b RValue<'b>>,
|
||
|
key: &str,
|
||
|
) -> Option<Result<&'b dyn IntoContextElement, WalkError>> {
|
||
|
match param_map.get(key) {
|
||
|
None => None,
|
||
|
Some(rval) => match rval {
|
||
|
RValue::RVLiteral(literal) => Some(Ok(literal)),
|
||
|
RValue::RVTemplate(template) => None,
|
||
|
// TODO: Do I resolve the IntoContextElement here for RVPath?
|
||
|
RValue::RVPath(path) => Some(walk_path(breadcrumbs, &path.keys)),
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn preprocess_filters(filters: &Vec<Filter>) -> Vec<Filter> {
|
||
|
let mut final_filters: Vec<Filter> = filters
|
||
|
.into_iter()
|
||
|
.filter(|f| f != &&Filter::DisableHtmlEncode)
|
||
|
.map(|f| f.clone())
|
||
|
.collect();
|
||
|
|
||
|
// If the user has not specified any escaping filter (|s or
|
||
|
// |h), automatically add an html escape filter
|
||
|
if !filters.iter().any(|f| f == &Filter::DisableHtmlEncode) {
|
||
|
final_filters.push(Filter::HtmlEncode);
|
||
|
}
|
||
|
final_filters
|
||
|
}
|
||
|
|
||
|
fn new_breadcrumbs_section<'b>(
|
||
|
&self,
|
||
|
breadcrumbs: &'b Vec<&'b dyn IntoContextElement>,
|
||
|
index_context: Option<&'b dyn IntoContextElement>,
|
||
|
injected_context: Option<&'b dyn IntoContextElement>,
|
||
|
explicit_context: &Option<Path<'b>>,
|
||
|
new_context_element: Option<&'b dyn ContextElement>,
|
||
|
) -> Option<Vec<&'b dyn IntoContextElement>> {
|
||
|
// If none of the additional contexts are present, return None
|
||
|
// to signal that the original breadcrumbs should be used
|
||
|
// rather than incurring a copy here.
|
||
|
match (
|
||
|
index_context,
|
||
|
injected_context,
|
||
|
explicit_context,
|
||
|
new_context_element,
|
||
|
) {
|
||
|
(None, None, None, None) => return None,
|
||
|
_ => (),
|
||
|
}
|
||
|
let mut new_stack = match explicit_context {
|
||
|
Some(_) => Vec::with_capacity(4),
|
||
|
None => breadcrumbs.clone(),
|
||
|
};
|
||
|
explicit_context.as_ref().map(|path| {
|
||
|
walk_path(breadcrumbs, &path.keys)
|
||
|
.map(|ice| ice.into_context_element(self, breadcrumbs))
|
||
|
.map(|val| {
|
||
|
if val.is_truthy() {
|
||
|
new_stack.push(val.from_context_element())
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
injected_context.map(|ctx| new_stack.push(ctx));
|
||
|
new_context_element.map(|ctx| new_stack.push(ctx.from_context_element()));
|
||
|
index_context.map(|ctx| new_stack.push(ctx));
|
||
|
Some(new_stack)
|
||
|
}
|
||
|
|
||
|
fn new_breadcrumbs_partial<'b>(
|
||
|
&self,
|
||
|
breadcrumbs: &'b Vec<&'b dyn IntoContextElement>,
|
||
|
explicit_context_breadcrumbs: &'b Vec<&'b dyn IntoContextElement>,
|
||
|
injected_context: Option<&'b dyn IntoContextElement>,
|
||
|
explicit_context: &Option<Path<'b>>,
|
||
|
) -> Option<Vec<&'b dyn IntoContextElement>> {
|
||
|
// If none of the additional contexts are present, return None
|
||
|
// to signal that the original breadcrumbs should be used
|
||
|
// rather than incurring a copy here.
|
||
|
match (injected_context, explicit_context) {
|
||
|
(None, None) => return None,
|
||
|
_ => (),
|
||
|
};
|
||
|
let mut new_stack = match explicit_context {
|
||
|
Some(_) => Vec::with_capacity(3),
|
||
|
None => breadcrumbs.clone(),
|
||
|
};
|
||
|
injected_context.map(|ctx| {
|
||
|
// Special case: when there is no explicit context, the
|
||
|
// injected context gets inserted 1 spot behind the
|
||
|
// current context. Otherwise, the injected context gets
|
||
|
// added after the current context but before the explicit
|
||
|
// context.
|
||
|
match explicit_context {
|
||
|
None => new_stack.insert(
|
||
|
Self::get_index_of_first_non_pseudo_element(&new_stack).unwrap_or(0),
|
||
|
ctx,
|
||
|
),
|
||
|
_ => new_stack.push(ctx),
|
||
|
}
|
||
|
});
|
||
|
explicit_context.as_ref().map(|path| {
|
||
|
walk_path(explicit_context_breadcrumbs, &path.keys)
|
||
|
// TODO should resolving the value here use explicit_context_breadcrumbs or breadcrumbs?
|
||
|
.map(|ice| ice.into_context_element(self, explicit_context_breadcrumbs))
|
||
|
.map(|val| {
|
||
|
if val.is_truthy() {
|
||
|
new_stack.push(val.from_context_element())
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
Some(new_stack)
|
||
|
}
|
||
|
|
||
|
fn get_index_of_first_non_pseudo_element<'b, B>(breadcrumbs: &'b Vec<B>) -> Option<usize>
|
||
|
where
|
||
|
B: Borrow<dyn IntoContextElement + 'a>,
|
||
|
{
|
||
|
breadcrumbs
|
||
|
.iter()
|
||
|
.rposition(|b| !(*b).borrow().is_pseudo_element())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct BlockContext<'a> {
|
||
|
/// The breadcrumbs at the time of entering the current partial
|
||
|
breadcrumbs: &'a Vec<&'a dyn IntoContextElement>,
|
||
|
blocks: &'a InlinePartialTreeElement<'a>,
|
||
|
}
|
||
|
|
||
|
#[cfg(test)]
|
||
|
mod tests {
|
||
|
use super::*;
|
||
|
use crate::parser::Filter;
|
||
|
use crate::renderer::context_element::Loopable;
|
||
|
use crate::renderer::context_element::Renderable;
|
||
|
use crate::renderer::context_element::Truthiness;
|
||
|
use crate::renderer::context_element::Walkable;
|
||
|
use crate::renderer::CompareContextElement;
|
||
|
use std::cmp::Ordering;
|
||
|
|
||
|
impl ContextElement for String {}
|
||
|
|
||
|
impl Truthiness for String {
|
||
|
fn is_truthy(&self) -> bool {
|
||
|
!self.is_empty()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Renderable for String {
|
||
|
fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
|
||
|
Ok(self.clone())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Loopable for String {
|
||
|
fn get_loop_elements(&self) -> Vec<&dyn ContextElement> {
|
||
|
Vec::new()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Walkable for String {
|
||
|
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> {
|
||
|
Err(WalkError::CantWalk)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl CompareContextElement for String {
|
||
|
fn equals(&self, other: &dyn ContextElement) -> bool {
|
||
|
match other.to_any().downcast_ref::<Self>() {
|
||
|
None => false,
|
||
|
Some(other_string) => self == other_string,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn partial_compare(&self, other: &dyn ContextElement) -> Option<Ordering> {
|
||
|
match other.to_any().downcast_ref::<Self>() {
|
||
|
None => None,
|
||
|
Some(other_string) => self.partial_cmp(other_string),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
impl ContextElement for u64 {}
|
||
|
|
||
|
impl Truthiness for u64 {
|
||
|
fn is_truthy(&self) -> bool {
|
||
|
true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Renderable for u64 {
|
||
|
fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
|
||
|
Ok(self.to_string())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Loopable for u64 {
|
||
|
fn get_loop_elements(&self) -> Vec<&dyn ContextElement> {
|
||
|
Vec::new()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Walkable for u64 {
|
||
|
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> {
|
||
|
Err(WalkError::CantWalk)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl CompareContextElement for u64 {
|
||
|
fn equals(&self, other: &dyn ContextElement) -> bool {
|
||
|
match other.to_any().downcast_ref::<Self>() {
|
||
|
None => false,
|
||
|
Some(other_num) => self == other_num,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn partial_compare(&self, other: &dyn ContextElement) -> Option<Ordering> {
|
||
|
match other.to_any().downcast_ref::<Self>() {
|
||
|
None => None,
|
||
|
Some(other_num) => self.partial_cmp(other_num),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl<I: 'static + ContextElement + Clone> ContextElement for HashMap<String, I> {}
|
||
|
|
||
|
impl<I: ContextElement> Truthiness for HashMap<String, I> {
|
||
|
fn is_truthy(&self) -> bool {
|
||
|
true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl<I: ContextElement> Renderable for HashMap<String, I> {
|
||
|
fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
|
||
|
// TODO: handle the filters
|
||
|
Ok("[object Object]".to_owned())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl<I: ContextElement> Walkable for HashMap<String, I> {
|
||
|
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> {
|
||
|
let child = self.get(segment).ok_or(WalkError::CantWalk)?;
|
||
|
Ok(child)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl<I: 'static + ContextElement + Clone> Loopable for HashMap<String, I> {
|
||
|
fn get_loop_elements(&self) -> Vec<&dyn ContextElement> {
|
||
|
Vec::new()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl<I: 'static + ContextElement + Clone> CompareContextElement for HashMap<String, I> {
|
||
|
fn equals(&self, other: &dyn ContextElement) -> bool {
|
||
|
false
|
||
|
}
|
||
|
|
||
|
fn partial_compare(&self, other: &dyn ContextElement) -> Option<Ordering> {
|
||
|
// TODO: Implement
|
||
|
None
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
fn test_walk_path() {
|
||
|
let context: HashMap<String, String> = [
|
||
|
("cat".to_string(), "kitty".to_string()),
|
||
|
("dog".to_string(), "doggy".to_string()),
|
||
|
("tiger".to_string(), "murderkitty".to_string()),
|
||
|
]
|
||
|
.iter()
|
||
|
.cloned()
|
||
|
.collect();
|
||
|
let number_context: HashMap<String, u64> = [
|
||
|
("cat".to_string(), 1),
|
||
|
("dog".to_string(), 2),
|
||
|
("tiger".to_string(), 3),
|
||
|
]
|
||
|
.iter()
|
||
|
.cloned()
|
||
|
.collect();
|
||
|
let deep_context: HashMap<String, HashMap<String, String>> = [
|
||
|
(
|
||
|
"cat".to_string(),
|
||
|
[("food".to_string(), "meat".to_string())]
|
||
|
.iter()
|
||
|
.cloned()
|
||
|
.collect(),
|
||
|
),
|
||
|
(
|
||
|
"dog".to_string(),
|
||
|
[("food".to_string(), "meat".to_string())]
|
||
|
.iter()
|
||
|
.cloned()
|
||
|
.collect(),
|
||
|
),
|
||
|
(
|
||
|
"tiger".to_string(),
|
||
|
[("food".to_string(), "people".to_string())]
|
||
|
.iter()
|
||
|
.cloned()
|
||
|
.collect(),
|
||
|
),
|
||
|
]
|
||
|
.iter()
|
||
|
.cloned()
|
||
|
.collect();
|
||
|
|
||
|
assert_eq!(
|
||
|
walk_path(&vec![&context as &dyn ContextElement], &vec!["cat"])
|
||
|
.unwrap()
|
||
|
.render(&Vec::new())
|
||
|
.unwrap(),
|
||
|
"kitty".to_owned()
|
||
|
);
|
||
|
assert_eq!(
|
||
|
walk_path(
|
||
|
&vec![&number_context as &dyn ContextElement],
|
||
|
&vec!["tiger"]
|
||
|
)
|
||
|
.unwrap()
|
||
|
.render(&Vec::new())
|
||
|
.unwrap(),
|
||
|
"3".to_owned()
|
||
|
);
|
||
|
assert_eq!(
|
||
|
walk_path(
|
||
|
&vec![&deep_context as &dyn ContextElement],
|
||
|
&vec!["tiger", "food"]
|
||
|
)
|
||
|
.unwrap()
|
||
|
.render(&Vec::new())
|
||
|
.unwrap(),
|
||
|
"people".to_owned()
|
||
|
);
|
||
|
}
|
||
|
}
|