mrml 1.2.2

Rust implementation of MJML renderer
Documentation
use super::MJBody;
use crate::helper::size::Pixel;
use crate::helper::tag::Tag;
use crate::prelude::render::{Error, Header, Options, Render, Renderable};
use std::cell::{Ref, RefCell};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::rc::Rc;

struct MJBodyRender<'e, 'h> {
    header: Rc<RefCell<Header<'h>>>,
    element: &'e MJBody,
}

impl<'e, 'h> MJBodyRender<'e, 'h> {
    fn get_width(&self) -> Option<Pixel> {
        self.attribute("width")
            .and_then(|value| Pixel::try_from(value.as_str()).ok())
    }

    fn get_body_tag(&self) -> Tag {
        self.set_body_style(Tag::new("body").add_style("word-spacing", "normal"))
    }

    fn get_content_div_tag(&self) -> Tag {
        self.set_body_style(Tag::new("div"))
            .maybe_add_attribute("class", self.attribute("css-class"))
    }

    fn set_body_style(&self, tag: Tag) -> Tag {
        tag.maybe_add_style("background-color", self.attribute("background-color"))
    }

    fn render_preview(&self) -> String {
        if let Some(value) = self
            .header
            .borrow()
            .head()
            .as_ref()
            .and_then(|h| h.preview())
            .map(|p| p.content())
        {
            String::from(
                r#"<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">"#,
            ) + value
                + "</div>"
        } else {
            String::default()
        }
    }

    fn render_content(&self, opts: &Options) -> Result<String, Error> {
        let div = self.get_content_div_tag();
        let element_width = self.get_width();
        let mut children = String::default();
        let raw_siblings = self
            .element
            .children
            .iter()
            .filter(|item| item.is_raw())
            .count();
        for (index, child) in self.element.children.iter().enumerate() {
            let mut renderer = child.renderer(Rc::clone(&self.header));
            renderer.set_container_width(element_width.clone());
            renderer.set_index(index);
            renderer.set_raw_siblings(raw_siblings);
            renderer.set_siblings(self.element.children.len());
            children.push_str(&renderer.render(opts)?);
        }
        Ok(div.render(children))
    }
}

impl<'e, 'h> Render<'h> for MJBodyRender<'e, 'h> {
    fn attributes(&self) -> Option<&HashMap<String, String>> {
        Some(&self.element.attributes)
    }

    fn default_attribute(&self, key: &str) -> Option<&str> {
        match key {
            "width" => Some("600px"),
            _ => None,
        }
    }

    fn header(&self) -> Ref<Header<'h>> {
        self.header.borrow()
    }

    fn render(&self, opts: &Options) -> Result<String, Error> {
        let body = self.get_body_tag();
        Ok(body.render(self.render_preview() + &self.render_content(opts)?))
    }
}

impl<'r, 'e: 'r, 'h: 'r> Renderable<'r, 'e, 'h> for MJBody {
    fn renderer(&'e self, header: Rc<RefCell<Header<'h>>>) -> Box<dyn Render<'h> + 'r> {
        Box::new(MJBodyRender::<'e, 'h> {
            element: self,
            header,
        })
    }
}

#[cfg(test)]
mod tests {
    use crate::helper::test::compare;
    use crate::mjml::MJML;
    use crate::prelude::render::Options;

    #[test]
    fn empty() {
        let opts = Options::default();
        let template = include_str!("../../resources/compare/success/mj-body.mjml");
        let expected = include_str!("../../resources/compare/success/mj-body.html");
        let root = MJML::parse(template.to_string()).unwrap();
        compare(expected, root.render(&opts).unwrap().as_str());
    }
}