mrml/mj_section/
render.rs

1use std::borrow::Cow;
2use std::convert::TryFrom;
3
4use super::{MjSection, NAME};
5use crate::helper::size::{Percent, Pixel};
6use crate::prelude::render::*;
7
8fn is_horizontal_position(value: &str) -> bool {
9    value == "left" || value == "right" || value == "center"
10}
11
12fn is_vertical_position(value: &str) -> bool {
13    value == "top" || value == "bottom" || value == "center"
14}
15
16pub(crate) trait WithMjSectionBackground<'root>: Render<'root> {
17    fn has_background(&self) -> bool {
18        self.attribute_exists("background-url")
19    }
20
21    fn parse_background_position<'a>(&'a self) -> (&'a str, &'a str)
22    where
23        'root: 'a,
24    {
25        let position = self
26            .attribute("background-position")
27            .unwrap_or(DEFAULT_BACKGROUND_POSITION);
28        let mut positions = position.split_whitespace();
29        if let Some(first) = positions.next() {
30            if let Some(second) = positions.next() {
31                if is_vertical_position(first) && is_horizontal_position(second) {
32                    (second, first)
33                } else {
34                    (first, second)
35                }
36            } else if is_vertical_position(first) {
37                ("center", first)
38            } else {
39                (first, "center")
40            }
41        } else {
42            ("center", "top")
43        }
44    }
45
46    fn get_background_position<'a>(&'a self) -> (&'a str, &'a str)
47    where
48        'root: 'a,
49    {
50        let (x, y) = self.parse_background_position();
51        (
52            self.attribute("background-position-x").unwrap_or(x),
53            self.attribute("background-position-y").unwrap_or(y),
54        )
55    }
56
57    fn get_background_position_str(&self) -> String {
58        let position = self.get_background_position();
59        format!("{} {}", position.0, position.1)
60    }
61
62    fn get_background(&self) -> Option<String> {
63        let mut res: Vec<Cow<'_, str>> = vec![];
64        if let Some(color) = self.attribute("background-color") {
65            res.push(color.into());
66        }
67        if let Some(url) = self.attribute("background-url") {
68            res.push(format!("url('{url}')").into());
69            // has default value
70            res.push(
71                format!(
72                    "{} / {}",
73                    self.get_background_position_str(),
74                    self.attribute("background-size")
75                        .unwrap_or(DEFAULT_BACKGROUND_SIZE)
76                )
77                .into(),
78            );
79            // has default value
80            res.push(
81                self.attribute("background-repeat")
82                    .unwrap_or(DEFAULT_BACKGROUND_REPEAT)
83                    .into(),
84            );
85        }
86
87        if res.is_empty() {
88            None
89        } else {
90            Some(res.join(" "))
91        }
92    }
93
94    fn set_background_style<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
95    where
96        'root: 'a,
97        'a: 't,
98    {
99        if self.has_background() {
100            tag.maybe_add_style("background", self.get_background())
101                .add_style("background-position", self.get_background_position_str())
102                .maybe_add_style("background-repeat", self.attribute("background-repeat"))
103                .maybe_add_style("background-size", self.attribute("background-size"))
104        } else {
105            tag.maybe_add_style("background", self.attribute("background-color"))
106                .maybe_add_style("background-color", self.attribute("background-color"))
107        }
108    }
109
110    fn get_vfill_position(&self) -> (Cow<'root, str>, Cow<'root, str>) {
111        if self.attribute_equals("background-size", "auto") {
112            return ("0.5, 0".into(), "0.5, 0".into());
113        }
114        let (bg_position_x, bg_position_y) = self.get_background_position();
115        let bg_repeat = self.attribute_equals("background-repeat", "repeat");
116        let bg_position_x = match bg_position_x {
117            "left" => "0%",
118            "center" => "50%",
119            "right" => "100%",
120            _ => {
121                if bg_position_x.ends_with('%') {
122                    bg_position_x
123                } else {
124                    "50%"
125                }
126            }
127        };
128        let bg_position_y = match bg_position_y {
129            "top" => "0%",
130            "center" => "50%",
131            "bottom" => "100%",
132            _ => {
133                if bg_position_y.ends_with('%') {
134                    bg_position_y
135                } else {
136                    "0%"
137                }
138            }
139        };
140        let position_x = if let Ok(position) = Percent::try_from(bg_position_x) {
141            if bg_repeat {
142                position.value() * 0.01
143            } else {
144                (position.value() - 50.0) * 0.01
145            }
146        } else if bg_repeat {
147            0.5
148        } else {
149            0.0
150        };
151        let position_y = if let Ok(position) = Percent::try_from(bg_position_y) {
152            if bg_repeat {
153                position.value() * 0.01
154            } else {
155                (position.value() - 50.0) * 0.01
156            }
157        } else if bg_repeat {
158            0.5
159        } else {
160            0.0
161        };
162        (
163            format!("{position_x}, {position_y}").into(),
164            format!("{position_x}, {position_y}").into(),
165        )
166    }
167
168    fn get_vfill_tag<'a>(&'a self) -> Tag<'a>
169    where
170        'root: 'a,
171    {
172        let bg_no_repeat = self.attribute_equals("background-repeat", "no-repeat");
173        let bg_size = self.attribute("background-size");
174        let bg_size_auto = bg_size
175            .as_ref()
176            .map(|value| *value == "auto")
177            .unwrap_or(false);
178        let vml_type = if bg_no_repeat && !bg_size_auto {
179            "frame"
180        } else {
181            "tile"
182        };
183        let vsize = match bg_size {
184            Some("cover") | Some("contain") => Some("1,1".to_string()),
185            Some("auto") => None,
186            Some(value) => Some(value.replace(' ', ",")),
187            None => None,
188        };
189        let aspect = match bg_size {
190            Some("cover") => Some("atleast".to_string()),
191            Some("contain") => Some("atmost".to_string()),
192            Some("auto") => None,
193            Some(other) => {
194                if other.split(' ').count() == 1 {
195                    Some("atmost".to_string())
196                } else {
197                    None
198                }
199            }
200            None => None,
201        };
202
203        let (vfill_position, vfill_origin) = self.get_vfill_position();
204        Tag::new("v:fill")
205            .add_attribute("position", vfill_position)
206            .add_attribute("origin", vfill_origin)
207            .maybe_add_attribute("src", self.attribute("background-url"))
208            .maybe_add_attribute("color", self.attribute("background-color"))
209            .maybe_add_attribute("size", vsize)
210            .add_attribute("type", vml_type)
211            .maybe_add_attribute("aspect", aspect)
212    }
213}
214
215pub trait SectionLikeRender<'root>: WithMjSectionBackground<'root> {
216    fn container_width(&self) -> &Option<Pixel>;
217    fn children(&self) -> &Vec<crate::mj_body::MjBodyChild>;
218
219    fn is_full_width(&self) -> bool {
220        self.attribute_exists("full-width")
221    }
222
223    fn render_with_background<F>(&self, cursor: &mut RenderCursor, content: F) -> Result<(), Error>
224    where
225        F: Fn(&mut RenderCursor) -> Result<(), Error>,
226    {
227        let full_width = self.is_full_width();
228        let vrect = Tag::new("v:rect")
229            .maybe_add_attribute(
230                "mso-width-percent",
231                if full_width { Some("1000") } else { None },
232            )
233            .maybe_add_style(
234                "width",
235                if full_width {
236                    None
237                } else {
238                    self.container_width().as_ref().map(|v| v.to_string())
239                },
240            )
241            .add_attribute("xmlns:v", "urn:schemas-microsoft-com:vml")
242            .add_attribute("fill", "true")
243            .add_attribute("stroke", "false");
244        let vfill = self.get_vfill_tag();
245        let vtextbox = Tag::new("v:textbox")
246            .add_attribute("inset", "0,0,0,0")
247            .add_style("mso-fit-shape-to-text", "true");
248
249        vrect.render_open(&mut cursor.buffer)?;
250        vfill.render_closed(&mut cursor.buffer)?;
251        vtextbox.render_open(&mut cursor.buffer)?;
252        cursor.buffer.end_conditional_tag();
253        content(cursor)?;
254        cursor.buffer.start_conditional_tag();
255        vtextbox.render_close(&mut cursor.buffer);
256        vrect.render_close(&mut cursor.buffer);
257
258        Ok(())
259    }
260
261    fn set_style_section_div<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
262    where
263        'root: 'a,
264        'a: 't,
265    {
266        let base = if self.is_full_width() {
267            tag
268        } else {
269            self.set_background_style(tag)
270        };
271        base.add_style("margin", "0px auto")
272            .maybe_add_style("border-radius", self.attribute("border-radius"))
273            .maybe_add_style(
274                "max-width",
275                self.container_width().as_ref().map(|item| item.to_string()),
276            )
277    }
278
279    fn render_wrap<F>(&self, cursor: &mut RenderCursor, content: F) -> Result<(), Error>
280    where
281        F: Fn(&mut RenderCursor) -> Result<(), Error>,
282    {
283        let table = Tag::table_presentation()
284            .maybe_add_attribute("bgcolor", self.attribute("background-color"))
285            .add_attribute("align", "center")
286            .maybe_add_attribute(
287                "width",
288                self.container_width()
289                    .as_ref()
290                    .map(|p| p.value().to_string()),
291            )
292            .maybe_add_style(
293                "width",
294                self.container_width().as_ref().map(|v| v.to_string()),
295            )
296            .maybe_add_suffixed_class(self.attribute("css-class"), "outlook");
297        let tr = Tag::tr();
298        let td = Tag::td()
299            .add_style("line-height", "0px")
300            .add_style("font-size", "0px")
301            .add_style("mso-line-height-rule", "exactly");
302
303        cursor.buffer.start_conditional_tag();
304        table.render_open(&mut cursor.buffer)?;
305        tr.render_open(&mut cursor.buffer)?;
306        td.render_open(&mut cursor.buffer)?;
307        content(cursor)?;
308        td.render_close(&mut cursor.buffer);
309        tr.render_close(&mut cursor.buffer);
310        table.render_close(&mut cursor.buffer);
311        cursor.buffer.end_conditional_tag();
312
313        Ok(())
314    }
315
316    fn get_siblings(&self) -> usize {
317        self.children().len()
318    }
319
320    fn get_raw_siblings(&self) -> usize {
321        self.children().iter().filter(|elt| elt.is_raw()).count()
322    }
323
324    fn render_wrapped_children(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
325        let siblings = self.get_siblings();
326        let raw_siblings = self.get_raw_siblings();
327        let tr = Tag::tr();
328
329        tr.render_open(&mut cursor.buffer)?;
330        for child in self.children().iter() {
331            let mut renderer = child.renderer(self.context());
332            renderer.set_siblings(siblings);
333            renderer.set_raw_siblings(raw_siblings);
334            renderer.set_container_width(*self.container_width());
335            if child.is_raw() {
336                cursor.buffer.end_conditional_tag();
337                renderer.render(cursor)?;
338                cursor.buffer.start_conditional_tag();
339            } else {
340                let td = renderer
341                    .set_style("td-outlook", Tag::td())
342                    .maybe_add_attribute("align", renderer.attribute("align"))
343                    .maybe_add_suffixed_class(renderer.attribute("css-class"), "outlook");
344                td.render_open(&mut cursor.buffer)?;
345                cursor.buffer.end_conditional_tag();
346                renderer.render(cursor)?;
347                cursor.buffer.start_conditional_tag();
348                td.render_close(&mut cursor.buffer);
349            }
350        }
351        tr.render_close(&mut cursor.buffer);
352        Ok(())
353    }
354
355    fn set_style_section_inner_div<'t>(&self, tag: Tag<'t>) -> Tag<'t> {
356        tag.add_style("line-height", "0")
357            .add_style("font-size", "0")
358    }
359
360    fn set_style_section_table<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
361    where
362        'root: 'a,
363        'a: 't,
364    {
365        let base = if self.is_full_width() {
366            tag
367        } else {
368            self.set_background_style(tag)
369        };
370        base.add_style("width", "100%")
371            .maybe_add_style("border-radius", self.attribute("border-radius"))
372    }
373
374    fn set_style_section_td<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
375    where
376        'root: 'a,
377        'a: 't,
378    {
379        tag.maybe_add_style("border", self.attribute("border"))
380            .maybe_add_style("border-bottom", self.attribute("border-bottom"))
381            .maybe_add_style("border-left", self.attribute("border-left"))
382            .maybe_add_style("border-right", self.attribute("border-right"))
383            .maybe_add_style("border-top", self.attribute("border-top"))
384            .maybe_add_style("direction", self.attribute("direction"))
385            .add_style("font-size", "0px")
386            .maybe_add_style("padding", self.attribute("padding"))
387            .maybe_add_style("padding-bottom", self.attribute("padding-bottom"))
388            .maybe_add_style("padding-left", self.attribute("padding-left"))
389            .maybe_add_style("padding-right", self.attribute("padding-right"))
390            .maybe_add_style("padding-top", self.attribute("padding-top"))
391            .maybe_add_style("text-align", self.attribute("text-align"))
392    }
393
394    fn render_section(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
395        let is_full_width = self.is_full_width();
396        let div = self
397            .set_style_section_div(Tag::div())
398            .maybe_add_class(if is_full_width {
399                None
400            } else {
401                self.attribute("css-class")
402            });
403        let inner_div = self.set_style_section_inner_div(Tag::div());
404        let table = self.set_style_section_table(
405            Tag::table_presentation()
406                .add_attribute("align", "center")
407                .maybe_add_attribute(
408                    "background",
409                    if is_full_width {
410                        None
411                    } else {
412                        self.attribute("background-url")
413                    },
414                ),
415        );
416        let tbody = Tag::tbody();
417        let tr = Tag::tr();
418        let td = self.set_style_section_td(Tag::td());
419        let inner_table = Tag::table_presentation();
420
421        let has_bg = self.has_background();
422        div.render_open(&mut cursor.buffer)?;
423        if has_bg {
424            inner_div.render_open(&mut cursor.buffer)?;
425        }
426        table.render_open(&mut cursor.buffer)?;
427        tbody.render_open(&mut cursor.buffer)?;
428        tr.render_open(&mut cursor.buffer)?;
429        td.render_open(&mut cursor.buffer)?;
430        cursor.buffer.start_conditional_tag();
431        inner_table.render_open(&mut cursor.buffer)?;
432        self.render_wrapped_children(cursor)?;
433        inner_table.render_close(&mut cursor.buffer);
434        cursor.buffer.end_conditional_tag();
435        td.render_close(&mut cursor.buffer);
436        tr.render_close(&mut cursor.buffer);
437        tbody.render_close(&mut cursor.buffer);
438        table.render_close(&mut cursor.buffer);
439        if has_bg {
440            inner_div.render_close(&mut cursor.buffer);
441        }
442        div.render_close(&mut cursor.buffer);
443
444        Ok(())
445    }
446
447    fn set_style_table_full_width<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t>
448    where
449        'root: 'a,
450        'a: 't,
451    {
452        let base = if self.is_full_width() {
453            self.set_background_style(tag)
454        } else {
455            tag
456        };
457        base.maybe_add_style("border-radius", self.attribute("border-radius"))
458            .add_style("width", "100%")
459    }
460
461    fn get_full_width_table<'a>(&'a self) -> Tag<'a>
462    where
463        'root: 'a,
464    {
465        self.set_style_table_full_width(Tag::table_presentation())
466            .add_attribute("align", "center")
467            .maybe_add_class(self.attribute("css-class"))
468            .maybe_add_attribute("background", self.attribute("background-url"))
469    }
470
471    fn render_full_width(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
472        let table = self.get_full_width_table();
473        let tbody = Tag::tbody();
474        let tr = Tag::tr();
475        let td = Tag::td();
476
477        table.render_open(&mut cursor.buffer)?;
478        tbody.render_open(&mut cursor.buffer)?;
479        tr.render_open(&mut cursor.buffer)?;
480        td.render_open(&mut cursor.buffer)?;
481        //
482        if self.has_background() {
483            self.render_with_background(cursor, |cursor| {
484                self.render_wrap(cursor, |cursor| {
485                    cursor.buffer.end_conditional_tag();
486                    self.render_section(cursor)?;
487                    cursor.buffer.start_conditional_tag();
488                    Ok(())
489                })
490            })?;
491        } else {
492            self.render_wrap(cursor, |cursor| {
493                cursor.buffer.end_conditional_tag();
494                self.render_section(cursor)?;
495                cursor.buffer.start_conditional_tag();
496                Ok(())
497            })?;
498        }
499        //
500        td.render_close(&mut cursor.buffer);
501        tr.render_close(&mut cursor.buffer);
502        tbody.render_close(&mut cursor.buffer);
503        table.render_close(&mut cursor.buffer);
504
505        Ok(())
506    }
507
508    fn render_simple(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
509        self.render_wrap(cursor, |cursor| {
510            if self.has_background() {
511                self.render_with_background(cursor, |cursor| self.render_section(cursor))?;
512            } else {
513                cursor.buffer.end_conditional_tag();
514                self.render_section(cursor)?;
515                cursor.buffer.start_conditional_tag();
516            }
517            Ok(())
518        })
519    }
520}
521
522impl<'root> WithMjSectionBackground<'root> for Renderer<'root, MjSection, ()> {}
523impl<'root> SectionLikeRender<'root> for Renderer<'root, MjSection, ()> {
524    fn children(&self) -> &Vec<crate::mj_body::MjBodyChild> {
525        &self.element.children
526    }
527
528    fn container_width(&self) -> &Option<Pixel> {
529        &self.container_width
530    }
531}
532
533const DEFAULT_BACKGROUND_POSITION: &str = "top center";
534const DEFAULT_BACKGROUND_REPEAT: &str = "repeat";
535const DEFAULT_BACKGROUND_SIZE: &str = "auto";
536
537impl<'root> Render<'root> for Renderer<'root, MjSection, ()> {
538    fn default_attribute(&self, name: &str) -> Option<&'static str> {
539        match name {
540            "background-position" => Some(DEFAULT_BACKGROUND_POSITION),
541            "background-repeat" => Some(DEFAULT_BACKGROUND_REPEAT),
542            "background-size" => Some(DEFAULT_BACKGROUND_SIZE),
543            "direction" => Some("ltr"),
544            "padding" => Some("20px 0"),
545            "text-align" => Some("center"),
546            "text-padding" => Some("4px 4px 4px 0"),
547            _ => None,
548        }
549    }
550
551    fn raw_attribute(&self, key: &str) -> Option<&'root str> {
552        match self.element.attributes.get(key) {
553            Some(Some(inner)) => Some(inner),
554            _ => None,
555        }
556    }
557
558    fn tag(&self) -> Option<&str> {
559        Some(NAME)
560    }
561
562    fn context(&self) -> &'root RenderContext<'root> {
563        self.context
564    }
565
566    fn set_container_width(&mut self, width: Option<Pixel>) {
567        self.container_width = width;
568    }
569
570    fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
571        if self.is_full_width() {
572            self.render_full_width(cursor)
573        } else {
574            self.render_simple(cursor)
575        }
576    }
577}
578
579impl<'render, 'root: 'render> Renderable<'render, 'root> for MjSection {
580    fn renderer(
581        &'root self,
582        context: &'root RenderContext<'root>,
583    ) -> Box<dyn Render<'root> + 'render> {
584        Box::new(Renderer::new(context, self, ()))
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    // error reported in https://github.com/jdrouet/mrml/issues/370
591    crate::should_render!(comment, "comment");
592
593    crate::should_render!(basic, "mj-section");
594    crate::should_render!(background_color, "mj-section-background-color");
595    crate::should_render!(background_url_full, "mj-section-background-url-full");
596    crate::should_render!(background_url, "mj-section-background-url");
597    crate::should_render!(body_width, "mj-section-body-width");
598    crate::should_render!(border, "mj-section-border");
599    crate::should_render!(border_radius, "mj-section-border-radius");
600    crate::should_render!(class, "mj-section-class");
601    crate::should_render!(direction, "mj-section-direction");
602    crate::should_render!(full_width, "mj-section-full-width");
603    crate::should_render!(padding, "mj-section-padding");
604    crate::should_render!(text_align, "mj-section-text-align");
605}