#[cfg(feature = "css-inline")]
use std::borrow::Cow;
use super::Mjml;
use crate::mj_head::MjHead;
use crate::prelude::render::*;
impl<'root> Render<'root> for Renderer<'root, Mjml, ()> {
fn context(&self) -> &'root RenderContext<'root> {
self.context
}
fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
if let Some(body) = self.element.body() {
body.renderer(self.context).render(cursor)?;
} else {
cursor.buffer.push_str("<body></body>");
}
let mut body = RenderBuffer::default();
std::mem::swap(&mut body, &mut cursor.buffer);
cursor.buffer.push_str("<!doctype html>");
cursor.buffer.open_tag("html");
cursor
.buffer
.push_attribute("lang", self.element.attributes.lang())?;
cursor
.buffer
.push_attribute("dir", self.element.attributes.dir())?;
cursor
.buffer
.push_attribute("xmlns", "http://www.w3.org/1999/xhtml")?;
cursor
.buffer
.push_attribute("xmlns:v", "urn:schemas-microsoft-com:vml")?;
cursor
.buffer
.push_attribute("xmlns:o", "urn:schemas-microsoft-com:office:office")?;
cursor.buffer.close_tag();
if let Some(head) = self.element.head() {
head.renderer(self.context).render(cursor)?;
} else {
MjHead::default().renderer(self.context).render(cursor)?;
}
cursor.buffer.push_str(body.as_ref());
cursor.buffer.end_tag("html");
Ok(())
}
}
impl<'render, 'root: 'render> Renderable<'render, 'root> for Mjml {
fn renderer(
&'root self,
context: &'root RenderContext<'root>,
) -> Box<dyn Render<'root> + 'render> {
Box::new(Renderer::new(context, self, ()))
}
}
impl Mjml {
pub fn render(&self, opts: &RenderOptions) -> Result<String, Error> {
let header = Header::new(
self.children.head.as_ref(),
self.attributes.lang(),
self.attributes.dir(),
);
let context = RenderContext::new(opts, header);
let mut cursor = RenderCursor::default();
self.renderer(&context).render(&mut cursor)?;
#[cfg(feature = "css-inline")]
if !cursor.header.inline_styles().is_empty() {
let inline_styles = cursor
.header
.inline_styles()
.iter()
.map(|s| s.as_ref())
.collect::<String>();
let inliner = css_inline::CSSInliner::options()
.inline_style_tags(false)
.keep_link_tags(true)
.keep_style_tags(true)
.load_remote_stylesheets(false)
.extra_css(Some(Cow::Owned(inline_styles)))
.build();
return inliner
.inline(cursor.buffer.as_ref())
.map_err(Error::InlineCSS);
}
Ok(cursor.buffer.into())
}
pub fn get_title(&self) -> Option<String> {
self.head()
.and_then(|head| head.title())
.map(|title| title.content().to_string())
}
pub fn get_preview(&self) -> Option<String> {
self.head()
.and_then(|head| head.preview())
.map(|preview| preview.content().to_string())
}
}
#[cfg(all(test, feature = "parse"))]
mod tests {
use crate::mjml::Mjml;
use crate::prelude::render::RenderOptions;
crate::should_render!(empty, "mjml");
#[test]
fn template_amario() {
let opts = RenderOptions::default();
let template = include_str!("../../resources/template/amario.mjml");
let root = Mjml::parse(template).unwrap();
assert!(root.element.render(&opts).is_ok());
}
#[test]
fn template_air_astana() {
let opts = RenderOptions::default();
let template = include_str!("../../resources/template/air-astana.mjml");
let expected = include_str!("../../resources/template/air-astana.html");
let root = Mjml::parse(template).unwrap();
html_compare::assert_similar(expected, root.element.render(&opts).unwrap().as_str());
}
#[test]
fn stable_output() {
let source = "<mjml><mj-body><mj-section><mj-column><mj-text>hi</mj-text></mj-column></mj-section></mj-body></mjml>";
let options = RenderOptions::default();
let root_1 = Mjml::parse(source).unwrap();
let root_2 = Mjml::parse(source).unwrap();
let output_1 = root_1.element.render(&options).unwrap();
let output_2 = root_2.element.render(&options).unwrap();
assert_eq!(output_1, output_2);
}
#[test]
#[cfg(feature = "css-inline")]
fn test_css_inlining() {
let source = r#"<mjml>
<mj-head>
<mj-style>
.red { color: red; }
</mj-style>
<mj-style inline="inline">
.blue { color: blue; }
</mj-style>
</mj-head>
<mj-body>
<mj-section>
<mj-column>
<mj-text>
<p class="blue">Blue text</p>
<p class="red">Red text</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>"#;
let options = RenderOptions::default();
let output = Mjml::parse(source)
.unwrap()
.element
.render(&options)
.unwrap();
assert!(
output.contains("<p class=\"blue\" style=\"color: blue;\">"),
"CSS inlining should happen for elements with inline styles"
);
assert!(
output.contains("<p class=\"red\">"),
"CSS inlining should not happen for elements without inline styles"
);
}
}