text-template 0.1.0

Small template engine for use with plain text (e.g. creating text email), not intended for HTML.
Documentation
//! # A Minimal Text Template Engine
//! 
//! ## Overview
//! This library implements templates consisting of text including named placeholders.
//! Placeholders are special character sequences: their names surrounded by `${` and `}`.
//! The example Template `Hello␣${name}` consists of the text `Hello␣` and the placeholder `name`.
//! 
//! Trailing and pending whitespace inside placeholder elements are significant and conditionally
//! included in the output if the value to be inserted for the placeholder is not empty. For
//! example `${title␣}${name}` may evaluate to `Dr.␣X` or `Y` while `${title}␣${name}` may evaluate to
//! `Dr.␣X` or `␣Y`.
//! 
//! Templates are represented by the structure [`Template`].
//! The templates implementation of `From<&str>` can be used to construct templates from strings.
//! Templates can be filled in by using [`fill_in`] or [`try_fill_in`], which replace any 
//! placeholders in the template by the given values. The returned [`Text`] structure is simply
//! a wrapper type around `Vec<&str>` and dereferences to it.
//! 
//! A text representation of the templates and the filled in results can be obtained by using `to_string`.
//! 
//! This library only stores references to the given template strings. It allocates a `Vec`
//! to store a list of text and placeholder elements while parsing a template string. A template,
//! once created, can be used multiple times to fill in different sets of values.
//! 
//! ## Example
//! 
//!     use text_template::*;
//!     use std::collections::HashMap;
//! 
//!     let template = Template::from("Hello ${title }${name}");
//! 
//!     let mut values = HashMap::new();
//!     values.insert("name", "Jane");
//! 
//!     let text = template.fill_in(&values);
//!
//!     assert_eq!(text.to_string(), "Hello Jane");
//!     assert_eq!(template.to_string(), "Hello ${title }${name}");
//!
//! [`Piece`]: enum.Piece.html
//! [`Piece::Text`]: enum.Piece.html#variant.Text
//! [`Piece::Placeholder`]: enum.Piece.html#variant.Placeholder
//! [`Pieces`]: struct.Pieces.html
//! [`Template`]: struct.Template.html
//! [`fill_in`]: struct.Template.html#method.fill_in
//! [`try_fill_in`]: struct.Template.html#method.try_fill_in
//! [`Text`]: struct.Text.html

use std::collections::HashMap;
use std::fmt;
use std::ops::{Deref,DerefMut};


/// Main data structure of this crate.
/// 
/// Use [`Template::from`] to create a template from a `&str` and [`fill_in`] or [`try_fill_in`] to replace
/// the placeholders with some actual values.
/// 
/// Each template is mainly a vector of `Piece`.
/// 
/// [`fill_in`]: struct.Template.html#method.fill_in
/// [`try_fill_in`]: struct.Template.html#method.try_fill_in
/// [`Template::from`]: struct.Template.html#method.from
#[derive(Clone,PartialEq,Debug)]
pub struct Template<'a>(Pieces<'a>);

impl<'a> Deref for Template<'a> {
    type Target = Vec<Piece<'a>>;
    fn deref(&self) -> &Self::Target {
        &(self.0).0
    }
}

impl<'a> DerefMut for Template<'a> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut (self.0).0
    }
}

impl<'a> fmt::Display for Template<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.0.fmt(f)
    }
}

impl<'a> Template<'a> {
    pub fn with_pieces(v: Vec<Piece<'a>>) -> Self {
        Template(Pieces(v))
    }

    /// Fills in all placeholder elements. If `lookup_tbl` does not contain the value for a placeholder,
    /// an empty string is inserted instead.
    pub fn fill_in(&'a self, lookup_tbl: &HashMap<&'a str, &'a str>) -> Text<'a> {
        let mut t = Text::new();

        for v in self.0.iter() {
            match v {
                Piece::Text(s) => t.push(s),
                Piece::Placeholder{name, before, after} => {
                    let entry = lookup_tbl.get(name);
                    match entry {
                        Some(value) if value.len() > 0 => {
                            if before.len() > 0 { t.push(before) };
                            t.push(value);
                            if after.len() > 0 { t.push(after) };
                        }
                        _ => {}
                    }
                }
            }
        }
        
        t
    }

    /// Tries to fill in all placeholder elements. If `lookup_tbl` does not contain the value for a placeholder,
    /// a `TemplateError` is returned.
    pub fn try_fill_in(&'a self, lookup_tbl: &HashMap<&'a str, &'a str>) -> Result<Text<'a>, TemplateError> {
        let mut t = Text::new();

        for v in self.0.iter() {
            match v {
                Piece::Text(s) => t.push(s),
                Piece::Placeholder{name, before, after} => {
                    let entry = lookup_tbl.get(name);
                    match entry {
                        Some(value) => {
                            if before.len() > 0 { t.push(before) };
                            t.push(value);
                            if after.len() > 0 { t.push(after) };
                        },
                        None => { return Err(TemplateError) }
                    }
                }
            }
        }
        
        Ok(t)
    }

    /*pub fn pieces(&self) -> &Pieces {
        &self.0
    }*/
}

impl<'a> From<&'a str> for Template<'a> {
    fn from(s: &'a str) -> Self {
        enum State{InText, InPlaceholder};

        let mut v: Vec<Piece> = vec![];
        let mut state = State::InText;
        let mut rest = s;
        
        while rest.len() > 0 {
            match state {
                State::InText => {
                    if let Some(idx) = rest.find("${") {
                        v.push(Piece::Text(&rest[..idx]));
                        rest = &rest[idx+2..];
                        state = State::InPlaceholder;
                    } else {
                        v.push(Piece::Text(rest));
                        rest = &rest[0..0];
                    }
                }
                State::InPlaceholder => {
                    if let Some(idx) = rest.find("}") {
                        let (before, name, after) = trim_split(&rest[..idx]);
                        v.push(Piece::Placeholder{name, before, after});

                        rest = &rest[idx+1..];
                        state = State::InText;                       
                    } else {
                        v.push(Piece::Text(rest));
                        rest = &rest[0..0];
                    }
                }
            }
        }
        Template::with_pieces(v)
    }    
}



/// A vector of pieces.
#[derive(Clone,PartialEq,Debug)]
struct Pieces<'a>(Vec<Piece<'a>>);

impl<'a> Pieces<'a> {
}

impl<'a> Deref for Pieces<'a> {
    type Target = Vec<Piece<'a>>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<'a> fmt::Display for Pieces<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.0.iter().map(|v| v.fmt(f) ).collect()
    }
}

/// A piece of template, either text or a placeholder to be substituted.
#[derive(Clone,PartialEq,Debug)]
pub enum Piece<'a> {
    Text(&'a str),
    Placeholder{name: &'a str, before: &'a str, after: &'a str}
}

impl<'a> fmt::Display for Piece<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Piece::Text(s) => { write!(f, "{}", s) },
            Piece::Placeholder{name, before, after} => {
                write!(f, "${{{}{}{}}}", before, name, after)
            }
        }
    }
}


/// Simple wrapper around Vec<&str>, returned from `Template::fill_in`.
/// 
/// Implements `Deref<Vec<&str>>` and `DerefMut<Vec<&str>>`.
#[derive(Debug)]
pub struct Text<'a>(Vec<&'a str>);

impl<'a> Deref for Text<'a> {
    type Target = Vec<&'a str>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<'a> DerefMut for Text<'a> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl<'a> Text<'a> {
    fn new() -> Self {
        Text(Vec::new())
    }
}

impl<'a> fmt::Display for Text<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.0.iter().map(|v| v.fmt(f) ).collect()
    }
}

#[derive(Debug)]
/// Returned by `Template::try_fill_in` in case of an error.
pub struct TemplateError;

// Paritions s into (whitespace, non-whitespace, whitespace).
fn trim_split
(s: &str) -> (&str, &str, &str) {
    let mut name = s;
    let before = if let Some((last,_)) = s.chars().enumerate().take_while(|(_,v)| v.is_whitespace()).last() {
        name = &name[last+1..];
        &s[..last+1]
    } else {
        ""
    };

    let after = if let Some((first,_)) = name.chars().rev().enumerate().take_while(|(_,v)| v.is_whitespace()).last() {
        let res = &name[name.len() - first - 1..];
        name = &name[..name.len() - first - 1];
        res
    } else {
        ""
    };

    (before, name, after)
}


#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;



    #[test]
    fn split() {
        assert_eq!(trim_split("Hallo"), ("", "Hallo", ""));
        assert_eq!(trim_split(" Hallo"), (" ", "Hallo", ""));
        assert_eq!(trim_split("Hallo "), ("", "Hallo", " "));
        assert_eq!(trim_split(" Hallo "), (" ", "Hallo", " "));
    }

    #[test]
    fn from() {
        assert_eq!(*Template::from(""), vec![]);
        assert_eq!(*Template::from("{"), vec![Piece::Text("{")]);
        assert_eq!(*Template::from("}"), vec![Piece::Text("}")]);
    }

    #[test]
    fn to_string() {
        assert_eq!(Template::from("").to_string(), "");
        assert_eq!(Template::from("{").to_string(), "{");
        assert_eq!(Template::from("}").to_string(), "}");
        assert_eq!(Template::from("${}").to_string(), "${}");
        assert_eq!(Template::from("${x}").to_string(), "${x}");
        assert_eq!(Template::from(" ${x}").to_string(), " ${x}");
        assert_eq!(Template::from("${x} ").to_string(), "${x} ");
        assert_eq!(Template::from("${x }").to_string(), "${x }");
        assert_eq!(Template::from("${ x}").to_string(), "${ x}");
        assert_eq!(Template::from("${ x }").to_string(), "${ x }");         
        assert_eq!(Template::from("Hallo ${name}").to_string(), "Hallo ${name}");
    }

    #[test]
    fn fill_in() {
        let mut dict = HashMap::new();
        dict.insert("k", "v");
        dict.insert("l", "");

        assert_eq!(Template::from("${}").fill_in(&dict).to_string(), "");
        assert_eq!(Template::from("${k}").fill_in(&dict).to_string(), "v");
        assert_eq!(Template::from("${ k }").fill_in(&dict).to_string(), " v ");
        assert_eq!(Template::from("${l}").fill_in(&dict).to_string(), "");
        assert_eq!(Template::from("${ l }").fill_in(&dict).to_string(), "");
    }
}