mrml/mj_hero/
render.rs

1use std::borrow::Cow;
2
3use super::{MjHero, NAME};
4use crate::helper::size::Pixel;
5use crate::prelude::render::*;
6
7impl<'root> Renderer<'root, MjHero, ()> {
8    fn set_style_div<'t>(&self, tag: Tag<'t>) -> Tag<'t> {
9        tag.add_style("margin", "0 auto").maybe_add_style(
10            "max-width",
11            self.container_width.as_ref().map(|w| w.to_string()),
12        )
13    }
14
15    fn set_style_table<'t>(&self, tag: Tag<'t>) -> Tag<'t> {
16        tag.add_style("width", "100%")
17    }
18
19    fn set_style_tr<'t>(&self, tag: Tag<'t>) -> Tag<'t> {
20        tag.add_style("vertical-align", "top")
21    }
22
23    fn set_style_td_fluid<'t>(&self, tag: Tag<'t>) -> Tag<'t> {
24        // TODO check size type compatibility
25        let bg_ratio = self
26            .attribute_as_size("background-height")
27            .and_then(|height| {
28                self.attribute_as_size("background-width")
29                    .map(|width| height.value() * 100.0 / width.value())
30            });
31        tag.add_style("mso-padding-bottom-alt", "0")
32            .maybe_add_style("padding-bottom", bg_ratio.map(|v| v.to_string()))
33            .add_style("width", "0.01%")
34    }
35
36    fn set_style_outlook_table<'t>(&self, tag: Tag<'t>) -> Tag<'t> {
37        tag.maybe_add_style(
38            "width",
39            self.container_width.as_ref().map(|w| w.to_string()),
40        )
41    }
42
43    fn set_style_outlook_inner_table<'t>(&self, tag: Tag<'t>) -> Tag<'t> {
44        self.set_style_outlook_table(tag)
45    }
46
47    fn set_style_outlook_inner_td<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
48    where
49        'root: 'a,
50        'a: 't,
51    {
52        tag.maybe_add_style("background-color", self.attribute("inner-background-color"))
53            .maybe_add_style("padding", self.attribute("inner-padding"))
54            .maybe_add_style("padding-top", self.attribute("inner-padding-top"))
55            .maybe_add_style("padding-right", self.attribute("inner-padding-right"))
56            .maybe_add_style("padding-bottom", self.attribute("inner-padding-bottom"))
57            .maybe_add_style("padding-left", self.attribute("inner-padding-left"))
58    }
59
60    fn set_style_inner_div<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
61    where
62        'root: 'a,
63        'a: 't,
64    {
65        tag.maybe_add_style("background-color", self.attribute("inner-background-color"))
66            .maybe_add_style("float", self.attribute("align"))
67            .add_style("margin", "0px auto")
68            .maybe_add_style("width", self.attribute("width"))
69    }
70
71    fn set_style_inner_table<'t>(&self, tag: Tag<'t>) -> Tag<'t> {
72        tag.add_style("width", "100%").add_style("margin", "0px")
73    }
74
75    fn set_style_outlook_image<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
76    where
77        'root: 'a,
78        'a: 't,
79    {
80        tag.add_style("border", "0")
81            .maybe_add_style("height", self.attribute("background-height"))
82            .add_style("mso-position-horizontal", "center")
83            .add_style("position", "absolute")
84            .add_style("top", "0")
85            .maybe_add_style(
86                "width",
87                self.attribute("background-width")
88                    .map(Cow::Borrowed)
89                    .or_else(|| {
90                        self.container_width
91                            .as_ref()
92                            .map(|w| Cow::Owned(w.to_string()))
93                    }),
94            )
95            .add_style("z-index", "-3")
96    }
97
98    fn set_style_outlook_td<'t>(&self, tag: Tag<'t>) -> Tag<'t> {
99        tag.add_style("line-height", "0")
100            .add_style("font-size", "0")
101            .add_style("mso-line-height-rule", "exactly")
102    }
103
104    fn get_background<'a>(&'a self) -> Option<Cow<'a, str>>
105    where
106        'root: 'a,
107    {
108        if let (Some(url), Some(color), Some(position)) = (
109            self.attribute("background-url"),
110            self.attribute("background-color"),
111            self.attribute("background-position"),
112        ) {
113            Some(Cow::Owned(format!(
114                "{color} url('{url}') no-repeat {position} / cover"
115            )))
116        } else {
117            self.attribute("background-color").map(Cow::Borrowed)
118        }
119    }
120
121    fn set_style_hero<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
122    where
123        'root: 'a,
124        'a: 't,
125    {
126        tag.maybe_add_style("background", self.get_background())
127            .maybe_add_style("background-position", self.attribute("background-position"))
128            .add_style("background-repeat", "no-repeat")
129            .maybe_add_style("padding", self.attribute("padding"))
130            .maybe_add_style("padding-top", self.attribute("padding-top"))
131            .maybe_add_style("padding-right", self.attribute("padding-right"))
132            .maybe_add_style("padding-bottom", self.attribute("padding-bottom"))
133            .maybe_add_style("padding-left", self.attribute("padding-left"))
134            .maybe_add_style("vertical-align", self.attribute("vertical-align"))
135    }
136
137    fn render_children(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
138        let siblings = self.element.children.len();
139        let raw_siblings = self.element.children.iter().filter(|c| c.is_raw()).count();
140        for (index, child) in self.element.children.iter().enumerate() {
141            let mut renderer = child.renderer(self.context());
142            renderer.set_index(index);
143            renderer.set_siblings(siblings);
144            renderer.set_raw_siblings(raw_siblings);
145            if child.is_raw() {
146                renderer.render(cursor)?;
147            } else {
148                let tr = Tag::tr();
149                let td = Tag::td()
150                    .maybe_add_style(
151                        "background",
152                        renderer.attribute("container-background-color"),
153                    )
154                    .add_style("font-size", "0px")
155                    .maybe_add_style("padding", renderer.attribute("padding"))
156                    .maybe_add_style("padding-top", renderer.attribute("padding-top"))
157                    .maybe_add_style("padding-right", renderer.attribute("padding-right"))
158                    .maybe_add_style("padding-bottom", renderer.attribute("padding-bottom"))
159                    .maybe_add_style("padding-left", renderer.attribute("padding-left"))
160                    .add_style("word-break", "break-word")
161                    .maybe_add_attribute("align", renderer.attribute("align"))
162                    .maybe_add_attribute(
163                        "background",
164                        renderer.attribute("container-background-color"),
165                    )
166                    .maybe_add_attribute("class", renderer.attribute("css-class"));
167
168                tr.render_open(&mut cursor.buffer)?;
169                td.render_open(&mut cursor.buffer)?;
170                renderer.render(cursor)?;
171                td.render_close(&mut cursor.buffer);
172                tr.render_close(&mut cursor.buffer);
173            };
174        }
175
176        Ok(())
177    }
178
179    fn render_content(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
180        let table = self
181            .set_style_outlook_inner_table(Tag::table_borderless())
182            .maybe_add_attribute("align", self.attribute("align"))
183            .maybe_add_attribute(
184                "width",
185                self.container_width.as_ref().map(|w| w.value().to_string()),
186            );
187        let tbody = Tag::tbody();
188        let tr = Tag::tr();
189        let td = Tag::td();
190        let outlook_inner_td = self.set_style_outlook_inner_td(Tag::td());
191        let outlook_inner_div = self
192            .set_style_inner_div(Tag::div())
193            .maybe_add_attribute("width", self.attribute("align"))
194            .add_class("mj-hero-content");
195        let inner_table = self.set_style_inner_table(Tag::table_presentation());
196
197        cursor.buffer.start_conditional_tag();
198        table.render_open(&mut cursor.buffer)?;
199        tr.render_open(&mut cursor.buffer)?;
200        outlook_inner_td.render_open(&mut cursor.buffer)?;
201        cursor.buffer.end_conditional_tag();
202
203        outlook_inner_div.render_open(&mut cursor.buffer)?;
204        inner_table.render_open(&mut cursor.buffer)?;
205        tbody.render_open(&mut cursor.buffer)?;
206        tr.render_open(&mut cursor.buffer)?;
207        td.render_open(&mut cursor.buffer)?;
208        inner_table.render_open(&mut cursor.buffer)?;
209        tbody.render_open(&mut cursor.buffer)?;
210        self.render_children(cursor)?;
211        tbody.render_close(&mut cursor.buffer);
212        inner_table.render_close(&mut cursor.buffer);
213        td.render_close(&mut cursor.buffer);
214        tr.render_close(&mut cursor.buffer);
215        tbody.render_close(&mut cursor.buffer);
216        inner_table.render_close(&mut cursor.buffer);
217        outlook_inner_div.render_close(&mut cursor.buffer);
218
219        cursor.buffer.start_conditional_tag();
220        outlook_inner_td.render_close(&mut cursor.buffer);
221        tr.render_close(&mut cursor.buffer);
222        table.render_close(&mut cursor.buffer);
223        cursor.buffer.end_conditional_tag();
224
225        Ok(())
226    }
227
228    fn render_mode_fluid(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
229        let td_fluid = self.set_style_td_fluid(Tag::td());
230        let td = self
231            .set_style_hero(Tag::td())
232            .maybe_add_attribute("background", self.attribute("background-url"));
233
234        td_fluid.render_closed(&mut cursor.buffer)?;
235        td.render_open(&mut cursor.buffer)?;
236        self.render_content(cursor)?;
237        td.render_close(&mut cursor.buffer);
238        td_fluid.render_closed(&mut cursor.buffer)?;
239
240        Ok(())
241    }
242
243    fn render_mode_fixed(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
244        // has a default value
245        let height = self
246            .attribute_as_pixel("height")
247            .map(|v| v.value())
248            .unwrap_or(0.0);
249        let padding = self.get_padding_vertical().value();
250        let height = height - padding;
251        let td = self
252            .set_style_hero(Tag::td())
253            .add_style("height", format!("{height}px"))
254            .maybe_add_attribute("background", self.attribute("background-url"))
255            .add_attribute("height", height.to_string());
256
257        td.render_open(&mut cursor.buffer)?;
258        self.render_content(cursor)?;
259        td.render_close(&mut cursor.buffer);
260
261        Ok(())
262    }
263
264    fn render_mode(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
265        match self.attribute("mode") {
266            Some(inner) if inner.eq("fluid") => self.render_mode_fluid(cursor),
267            _ => self.render_mode_fixed(cursor),
268        }
269    }
270}
271
272impl<'root> Render<'root> for Renderer<'root, MjHero, ()> {
273    fn default_attribute(&self, name: &str) -> Option<&'static str> {
274        match name {
275            "background-color" => Some("#ffffff"),
276            "background-position" => Some("center center"),
277            "height" => Some("0px"),
278            "mode" => Some("fixed-height"),
279            "padding" => Some("0px"),
280            "vertical-align" => Some("top"),
281            _ => None,
282        }
283    }
284
285    fn raw_attribute(&self, key: &str) -> Option<&'root str> {
286        match self.element.attributes.get(key) {
287            Some(Some(inner)) => Some(inner),
288            _ => None,
289        }
290    }
291
292    fn tag(&self) -> Option<&str> {
293        Some(NAME)
294    }
295
296    fn context(&self) -> &'root RenderContext<'root> {
297        self.context
298    }
299
300    fn set_container_width(&mut self, width: Option<Pixel>) {
301        self.container_width = width;
302    }
303
304    fn set_siblings(&mut self, value: usize) {
305        self.siblings = value;
306    }
307
308    fn set_raw_siblings(&mut self, value: usize) {
309        self.raw_siblings = value;
310    }
311
312    fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
313        let outlook_table = self
314            .set_style_outlook_table(Tag::table_presentation())
315            .add_attribute("align", "center")
316            .maybe_add_attribute(
317                "width",
318                self.container_width.as_ref().map(|v| v.value().to_string()),
319            );
320        let outlook_tr = Tag::tr();
321        let outlook_td = self.set_style_outlook_td(Tag::td());
322        let v_image = self
323            .set_style_outlook_image(Tag::new("v:image"))
324            .maybe_add_attribute("src", self.attribute("background-url"))
325            .add_attribute("xmlns:v", "urn:schemas-microsoft-com:vml");
326        let div = self
327            .set_style_div(Tag::div())
328            .maybe_add_attribute("align", self.attribute("align"))
329            .maybe_add_class(self.attribute("css-class"));
330        let table = self.set_style_table(Tag::table_presentation());
331        let tbody = Tag::tbody();
332        let tr = self.set_style_tr(Tag::tr());
333
334        cursor.buffer.start_conditional_tag();
335        outlook_table.render_open(&mut cursor.buffer)?;
336        outlook_tr.render_open(&mut cursor.buffer)?;
337        outlook_td.render_open(&mut cursor.buffer)?;
338        v_image.render_closed(&mut cursor.buffer)?;
339        cursor.buffer.end_conditional_tag();
340
341        div.render_open(&mut cursor.buffer)?;
342        table.render_open(&mut cursor.buffer)?;
343        tbody.render_open(&mut cursor.buffer)?;
344        tr.render_open(&mut cursor.buffer)?;
345
346        self.render_mode(cursor)?;
347
348        tr.render_close(&mut cursor.buffer);
349        tbody.render_close(&mut cursor.buffer);
350        table.render_close(&mut cursor.buffer);
351        div.render_close(&mut cursor.buffer);
352
353        cursor.buffer.start_conditional_tag();
354        outlook_td.render_close(&mut cursor.buffer);
355        outlook_tr.render_close(&mut cursor.buffer);
356        outlook_table.render_close(&mut cursor.buffer);
357        cursor.buffer.end_conditional_tag();
358
359        Ok(())
360    }
361}
362
363impl<'render, 'root: 'render> Renderable<'render, 'root> for MjHero {
364    fn renderer(
365        &'root self,
366        context: &'root RenderContext<'root>,
367    ) -> Box<dyn Render<'root> + 'render> {
368        Box::new(Renderer::new(context, self, ()))
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    crate::should_render!(basic, "mj-hero");
375    crate::should_render!(background_color, "mj-hero-background-color");
376    crate::should_render!(background_height, "mj-hero-background-height");
377    crate::should_render!(background_position, "mj-hero-background-position");
378    crate::should_render!(background_url, "mj-hero-background-url");
379    crate::should_render!(background_width, "mj-hero-background-width");
380    crate::should_render!(class, "mj-hero-class");
381    crate::should_render!(height, "mj-hero-height");
382    crate::should_render!(mode, "mj-hero-mode");
383    crate::should_render!(vertical_align, "mj-hero-vertical-align");
384    crate::should_render!(width, "mj-hero-width");
385}