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                "{} url('{}') no-repeat {} / cover",
115                // has default value
116                color,
117                url,
118                // has default value
119                position
120            )))
121        } else {
122            self.attribute("background-color").map(Cow::Borrowed)
123        }
124    }
125
126    fn set_style_hero<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
127    where
128        'root: 'a,
129        'a: 't,
130    {
131        tag.maybe_add_style("background", self.get_background())
132            .maybe_add_style("background-position", self.attribute("background-position"))
133            .add_style("background-repeat", "no-repeat")
134            .maybe_add_style("padding", self.attribute("padding"))
135            .maybe_add_style("padding-top", self.attribute("padding-top"))
136            .maybe_add_style("padding-right", self.attribute("padding-right"))
137            .maybe_add_style("padding-bottom", self.attribute("padding-bottom"))
138            .maybe_add_style("padding-left", self.attribute("padding-left"))
139            .maybe_add_style("vertical-align", self.attribute("vertical-align"))
140    }
141
142    fn render_children(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
143        let siblings = self.element.children.len();
144        let raw_siblings = self.element.children.iter().filter(|c| c.is_raw()).count();
145        for (index, child) in self.element.children.iter().enumerate() {
146            let mut renderer = child.renderer(self.context());
147            renderer.set_index(index);
148            renderer.set_siblings(siblings);
149            renderer.set_raw_siblings(raw_siblings);
150            if child.is_raw() {
151                renderer.render(cursor)?;
152            } else {
153                let tr = Tag::tr();
154                let td = Tag::td()
155                    .maybe_add_style(
156                        "background",
157                        renderer.attribute("container-background-color"),
158                    )
159                    .add_style("font-size", "0px")
160                    .maybe_add_style("padding", renderer.attribute("padding"))
161                    .maybe_add_style("padding-top", renderer.attribute("padding-top"))
162                    .maybe_add_style("padding-right", renderer.attribute("padding-right"))
163                    .maybe_add_style("padding-bottom", renderer.attribute("padding-bottom"))
164                    .maybe_add_style("padding-left", renderer.attribute("padding-left"))
165                    .add_style("word-break", "break-word")
166                    .maybe_add_attribute("align", renderer.attribute("align"))
167                    .maybe_add_attribute(
168                        "background",
169                        renderer.attribute("container-background-color"),
170                    )
171                    .maybe_add_attribute("class", renderer.attribute("css-class"));
172
173                tr.render_open(&mut cursor.buffer)?;
174                td.render_open(&mut cursor.buffer)?;
175                renderer.render(cursor)?;
176                td.render_close(&mut cursor.buffer);
177                tr.render_close(&mut cursor.buffer);
178            };
179        }
180
181        Ok(())
182    }
183
184    fn render_content(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
185        let table = self
186            .set_style_outlook_inner_table(Tag::table_borderless())
187            .maybe_add_attribute("align", self.attribute("align"))
188            .maybe_add_attribute(
189                "width",
190                self.container_width.as_ref().map(|w| w.value().to_string()),
191            );
192        let tbody = Tag::tbody();
193        let tr = Tag::tr();
194        let td = Tag::td();
195        let outlook_inner_td = self.set_style_outlook_inner_td(Tag::td());
196        let outlook_inner_div = self
197            .set_style_inner_div(Tag::div())
198            .maybe_add_attribute("width", self.attribute("align"))
199            .add_class("mj-hero-content");
200        let inner_table = self.set_style_inner_table(Tag::table_presentation());
201
202        cursor.buffer.start_conditional_tag();
203        table.render_open(&mut cursor.buffer)?;
204        tr.render_open(&mut cursor.buffer)?;
205        outlook_inner_td.render_open(&mut cursor.buffer)?;
206        cursor.buffer.end_conditional_tag();
207
208        outlook_inner_div.render_open(&mut cursor.buffer)?;
209        inner_table.render_open(&mut cursor.buffer)?;
210        tbody.render_open(&mut cursor.buffer)?;
211        tr.render_open(&mut cursor.buffer)?;
212        td.render_open(&mut cursor.buffer)?;
213        inner_table.render_open(&mut cursor.buffer)?;
214        tbody.render_open(&mut cursor.buffer)?;
215        self.render_children(cursor)?;
216        tbody.render_close(&mut cursor.buffer);
217        inner_table.render_close(&mut cursor.buffer);
218        td.render_close(&mut cursor.buffer);
219        tr.render_close(&mut cursor.buffer);
220        tbody.render_close(&mut cursor.buffer);
221        inner_table.render_close(&mut cursor.buffer);
222        outlook_inner_div.render_close(&mut cursor.buffer);
223
224        cursor.buffer.start_conditional_tag();
225        outlook_inner_td.render_close(&mut cursor.buffer);
226        tr.render_close(&mut cursor.buffer);
227        table.render_close(&mut cursor.buffer);
228        cursor.buffer.end_conditional_tag();
229
230        Ok(())
231    }
232
233    fn render_mode_fluid(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
234        let td_fluid = self.set_style_td_fluid(Tag::td());
235        let td = self
236            .set_style_hero(Tag::td())
237            .maybe_add_attribute("background", self.attribute("background-url"));
238
239        td_fluid.render_closed(&mut cursor.buffer)?;
240        td.render_open(&mut cursor.buffer)?;
241        self.render_content(cursor)?;
242        td.render_close(&mut cursor.buffer);
243        td_fluid.render_closed(&mut cursor.buffer)?;
244
245        Ok(())
246    }
247
248    fn render_mode_fixed(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
249        // has a default value
250        let height = self
251            .attribute_as_pixel("height")
252            .map(|v| v.value())
253            .unwrap_or(0.0);
254        let padding = self.get_padding_vertical().value();
255        let height = height - padding;
256        let td = self
257            .set_style_hero(Tag::td())
258            .add_style("height", format!("{height}px"))
259            .maybe_add_attribute("background", self.attribute("background-url"))
260            .add_attribute("height", height.to_string());
261
262        td.render_open(&mut cursor.buffer)?;
263        self.render_content(cursor)?;
264        td.render_close(&mut cursor.buffer);
265
266        Ok(())
267    }
268
269    fn render_mode(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
270        match self.attribute("mode") {
271            Some(inner) if inner.eq("fluid") => self.render_mode_fluid(cursor),
272            _ => self.render_mode_fixed(cursor),
273        }
274    }
275}
276
277impl<'root> Render<'root> for Renderer<'root, MjHero, ()> {
278    fn default_attribute(&self, name: &str) -> Option<&'static str> {
279        match name {
280            "background-color" => Some("#ffffff"),
281            "background-position" => Some("center center"),
282            "height" => Some("0px"),
283            "mode" => Some("fixed-height"),
284            "padding" => Some("0px"),
285            "vertical-align" => Some("top"),
286            _ => None,
287        }
288    }
289
290    fn raw_attribute(&self, key: &str) -> Option<&'root str> {
291        match self.element.attributes.get(key) {
292            Some(Some(inner)) => Some(inner),
293            _ => None,
294        }
295    }
296
297    fn tag(&self) -> Option<&str> {
298        Some(NAME)
299    }
300
301    fn context(&self) -> &'root RenderContext<'root> {
302        self.context
303    }
304
305    fn set_container_width(&mut self, width: Option<Pixel>) {
306        self.container_width = width;
307    }
308
309    fn set_siblings(&mut self, value: usize) {
310        self.siblings = value;
311    }
312
313    fn set_raw_siblings(&mut self, value: usize) {
314        self.raw_siblings = value;
315    }
316
317    fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
318        let outlook_table = self
319            .set_style_outlook_table(Tag::table_presentation())
320            .add_attribute("align", "center")
321            .maybe_add_attribute(
322                "width",
323                self.container_width.as_ref().map(|v| v.value().to_string()),
324            );
325        let outlook_tr = Tag::tr();
326        let outlook_td = self.set_style_outlook_td(Tag::td());
327        let v_image = self
328            .set_style_outlook_image(Tag::new("v:image"))
329            .maybe_add_attribute("src", self.attribute("background-url"))
330            .add_attribute("xmlns:v", "urn:schemas-microsoft-com:vml");
331        let div = self
332            .set_style_div(Tag::div())
333            .maybe_add_attribute("align", self.attribute("align"))
334            .maybe_add_class(self.attribute("css-class"));
335        let table = self.set_style_table(Tag::table_presentation());
336        let tbody = Tag::tbody();
337        let tr = self.set_style_tr(Tag::tr());
338
339        cursor.buffer.start_conditional_tag();
340        outlook_table.render_open(&mut cursor.buffer)?;
341        outlook_tr.render_open(&mut cursor.buffer)?;
342        outlook_td.render_open(&mut cursor.buffer)?;
343        v_image.render_closed(&mut cursor.buffer)?;
344        cursor.buffer.end_conditional_tag();
345
346        div.render_open(&mut cursor.buffer)?;
347        table.render_open(&mut cursor.buffer)?;
348        tbody.render_open(&mut cursor.buffer)?;
349        tr.render_open(&mut cursor.buffer)?;
350
351        self.render_mode(cursor)?;
352
353        tr.render_close(&mut cursor.buffer);
354        tbody.render_close(&mut cursor.buffer);
355        table.render_close(&mut cursor.buffer);
356        div.render_close(&mut cursor.buffer);
357
358        cursor.buffer.start_conditional_tag();
359        outlook_td.render_close(&mut cursor.buffer);
360        outlook_tr.render_close(&mut cursor.buffer);
361        outlook_table.render_close(&mut cursor.buffer);
362        cursor.buffer.end_conditional_tag();
363
364        Ok(())
365    }
366}
367
368impl<'render, 'root: 'render> Renderable<'render, 'root> for MjHero {
369    fn renderer(
370        &'root self,
371        context: &'root RenderContext<'root>,
372    ) -> Box<dyn Render<'root> + 'render> {
373        Box::new(Renderer::new(context, self, ()))
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    crate::should_render!(basic, "mj-hero");
380    crate::should_render!(background_color, "mj-hero-background-color");
381    crate::should_render!(background_height, "mj-hero-background-height");
382    crate::should_render!(background_position, "mj-hero-background-position");
383    crate::should_render!(background_url, "mj-hero-background-url");
384    crate::should_render!(background_width, "mj-hero-background-width");
385    crate::should_render!(class, "mj-hero-class");
386    crate::should_render!(height, "mj-hero-height");
387    crate::should_render!(mode, "mj-hero-mode");
388    crate::should_render!(vertical_align, "mj-hero-vertical-align");
389    crate::should_render!(width, "mj-hero-width");
390}