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