mrml/mj_head/
render.rs

1use super::MjHead;
2use crate::helper::sort::sort_by_key;
3use crate::prelude::hash::Map;
4use crate::prelude::render::*;
5
6const STYLE_BASE: &str = r#"
7<style type="text/css">
8#outlook a { padding: 0; }
9body { margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
10table, td { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
11img { border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; }
12p { display: block; margin: 13px 0; }
13</style>
14<!--[if mso]>
15<noscript>
16<xml>
17<o:OfficeDocumentSettings>
18  <o:AllowPNG/>
19  <o:PixelsPerInch>96</o:PixelsPerInch>
20</o:OfficeDocumentSettings>
21</xml>
22</noscript>
23<![endif]-->
24<!--[if lte mso 11]>
25<style type="text/css">
26.mj-outlook-group-fix { width:100% !important; }
27</style>
28<![endif]-->
29"#;
30
31fn combine_attribute_map<'a>(
32    mut res: Map<&'a str, Map<&'a str, &'a str>>,
33    (name, key, value): (&'a str, &'a str, &'a str),
34) -> Map<&'a str, Map<&'a str, &'a str>> {
35    let entry = res.entry(name).or_default();
36    entry.insert(key, value);
37    res
38}
39
40impl MjHead {
41    pub fn build_attributes_all(&self) -> Map<&str, &str> {
42        self.children
43            .iter()
44            .flat_map(|item| {
45                item.as_mj_attributes()
46                    .into_iter()
47                    .flat_map(|inner| inner.mj_attributes_all_iter())
48                    .chain(
49                        item.as_mj_include()
50                            .filter(|item| item.0.attributes.kind.is_mjml())
51                            .into_iter()
52                            .flat_map(|inner| inner.mj_attributes_all_iter()),
53                    )
54            })
55            .collect()
56    }
57
58    pub fn build_attributes_class(&self) -> Map<&str, Map<&str, &str>> {
59        self.children
60            .iter()
61            .flat_map(|item| {
62                item.as_mj_attributes()
63                    .into_iter()
64                    .flat_map(|inner| inner.mj_attributes_class_iter())
65                    .chain(
66                        item.as_mj_include()
67                            .filter(|item| item.0.attributes.kind.is_mjml())
68                            .into_iter()
69                            .flat_map(|inner| inner.mj_attributes_class_iter()),
70                    )
71            })
72            .fold(Map::new(), combine_attribute_map)
73    }
74
75    pub fn build_attributes_element(&self) -> Map<&str, Map<&str, &str>> {
76        self.children
77            .iter()
78            .flat_map(|item| {
79                item.as_mj_attributes()
80                    .into_iter()
81                    .flat_map(|inner| inner.mj_attributes_element_iter())
82                    .chain(
83                        item.as_mj_include()
84                            .filter(|item| item.0.attributes.kind.is_mjml())
85                            .into_iter()
86                            .flat_map(|inner| inner.mj_attributes_element_iter()),
87                    )
88            })
89            .fold(Map::new(), combine_attribute_map)
90    }
91
92    pub fn build_font_families(&self) -> Map<&str, &str> {
93        self.children
94            .iter()
95            .flat_map(|item| {
96                item.as_mj_font()
97                    .into_iter()
98                    .chain(item.as_mj_include().into_iter().flat_map(|incl| {
99                        incl.0
100                            .children
101                            .iter()
102                            .filter_map(|child| child.as_mj_font())
103                    }))
104            })
105            .map(|font| (font.name(), font.href()))
106            .collect()
107    }
108}
109
110fn render_font_import(target: &mut String, href: &str) {
111    target.push_str("@import url(");
112    target.push_str(href);
113    target.push_str(");");
114}
115
116fn render_font_link(target: &mut String, href: &str) {
117    target.push_str("<link href=\"");
118    target.push_str(href);
119    target.push_str("\" rel=\"stylesheet\" type=\"text/css\">");
120}
121
122impl Renderer<'_, MjHead, ()> {
123    fn mj_style_iter(&self) -> impl Iterator<Item = &str> {
124        self.element.children.iter().flat_map(|item| {
125            item.as_mj_include()
126                .into_iter()
127                .flat_map(|inner| {
128                    inner
129                        .0
130                        .children
131                        .iter()
132                        .filter_map(|child| child.as_mj_style())
133                        .map(|child| child.children.trim())
134                })
135                .chain(
136                    item.as_mj_include()
137                        .into_iter()
138                        .filter(|child| child.0.attributes.kind.is_css(false))
139                        .flat_map(|child| {
140                            child
141                                .0
142                                .children
143                                .iter()
144                                .filter_map(|item| item.as_text())
145                                .map(|text| text.inner_str().trim())
146                        }),
147                )
148                .chain(
149                    item.as_mj_style()
150                        .into_iter()
151                        .map(|item| item.children.trim()),
152                )
153        })
154    }
155
156    fn render_font_families(&self, cursor: &mut RenderCursor) {
157        let used_font_families = cursor.header.used_font_families();
158        if used_font_families.is_empty() {
159            return;
160        }
161
162        let mut links = String::default();
163        let mut imports = String::default();
164        for name in cursor.header.used_font_families().iter() {
165            if let Some(href) = self.context.header.font_families().get(name.as_str()) {
166                render_font_link(&mut links, href);
167                render_font_import(&mut imports, href);
168            } else if let Some(href) = self.context.options.fonts.get(name) {
169                render_font_link(&mut links, href);
170                render_font_import(&mut imports, href);
171            } else {
172                // TODO log a warning
173            }
174        }
175
176        if links.is_empty() && imports.is_empty() {
177        } else {
178            cursor.buffer.start_mso_negation_conditional_tag();
179            cursor.buffer.push_str(&links);
180            if !imports.is_empty() {
181                cursor.buffer.push_str("<style type=\"text/css\">");
182                cursor.buffer.push_str(&imports);
183                cursor.buffer.push_str("</style>");
184            }
185            cursor.buffer.end_negation_conditional_tag();
186        }
187    }
188
189    fn render_media_queries(&self, cursor: &mut RenderCursor) {
190        if cursor.header.media_queries().is_empty() {
191            return;
192        }
193        let mut classnames = cursor.header.media_queries().iter().collect::<Vec<_>>();
194        classnames.sort_by(sort_by_key);
195        let breakpoint = self.context.header.breakpoint().to_string();
196        cursor.buffer.push_str("<style type=\"text/css\">");
197        cursor.buffer.push_str("@media only screen and (min-width:");
198        cursor.buffer.push_str(breakpoint.as_str());
199        cursor.buffer.push_str(") { ");
200        for (classname, size) in classnames.iter() {
201            let size = size.to_string();
202            cursor.buffer.push('.');
203            cursor.buffer.push_str(classname);
204            cursor.buffer.push_str(" { width:");
205            cursor.buffer.push_str(size.as_str());
206            cursor.buffer.push_str(" !important; max-width:");
207            cursor.buffer.push_str(size.as_str());
208            cursor.buffer.push_str("; } ");
209        }
210        cursor.buffer.push_str(" }");
211        cursor.buffer.push_str("</style>");
212        cursor
213            .buffer
214            .push_str("<style media=\"screen and (min-width:");
215        cursor.buffer.push_str(breakpoint.as_str());
216        cursor.buffer.push_str(")\">");
217        for (classname, size) in classnames.iter() {
218            let size = size.to_string();
219            cursor.buffer.push_str(".moz-text-html .");
220            cursor.buffer.push_str(classname);
221            cursor.buffer.push_str(" { width:");
222            cursor.buffer.push_str(size.as_str());
223            cursor.buffer.push_str(" !important; max-width:");
224            cursor.buffer.push_str(size.as_str());
225            cursor.buffer.push_str("; } ");
226        }
227        cursor.buffer.push_str("</style>");
228    }
229
230    fn render_styles(&self, cursor: &mut RenderCursor) {
231        if !cursor.header.styles().is_empty() {
232            cursor.buffer.push_str("<style type=\"text/css\">");
233            for style in cursor.header.styles().iter() {
234                cursor.buffer.push_str(style);
235            }
236            cursor.buffer.push_str("</style>");
237        }
238
239        // TODO this should be optional
240        cursor.buffer.push_str("<style type=\"text/css\">");
241        for item in self.mj_style_iter() {
242            cursor.buffer.push_str(item);
243        }
244        cursor.buffer.push_str("</style>");
245    }
246
247    fn render_raw(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
248        let mut index: usize = 0;
249        let siblings = self.element.children.len();
250        for child in self.element.children.iter() {
251            if let Some(mj_raw) = child.as_mj_raw() {
252                let mut renderer = mj_raw.renderer(self.context());
253                renderer.set_index(index);
254                renderer.set_siblings(siblings);
255                renderer.render(cursor)?;
256                index += 1;
257            } else if let Some(mj_include) = child.as_mj_include() {
258                for include_child in mj_include.0.children.iter() {
259                    if let Some(mj_raw) = include_child.as_mj_raw() {
260                        let mut renderer = mj_raw.renderer(self.context());
261                        renderer.set_index(index);
262                        renderer.set_siblings(siblings);
263                        renderer.render(cursor)?;
264                        index += 1;
265                    }
266                }
267            }
268        }
269        Ok(())
270    }
271}
272
273impl<'root> Render<'root> for Renderer<'root, MjHead, ()> {
274    fn context(&self) -> &'root RenderContext<'root> {
275        self.context
276    }
277
278    fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> {
279        cursor.buffer.push_str("<head>");
280        // we write the title even though there is no content
281        cursor.buffer.push_str("<title>");
282        if let Some(title) = self.element.title().map(|item| item.content()) {
283            cursor.buffer.push_str(title);
284        }
285        cursor.buffer.push_str("</title>");
286        cursor.buffer.start_mso_negation_conditional_tag();
287        cursor
288            .buffer
289            .push_str("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">");
290        cursor.buffer.end_negation_conditional_tag();
291        cursor
292            .buffer
293            .push_str("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">");
294        cursor
295            .buffer
296            .push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
297        cursor.buffer.push_str(STYLE_BASE);
298        self.render_font_families(cursor);
299        self.render_media_queries(cursor);
300        self.render_styles(cursor);
301        self.render_raw(cursor)?;
302        cursor.buffer.push_str("</head>");
303        Ok(())
304    }
305}
306
307impl<'render, 'root: 'render> Renderable<'render, 'root> for MjHead {
308    fn renderer(
309        &'root self,
310        context: &'root RenderContext<'root>,
311    ) -> Box<dyn Render<'root> + 'render> {
312        Box::new(Renderer::new(context, self, ()))
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use std::iter::FromIterator;
319
320    use crate::mj_attributes::{MjAttributes, MjAttributesChild};
321    use crate::mj_attributes_all::MjAttributesAll;
322    use crate::mj_attributes_class::{MjAttributesClass, MjAttributesClassAttributes};
323    use crate::mj_attributes_element::MjAttributesElement;
324    use crate::mj_font::MjFont;
325    use crate::mj_head::{MjHead, MjHeadChild};
326    use crate::mj_include::head::{MjIncludeHead, MjIncludeHeadAttributes, MjIncludeHeadChild};
327    use crate::prelude::hash::Map;
328
329    crate::should_render!(attributes_basic, "mj-attributes");
330    crate::should_render!(style_basic, "mj-style");
331
332    #[test]
333    fn should_keep_order_with_mj_include_attributes_all() {
334        let element = MjHead::new(
335            (),
336            vec![
337                MjHeadChild::MjAttributes(MjAttributes::new(
338                    (),
339                    vec![MjAttributesChild::MjAttributesAll(MjAttributesAll::new(
340                        Map::from_iter([(String::from("font-size"), Some(String::from("42px")))]),
341                        (),
342                    ))],
343                )),
344                MjHeadChild::MjInclude(MjIncludeHead::new(
345                    MjIncludeHeadAttributes {
346                        path: String::from("foo"),
347                        kind: crate::mj_include::head::MjIncludeHeadKind::Mjml,
348                    },
349                    vec![MjIncludeHeadChild::MjAttributes(MjAttributes::new(
350                        (),
351                        vec![MjAttributesChild::MjAttributesAll(MjAttributesAll::new(
352                            Map::from_iter([
353                                (String::from("font-size"), Some(String::from("21px"))),
354                                (String::from("text-align"), Some(String::from("center"))),
355                            ]),
356                            (),
357                        ))],
358                    ))],
359                )),
360                MjHeadChild::MjAttributes(MjAttributes::new(
361                    (),
362                    vec![MjAttributesChild::MjAttributesAll(MjAttributesAll::new(
363                        Map::from_iter([(String::from("text-align"), Some(String::from("right")))]),
364                        (),
365                    ))],
366                )),
367            ],
368        );
369        assert_eq!(
370            element.build_attributes_all().get("font-size"),
371            Some("21px").as_ref()
372        );
373        assert_eq!(
374            element.build_attributes_all().get("text-align"),
375            Some("right").as_ref()
376        );
377    }
378
379    #[test]
380    fn should_keep_order_with_mj_include_attributes_class() {
381        let element = MjHead::new(
382            (),
383            vec![
384                MjHeadChild::MjAttributes(MjAttributes::new(
385                    (),
386                    vec![MjAttributesChild::MjAttributesClass(
387                        MjAttributesClass::new(
388                            MjAttributesClassAttributes {
389                                name: String::from("foo"),
390                                others: Map::from_iter([(
391                                    String::from("font-size"),
392                                    Some(String::from("42px")),
393                                )]),
394                            },
395                            (),
396                        ),
397                    )],
398                )),
399                MjHeadChild::MjInclude(MjIncludeHead::new(
400                    MjIncludeHeadAttributes {
401                        path: String::from("foo"),
402                        kind: crate::mj_include::head::MjIncludeHeadKind::Mjml,
403                    },
404                    vec![MjIncludeHeadChild::MjAttributes(MjAttributes::new(
405                        (),
406                        vec![
407                            MjAttributesChild::MjAttributesClass(MjAttributesClass::new(
408                                MjAttributesClassAttributes {
409                                    name: String::from("foo"),
410                                    others: Map::from_iter([(
411                                        String::from("font-size"),
412                                        Some(String::from("21px")),
413                                    )]),
414                                },
415                                (),
416                            )),
417                            MjAttributesChild::MjAttributesClass(MjAttributesClass::new(
418                                MjAttributesClassAttributes {
419                                    name: String::from("bar"),
420                                    others: Map::from_iter([(
421                                        String::from("text-align"),
422                                        Some(String::from("center")),
423                                    )]),
424                                },
425                                (),
426                            )),
427                        ],
428                    ))],
429                )),
430                MjHeadChild::MjAttributes(MjAttributes::new(
431                    (),
432                    vec![MjAttributesChild::MjAttributesClass(
433                        MjAttributesClass::new(
434                            MjAttributesClassAttributes {
435                                name: String::from("bar"),
436                                others: Map::from_iter([(
437                                    String::from("text-align"),
438                                    Some(String::from("left")),
439                                )]),
440                            },
441                            (),
442                        ),
443                    )],
444                )),
445            ],
446        );
447        let attributes = element.build_attributes_class();
448        assert_eq!(
449            attributes.get("foo").unwrap().get("font-size"),
450            Some("21px").as_ref()
451        );
452        assert_eq!(
453            attributes.get("bar").unwrap().get("text-align"),
454            Some("left").as_ref()
455        );
456    }
457
458    #[test]
459    fn should_keep_order_with_mj_include_attributes_element() {
460        let element = MjHead::new(
461            (),
462            vec![
463                MjHeadChild::MjAttributes(MjAttributes::new(
464                    (),
465                    vec![MjAttributesChild::MjAttributesElement(
466                        MjAttributesElement {
467                            name: String::from("mj-text"),
468                            attributes: Map::from_iter([(
469                                String::from("font-size"),
470                                Some(String::from("42px")),
471                            )]),
472                        },
473                    )],
474                )),
475                MjHeadChild::MjInclude(MjIncludeHead::new(
476                    MjIncludeHeadAttributes {
477                        path: String::from("foo"),
478                        kind: crate::mj_include::head::MjIncludeHeadKind::Mjml,
479                    },
480                    vec![MjIncludeHeadChild::MjAttributes(MjAttributes::new(
481                        (),
482                        vec![MjAttributesChild::MjAttributesElement(
483                            MjAttributesElement {
484                                name: String::from("mj-text"),
485                                attributes: Map::from_iter([
486                                    (String::from("font-size"), Some(String::from("21px"))),
487                                    (String::from("text-align"), Some(String::from("center"))),
488                                ]),
489                            },
490                        )],
491                    ))],
492                )),
493                MjHeadChild::MjAttributes(MjAttributes::new(
494                    (),
495                    vec![MjAttributesChild::MjAttributesElement(
496                        MjAttributesElement {
497                            name: String::from("mj-text"),
498                            attributes: Map::from_iter([(
499                                String::from("text-align"),
500                                Some(String::from("left")),
501                            )]),
502                        },
503                    )],
504                )),
505            ],
506        );
507        let attributes = element.build_attributes_element();
508        assert_eq!(
509            attributes.get("mj-text").unwrap().get("font-size"),
510            Some("21px").as_ref()
511        );
512        assert_eq!(
513            attributes.get("mj-text").unwrap().get("text-align"),
514            Some("left").as_ref()
515        );
516    }
517
518    #[test]
519    fn should_keep_order_with_mj_font() {
520        let element = MjHead::new(
521            (),
522            vec![
523                MjHeadChild::MjFont(MjFont::build("foo", "http://foo/root")),
524                MjHeadChild::MjInclude(MjIncludeHead::new(
525                    MjIncludeHeadAttributes {
526                        path: String::from("foo"),
527                        kind: crate::mj_include::head::MjIncludeHeadKind::Mjml,
528                    },
529                    vec![
530                        MjIncludeHeadChild::MjFont(MjFont::build("foo", "http://foo/include")),
531                        MjIncludeHeadChild::MjFont(MjFont::build("bar", "http://bar/include")),
532                    ],
533                )),
534                MjHeadChild::MjFont(MjFont::build("bar", "http://bar/root")),
535            ],
536        );
537        let fonts = element.build_font_families();
538        assert_eq!(fonts.get("foo"), Some("http://foo/include").as_ref());
539        assert_eq!(fonts.get("bar"), Some("http://bar/root").as_ref());
540    }
541}