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 }
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 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 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}