400 lines
15 KiB
Rust
400 lines
15 KiB
Rust
use crate::parser::template;
|
|
use crate::parser::Body;
|
|
use crate::parser::DustTag;
|
|
use crate::parser::KVPair;
|
|
use crate::parser::RValue;
|
|
use crate::parser::Special;
|
|
use crate::parser::Template;
|
|
use crate::parser::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::parameters_context::ParametersContext;
|
|
use crate::renderer::walking::walk_path;
|
|
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: Make this all consuming
|
|
// 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);
|
|
self.render_body(&main_template.contents, breadcrumbs, &new_blocks)
|
|
}
|
|
|
|
fn render_body(
|
|
&'a self,
|
|
body: &'a Body,
|
|
breadcrumbs: &Vec<&'a dyn ContextElement>,
|
|
blocks: &'a InlinePartialTreeElement<'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)
|
|
}
|
|
|
|
fn render_tag(
|
|
&'a self,
|
|
tag: &'a DustTag,
|
|
breadcrumbs: &Vec<&'a dyn ContextElement>,
|
|
blocks: &'a InlinePartialTreeElement<'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::DTReference(reference) => {
|
|
let val = walk_path(breadcrumbs, &reference.path.keys);
|
|
match val {
|
|
Err(WalkError::CantWalk) => return Ok("".to_owned()),
|
|
Ok(final_val) => {
|
|
let loop_elements = final_val.get_loop_elements();
|
|
if loop_elements.is_empty() {
|
|
return Ok("".to_owned());
|
|
} else {
|
|
return final_val.render(&reference.filters);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
DustTag::DTSection(container) => {
|
|
let val = walk_path(breadcrumbs, &container.path.keys);
|
|
let loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val);
|
|
if loop_elements.is_empty() {
|
|
// 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
|
|
return match &container.else_contents {
|
|
Some(body) => self.render_body(&body, breadcrumbs, blocks),
|
|
None => Ok("".to_owned()),
|
|
};
|
|
} else {
|
|
match &container.contents {
|
|
None => return Ok("".to_owned()),
|
|
Some(body) => {
|
|
let rendered_results: Result<Vec<String>, RenderError> = loop_elements
|
|
.into_iter()
|
|
.map(|array_elem| {
|
|
let mut new_breadcumbs = breadcrumbs.clone();
|
|
new_breadcumbs.push(array_elem);
|
|
self.render_body(&body, &new_breadcumbs, blocks)
|
|
})
|
|
.collect();
|
|
let rendered_slice: &[String] = &rendered_results?;
|
|
return Ok(rendered_slice.join(""));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
DustTag::DTExists(container) => {
|
|
let val = walk_path(breadcrumbs, &container.path.keys);
|
|
let loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val);
|
|
if loop_elements.is_empty() {
|
|
return match &container.else_contents {
|
|
Some(body) => self.render_body(&body, breadcrumbs, blocks),
|
|
None => Ok("".to_owned()),
|
|
};
|
|
} else {
|
|
return match &container.contents {
|
|
None => Ok("".to_owned()),
|
|
Some(body) => self.render_body(&body, breadcrumbs, blocks),
|
|
};
|
|
}
|
|
}
|
|
DustTag::DTNotExists(container) => {
|
|
let val = walk_path(breadcrumbs, &container.path.keys);
|
|
let loop_elements: Vec<&dyn ContextElement> = self.get_loop_elements(val);
|
|
if !loop_elements.is_empty() {
|
|
return match &container.else_contents {
|
|
Some(body) => self.render_body(&body, breadcrumbs, blocks),
|
|
None => Ok("".to_owned()),
|
|
};
|
|
} else {
|
|
return match &container.contents {
|
|
None => Ok("".to_owned()),
|
|
Some(body) => self.render_body(&body, breadcrumbs, blocks),
|
|
};
|
|
}
|
|
}
|
|
DustTag::DTPartial(partial) => {
|
|
if partial.params.is_empty() {
|
|
let rendered_content =
|
|
self.render_template(&partial.name, breadcrumbs, Some(blocks))?;
|
|
return Ok(rendered_content);
|
|
} else {
|
|
let injected_context = ParametersContext::new(breadcrumbs, &partial.params);
|
|
let mut new_breadcrumbs = breadcrumbs.clone();
|
|
new_breadcrumbs.insert(new_breadcrumbs.len() - 1, &injected_context);
|
|
let rendered_content =
|
|
self.render_template(&partial.name, &new_breadcrumbs, Some(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) => {
|
|
match blocks.get_block(named_block.name) {
|
|
None => match &named_block.contents {
|
|
None => return Ok("".to_owned()),
|
|
Some(body) => {
|
|
let rendered_content = self.render_body(body, breadcrumbs, blocks)?;
|
|
return Ok(rendered_content);
|
|
}
|
|
},
|
|
Some(interior) => match interior {
|
|
None => return Ok("".to_owned()),
|
|
Some(body) => {
|
|
let rendered_content = self.render_body(body, breadcrumbs, blocks)?;
|
|
return Ok(rendered_content);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
DustTag::DTHelperEquals(parameterized_block) => {
|
|
let param_map: HashMap<&str, &RValue<'a>> = parameterized_block
|
|
.params
|
|
.iter()
|
|
.map(|pair: &KVPair<'a>| (pair.key, &pair.value))
|
|
.collect();
|
|
let left_side: Result<&dyn ContextElement, WalkError> = match param_map.get("key") {
|
|
None => return Ok("".to_owned()),
|
|
Some(rval) => match rval {
|
|
RValue::RVString(text) => Ok(text),
|
|
RValue::RVPath(path) => walk_path(breadcrumbs, &path.keys),
|
|
},
|
|
};
|
|
let right_side: Result<&dyn ContextElement, WalkError> =
|
|
match param_map.get("value") {
|
|
None => Err(WalkError::CantWalk),
|
|
Some(rval) => match rval {
|
|
RValue::RVString(text) => Ok(text),
|
|
RValue::RVPath(path) => walk_path(breadcrumbs, &path.keys),
|
|
},
|
|
};
|
|
// let x = WalkError::CantWalk;
|
|
// let y = WalkError::CantWalk;
|
|
// if x == y {
|
|
// panic!("placeholder");
|
|
// }
|
|
// if left_side.unwrap() == right_side.unwrap() {
|
|
// panic!("placeholder");
|
|
// }
|
|
}
|
|
_ => (), // TODO: Implement the rest
|
|
}
|
|
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>(
|
|
&'a self,
|
|
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(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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::Walkable;
|
|
|
|
impl ContextElement for u32 {}
|
|
impl ContextElement for &str {}
|
|
impl<I: ContextElement> ContextElement for HashMap<&str, I> {}
|
|
|
|
impl Renderable for u32 {
|
|
fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
|
|
// TODO: handle the filters
|
|
Ok(self.to_string())
|
|
}
|
|
}
|
|
|
|
impl Renderable for &str {
|
|
fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
|
|
// TODO: handle the filters
|
|
Ok(self.to_string())
|
|
}
|
|
}
|
|
|
|
impl<I: ContextElement> Renderable for HashMap<&str, 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<&str, I> {
|
|
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> {
|
|
let child = self.get(segment).ok_or(WalkError::CantWalk)?;
|
|
Ok(child)
|
|
}
|
|
}
|
|
|
|
impl Walkable for &str {
|
|
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> {
|
|
Err(WalkError::CantWalk)
|
|
}
|
|
}
|
|
|
|
impl Walkable for u32 {
|
|
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, WalkError> {
|
|
Err(WalkError::CantWalk)
|
|
}
|
|
}
|
|
|
|
impl Loopable for &str {
|
|
fn get_loop_elements(&self) -> Vec<&dyn ContextElement> {
|
|
if self.is_empty() {
|
|
Vec::new()
|
|
} else {
|
|
vec![self]
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Loopable for u32 {
|
|
fn get_loop_elements(&self) -> Vec<&dyn ContextElement> {
|
|
vec![self]
|
|
}
|
|
}
|
|
|
|
impl<I: ContextElement> Loopable for HashMap<&str, I> {
|
|
fn get_loop_elements(&self) -> Vec<&dyn ContextElement> {
|
|
vec![self]
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_walk_path() {
|
|
let context: HashMap<&str, &str> =
|
|
[("cat", "kitty"), ("dog", "doggy"), ("tiger", "murderkitty")]
|
|
.iter()
|
|
.cloned()
|
|
.collect();
|
|
let number_context: HashMap<&str, u32> = [("cat", 1), ("dog", 2), ("tiger", 3)]
|
|
.iter()
|
|
.cloned()
|
|
.collect();
|
|
let deep_context: HashMap<&str, HashMap<&str, &str>> = [
|
|
("cat", [("food", "meat")].iter().cloned().collect()),
|
|
("dog", [("food", "meat")].iter().cloned().collect()),
|
|
("tiger", [("food", "people")].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()
|
|
);
|
|
}
|
|
}
|