use super::BodyElement;
use crate::mjml::body::prelude::*;
use crate::mjml::error::Error;
use crate::mjml::prelude::*;
use crate::util::condition::*;
use crate::util::prelude::*;
use crate::util::{Attributes, Context, Header, Size, Style, Tag};
use crate::Options;
use log::debug;
use roxmltree::Node;
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Clone, Debug)]
pub struct MJGroup {
attributes: HashMap<String, String>,
context: Option<Context>,
children: Vec<BodyElement>,
}
impl MJGroup {
pub fn parse<'a, 'b>(
node: Node<'a, 'b>,
opts: &Options,
_extra: Option<&Attributes>,
) -> Result<MJGroup, Error> {
let mut children = vec![];
let mut attrs = Attributes::new();
attrs.set("mobile-width", "mobile-width");
for child in node.children() {
children.push(BodyElement::parse(child, opts, Some(&attrs))?);
}
Ok(MJGroup {
attributes: get_node_attributes(&node),
context: None,
children,
})
}
fn get_width(&self) -> Option<Size> {
self.get_current_width().and_then(|width| {
if width.is_percent() {
self.get_container_width().and_then(|container| {
Some(Size::Pixel(container.value() * width.value() / 100.0))
})
} else {
Some(width)
}
})
}
fn get_style_div(&self) -> Style {
let mut res = Style::new();
res.set("font-size", "0");
res.set("line-height", "0");
res.set("text-align", "left");
res.set("display", "inline-block");
res.set("width", "100%");
res.maybe_set("background-color", self.get_attribute("background-color"));
res.maybe_set("direction", self.get_attribute("direction"));
res.maybe_set("vertical-align", self.get_attribute("vertical-align"));
res
}
fn get_style_td_outlook(&self) -> Style {
let mut res = Style::new();
res.maybe_set("vertical-align", self.get_attribute("vertical-align"));
res.maybe_set("width", self.get_width());
res
}
fn get_parsed_width(&self) -> Size {
let non_raw_siblings = self
.context()
.and_then(|ctx| Some(ctx.non_raw_siblings()))
.or(Some(1))
.unwrap();
match self.get_size_attribute("width") {
Some(size) => size,
None => Size::Percent(100.0 / (non_raw_siblings as f32)),
}
}
fn get_column_class(&self) -> (String, Size) {
let parsed_width = self.get_parsed_width();
let classname = match parsed_width {
Size::Percent(value) => format!("mj-column-per-{}", value),
_ => format!("mj-column-px-{}", parsed_width.value()),
};
(classname.replace(".", "-"), parsed_width)
}
fn render_child(&self, header: &Header, child: &BodyElement) -> Result<String, Error> {
let td = Tag::new("td")
.maybe_set_style("align", child.get_attribute("align"))
.maybe_set_style("vertical-align", child.get_attribute("vertical-align"))
.maybe_set_style(
"width",
child.get_width().or_else(|| {
child
.get_attribute("width")
.and_then(|value| Size::from_str(value.as_str()).ok())
}),
);
Ok(format!(
"{}{}{}",
conditional_tag(td.open()),
child.render(header)?,
conditional_tag(td.close())
))
}
fn render_children(&self, header: &Header) -> Result<String, Error> {
let mut res = vec![];
for child in self.children.iter() {
if child.is_raw() {
res.push(child.render(header)?);
} else {
res.push(self.render_child(header, child)?);
}
}
Ok(res.join(""))
}
}
impl Component for MJGroup {
fn context(&self) -> Option<&Context> {
self.context.as_ref()
}
fn update_header(&self, header: &mut Header) {
let (classname, size) = self.get_column_class();
header.add_media_query(classname, size);
for child in self.children.iter() {
child.update_header(header);
}
}
fn set_context(&mut self, ctx: Context) {
self.context = Some(ctx.clone());
let children_ctx = Context::from(
&ctx,
self.get_current_width(),
self.get_siblings(),
self.get_raw_siblings(),
0,
);
for (idx, item) in self.children.iter_mut().enumerate() {
let mut child_ctx = children_ctx.clone();
child_ctx.set_index(idx);
item.set_context(child_ctx.clone());
}
}
fn render(&self, header: &Header) -> Result<String, Error> {
let div = Tag::new("div")
.set_class(self.get_column_class().0)
.set_class("mj-outlook-group-fix")
.maybe_set_class(self.get_attribute("css-class"))
.insert_style(self.get_style_div().inner());
let table = Tag::table();
let tr = Tag::new("tr");
let mut res: Vec<String> = vec![];
res.push(div.open());
res.push(START_CONDITIONAL_TAG.into());
res.push(table.open());
res.push(tr.open());
res.push(END_CONDITIONAL_TAG.into());
res.push(self.render_children(header)?);
res.push(START_CONDITIONAL_TAG.into());
res.push(tr.close());
res.push(table.close());
res.push(END_CONDITIONAL_TAG.into());
res.push(div.close());
Ok(res.join(""))
}
}
impl ComponentWithAttributes for MJGroup {
fn default_attribute(&self, key: &str) -> Option<String> {
debug!("default_attribute {}", key);
match key {
"direction" => Some("ltr".into()),
_ => None,
}
}
fn source_attributes(&self) -> Option<&HashMap<String, String>> {
Some(&self.attributes)
}
}
impl BodyComponent for MJGroup {
fn get_style(&self, key: &str) -> Style {
match key {
"div" => self.get_style_div(),
"td-outlook" => self.get_style_td_outlook(),
_ => Style::new(),
}
}
}
impl ComponentWithChildren for MJGroup {
fn get_children(&self) -> &Vec<BodyElement> {
&self.children
}
fn get_current_width(&self) -> Option<Size> {
let ctx = match self.context() {
Some(value) => value,
None => return None,
};
let parent_width = ctx.container_width().unwrap();
let non_raw_siblings = ctx.non_raw_siblings();
let borders = self.get_border_horizontal_width();
let paddings = self.get_padding_horizontal_width();
let inner_border_left = match self.get_prefixed_border_left("inner") {
Some(size) => size.value(),
None => 0.0,
};
let inner_border_right = match self.get_prefixed_border_right("inner") {
Some(size) => size.value(),
None => 0.0,
};
let inner_borders = inner_border_left + inner_border_right;
let all_paddings = paddings.value() + borders.value() + inner_borders;
let container_width = match self.get_size_attribute("width") {
Some(value) => value,
None => Size::Pixel(parent_width.value() / (non_raw_siblings as f32)),
};
if container_width.is_percent() {
Some(Size::Pixel(
(parent_width.value() * container_width.value() / 100.0) - all_paddings,
))
} else {
Some(Size::Pixel(container_width.value() - all_paddings))
}
}
}
impl ComponentWithSizeAttribute for MJGroup {}
impl BodyContainedComponent for MJGroup {}
impl BodyComponentWithBorder for MJGroup {}
impl BodyComponentWithBoxWidths for MJGroup {}
impl BodyComponentWithPadding for MJGroup {}
#[cfg(test)]
pub mod tests {
use crate::tests::compare_render;
#[test]
fn base() {
compare_render(
include_str!("../../../test/mj-group.mjml"),
include_str!("../../../test/mj-group.html"),
);
}
#[test]
fn with_background_color() {
compare_render(
include_str!("../../../test/mj-group-background-color.mjml"),
include_str!("../../../test/mj-group-background-color.html"),
);
}
#[test]
fn with_css_class() {
compare_render(
include_str!("../../../test/mj-group-class.mjml"),
include_str!("../../../test/mj-group-class.html"),
);
}
#[test]
fn with_direction() {
compare_render(
include_str!("../../../test/mj-group-direction.mjml"),
include_str!("../../../test/mj-group-direction.html"),
);
}
#[test]
fn with_vertical_align() {
compare_render(
include_str!("../../../test/mj-group-vertical-align.mjml"),
include_str!("../../../test/mj-group-vertical-align.html"),
);
}
#[test]
fn with_width() {
compare_render(
include_str!("../../../test/mj-group-width.mjml"),
include_str!("../../../test/mj-group-width.html"),
);
}
}