duster/src/renderer/renderer.rs

389 lines
13 KiB
Rust
Raw Normal View History

use crate::parser::template;
use crate::parser::Body;
use crate::parser::DustTag;
2020-05-03 18:52:08 +00:00
use crate::parser::Special;
use crate::parser::Template;
2020-04-11 22:25:48 +00:00
use crate::parser::TemplateElement;
use crate::renderer::context_element::ContextElement;
2020-04-11 22:25:48 +00:00
use crate::renderer::errors::CompileError;
use crate::renderer::errors::RenderError;
use crate::renderer::parameters_context::ParametersContext;
2020-04-11 22:25:48 +00:00
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct CompiledTemplate<'a> {
template: Template<'a>,
2020-04-11 22:25:48 +00:00
pub name: String,
}
#[derive(Clone, Debug)]
2020-04-11 00:58:55 +00:00
pub struct DustRenderer<'a> {
2020-04-11 22:25:48 +00:00
templates: HashMap<String, &'a Template<'a>>,
}
2020-04-11 22:25:48 +00:00
pub fn compile_template<'a>(
source: &'a str,
name: String,
) -> Result<CompiledTemplate<'a>, CompileError> {
// TODO: Make this all consuming
2020-04-11 22:25:48 +00:00
// TODO: This could use better error management
let (_remaining, parsed_template) = template(source).expect("Failed to compile template");
2020-04-11 22:25:48 +00:00
Ok(CompiledTemplate {
template: parsed_template,
name: name,
2020-04-11 22:25:48 +00:00
})
}
2020-04-11 00:58:55 +00:00
impl<'a> DustRenderer<'a> {
pub fn new() -> DustRenderer<'a> {
DustRenderer {
2020-04-11 22:25:48 +00:00
templates: HashMap::new(),
}
}
pub fn load_source(&mut self, template: &'a CompiledTemplate) {
2020-04-11 22:25:48 +00:00
self.templates
.insert(template.name.clone(), &template.template);
}
2020-04-11 00:58:55 +00:00
pub fn render(
2020-05-05 23:51:07 +00:00
&'a self,
name: &str,
breadcrumbs: &Vec<&'a dyn ContextElement>,
) -> Result<String, RenderError<'a>> {
let main_template = match self.templates.get(name) {
Some(tmpl) => tmpl,
None => {
return Err(RenderError::Generic(format!(
"No template named {} in context",
name
)));
}
};
self.render_body(&main_template.contents, breadcrumbs)
2020-05-05 23:51:07 +00:00
}
fn render_body(
2020-05-05 23:51:07 +00:00
&'a self,
body: &'a Body,
breadcrumbs: &Vec<&'a dyn ContextElement>,
) -> Result<String, RenderError<'a>> {
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)?);
2020-05-05 23:51:07 +00:00
}
}
}
Ok(output)
}
fn render_tag(
2020-05-05 23:51:07 +00:00
&'a self,
tag: &'a DustTag,
breadcrumbs: &Vec<&'a dyn ContextElement>,
) -> Result<String, RenderError<'a>> {
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(RenderError::NotFound { .. }) => 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);
}
}
Err(render_error) => return Err(render_error),
2020-05-05 23:51:07 +00:00
}
}
DustTag::DTSection(container) => {
let val = walk_path(breadcrumbs, &container.path.keys);
2020-05-05 23:51:07 +00:00
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),
2020-05-05 23:51:07 +00:00
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)
2020-05-05 23:51:07 +00:00
})
.collect();
let rendered_slice: &[String] = &rendered_results?;
return Ok(rendered_slice.join(""));
}
}
}
}
2020-05-06 23:10:09 +00:00
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),
None => Ok("".to_owned()),
};
} else {
return match &container.contents {
None => Ok("".to_owned()),
Some(body) => self.render_body(&body, breadcrumbs),
};
}
}
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),
None => Ok("".to_owned()),
};
} else {
return match &container.contents {
None => Ok("".to_owned()),
Some(body) => self.render_body(&body, breadcrumbs),
};
}
}
2020-05-08 23:24:32 +00:00
DustTag::DTPartial(partial) => {
let rendered_content = self.render(&partial.name, breadcrumbs)?;
return Ok(rendered_content);
}
2020-05-05 23:51:07 +00:00
_ => (), // 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, RenderError<'b>>,
) -> Result<Vec<&'b dyn ContextElement>, RenderError<'b>> {
2020-05-06 00:32:30 +00:00
if let Err(RenderError::NotFound { .. }) = walk_result {
// If reference does not exist in the context, render the else block
Ok(vec![])
} else {
Ok(walk_result?.get_loop_elements()?)
}
}
}
2020-04-12 00:31:44 +00:00
enum WalkResult<'a> {
NoWalk,
PartialWalk,
FullyWalked(&'a dyn ContextElement),
}
fn walk_path_from_single_level<'a>(
context: &'a dyn ContextElement,
path: &Vec<&str>,
) -> Result<WalkResult<'a>, RenderError<'a>> {
if path.is_empty() {
return Ok(WalkResult::FullyWalked(context));
}
2020-04-12 00:31:44 +00:00
let mut walk_failure = WalkResult::NoWalk;
let mut output = context;
2020-04-12 02:36:22 +00:00
for elem in path.iter() {
let new_val = output.walk(elem);
if let Err(RenderError::WontWalk { .. }) = new_val {
return Ok(walk_failure);
} else if let Err(RenderError::CantWalk { .. }) = new_val {
return Ok(walk_failure);
}
walk_failure = WalkResult::PartialWalk;
output = new_val?;
2020-04-12 02:36:22 +00:00
}
2020-04-12 01:57:24 +00:00
2020-05-06 00:22:25 +00:00
Ok(WalkResult::FullyWalked(output))
}
fn walk_path<'a>(
2020-05-05 23:51:07 +00:00
breadcrumbs: &Vec<&'a dyn ContextElement>,
path: &'a Vec<&str>,
) -> Result<&'a dyn ContextElement, RenderError<'a>> {
2020-05-05 03:36:13 +00:00
for context in breadcrumbs.iter().rev() {
match walk_path_from_single_level(*context, path)? {
// If no walking was done at all, keep looping
2020-05-05 03:36:13 +00:00
WalkResult::NoWalk => {}
// If we partially walked then stop trying to find
// anything
WalkResult::PartialWalk => {
return Err(RenderError::NotFound {
path: path,
2020-05-05 23:51:07 +00:00
breadcrumbs: breadcrumbs.clone(),
2020-05-06 00:22:25 +00:00
});
}
WalkResult::FullyWalked(new_context) => return Ok(new_context),
2020-05-05 03:36:13 +00:00
}
}
Err(RenderError::NotFound {
path: path,
2020-05-05 23:51:07 +00:00
breadcrumbs: breadcrumbs.clone(),
})
2020-04-12 02:36:22 +00:00
}
2020-04-12 00:31:44 +00:00
#[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;
2020-04-12 00:31:44 +00:00
2020-05-06 00:22:25 +00:00
impl ContextElement for u32 {}
impl ContextElement for &str {}
impl<I: ContextElement> ContextElement for HashMap<&str, I> {}
2020-05-06 00:22:25 +00:00
impl Renderable for u32 {
fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
// TODO: handle the filters
Ok(self.to_string())
}
2020-05-06 00:22:25 +00:00
}
2020-05-06 00:22:25 +00:00
impl Renderable for &str {
fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
// TODO: handle the filters
Ok(self.to_string())
}
2020-05-06 00:22:25 +00:00
}
2020-05-06 00:22:25 +00:00
impl<I: ContextElement> Renderable for HashMap<&str, I> {
fn render(&self, _filters: &Vec<Filter>) -> Result<String, RenderError> {
// TODO: handle the filters
Err(RenderError::CantRender { elem: self })
2020-04-12 00:31:44 +00:00
}
2020-05-06 00:22:25 +00:00
}
2020-04-12 00:31:44 +00:00
2020-05-06 00:22:25 +00:00
impl<I: ContextElement> Walkable for HashMap<&str, I> {
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> {
let child = self.get(segment).ok_or(RenderError::WontWalk {
segment: segment.to_string(),
elem: self,
})?;
Ok(child)
2020-04-12 01:07:12 +00:00
}
2020-05-06 00:22:25 +00:00
}
2020-04-12 01:07:12 +00:00
2020-05-06 00:22:25 +00:00
impl Walkable for &str {
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> {
Err(RenderError::CantWalk {
segment: segment.to_string(),
elem: self,
})
2020-04-12 00:34:16 +00:00
}
2020-05-06 00:22:25 +00:00
}
2020-04-12 00:34:16 +00:00
2020-05-06 00:22:25 +00:00
impl Walkable for u32 {
fn walk(&self, segment: &str) -> Result<&dyn ContextElement, RenderError> {
Err(RenderError::CantWalk {
segment: segment.to_string(),
elem: self,
})
}
2020-05-06 00:22:25 +00:00
}
2020-05-06 00:22:25 +00:00
impl Loopable for &str {
fn get_loop_elements(&self) -> Result<Vec<&dyn ContextElement>, RenderError> {
if self.is_empty() {
Ok(Vec::new())
} else {
Ok(vec![self])
}
}
2020-05-06 00:22:25 +00:00
}
2020-05-06 00:22:25 +00:00
impl Loopable for u32 {
fn get_loop_elements(&self) -> Result<Vec<&dyn ContextElement>, RenderError> {
Ok(vec![self])
}
2020-05-06 00:22:25 +00:00
}
impl<I: ContextElement> Loopable for HashMap<&str, I> {
fn get_loop_elements(&self) -> Result<Vec<&dyn ContextElement>, RenderError> {
Ok(vec![self])
}
}
#[test]
2020-05-06 00:43:53 +00:00
fn test_walk_path() {
2020-05-06 00:22:25 +00:00
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"])
2020-05-06 00:22:25 +00:00
.unwrap()
.render(&Vec::new())
.unwrap(),
"kitty".to_owned()
);
2020-05-06 00:38:42 +00:00
assert_eq!(
walk_path(
2020-05-06 00:38:42 +00:00
&vec![&number_context as &dyn ContextElement],
&vec!["tiger"]
)
.unwrap()
.render(&Vec::new())
.unwrap(),
"3".to_owned()
);
assert_eq!(
walk_path(
2020-05-06 00:38:42 +00:00
&vec![&deep_context as &dyn ContextElement],
&vec!["tiger", "food"]
)
.unwrap()
.render(&Vec::new())
.unwrap(),
"people".to_owned()
);
2020-05-06 00:22:25 +00:00
}
2020-04-12 00:31:44 +00:00
}