mod parse;
mod section;
use std::fs::File;
use std::hash::Hasher;
use std::io;
use std::path::Path;
use crate::encoding::EscapingIOEncoder;
use crate::{Content, Error};
use crate::Partials;
use beef::Cow;
use fnv::FnvHasher;
pub use section::Section;
pub struct Template<'tpl> {
blocks: Vec<Block<'tpl>>,
capacity_hint: usize,
source: Cow<'tpl, str>,
}
impl<'tpl> Template<'tpl> {
pub fn new<S>(source: S) -> Result<Self, Error>
where
S: Into<Cow<'tpl, str>>,
{
Template::load(source, &mut NoPartials)
}
pub(crate) fn load<S>(source: S, partials: &mut impl Partials<'tpl>) -> Result<Self, Error>
where
S: Into<Cow<'tpl, str>>,
{
let source = source.into();
let unsafe_source: &'tpl str = unsafe {
&*(&*source as *const str)
};
let mut tpl = Template {
blocks: Vec::with_capacity(16),
capacity_hint: 0,
source,
};
let last = tpl.parse(unsafe_source, partials)?;
let tail = &unsafe_source[last..].trim_end();
tpl.blocks.push(Block::new(tail, "", Tag::Tail));
tpl.capacity_hint += tail.len();
Ok(tpl)
}
pub fn capacity_hint(&self) -> usize {
self.capacity_hint
}
pub fn render<C: crate::Content>(&self, content: &C) -> String {
let mut capacity = content.capacity_hint(self);
capacity += capacity / 4;
let mut buf = String::with_capacity(capacity);
let _ = Section::new(&self.blocks).with(content).render(&mut buf);
buf
}
pub fn render_to_writer<W, C>(&self, writer: &mut W, content: &C) -> io::Result<()>
where
W: io::Write,
C: Content,
{
let mut encoder = EscapingIOEncoder::new(writer);
Section::new(&self.blocks).with(content).render(&mut encoder)
}
pub fn render_to_file<P, C>(&self, path: P, content: &C) -> io::Result<()>
where
P: AsRef<Path>,
C: Content,
{
use io::BufWriter;
let writer = BufWriter::new(File::create(path)?);
let mut encoder = EscapingIOEncoder::new(writer);
Section::new(&self.blocks).with(content).render(&mut encoder)
}
pub fn source(&self) -> &str {
&self.source
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tag {
Escaped,
Unescaped,
Section,
Inverse,
Closing,
Comment,
Partial,
Tail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Block<'tpl> {
html: &'tpl str,
name: &'tpl str,
hash: u64,
tag: Tag,
children: u32,
}
impl<'tpl> Block<'tpl> {
fn new(html: &'tpl str, name: &'tpl str, tag: Tag) -> Self {
let mut hasher = FnvHasher::default();
hasher.write(name.as_bytes());
let hash = hasher.finish();
Block {
html,
name,
hash,
tag,
children: 0,
}
}
}
struct NoPartials;
impl<'tpl> Partials<'tpl> for NoPartials {
fn get_partial(&mut self, _name: &'tpl str) -> Result<&Template<'tpl>, Error> {
Err(Error::PartialsDisabled)
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
impl Block<'_> {
fn children(self, children: u32) -> Self {
Block { children, ..self }
}
}
#[test]
fn template_from_string_is_static() {
let tpl: Template<'static> = Template::new(String::from("Ramhorns")).unwrap();
assert_eq!(tpl.source(), "Ramhorns");
}
#[test]
fn block_hashes_correctly() {
assert_eq!(
Block::new("", "test", Tag::Escaped),
Block {
html: "",
name: "test",
hash: 0xf9e6e6ef197c2b25,
tag: Tag::Escaped,
children: 0,
}
);
}
#[test]
fn constructs_blocks_correctly() {
let source = "<title>{{title}}</title><h1>{{ title }}</h1><div>{{{body}}}</div>";
let tpl = Template::new(source).unwrap();
assert_eq!(
&tpl.blocks,
&[
Block::new("<title>", "title", Tag::Escaped),
Block::new("</title><h1>", "title", Tag::Escaped),
Block::new("</h1><div>", "body", Tag::Unescaped),
Block::new("</div>", "", Tag::Tail),
]
);
}
#[test]
fn constructs_nested_sections_correctly() {
let source = "<body><h1>{{ title }}</h1>{{#posts}}<article>{{name}}</article>{{/posts}}{{^posts}}<p>Nothing here :(</p>{{/posts}}</body>";
let tpl = Template::new(source).unwrap();
assert_eq!(
&tpl.blocks,
&[
Block::new("<body><h1>", "title", Tag::Escaped),
Block::new("</h1>", "posts", Tag::Section).children(2),
Block::new("<article>", "name", Tag::Escaped),
Block::new("</article>", "posts", Tag::Closing),
Block::new("", "posts", Tag::Inverse).children(1),
Block::new("<p>Nothing here :(</p>", "posts", Tag::Closing),
Block::new("</body>", "", Tag::Tail),
]
);
}
}