duster/src/renderer/renderer.rs

1081 lines
43 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::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 ContextElement>,
) -> Result<String, RenderError> {
self.render_template(name, breadcrumbs, None)
}
fn render_template(
&'a self,
name: &str,
breadcrumbs: &Vec<&'a dyn ContextElement>,
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 ContextElement>,
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 ContextElement>,
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
fn render_partial_name(
&'a self,
body: &'a Vec<PartialNameElement>,
breadcrumbs: &Vec<&'a dyn ContextElement>,
blocks: &'a BlockContext<'a>,
) -> Result<String, RenderError> {
let converted_to_template_elements: Vec<TemplateElement<'a>> =
body.into_iter().map(|e| e.into()).collect();
self.render_body(
&Body {
elements: converted_to_template_elements,
},
breadcrumbs,
blocks,
)
}
fn render_tag(
&'a self,
tag: &'a DustTag,
breadcrumbs: &Vec<&'a dyn ContextElement>,
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);
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);
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);
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);
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, blocks)?;
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.name) {
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,
&parameterized_block.explicit_context,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_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(&param_map) {
return match &parameterized_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, &param_map, "key") {
None => return Ok("".to_owned()),
Some(res) => res,
};
let right_side: Result<&dyn ContextElement, WalkError> =
Self::get_rval(breadcrumbs, &param_map, "value")
.unwrap_or(Err(WalkError::CantWalk));
if left_side == right_side {
return self.render_maybe_body(
&parameterized_block.contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_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,
&parameterized_block.explicit_context,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_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(&param_map) {
return match &parameterized_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, &param_map, "key") {
None => return Ok("".to_owned()),
Some(res) => res,
};
let right_side: Result<&dyn ContextElement, WalkError> =
Self::get_rval(breadcrumbs, &param_map, "value")
.unwrap_or(Err(WalkError::CantWalk));
if left_side != right_side {
return self.render_maybe_body(
&parameterized_block.contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_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,
&parameterized_block.explicit_context,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
let left_side: Result<&dyn ContextElement, WalkError> =
match Self::get_rval(breadcrumbs, &param_map, "key") {
None => return Ok("".to_owned()),
Some(res) => res,
};
let right_side: Result<&dyn ContextElement, WalkError> =
Self::get_rval(breadcrumbs, &param_map, "value")
.unwrap_or(Err(WalkError::CantWalk));
match (left_side, right_side) {
(Err(_), _) | (_, Err(_)) => {
return self.render_maybe_body(
&parameterized_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(
&parameterized_block.contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_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,
&parameterized_block.explicit_context,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
let left_side: Result<&dyn ContextElement, WalkError> =
match Self::get_rval(breadcrumbs, &param_map, "key") {
None => return Ok("".to_owned()),
Some(res) => res,
};
let right_side: Result<&dyn ContextElement, WalkError> =
Self::get_rval(breadcrumbs, &param_map, "value")
.unwrap_or(Err(WalkError::CantWalk));
match (left_side, right_side) {
(Err(_), _) | (_, Err(_)) => {
return self.render_maybe_body(
&parameterized_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(
&parameterized_block.contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_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,
&parameterized_block.explicit_context,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
let left_side: Result<&dyn ContextElement, WalkError> =
match Self::get_rval(breadcrumbs, &param_map, "key") {
None => return Ok("".to_owned()),
Some(res) => res,
};
let right_side: Result<&dyn ContextElement, WalkError> =
Self::get_rval(breadcrumbs, &param_map, "value")
.unwrap_or(Err(WalkError::CantWalk));
match (left_side, right_side) {
(Err(_), _) | (_, Err(_)) => {
return self.render_maybe_body(
&parameterized_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(
&parameterized_block.contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_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,
&parameterized_block.explicit_context,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
let left_side: Result<&dyn ContextElement, WalkError> =
match Self::get_rval(breadcrumbs, &param_map, "key") {
None => return Ok("".to_owned()),
Some(res) => res,
};
let right_side: Result<&dyn ContextElement, WalkError> =
Self::get_rval(breadcrumbs, &param_map, "value")
.unwrap_or(Err(WalkError::CantWalk));
match (left_side, right_side) {
(Err(_), _) | (_, Err(_)) => {
return self.render_maybe_body(
&parameterized_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(
&parameterized_block.contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_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 ContextElement>,
param_map: &HashMap<&str, &'b RValue<'b>>,
key: &str,
) -> Option<Result<&'b dyn ContextElement, WalkError>> {
match param_map.get(key) {
None => None,
Some(rval) => match rval {
RValue::RVLiteral(literal) => Some(Ok(literal)),
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
}
/// Generate a new breadcrumbs object
///
/// This function generates a new breadcrumbs object based on the
/// new context information provided.
///
/// breadcrumbs are the breadcrumbs that will be used in the final
/// breadcrumbs (unless omitted due to an explicit context)
///
/// explicit_context_breadcrumbs are the breadcrumbs used to
/// evaluate an explicit context path. Most of the time the two
/// breadcrumbs parameters will be identical, but for
/// blocks/inline partials the explicit_context_breadcrumbs will
/// be the breadcrumbs from the start of the partial containing
/// the block.
///
/// explicit_context is for contexts specified with a `:path`
/// inside a dust tag.
///
/// injected_context is for any generated context. This includes
/// both parameters on a tag and also the handling of $idx and
/// $len.
///
/// New context element is the element is an element to append to
/// the end, generally for use in section tags which walk to a new
/// context.
///
/// If explicit_context is not None, then the final breadcrumb stack will be:
///
/// ```text
/// breadcrumbs
/// injected_context
/// new_context_element
/// ```
///
/// However, if explicit_context is not None, then the old
/// breadcrumbs are omitted, leading to the new breadcrumb stack
/// as:
///
/// ```text
/// injected_context
/// explicit_context
/// new_context_element
/// ```
fn new_breadcrumbs_deprecated<'b>(
breadcrumbs: &'b Vec<&'b dyn ContextElement>,
explicit_context_breadcrumbs: &'b Vec<&'b dyn ContextElement>,
injected_context: Option<&'b dyn ContextElement>,
explicit_context: &Option<Path<'b>>,
new_context_element: Option<&'b dyn ContextElement>,
) -> Option<Vec<&'b dyn ContextElement>> {
// 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, new_context_element) {
(None, None, None) => return None,
_ => (),
};
let mut new_stack = match explicit_context {
Some(_) => Vec::with_capacity(3),
None => breadcrumbs.clone(),
};
// TODO: Can sections have parameters, and if so, what happens then? Currently when there is an injected context or an explicit context it gets inserted behind the current context, so 1->2->3 becomes 1->2->injected->3 or explicit->3. When there is a new context(4) with injected we're doing 1->2->3->injected->4. When there is an explicit context and a new context we're doing explicit->4. But what happens if there is a section with parameters and an explicit context, hitting all the categories? Would it be parameters->explicit->4? I would definitely have to change the parameters to this function since right now iteration variables and parameters are both sharing injected_context.
injected_context.map(|ctx| {
// Special case: when there is no explicit context or new
// context element, 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 and new context
// element.
match (explicit_context, new_context_element) {
(None, None) => new_stack.insert(std::cmp::max(new_stack.len() - 1, 0), ctx),
_ => new_stack.push(ctx),
}
});
explicit_context.as_ref().map(|path| {
walk_path(explicit_context_breadcrumbs, &path.keys).map(|val| {
if val.is_truthy() {
new_stack.push(val)
}
});
});
new_context_element.map(|ctx| new_stack.push(ctx));
Some(new_stack)
}
fn new_breadcrumbs_section<'b>(
breadcrumbs: &'b Vec<&'b dyn ContextElement>,
index_context: Option<&'b dyn ContextElement>,
injected_context: Option<&'b dyn ContextElement>,
explicit_context: &Option<Path<'b>>,
new_context_element: Option<&'b dyn ContextElement>,
) -> Option<Vec<&'b dyn ContextElement>> {
// 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(|val| {
if val.is_truthy() {
new_stack.push(val)
}
});
});
injected_context.map(|ctx| new_stack.push(ctx));
new_context_element.map(|ctx| new_stack.push(ctx));
index_context.map(|ctx| new_stack.push(ctx));
Some(new_stack)
}
fn get_index_of_first_non_pseudo_element<'b, B>(breadcrumbs: &'b Vec<B>) -> Option<usize>
where
B: Borrow<dyn ContextElement + 'a>,
{
breadcrumbs
.iter()
.rposition(|b| !(*b).borrow().is_pseudo_element())
}
fn new_breadcrumbs_partial<'b>(
breadcrumbs: &'b Vec<&'b dyn ContextElement>,
explicit_context_breadcrumbs: &'b Vec<&'b dyn ContextElement>,
injected_context: Option<&'b dyn ContextElement>,
explicit_context: &Option<Path<'b>>,
) -> Option<Vec<&'b dyn ContextElement>> {
// 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).map(|val| {
if val.is_truthy() {
new_stack.push(val)
}
});
});
Some(new_stack)
}
}
struct BlockContext<'a> {
/// The breadcrumbs at the time of entering the current partial
breadcrumbs: &'a Vec<&'a dyn ContextElement>,
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()
);
}
}