diff --git a/packages/mrml-core/src/lib.rs b/packages/mrml-core/src/lib.rs index 21e4affa..a31dbc08 100644 --- a/packages/mrml-core/src/lib.rs +++ b/packages/mrml-core/src/lib.rs @@ -144,6 +144,8 @@ pub mod mj_font; pub mod mj_group; pub mod mj_head; pub mod mj_hero; +pub mod mj_html_attribute; +pub mod mj_html_attributes; pub mod mj_image; pub mod mj_include; pub mod mj_navbar; @@ -151,6 +153,7 @@ pub mod mj_navbar_link; pub mod mj_preview; pub mod mj_raw; pub mod mj_section; +pub mod mj_selector; pub mod mj_social; pub mod mj_social_element; pub mod mj_spacer; diff --git a/packages/mrml-core/src/mj_accordion/render.rs b/packages/mrml-core/src/mj_accordion/render.rs index c048273d..df8179ca 100644 --- a/packages/mrml-core/src/mj_accordion/render.rs +++ b/packages/mrml-core/src/mj_accordion/render.rs @@ -91,7 +91,7 @@ impl<'root> Render<'root> for Renderer<'root, MjAccordion, ()> { fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> { self.update_header(&mut cursor.header); - let tbody = Tag::tbody(); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); let table = Tag::table() .add_style("width", "100%") .add_style("border-collapse", "collapse") @@ -100,7 +100,8 @@ impl<'root> Render<'root> for Renderer<'root, MjAccordion, ()> { .maybe_add_style("font-family", self.attribute("font-family")) .add_attribute("cellspacing", "0") .add_attribute("cellpadding", "0") - .add_class("mj-accordion"); + .add_class("mj-accordion") + .set_html_attributes(self.context.header.html_attributes()); table.render_open(&mut cursor.buffer)?; tbody.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_accordion_element/render.rs b/packages/mrml-core/src/mj_accordion_element/render.rs index 06ade488..7b1d67e8 100644 --- a/packages/mrml-core/src/mj_accordion_element/render.rs +++ b/packages/mrml-core/src/mj_accordion_element/render.rs @@ -91,16 +91,21 @@ impl<'root> Render<'root> for Renderer<'root, MjAccordionElement, MjAccordionEle let input = Tag::new("input") .add_attribute("type", "checkbox") .add_class("mj-accordion-checkbox") - .add_style("display", "none"); - let div = Tag::div(); + .add_style("display", "none") + .set_html_attributes(self.context.header.html_attributes()); + let div = Tag::div().set_html_attributes(self.context.header.html_attributes()); let label = Tag::new("label") .add_class("mj-accordion-element") .add_style("font-size", "13px") - .maybe_add_style("font-family", self.attribute("font-family")); + .maybe_add_style("font-family", self.attribute("font-family")) + .set_html_attributes(self.context.header.html_attributes()); let td = Tag::td() .add_style("padding", "0px") - .maybe_add_style("background-color", self.attribute("background-color")); - let tr = Tag::tr().maybe_add_class(self.attribute("css-class")); + .maybe_add_style("background-color", self.attribute("background-color")) + .set_html_attributes(self.context.header.html_attributes()); + let tr = Tag::tr() + .maybe_add_class(self.attribute("css-class")) + .set_html_attributes(self.context.header.html_attributes()); tr.render_open(&mut cursor.buffer)?; td.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_accordion_text/render.rs b/packages/mrml-core/src/mj_accordion_text/render.rs index af229715..6e7b9878 100644 --- a/packages/mrml-core/src/mj_accordion_text/render.rs +++ b/packages/mrml-core/src/mj_accordion_text/render.rs @@ -21,7 +21,8 @@ impl<'root> Renderer<'root, MjAccordionText, MjAccordionTextExtra<'root>> { .maybe_add_style("padding-right", self.attribute("padding-right")) .maybe_add_style("padding-bottom", self.attribute("padding-bottom")) .maybe_add_style("padding-left", self.attribute("padding-left")) - .maybe_add_style("padding", self.attribute("padding")); + .maybe_add_style("padding", self.attribute("padding")) + .set_html_attributes(self.context.header.html_attributes()); td.render_open(&mut cursor.buffer)?; for child in self.element.children.iter() { @@ -71,14 +72,17 @@ impl<'root> Render<'root> for Renderer<'root, MjAccordionText, MjAccordionTextEx let font_families = self.attribute("font-family"); cursor.header.maybe_add_font_families(font_families); - let tr = Tag::tr(); - let tbody = Tag::tbody(); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); let table = Tag::table() .add_attribute("cellspacing", "0") .add_attribute("cellpadding", "0") .add_style("width", "100%") - .maybe_add_style("border-bottom", self.attribute("border")); - let div = Tag::div().add_class("mj-accordion-content"); + .maybe_add_style("border-bottom", self.attribute("border")) + .set_html_attributes(self.context.header.html_attributes()); + let div = Tag::div() + .add_class("mj-accordion-content") + .set_html_attributes(self.context.header.html_attributes()); div.render_open(&mut cursor.buffer)?; table.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_accordion_title/render.rs b/packages/mrml-core/src/mj_accordion_title/render.rs index 81b4ea10..a98d71e6 100644 --- a/packages/mrml-core/src/mj_accordion_title/render.rs +++ b/packages/mrml-core/src/mj_accordion_title/render.rs @@ -15,6 +15,7 @@ impl<'root> Renderer<'root, MjAccordionTitle, MjAccordionTitleExtra<'root>> { tag.add_style("display", "none") .maybe_add_style("width", self.attribute("icon-width")) .maybe_add_style("height", self.attribute("icon-height")) + .set_html_attributes(self.context.header.html_attributes()) } fn render_title(&self, cursor: &mut RenderCursor) -> Result<(), Error> { @@ -29,7 +30,8 @@ impl<'root> Renderer<'root, MjAccordionTitle, MjAccordionTitleExtra<'root>> { .maybe_add_style("padding-bottom", self.attribute("padding-bottom")) .maybe_add_style("padding-left", self.attribute("padding-left")) .maybe_add_style("padding", self.attribute("padding")) - .maybe_add_class(self.attribute("css-class")); + .maybe_add_class(self.attribute("css-class")) + .set_html_attributes(self.context.header.html_attributes()); td.render_open(&mut cursor.buffer)?; for child in self.element.children.iter() { @@ -46,17 +48,20 @@ impl<'root> Renderer<'root, MjAccordionTitle, MjAccordionTitleExtra<'root>> { .set_style_img(Tag::new("img")) .maybe_add_attribute("src", self.attribute("icon-wrapped-url")) .maybe_add_attribute("alt", self.attribute("icon-wrapped-alt")) - .add_class("mj-accordion-more"); + .add_class("mj-accordion-more") + .set_html_attributes(self.context.header.html_attributes()); let img_less = self .set_style_img(Tag::new("img")) .maybe_add_attribute("src", self.attribute("icon-unwrapped-url")) .maybe_add_attribute("alt", self.attribute("icon-unwrapped-alt")) - .add_class("mj-accordion-less"); + .add_class("mj-accordion-less") + .set_html_attributes(self.context.header.html_attributes()); let td = Tag::td() .add_style("padding", "16px") .maybe_add_style("background", self.attribute("background-color")) .maybe_add_style("vertical-align", self.attribute("icon-align")) - .add_class("mj-accordion-ico"); + .add_class("mj-accordion-ico") + .set_html_attributes(self.context.header.html_attributes()); buf.start_negation_conditional_tag(); td.render_open(buf)?; @@ -105,14 +110,17 @@ impl<'root> Render<'root> for Renderer<'root, MjAccordionTitle, MjAccordionTitle let font_families = self.attribute("font-family"); cursor.header.maybe_add_font_families(font_families); - let tr = Tag::tr(); - let tbody = Tag::tbody(); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); let table = Tag::table() .add_attribute("cellspacing", "0") .add_attribute("cellpadding", "0") .add_style("width", "100%") - .maybe_add_style("border-bottom", self.attribute("border")); - let div = Tag::div().add_class("mj-accordion-title"); + .maybe_add_style("border-bottom", self.attribute("border")) + .set_html_attributes(self.context.header.html_attributes()); + let div = Tag::div() + .add_class("mj-accordion-title") + .set_html_attributes(self.context.header.html_attributes()); div.render_open(&mut cursor.buffer)?; table.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_body/render.rs b/packages/mrml-core/src/mj_body/render.rs index 18a619e1..15a32027 100644 --- a/packages/mrml-core/src/mj_body/render.rs +++ b/packages/mrml-core/src/mj_body/render.rs @@ -11,13 +11,18 @@ impl<'root> Renderer<'root, MjBody, ()> { } fn get_body_tag(&self) -> Tag { - self.set_body_style(Tag::new("body").add_style("word-spacing", "normal")) + self.set_body_style( + Tag::new("body") + .add_style("word-spacing", "normal") + .set_html_attributes(self.context.header.html_attributes()), + ) } fn get_content_div_tag(&self) -> Tag { self.set_body_style(Tag::new("div")) .maybe_add_attribute("class", self.attribute("css-class")) .maybe_add_attribute("lang", self.context.header.lang()) + .set_html_attributes(self.context.header.html_attributes()) } fn set_body_style<'a, 't>(&'a self, tag: Tag<'t>) -> Tag<'t> @@ -26,6 +31,7 @@ impl<'root> Renderer<'root, MjBody, ()> { 'a: 't, { tag.maybe_add_style("background-color", self.attribute("background-color")) + .set_html_attributes(self.context.header.html_attributes()) } fn render_preview(&self, buf: &mut RenderBuffer) { @@ -80,7 +86,9 @@ impl<'root> Render<'root> for Renderer<'root, MjBody, ()> { } fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let body = self.get_body_tag(); + let body = self + .get_body_tag() + .set_html_attributes(self.context.header.html_attributes()); body.render_open(&mut cursor.buffer)?; self.render_preview(&mut cursor.buffer); self.render_content(cursor)?; diff --git a/packages/mrml-core/src/mj_button/render.rs b/packages/mrml-core/src/mj_button/render.rs index 5aba4ca2..bd1e25f3 100644 --- a/packages/mrml-core/src/mj_button/render.rs +++ b/packages/mrml-core/src/mj_button/render.rs @@ -130,15 +130,18 @@ impl<'root> Render<'root> for Renderer<'root, MjButton, ()> { let font_family = self.attribute("font-family"); cursor.header.maybe_add_font_families(font_family); - let table = self.set_style_table(Tag::table_presentation()); - let tbody = Tag::tbody(); - let tr = Tag::tr(); + let table = self + .set_style_table(Tag::table_presentation()) + .set_html_attributes(self.context.header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); let td = self .set_style_td(Tag::td()) .add_attribute("align", "center") .maybe_add_attribute("bgcolor", self.attribute("background-color")) .add_attribute("role", "presentation") - .maybe_add_attribute("valign", self.attribute("vertical-align")); + .maybe_add_attribute("valign", self.attribute("vertical-align")) + .set_html_attributes(self.context.header.html_attributes()); let link = Tag::new(self.attribute("href").map(|_| "a").unwrap_or("p")) .maybe_add_attribute("href", self.attribute("href")) .maybe_add_attribute("rel", self.attribute("rel")) @@ -147,7 +150,8 @@ impl<'root> Render<'root> for Renderer<'root, MjButton, ()> { "target", self.attribute("href") .and_then(|_v| self.attribute("target")), - ); + ) + .set_html_attributes(self.context.header.html_attributes()); let link = self.set_style_content(link); table.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_carousel/render.rs b/packages/mrml-core/src/mj_carousel/render.rs index cff59b98..5a45d6db 100644 --- a/packages/mrml-core/src/mj_carousel/render.rs +++ b/packages/mrml-core/src/mj_carousel/render.rs @@ -146,10 +146,12 @@ impl<'root> Renderer<'root, MjCarousel, MjCarouselExtra> { .map(|value| value.value()); let div = self .set_style_controls_div(Tag::div()) - .add_class(format!("mj-carousel-{direction}-icons")); + .add_class(format!("mj-carousel-{direction}-icons")) + .set_html_attributes(self.context.header.html_attributes()); let td = self .set_style_controls_td(Tag::td()) - .add_class(format!("mj-carousel-{}-icons-cell", self.extra.id)); + .add_class(format!("mj-carousel-{}-icons-cell", self.extra.id)) + .set_html_attributes(self.context.header.html_attributes()); td.render_open(buf)?; div.render_open(buf)?; @@ -158,14 +160,16 @@ impl<'root> Renderer<'root, MjCarousel, MjCarouselExtra> { .set_style_controls_img(Tag::new("img")) .add_attribute("src", icon.to_string()) .add_attribute("alt", direction.to_string()) - .maybe_add_attribute("width", icon_width.map(|v| v.to_string())); + .maybe_add_attribute("width", icon_width.map(|v| v.to_string())) + .set_html_attributes(self.context.header.html_attributes()); let label = Tag::new("label") .add_attribute( "for", format!("mj-carousel-{}-radio-{}", self.extra.id, index + 1), ) .add_class(format!("mj-carousel-{direction}")) - .add_class(format!("mj-carousel-{}-{}", direction, index + 1)); + .add_class(format!("mj-carousel-{}-{}", direction, index + 1)) + .set_html_attributes(self.context.header.html_attributes()); label.render_open(buf)?; img.render_closed(buf)?; label.render_close(buf); @@ -177,8 +181,12 @@ impl<'root> Renderer<'root, MjCarousel, MjCarouselExtra> { } fn render_images(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let div = Tag::div().add_class("mj-carousel-images"); - let td = self.set_style_images_td(Tag::td()); + let div = Tag::div() + .add_class("mj-carousel-images") + .set_html_attributes(self.context.header.html_attributes()); + let td = self + .set_style_images_td(Tag::td()) + .set_html_attributes(self.context.header.html_attributes()); td.render_open(&mut cursor.buffer)?; div.render_open(&mut cursor.buffer)?; @@ -202,12 +210,13 @@ impl<'root> Renderer<'root, MjCarousel, MjCarouselExtra> { } fn render_carousel(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let tr = Tag::tr(); - let tbody = Tag::tbody(); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); let table = self .set_style_carousel_table(Tag::table_presentation()) .add_attribute("width", "100%") - .add_class("mj-carousel-main"); + .add_class("mj-carousel-main") + .set_html_attributes(self.context.header.html_attributes()); table.render_open(&mut cursor.buffer)?; tbody.render_open(&mut cursor.buffer)?; @@ -468,8 +477,11 @@ impl<'root> Render<'root> for Renderer<'root, MjCarousel, MjCarouselExtra> { let inner_div = self .set_style_carousel_div(Tag::div()) .add_class("mj-carousel-content") - .add_class(format!("mj-carousel-{}-content", self.extra.id)); - let div = Tag::div().add_class("mj-carousel"); + .add_class(format!("mj-carousel-{}-content", self.extra.id)) + .set_html_attributes(self.context.header.html_attributes()); + let div = Tag::div() + .add_class("mj-carousel") + .set_html_attributes(self.context.header.html_attributes()); cursor.buffer.start_mso_negation_conditional_tag(); div.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_carousel_image/render.rs b/packages/mrml-core/src/mj_carousel_image/render.rs index b2b17ac5..572bdbcb 100644 --- a/packages/mrml-core/src/mj_carousel_image/render.rs +++ b/packages/mrml-core/src/mj_carousel_image/render.rs @@ -84,6 +84,7 @@ impl<'root> Renderer<'root, MjCarouselImage, MjCarouselImageExtra<'root>> { .get("carousel-id") .map(|id| format!("mj-carousel-{}-radio-{}", id, self.index + 1)), ) + .set_html_attributes(self.context.header.html_attributes()) .render_closed(buf) .map_err(Error::from) } @@ -102,14 +103,17 @@ impl<'root> Renderer<'root, MjCarouselImage, MjCarouselImageExtra<'root>> { self.container_width .as_ref() .map(|item| item.value().to_string()), - ); - let label = Tag::new("label").maybe_add_attribute( - "for", - self.extra - .attributes - .get("carousel-id") - .map(|id| format!("mj-carousel-{}-radio-{}", id, self.index + 1)), - ); + ) + .set_html_attributes(self.context.header.html_attributes()); + let label = Tag::new("label") + .maybe_add_attribute( + "for", + self.extra + .attributes + .get("carousel-id") + .map(|id| format!("mj-carousel-{}-radio-{}", id, self.index + 1)), + ) + .set_html_attributes(self.context.header.html_attributes()); let link = self .set_style_thumbnails_a(Tag::new("a")) .add_attribute("href", format!("#{}", self.index + 1)) @@ -131,7 +135,8 @@ impl<'root> Renderer<'root, MjCarouselImage, MjCarouselImageExtra<'root>> { .maybe_add_style( "width", self.container_width.as_ref().map(|item| item.to_string()), - ); + ) + .set_html_attributes(self.context.header.html_attributes()); link.render_open(buf)?; label.render_open(buf)?; @@ -206,7 +211,8 @@ impl<'root> Render<'root> for Renderer<'root, MjCarouselImage, MjCarouselImageEx self.container_width .as_ref() .map(|width| width.value().to_string()), - ); + ) + .set_html_attributes(self.context.header.html_attributes()); let div = if self.index == 0 { Tag::div() } else { @@ -217,14 +223,16 @@ impl<'root> Render<'root> for Renderer<'root, MjCarouselImage, MjCarouselImageEx let div = div .add_class("mj-carousel-image") .add_class(format!("mj-carousel-image-{}", self.index + 1)) - .maybe_add_class(self.attribute("css-class")); + .maybe_add_class(self.attribute("css-class")) + .set_html_attributes(self.context.header.html_attributes()); div.render_open(&mut cursor.buffer)?; if let Some(href) = self.attribute("href") { let link = Tag::new("a") .add_attribute("href", href) .maybe_add_attribute("rel", self.attribute("rel")) - .add_attribute("target", "_blank"); + .add_attribute("target", "_blank") + .set_html_attributes(self.context.header.html_attributes()); link.render_open(&mut cursor.buffer)?; img.render_closed(&mut cursor.buffer)?; link.render_close(&mut cursor.buffer); diff --git a/packages/mrml-core/src/mj_column/render.rs b/packages/mrml-core/src/mj_column/render.rs index 3313d054..01d2c01d 100644 --- a/packages/mrml-core/src/mj_column/render.rs +++ b/packages/mrml-core/src/mj_column/render.rs @@ -161,10 +161,14 @@ impl<'root> Renderer<'root, MjColumn, MjColumnExtra<'root>> { } fn render_gutter(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let table = Tag::table_presentation().add_attribute("width", "100%"); - let tbody = Tag::tbody(); - let tr = Tag::tr(); - let td = self.set_style_gutter_td(Tag::td()); + let table = Tag::table_presentation() + .add_attribute("width", "100%") + .set_html_attributes(self.context.header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); + let td = self + .set_style_gutter_td(Tag::td()) + .set_html_attributes(self.context.header.html_attributes()); table.render_open(&mut cursor.buffer)?; tbody.render_open(&mut cursor.buffer)?; @@ -194,8 +198,9 @@ impl<'root> Renderer<'root, MjColumn, MjColumnExtra<'root>> { fn render_column(&self, cursor: &mut RenderCursor, gutter: bool) -> Result<(), Error> { let table = self .set_style_table(Tag::table_presentation(), gutter) - .add_attribute("width", "100%"); - let tbody = Tag::tbody(); + .add_attribute("width", "100%") + .set_html_attributes(self.context.header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); let siblings = self.element.children.len(); let raw_siblings = self.element.children.iter().filter(|i| i.is_raw()).count(); let current_width = self.current_width(); @@ -212,7 +217,7 @@ impl<'root> Renderer<'root, MjColumn, MjColumnExtra<'root>> { if child.is_raw() { renderer.render(cursor)?; } else { - let tr = Tag::tr(); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); let td = Tag::td() .maybe_add_style( "background", @@ -227,7 +232,8 @@ impl<'root> Renderer<'root, MjColumn, MjColumnExtra<'root>> { .add_style("word-break", "break-word") .maybe_add_attribute("align", renderer.attribute("align")) .maybe_add_attribute("vertical-align", renderer.attribute("vertical-align")) - .maybe_add_class(renderer.attribute("css-class")); + .maybe_add_class(renderer.attribute("css-class")) + .set_html_attributes(self.context.header.html_attributes()); tr.render_open(&mut cursor.buffer)?; td.render_open(&mut cursor.buffer)?; @@ -311,7 +317,8 @@ impl<'root> Render<'root> for Renderer<'root, MjColumn, MjColumnExtra<'root>> { .set_style_root_div(Tag::div()) .add_class("mj-outlook-group-fix") .add_class(classname) - .maybe_add_class(self.attribute("css-class")); + .maybe_add_class(self.attribute("css-class")) + .set_html_attributes(self.context.header.html_attributes()); div.render_open(&mut cursor.buffer)?; if self.has_gutter() { diff --git a/packages/mrml-core/src/mj_divider/render.rs b/packages/mrml-core/src/mj_divider/render.rs index 5d1418a0..53144798 100644 --- a/packages/mrml-core/src/mj_divider/render.rs +++ b/packages/mrml-core/src/mj_divider/render.rs @@ -54,11 +54,13 @@ impl<'root> Renderer<'root, MjDivider, ()> { let table = self .set_style_outlook(Tag::table_presentation()) .add_attribute("align", "center") - .maybe_add_attribute("width", self.get_outlook_width().map(|v| v.to_string())); - let tr = Tag::tr(); + .maybe_add_attribute("width", self.get_outlook_width().map(|v| v.to_string())) + .set_html_attributes(self.context.header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); let td = Tag::td() .add_style("height", "0") - .add_style("line-height", "0"); + .add_style("line-height", "0") + .set_html_attributes(self.context.header.html_attributes()); buf.start_conditional_tag(); table.render_open(buf)?; @@ -109,7 +111,9 @@ impl<'root> Render<'root> for Renderer<'root, MjDivider, ()> { } fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let p = self.set_style_p(Tag::new("p")); + let p = self + .set_style_p(Tag::new("p")) + .set_html_attributes(self.context.header.html_attributes()); p.render_text(&mut cursor.buffer, "")?; self.render_after(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_group/render.rs b/packages/mrml-core/src/mj_group/render.rs index 7efc1e9e..fe056d23 100644 --- a/packages/mrml-core/src/mj_group/render.rs +++ b/packages/mrml-core/src/mj_group/render.rs @@ -103,7 +103,8 @@ impl<'root> Renderer<'root, MjGroup, ()> { .get_width() .map(|w| Cow::Owned(w.to_string())) .or_else(|| renderer.attribute("width").map(Cow::Borrowed)), - ); + ) + .set_html_attributes(self.context.header.html_attributes()); cursor.buffer.start_conditional_tag(); td.render_open(&mut cursor.buffer)?; @@ -176,18 +177,21 @@ impl<'root> Render<'root> for Renderer<'root, MjGroup, ()> { .set_style_root_div(Tag::div()) .add_class(classname) .add_class("mj-outlook-group-fix") - .maybe_add_class(self.attribute("css-class")); - let table = Tag::table_presentation().maybe_add_attribute( - "bgcolor", - self.attribute("background-color").and_then(|color| { - if color == "none" { - None - } else { - Some(color) - } - }), - ); - let tr = Tag::tr(); + .maybe_add_class(self.attribute("css-class")) + .set_html_attributes(self.context.header.html_attributes()); + let table = Tag::table_presentation() + .maybe_add_attribute( + "bgcolor", + self.attribute("background-color").and_then(|color| { + if color == "none" { + None + } else { + Some(color) + } + }), + ) + .set_html_attributes(self.context.header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); div.render_open(&mut cursor.buffer)?; cursor.buffer.start_conditional_tag(); diff --git a/packages/mrml-core/src/mj_head/children.rs b/packages/mrml-core/src/mj_head/children.rs index 7ac0931d..e35723e3 100644 --- a/packages/mrml-core/src/mj_head/children.rs +++ b/packages/mrml-core/src/mj_head/children.rs @@ -2,6 +2,7 @@ use crate::comment::Comment; use crate::mj_attributes::MjAttributes; use crate::mj_breakpoint::MjBreakpoint; use crate::mj_font::MjFont; +use crate::mj_html_attributes::MjHtmlAttributes; use crate::mj_include::head::MjIncludeHead; use crate::mj_preview::MjPreview; use crate::mj_raw::MjRaw; @@ -23,4 +24,5 @@ pub enum MjHeadChild { MjRaw(MjRaw), MjStyle(MjStyle), MjTitle(MjTitle), + MjHtmlAttributes(MjHtmlAttributes), } diff --git a/packages/mrml-core/src/mj_head/parse.rs b/packages/mrml-core/src/mj_head/parse.rs index fbd87783..6176f836 100644 --- a/packages/mrml-core/src/mj_head/parse.rs +++ b/packages/mrml-core/src/mj_head/parse.rs @@ -5,6 +5,7 @@ use crate::comment::Comment; use crate::mj_attributes::NAME as MJ_ATTRIBUTES; use crate::mj_breakpoint::NAME as MJ_BREAKPOINT; use crate::mj_font::NAME as MJ_FONT; +use crate::mj_html_attributes::NAME as MJ_HTML_ATTRIBUTES; use crate::mj_include::NAME as MJ_INCLUDE; use crate::mj_preview::NAME as MJ_PREVIEW; use crate::mj_raw::NAME as MJ_RAW; @@ -83,6 +84,7 @@ impl ParseElement for MrmlParser<'_> { tag: StrSpan<'a>, ) -> Result { match tag.as_str() { + MJ_HTML_ATTRIBUTES => self.parse(cursor, tag).map(MjHeadChild::MjHtmlAttributes), MJ_ATTRIBUTES => self.parse(cursor, tag).map(MjHeadChild::MjAttributes), MJ_BREAKPOINT => self.parse(cursor, tag).map(MjHeadChild::MjBreakpoint), MJ_FONT => self.parse(cursor, tag).map(MjHeadChild::MjFont), @@ -109,6 +111,10 @@ impl AsyncParseElement for AsyncMrmlParser { tag: StrSpan<'a>, ) -> Result { match tag.as_str() { + MJ_HTML_ATTRIBUTES => self + .async_parse(cursor, tag) + .await + .map(MjHeadChild::MjHtmlAttributes), MJ_ATTRIBUTES => self .async_parse(cursor, tag) .await diff --git a/packages/mrml-core/src/mj_head/render.rs b/packages/mrml-core/src/mj_head/render.rs index b815333c..1534c821 100644 --- a/packages/mrml-core/src/mj_head/render.rs +++ b/packages/mrml-core/src/mj_head/render.rs @@ -105,6 +105,17 @@ impl MjHead { .map(|font| (font.name(), font.href())) .collect() } + + pub fn build_html_attributes(&self) -> Map<&str, Map<&str, &str>> { + self.children + .iter() + .flat_map(|item| { + item.as_mj_html_attributes() + .into_iter() + .flat_map(|inner| inner.mj_selector_iter()) + }) + .fold(Map::new(), combine_attribute_map) + } } fn render_font_import(target: &mut String, href: &str) { diff --git a/packages/mrml-core/src/mj_hero/render.rs b/packages/mrml-core/src/mj_hero/render.rs index a575009a..051c220a 100644 --- a/packages/mrml-core/src/mj_hero/render.rs +++ b/packages/mrml-core/src/mj_hero/render.rs @@ -150,7 +150,7 @@ impl<'root> Renderer<'root, MjHero, ()> { if child.is_raw() { renderer.render(cursor)?; } else { - let tr = Tag::tr(); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); let td = Tag::td() .maybe_add_style( "background", @@ -168,7 +168,8 @@ impl<'root> Renderer<'root, MjHero, ()> { "background", renderer.attribute("container-background-color"), ) - .maybe_add_attribute("class", renderer.attribute("css-class")); + .maybe_add_attribute("class", renderer.attribute("css-class")) + .set_html_attributes(self.context.header.html_attributes()); tr.render_open(&mut cursor.buffer)?; td.render_open(&mut cursor.buffer)?; @@ -188,16 +189,22 @@ impl<'root> Renderer<'root, MjHero, ()> { .maybe_add_attribute( "width", self.container_width.as_ref().map(|w| w.value().to_string()), - ); - let tbody = Tag::tbody(); - let tr = Tag::tr(); - let td = Tag::td(); - let outlook_inner_td = self.set_style_outlook_inner_td(Tag::td()); + ) + .set_html_attributes(self.context.header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); + let td = Tag::td().set_html_attributes(self.context.header.html_attributes()); + let outlook_inner_td = self + .set_style_outlook_inner_td(Tag::td()) + .set_html_attributes(self.context.header.html_attributes()); let outlook_inner_div = self .set_style_inner_div(Tag::div()) .maybe_add_attribute("width", self.attribute("align")) - .add_class("mj-hero-content"); - let inner_table = self.set_style_inner_table(Tag::table_presentation()); + .add_class("mj-hero-content") + .set_html_attributes(self.context.header.html_attributes()); + let inner_table = self + .set_style_inner_table(Tag::table_presentation()) + .set_html_attributes(self.context.header.html_attributes()); cursor.buffer.start_conditional_tag(); table.render_open(&mut cursor.buffer)?; @@ -231,10 +238,13 @@ impl<'root> Renderer<'root, MjHero, ()> { } fn render_mode_fluid(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let td_fluid = self.set_style_td_fluid(Tag::td()); + let td_fluid = self + .set_style_td_fluid(Tag::td()) + .set_html_attributes(self.context.header.html_attributes()); let td = self .set_style_hero(Tag::td()) - .maybe_add_attribute("background", self.attribute("background-url")); + .maybe_add_attribute("background", self.attribute("background-url")) + .set_html_attributes(self.context.header.html_attributes()); td_fluid.render_closed(&mut cursor.buffer)?; td.render_open(&mut cursor.buffer)?; @@ -257,7 +267,8 @@ impl<'root> Renderer<'root, MjHero, ()> { .set_style_hero(Tag::td()) .add_style("height", format!("{height}px")) .maybe_add_attribute("background", self.attribute("background-url")) - .add_attribute("height", height.to_string()); + .add_attribute("height", height.to_string()) + .set_html_attributes(self.context.header.html_attributes()); td.render_open(&mut cursor.buffer)?; self.render_content(cursor)?; @@ -321,20 +332,28 @@ impl<'root> Render<'root> for Renderer<'root, MjHero, ()> { .maybe_add_attribute( "width", self.container_width.as_ref().map(|v| v.value().to_string()), - ); - let outlook_tr = Tag::tr(); - let outlook_td = self.set_style_outlook_td(Tag::td()); + ) + .set_html_attributes(self.context.header.html_attributes()); + let outlook_tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); + let outlook_td = self + .set_style_outlook_td(Tag::td()) + .set_html_attributes(self.context.header.html_attributes()); let v_image = self .set_style_outlook_image(Tag::new("v:image")) .maybe_add_attribute("src", self.attribute("background-url")) - .add_attribute("xmlns:v", "urn:schemas-microsoft-com:vml"); + .add_attribute("xmlns:v", "urn:schemas-microsoft-com:vml") + .set_html_attributes(self.context.header.html_attributes()); let div = self .set_style_div(Tag::div()) .maybe_add_attribute("align", self.attribute("align")) .maybe_add_class(self.attribute("css-class")); - let table = self.set_style_table(Tag::table_presentation()); - let tbody = Tag::tbody(); - let tr = self.set_style_tr(Tag::tr()); + let table = self + .set_style_table(Tag::table_presentation()) + .set_html_attributes(self.context.header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); + let tr = self + .set_style_tr(Tag::tr()) + .set_html_attributes(self.context.header.html_attributes()); cursor.buffer.start_conditional_tag(); outlook_table.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_html_attribute/json.rs b/packages/mrml-core/src/mj_html_attribute/json.rs new file mode 100644 index 00000000..3edb561b --- /dev/null +++ b/packages/mrml-core/src/mj_html_attribute/json.rs @@ -0,0 +1,33 @@ +use crate::prelude::json::JsonAttributes; + +impl JsonAttributes for super::MjHtmlAttributeAttributes { + fn has_attributes(&self) -> bool { + true + } + + fn try_from_serde(this: Option) -> Result + where + Self: Sized, + { + this.ok_or_else(|| serde::de::Error::missing_field("attributes")) + } +} + +#[cfg(test)] +mod tests { + use crate::mj_html_attribute::{MjHtmlAttribute, MjHtmlAttributeAttributes}; + + #[test] + fn serialize() { + let elt = MjHtmlAttribute::new( + MjHtmlAttributeAttributes { + name: ".classname".into(), + }, + "42".into(), + ); + assert_eq!( + serde_json::to_string(&elt).unwrap(), + r#"{"type":"mj-html-attribute","attributes":{"name":".classname"},"children":"42"}"# + ); + } +} diff --git a/packages/mrml-core/src/mj_html_attribute/mod.rs b/packages/mrml-core/src/mj_html_attribute/mod.rs new file mode 100644 index 00000000..92093a34 --- /dev/null +++ b/packages/mrml-core/src/mj_html_attribute/mod.rs @@ -0,0 +1,49 @@ +use crate::prelude::{Component, StaticTag}; +use std::marker::PhantomData; + +#[cfg(feature = "json")] +mod json; +#[cfg(feature = "parse")] +mod parse; +#[cfg(feature = "print")] +mod print; + +pub const NAME: &str = "mj-html-attribute"; + +#[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))] +pub struct MjHtmlAttributeAttributes { + pub name: String, +} +pub struct MjHtmlAttributeTag; + +impl StaticTag for MjHtmlAttributeTag { + fn static_tag() -> &'static str { + NAME + } +} + +pub type MjHtmlAttribute = + Component, MjHtmlAttributeAttributes, String>; + +impl MjHtmlAttribute { + pub fn name(&self) -> &str { + self.attributes.name.as_str() + } + + pub fn children(&self) -> &str { + &self.children + } +} + +impl From for MjHtmlAttribute { + fn from(children: String) -> Self { + Self::new(MjHtmlAttributeAttributes::default(), children) + } +} + +impl From<&str> for MjHtmlAttribute { + fn from(value: &str) -> Self { + Self::from(value.to_string()) + } +} diff --git a/packages/mrml-core/src/mj_html_attribute/parse.rs b/packages/mrml-core/src/mj_html_attribute/parse.rs new file mode 100644 index 00000000..3c13d52f --- /dev/null +++ b/packages/mrml-core/src/mj_html_attribute/parse.rs @@ -0,0 +1,72 @@ +#[cfg(feature = "async")] +use crate::prelude::parser::AsyncMrmlParser; +use crate::prelude::parser::{Error, MrmlCursor, MrmlParser, ParseAttributes, WarningKind}; +use htmlparser::StrSpan; + +use super::MjHtmlAttributeAttributes; + +#[inline(always)] +fn parse_attributes( + cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, +) -> Result { + let mut name = None; + + while let Some(attr) = cursor.next_attribute()? { + match (attr.local.as_str(), attr.value) { + ("name", Some(value)) => { + name = Some(value.to_string()); + } + _ => { + cursor.add_warning(WarningKind::UnexpectedAttribute, attr.span); + } + } + } + + Ok(MjHtmlAttributeAttributes { + name: name.ok_or_else(|| Error::MissingAttribute { + name: "name", + origin: cursor.origin(), + position: tag.into(), + })?, + }) +} + +impl ParseAttributes for MrmlParser<'_> { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, + ) -> Result { + parse_attributes(cursor, tag) + } +} + +#[cfg(feature = "async")] +impl ParseAttributes for AsyncMrmlParser { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, + ) -> Result { + parse_attributes(cursor, tag) + } +} + +#[cfg(test)] +mod tests { + use crate::mj_html_attribute::MjHtmlAttribute; + + crate::should_parse!( + basic_with_children, + MjHtmlAttribute, + r#"42"# + ); + + crate::should_not_parse!( + missing_attribute, + MjHtmlAttribute, + r#"42"#, + r#"MissingAttribute { name: "name", origin: Root, position: Span { start: 1, end: 18 } }"# + ); +} diff --git a/packages/mrml-core/src/mj_html_attribute/print.rs b/packages/mrml-core/src/mj_html_attribute/print.rs new file mode 100644 index 00000000..7ccbf45e --- /dev/null +++ b/packages/mrml-core/src/mj_html_attribute/print.rs @@ -0,0 +1,34 @@ +use crate::prelude::print::{Printable, PrintableAttributes}; + +impl Printable for super::MjHtmlAttribute { + fn print(&self, printer: &mut P) -> std::fmt::Result { + printer.push_indent(); + printer.open_tag(super::NAME)?; + printer.push_attribute("name", self.attributes.name.as_str())?; + printer.close_tag(); + printer.push_str(self.children.as_str()); + printer.end_tag(super::NAME)?; + printer.push_new_line(); + Ok(()) + } +} + +impl PrintableAttributes for super::MjHtmlAttributeAttributes { + fn print(&self, printer: &mut P) -> std::fmt::Result { + printer.push_attribute("name", self.name.as_str()) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::print::Printable; + + #[test] + fn empty() { + let item = crate::mj_html_attribute::MjHtmlAttribute::default(); + assert_eq!( + r#""#, + item.print_dense().unwrap() + ) + } +} diff --git a/packages/mrml-core/src/mj_html_attributes/children.rs b/packages/mrml-core/src/mj_html_attributes/children.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packages/mrml-core/src/mj_html_attributes/children.rs @@ -0,0 +1 @@ + diff --git a/packages/mrml-core/src/mj_html_attributes/json.rs b/packages/mrml-core/src/mj_html_attributes/json.rs new file mode 100644 index 00000000..3141fc8b --- /dev/null +++ b/packages/mrml-core/src/mj_html_attributes/json.rs @@ -0,0 +1,31 @@ +#[cfg(test)] +mod tests { + use crate::mj_html_attributes::{MjHtmlAttributes, MjSelector}; + use crate::mj_selector::MjSelectorAttributes; + + #[test] + fn serialize() { + let mut elt = MjHtmlAttributes::default(); + + elt.children.push(MjSelector::new( + MjSelectorAttributes { + path: ".class".into(), + }, + Vec::new(), + )); + + assert_eq!( + serde_json::to_string(&elt).unwrap(), + r#"{"type":"mj-html-attributes","children":[{"type":"mj-selector","attributes":{"path":".class"}}]}"# + ) + } + + #[test] + fn deserialize() { + let json = r#"{"type":"mj-html-attributes","children":[{"type":"mj-selector","attributes":{"path":".class"}},{"type":"mj-selector","attributes":{"path":"a[href]"}}]}"#; + let res: MjHtmlAttributes = serde_json::from_str(json).unwrap(); + assert_eq!(res.children.len(), 2); + let next = serde_json::to_string(&res).unwrap(); + assert_eq!(next, json); + } +} diff --git a/packages/mrml-core/src/mj_html_attributes/mod.rs b/packages/mrml-core/src/mj_html_attributes/mod.rs new file mode 100644 index 00000000..5fed341a --- /dev/null +++ b/packages/mrml-core/src/mj_html_attributes/mod.rs @@ -0,0 +1,46 @@ +mod children; +#[cfg(feature = "json")] +mod json; +#[cfg(feature = "parse")] +mod parse; +#[cfg(feature = "print")] +mod print; + +use std::marker::PhantomData; + +pub use crate::mj_selector::MjSelector; + +use crate::prelude::{Component, StaticTag}; + +pub const NAME: &str = "mj-html-attributes"; + +pub struct MjHtmlAttributesTag; + +impl StaticTag for MjHtmlAttributesTag { + fn static_tag() -> &'static str { + NAME + } +} + +pub type MjHtmlAttributes = Component, (), Vec>; + +#[cfg(feature = "render")] +impl MjHtmlAttributes { + pub(crate) fn mj_selector_iter(&self) -> impl Iterator { + self.children.iter().flat_map(|child| { + child.children.iter().map(|c| { + ( + child.attributes.path.as_str(), + c.attributes.name.as_str(), + c.children.as_str(), + ) + }) + }) + } +} + +impl MjHtmlAttributes { + pub fn children(&self) -> &Vec { + &self.children + } +} diff --git a/packages/mrml-core/src/mj_html_attributes/parse.rs b/packages/mrml-core/src/mj_html_attributes/parse.rs new file mode 100644 index 00000000..d400d06e --- /dev/null +++ b/packages/mrml-core/src/mj_html_attributes/parse.rs @@ -0,0 +1,86 @@ +use super::MjSelector; +use crate::mj_selector::NAME as MJ_SELECTOR; + +#[cfg(feature = "async")] +use crate::prelude::parser::{AsyncMrmlParser, AsyncParseChildren, AsyncParseElement}; +use crate::prelude::parser::{ + Error, MrmlCursor, MrmlParser, MrmlToken, ParseChildren, ParseElement, +}; + +impl ParseChildren> for MrmlParser<'_> { + fn parse_children<'a>(&self, cursor: &mut MrmlCursor<'a>) -> Result, Error> { + let mut children = Vec::new(); + + loop { + match cursor.assert_next()? { + MrmlToken::ElementStart(inner) => { + if inner.local.as_str() == MJ_SELECTOR { + children.push(self.parse(cursor, inner.local)?) + } else { + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: inner.span.into(), + }); + } + } + MrmlToken::ElementClose(inner) => { + cursor.rewind(MrmlToken::ElementClose(inner)); + return Ok(children); + } + other => { + return Err(Error::UnexpectedElement { + origin: cursor.origin(), + position: other.span(), + }); + } + } + } + } +} + +#[cfg(feature = "async")] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl AsyncParseChildren> for AsyncMrmlParser { + async fn async_parse_children<'a>( + &self, + cursor: &mut MrmlCursor<'a>, + ) -> Result, Error> { + let mut result = Vec::new(); + + loop { + match cursor.assert_next()? { + MrmlToken::ElementStart(inner) => { + result.push(self.async_parse(cursor, inner.local).await?); + } + MrmlToken::ElementClose(inner) => { + cursor.rewind(MrmlToken::ElementClose(inner)); + return Ok(result); + } + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::mj_html_attributes::MjHtmlAttributes; + + crate::should_sync_parse!( + parse_complete, + MjHtmlAttributes, + r#" + + + 43 + + +"# + ); +} diff --git a/packages/mrml-core/src/mj_html_attributes/print.rs b/packages/mrml-core/src/mj_html_attributes/print.rs new file mode 100644 index 00000000..34c93070 --- /dev/null +++ b/packages/mrml-core/src/mj_html_attributes/print.rs @@ -0,0 +1,10 @@ +#[cfg(test)] +mod tests { + use crate::prelude::print::Printable; + + #[test] + fn empty() { + let item = crate::mj_html_attributes::MjHtmlAttributes::default(); + assert_eq!("", item.print_dense().unwrap()); + } +} diff --git a/packages/mrml-core/src/mj_image/render.rs b/packages/mrml-core/src/mj_image/render.rs index 2e81aa54..a7b3ac8f 100644 --- a/packages/mrml-core/src/mj_image/render.rs +++ b/packages/mrml-core/src/mj_image/render.rs @@ -101,7 +101,8 @@ impl<'root> Renderer<'root, MjImage, ()> { self.get_content_width() .map(|size| size.value().to_string()), ) - .maybe_add_attribute("usemap", self.attribute("usemap")); + .maybe_add_attribute("usemap", self.attribute("usemap")) + .set_html_attributes(self.context.header.html_attributes()); let img = self.set_style_img(img); img.render_closed(buf) } @@ -112,6 +113,7 @@ impl<'root> Renderer<'root, MjImage, ()> { .maybe_add_attribute("name", self.attribute("name")) .maybe_add_attribute("rel", self.attribute("rel")) .maybe_add_attribute("target", self.attribute("target")) + .set_html_attributes(self.context.header.html_attributes()) .render_with(buf, |b| self.render_image(b)) } @@ -169,10 +171,14 @@ impl<'root> Render<'root> for Renderer<'root, MjImage, ()> { }; let table = self .set_style_table(Tag::table_presentation()) - .maybe_add_class(class); - let tbody = Tag::tbody(); - let tr = Tag::tr(); - let td = self.set_style_td(Tag::td()).maybe_add_class(class); + .maybe_add_class(class) + .set_html_attributes(self.context.header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context.header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); + let td = self + .set_style_td(Tag::td()) + .maybe_add_class(class) + .set_html_attributes(self.context.header.html_attributes()); table.render_open(&mut cursor.buffer)?; tbody.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_include/head/mod.rs b/packages/mrml-core/src/mj_include/head/mod.rs index 886459fd..039f96fa 100644 --- a/packages/mrml-core/src/mj_include/head/mod.rs +++ b/packages/mrml-core/src/mj_include/head/mod.rs @@ -18,6 +18,7 @@ use crate::prelude::{Component, StaticTag}; pub enum MjIncludeHeadChild { Comment(crate::comment::Comment), MjAttributes(crate::mj_attributes::MjAttributes), + MjHtmlAttributes(crate::mj_html_attributes::MjHtmlAttributes), MjBreakpoint(crate::mj_breakpoint::MjBreakpoint), MjFont(crate::mj_font::MjFont), MjPreview(crate::mj_preview::MjPreview), diff --git a/packages/mrml-core/src/mj_navbar/render.rs b/packages/mrml-core/src/mj_navbar/render.rs index bb9d4c1d..40085093 100644 --- a/packages/mrml-core/src/mj_navbar/render.rs +++ b/packages/mrml-core/src/mj_navbar/render.rs @@ -82,21 +82,26 @@ impl<'root> Renderer<'root, MjNavbar, MjNavbarExtra> { .set_style_input(Tag::new("input")) .add_class("mj-menu-checkbox") .add_attribute("id", self.extra.id.clone()) - .add_attribute("type", "checkbox"); + .add_attribute("type", "checkbox") + .set_html_attributes(self.context.header.html_attributes()); let div = self .set_style_trigger(Tag::div()) - .add_class("mj-menu-trigger"); + .add_class("mj-menu-trigger") + .set_html_attributes(self.context.header.html_attributes()); let label = self .set_style_label(Tag::new("label")) .maybe_add_attribute("align", self.attribute("ico-align")) .add_class("mj-menu-label") - .add_attribute("for", self.extra.id.clone()); + .add_attribute("for", self.extra.id.clone()) + .set_html_attributes(self.context.header.html_attributes()); let span_open = self .set_style_ico_open(Tag::new("span")) - .add_class("mj-menu-icon-open"); + .add_class("mj-menu-icon-open") + .set_html_attributes(self.context.header.html_attributes()); let span_close = self .set_style_ico_close(Tag::new("span")) - .add_class("mj-menu-icon-close"); + .add_class("mj-menu-icon-close") + .set_html_attributes(self.context.header.html_attributes()); buf.start_mso_negation_conditional_tag(); input.render_closed(buf)?; @@ -192,9 +197,13 @@ impl<'root> Render<'root> for Renderer<'root, MjNavbar, MjNavbarExtra> { fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> { cursor.header.add_style(self.render_style()); - let div = Tag::div().add_class("mj-inline-links"); - let table = Tag::table_presentation().maybe_add_attribute("align", self.attribute("align")); - let tr = Tag::tr(); + let div = Tag::div() + .add_class("mj-inline-links") + .set_html_attributes(self.context.header.html_attributes()); + let table = Tag::table_presentation() + .maybe_add_attribute("align", self.attribute("align")) + .set_html_attributes(self.context.header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context.header.html_attributes()); let base_url = self.attribute("base-url"); if self.has_hamburger() { diff --git a/packages/mrml-core/src/mj_navbar_link/render.rs b/packages/mrml-core/src/mj_navbar_link/render.rs index 7e1cc083..20e89ead 100644 --- a/packages/mrml-core/src/mj_navbar_link/render.rs +++ b/packages/mrml-core/src/mj_navbar_link/render.rs @@ -66,7 +66,8 @@ impl<'root> Renderer<'root, MjNavbarLink, MjNavbarLinkExtra<'root>> { .maybe_add_attribute("href", self.get_link()) .maybe_add_attribute("rel", self.attribute("rel")) .maybe_add_attribute("target", self.attribute("target")) - .maybe_add_attribute("name", self.attribute("name")); + .maybe_add_attribute("name", self.attribute("name")) + .set_html_attributes(self.context.header.html_attributes()); link.render_open(&mut cursor.buffer)?; for child in self.element.children.iter() { @@ -128,7 +129,8 @@ impl<'root> Render<'root> for Renderer<'root, MjNavbarLink, MjNavbarLinkExtra<'r let td = self .set_style_td(Tag::td()) - .maybe_add_suffixed_class(self.attribute("css-class"), "outlook"); + .maybe_add_suffixed_class(self.attribute("css-class"), "outlook") + .set_html_attributes(self.context.header.html_attributes()); cursor.buffer.start_conditional_tag(); td.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_section/render.rs b/packages/mrml-core/src/mj_section/render.rs index c0795df8..9cdac55d 100644 --- a/packages/mrml-core/src/mj_section/render.rs +++ b/packages/mrml-core/src/mj_section/render.rs @@ -240,11 +240,15 @@ pub trait SectionLikeRender<'root>: WithMjSectionBackground<'root> { ) .add_attribute("xmlns:v", "urn:schemas-microsoft-com:vml") .add_attribute("fill", "true") - .add_attribute("stroke", "false"); - let vfill = self.get_vfill_tag(); + .add_attribute("stroke", "false") + .set_html_attributes(self.context().header.html_attributes()); + let vfill = self + .get_vfill_tag() + .set_html_attributes(self.context().header.html_attributes()); let vtextbox = Tag::new("v:textbox") .add_attribute("inset", "0,0,0,0") - .add_style("mso-fit-shape-to-text", "true"); + .add_style("mso-fit-shape-to-text", "true") + .set_html_attributes(self.context().header.html_attributes()); vrect.render_open(&mut cursor.buffer)?; vfill.render_closed(&mut cursor.buffer)?; @@ -293,12 +297,14 @@ pub trait SectionLikeRender<'root>: WithMjSectionBackground<'root> { "width", self.container_width().as_ref().map(|v| v.to_string()), ) - .maybe_add_suffixed_class(self.attribute("css-class"), "outlook"); - let tr = Tag::tr(); + .maybe_add_suffixed_class(self.attribute("css-class"), "outlook") + .set_html_attributes(self.context().header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context().header.html_attributes()); let td = Tag::td() .add_style("line-height", "0px") .add_style("font-size", "0px") - .add_style("mso-line-height-rule", "exactly"); + .add_style("mso-line-height-rule", "exactly") + .set_html_attributes(self.context().header.html_attributes()); cursor.buffer.start_conditional_tag(); table.render_open(&mut cursor.buffer)?; @@ -324,7 +330,7 @@ pub trait SectionLikeRender<'root>: WithMjSectionBackground<'root> { fn render_wrapped_children(&self, cursor: &mut RenderCursor) -> Result<(), Error> { let siblings = self.get_siblings(); let raw_siblings = self.get_raw_siblings(); - let tr = Tag::tr(); + let tr = Tag::tr().set_html_attributes(self.context().header.html_attributes()); tr.render_open(&mut cursor.buffer)?; for child in self.children().iter() { @@ -340,7 +346,8 @@ pub trait SectionLikeRender<'root>: WithMjSectionBackground<'root> { let td = renderer .set_style("td-outlook", Tag::td()) .maybe_add_attribute("align", renderer.attribute("align")) - .maybe_add_suffixed_class(renderer.attribute("css-class"), "outlook"); + .maybe_add_suffixed_class(renderer.attribute("css-class"), "outlook") + .set_html_attributes(self.context().header.html_attributes()); td.render_open(&mut cursor.buffer)?; cursor.buffer.end_conditional_tag(); renderer.render(cursor)?; @@ -399,24 +406,30 @@ pub trait SectionLikeRender<'root>: WithMjSectionBackground<'root> { None } else { self.attribute("css-class") - }); + }) + .set_html_attributes(self.context().header.html_attributes()); let inner_div = self.set_style_section_inner_div(Tag::div()); - let table = self.set_style_section_table( - Tag::table_presentation() - .add_attribute("align", "center") - .maybe_add_attribute( - "background", - if is_full_width { - None - } else { - self.attribute("background-url") - }, - ), - ); - let tbody = Tag::tbody(); - let tr = Tag::tr(); - let td = self.set_style_section_td(Tag::td()); - let inner_table = Tag::table_presentation(); + let table = self + .set_style_section_table( + Tag::table_presentation() + .add_attribute("align", "center") + .maybe_add_attribute( + "background", + if is_full_width { + None + } else { + self.attribute("background-url") + }, + ), + ) + .set_html_attributes(self.context().header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context().header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context().header.html_attributes()); + let td = self + .set_style_section_td(Tag::td()) + .set_html_attributes(self.context().header.html_attributes()); + let inner_table = + Tag::table_presentation().set_html_attributes(self.context().header.html_attributes()); let has_bg = self.has_background(); div.render_open(&mut cursor.buffer)?; @@ -469,10 +482,12 @@ pub trait SectionLikeRender<'root>: WithMjSectionBackground<'root> { } fn render_full_width(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let table = self.get_full_width_table(); - let tbody = Tag::tbody(); - let tr = Tag::tr(); - let td = Tag::td(); + let table = self + .get_full_width_table() + .set_html_attributes(self.context().header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context().header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context().header.html_attributes()); + let td = Tag::td().set_html_attributes(self.context().header.html_attributes()); table.render_open(&mut cursor.buffer)?; tbody.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_selector/json.rs b/packages/mrml-core/src/mj_selector/json.rs new file mode 100644 index 00000000..5a7609af --- /dev/null +++ b/packages/mrml-core/src/mj_selector/json.rs @@ -0,0 +1,37 @@ +use crate::prelude::json::JsonAttributes; + +impl JsonAttributes for super::MjSelectorAttributes { + fn has_attributes(&self) -> bool { + true + } + + fn try_from_serde(this: Option) -> Result + where + Self: Sized, + { + this.ok_or_else(|| serde::de::Error::missing_field("attributes")) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + mj_html_attribute::MjHtmlAttribute, mj_selector::MjSelector, + mj_selector::MjSelectorAttributes, + }; + + #[test] + fn serialize() { + let elt = MjSelector::new( + MjSelectorAttributes { + path: ".test".into(), + }, + Vec::::new(), + ); + + assert_eq!( + serde_json::to_string(&elt).unwrap(), + r#"{"type":"mj-selector","attributes":{"path":".test"}}"#, + ); + } +} diff --git a/packages/mrml-core/src/mj_selector/mod.rs b/packages/mrml-core/src/mj_selector/mod.rs new file mode 100644 index 00000000..ab79bbfb --- /dev/null +++ b/packages/mrml-core/src/mj_selector/mod.rs @@ -0,0 +1,32 @@ +use std::marker::PhantomData; + +use crate::{ + mj_html_attribute::MjHtmlAttribute, + prelude::{Component, StaticTag}, +}; + +#[cfg(feature = "json")] +mod json; +#[cfg(feature = "parse")] +mod parse; +#[cfg(feature = "print")] +mod print; + +pub const NAME: &str = "mj-selector"; + +pub struct MjSelectorTag; + +impl StaticTag for MjSelectorTag { + fn static_tag() -> &'static str { + NAME + } +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))] +pub struct MjSelectorAttributes { + pub path: String, +} + +pub type MjSelector = + Component, MjSelectorAttributes, Vec>; diff --git a/packages/mrml-core/src/mj_selector/parse.rs b/packages/mrml-core/src/mj_selector/parse.rs new file mode 100644 index 00000000..100d7295 --- /dev/null +++ b/packages/mrml-core/src/mj_selector/parse.rs @@ -0,0 +1,139 @@ +use htmlparser::StrSpan; + +#[cfg(feature = "async")] +use super::MjSelector; +use super::MjSelectorAttributes; +use crate::mj_html_attribute::MjHtmlAttribute; +#[cfg(feature = "async")] +use crate::prelude::parser::{AsyncMrmlParser, AsyncParseChildren, AsyncParseElement}; +use crate::prelude::parser::{ + Error, MrmlCursor, MrmlParser, MrmlToken, ParseAttributes, ParseChildren, ParseElement, + WarningKind, +}; + +#[inline(always)] +fn parse_attributes( + cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, +) -> Result { + let mut path = None; + + while let Some(attr) = cursor.next_attribute()? { + match (attr.local.as_str(), attr.value) { + ("path", Some(value)) => { + path = Some(value.to_string()); + } + _ => { + cursor.add_warning(WarningKind::UnexpectedAttribute, attr.span); + } + } + } + + Ok(MjSelectorAttributes { + path: path.ok_or_else(|| Error::MissingAttribute { + name: "path", + origin: cursor.origin(), + position: tag.into(), + })?, + }) +} + +impl ParseAttributes for MrmlParser<'_> { + fn parse_attributes( + &self, + cursor: &mut MrmlCursor<'_>, + tag: &StrSpan<'_>, + ) -> Result { + parse_attributes(cursor, tag) + } +} + +impl ParseChildren> for MrmlParser<'_> { + fn parse_children(&self, cursor: &mut MrmlCursor<'_>) -> Result, Error> { + let mut children = Vec::new(); + + loop { + match cursor.assert_next()? { + MrmlToken::ElementStart(inner) => { + children.push(self.parse(cursor, inner.local)?); + } + MrmlToken::ElementClose(close) => { + cursor.rewind(MrmlToken::ElementClose(close)); + return Ok(children); + } + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); + } + } + } + } +} + +#[cfg(feature = "async")] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl AsyncParseChildren> for AsyncMrmlParser { + async fn async_parse_children<'a>( + &self, + cursor: &mut MrmlCursor<'a>, + ) -> Result, Error> { + let mut children = Vec::new(); + + loop { + match cursor.assert_next()? { + MrmlToken::ElementStart(inner) => { + children.push(self.async_parse(cursor, inner.local).await?); + } + MrmlToken::ElementClose(close) => { + cursor.rewind(MrmlToken::ElementClose(close)); + return Ok(children); + } + other => { + return Err(Error::UnexpectedToken { + origin: cursor.origin(), + position: other.span(), + }); + } + } + } + } +} + +#[cfg(feature = "async")] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl AsyncParseElement for AsyncMrmlParser { + async fn async_parse<'a>( + &self, + cursor: &mut MrmlCursor<'a>, + tag: StrSpan<'a>, + ) -> Result { + let attributes = parse_attributes(cursor, &tag)?; + + let children = self.async_parse_children(cursor).await?; + cursor.assert_element_close()?; + + Ok(MjSelector::new(attributes, children)) + } +} + +#[cfg(test)] +mod tests { + use crate::mj_selector::MjSelector; + + crate::should_sync_parse!( + parse_complete, + MjSelector, + r#""# + ); + + crate::should_not_sync_parse!( + should_have_name, + MjSelector, + r#""#, + r#"MissingAttribute { name: "path", origin: Root, position: Span { start: 1, end: 12 } }"# + ); +} diff --git a/packages/mrml-core/src/mj_selector/print.rs b/packages/mrml-core/src/mj_selector/print.rs new file mode 100644 index 00000000..80cec501 --- /dev/null +++ b/packages/mrml-core/src/mj_selector/print.rs @@ -0,0 +1,39 @@ +use crate::prelude::print::PrintableAttributes; + +impl PrintableAttributes for super::MjSelectorAttributes { + fn print(&self, printer: &mut P) -> std::fmt::Result { + printer.push_attribute("path", self.path.as_str()) + } +} + +#[cfg(test)] +mod tests { + use crate::mj_selector::{MjSelector, MjSelectorAttributes}; + use crate::prelude::print::Printable; + + #[test] + fn normal() { + let item = MjSelector::new( + MjSelectorAttributes { + path: String::from(".cool_class"), + }, + Vec::new(), + ); + + assert_eq!( + "", + item.print_dense().unwrap() + ); + } + + #[test] + fn empty() { + let item = MjSelector::new( + MjSelectorAttributes { + path: String::from(""), + }, + Vec::new(), + ); + assert_eq!("", item.print_dense().unwrap()); + } +} diff --git a/packages/mrml-core/src/mj_social/render.rs b/packages/mrml-core/src/mj_social/render.rs index e0dc5850..a0c1106a 100644 --- a/packages/mrml-core/src/mj_social/render.rs +++ b/packages/mrml-core/src/mj_social/render.rs @@ -67,14 +67,17 @@ impl Renderer<'_, MjSocial, ()> { } fn render_horizontal(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let table = Tag::table_presentation().maybe_add_attribute("align", self.attribute("align")); - let tr = Tag::tr(); - let td = Tag::td(); + let table = Tag::table_presentation() + .maybe_add_attribute("align", self.attribute("align")) + .set_html_attributes(self.context().header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context().header.html_attributes()); + let td = Tag::td().set_html_attributes(self.context().header.html_attributes()); let inner_table = Tag::table_presentation() .maybe_add_attribute("align", self.attribute("align")) .add_style("float", "none") - .add_style("display", "inline-table"); - let inner_tbody = Tag::tbody(); + .add_style("display", "inline-table") + .set_html_attributes(self.context().header.html_attributes()); + let inner_tbody = Tag::tbody().set_html_attributes(self.context().header.html_attributes()); let child_attributes = self.build_child_attributes(); cursor.buffer.start_conditional_tag(); @@ -109,7 +112,9 @@ impl Renderer<'_, MjSocial, ()> { } fn render_vertical(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let table = self.set_style_table_vertical(Tag::table_presentation()); + let table = self + .set_style_table_vertical(Tag::table_presentation()) + .set_html_attributes(self.context().header.html_attributes()); let tbody = Tag::tbody(); let child_attributes = self.build_child_attributes(); diff --git a/packages/mrml-core/src/mj_social_element/render.rs b/packages/mrml-core/src/mj_social_element/render.rs index d0491614..bb7b398b 100644 --- a/packages/mrml-core/src/mj_social_element/render.rs +++ b/packages/mrml-core/src/mj_social_element/render.rs @@ -152,14 +152,19 @@ impl<'root> Renderer<'root, MjSocialElement, MjSocialElementExtra<'root>> { href: &Option>, cursor: &mut RenderCursor, ) -> Result<(), Error> { - let table = self.set_style_table(Tag::table_presentation()); - let tbody = Tag::tbody(); - let tr = Tag::tr(); - let td = self.set_style_icon(Tag::td()); + let table = self + .set_style_table(Tag::table_presentation()) + .set_html_attributes(self.context().header.html_attributes()); + let tbody = Tag::tbody().set_html_attributes(self.context().header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context().header.html_attributes()); + let td = self + .set_style_icon(Tag::td()) + .set_html_attributes(self.context().header.html_attributes()); let a = Tag::new("a") .maybe_add_attribute("href", href.clone()) .maybe_add_attribute("rel", self.attribute("rel")) - .maybe_add_attribute("target", self.attribute("target")); + .maybe_add_attribute("target", self.attribute("target")) + .set_html_attributes(self.context().header.html_attributes()); let img = self .set_style_img(Tag::new("img")) .maybe_add_attribute("alt", self.attribute("alt")) @@ -174,7 +179,8 @@ impl<'root> Renderer<'root, MjSocialElement, MjSocialElementExtra<'root>> { .maybe_add_attribute( "width", self.get_icon_size().map(|size| size.value().to_string()), - ); + ) + .set_html_attributes(self.context().header.html_attributes()); table.render_open(&mut cursor.buffer)?; tbody.render_open(&mut cursor.buffer)?; @@ -199,16 +205,21 @@ impl<'root> Renderer<'root, MjSocialElement, MjSocialElementExtra<'root>> { href: &Option>, cursor: &mut RenderCursor, ) -> Result<(), Error> { - let td = self.set_style_td_text(Tag::td()); + let td = self + .set_style_td_text(Tag::td()) + .set_html_attributes(self.context().header.html_attributes()); let wrapper = if href.is_some() { Tag::new("a") .maybe_add_attribute("href", href.clone()) .maybe_add_attribute("rel", self.attribute("rel")) .maybe_add_attribute("target", self.attribute("target")) + .set_html_attributes(self.context().header.html_attributes()) } else { - Tag::new("span") + Tag::new("span").set_html_attributes(self.context().header.html_attributes()) }; - let wrapper = self.set_style_text(wrapper); + let wrapper = self + .set_style_text(wrapper) + .set_html_attributes(self.context().header.html_attributes()); td.render_open(&mut cursor.buffer)?; wrapper.render_open(&mut cursor.buffer)?; @@ -269,8 +280,12 @@ impl<'root> Render<'root> for Renderer<'root, MjSocialElement, MjSocialElementEx fn render(&self, cursor: &mut RenderCursor) -> Result<(), Error> { let href = self.get_href(); - let tr = Tag::tr().maybe_add_class(self.attribute("css-class")); - let td = self.set_style_td(Tag::td()); + let tr = Tag::tr() + .maybe_add_class(self.attribute("css-class")) + .set_html_attributes(self.context().header.html_attributes()); + let td = self + .set_style_td(Tag::td()) + .set_html_attributes(self.context().header.html_attributes()); tr.render_open(&mut cursor.buffer)?; td.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_spacer/render.rs b/packages/mrml-core/src/mj_spacer/render.rs index 722e67d7..8d9677b5 100644 --- a/packages/mrml-core/src/mj_spacer/render.rs +++ b/packages/mrml-core/src/mj_spacer/render.rs @@ -33,6 +33,7 @@ impl<'root> Render<'root> for Renderer<'root, MjSpacer, ()> { Tag::div() .maybe_add_style("height", self.attribute("height")) .maybe_add_style("line-height", self.attribute("height")) + .set_html_attributes(self.context().header.html_attributes()) .render_text(&mut cursor.buffer, " ") .map_err(Error::from) } diff --git a/packages/mrml-core/src/mj_table/render.rs b/packages/mrml-core/src/mj_table/render.rs index 2d0af176..732a6980 100644 --- a/packages/mrml-core/src/mj_table/render.rs +++ b/packages/mrml-core/src/mj_table/render.rs @@ -67,7 +67,8 @@ impl<'root> Render<'root> for Renderer<'root, MjTable, ()> { .add_attribute("border", "0") .maybe_add_attribute("cellpadding", self.attribute("cellpadding")) .maybe_add_attribute("cellspacing", self.attribute("cellspacing")) - .maybe_add_attribute("width", self.attribute("width")); + .maybe_add_attribute("width", self.attribute("width")) + .set_html_attributes(self.context().header.html_attributes()); table.render_open(&mut cursor.buffer)?; for (index, child) in self.element.children.iter().enumerate() { let mut renderer = child.renderer(self.context()); diff --git a/packages/mrml-core/src/mj_text/render.rs b/packages/mrml-core/src/mj_text/render.rs index e1161d28..5a799a61 100644 --- a/packages/mrml-core/src/mj_text/render.rs +++ b/packages/mrml-core/src/mj_text/render.rs @@ -21,7 +21,9 @@ impl<'root> Renderer<'root, MjText, ()> { } fn render_content(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let root = self.set_style_text(Tag::div()); + let root = self + .set_style_text(Tag::div()) + .set_html_attributes(self.context().header.html_attributes()); root.render_open(&mut cursor.buffer)?; for child in self.element.children.iter() { child.renderer(self.context()).render(cursor)?; @@ -31,12 +33,14 @@ impl<'root> Renderer<'root, MjText, ()> { } fn render_with_height(&self, height: &str, cursor: &mut RenderCursor) -> Result<(), Error> { - let table = Tag::table_presentation(); - let tr = Tag::tr(); + let table = + Tag::table_presentation().set_html_attributes(self.context().header.html_attributes()); + let tr = Tag::tr().set_html_attributes(self.context().header.html_attributes()); let td = Tag::td() .add_attribute("height", height) .add_style("vertical-align", "top") - .add_style("height", height); + .add_style("height", height) + .set_html_attributes(self.context().header.html_attributes()); cursor.buffer.start_conditional_tag(); table.render_open(&mut cursor.buffer)?; diff --git a/packages/mrml-core/src/mj_wrapper/render.rs b/packages/mrml-core/src/mj_wrapper/render.rs index 56add34b..62eadcdd 100644 --- a/packages/mrml-core/src/mj_wrapper/render.rs +++ b/packages/mrml-core/src/mj_wrapper/render.rs @@ -25,7 +25,7 @@ impl<'root> SectionLikeRender<'root> for Renderer<'root, MjWrapper, ()> { } fn render_wrapped_children(&self, cursor: &mut RenderCursor) -> Result<(), Error> { - let tr = Tag::tr(); + let tr = Tag::tr().set_html_attributes(self.context().header.html_attributes()); let siblings = self.get_siblings(); let raw_siblings = self.get_raw_siblings(); let current_width = self.current_width(); @@ -42,7 +42,8 @@ impl<'root> SectionLikeRender<'root> for Renderer<'root, MjWrapper, ()> { .set_style("td-outlook", Tag::td()) .maybe_add_attribute("align", renderer.attribute("align")) .maybe_add_attribute("width", container_width.as_ref().cloned()) - .maybe_add_suffixed_class(renderer.attribute("css-class"), "outlook"); + .maybe_add_suffixed_class(renderer.attribute("css-class"), "outlook") + .set_html_attributes(self.context().header.html_attributes()); tr.render_open(&mut cursor.buffer)?; td.render_open(&mut cursor.buffer)?; cursor.buffer.end_conditional_tag(); diff --git a/packages/mrml-core/src/prelude/print.rs b/packages/mrml-core/src/prelude/print.rs index 5f4f7954..d979348b 100644 --- a/packages/mrml-core/src/prelude/print.rs +++ b/packages/mrml-core/src/prelude/print.rs @@ -88,6 +88,7 @@ use crate::mj_font::MjFont; use crate::mj_group::MjGroup; use crate::mj_head::MjHeadChild; use crate::mj_hero::MjHero; +use crate::mj_html_attributes::MjHtmlAttributes; use crate::mj_image::MjImage; use crate::mj_include::body::MjIncludeBody; use crate::mj_include::head::MjIncludeHead; @@ -110,6 +111,7 @@ use crate::text::Text; #[enum_dispatch::enum_dispatch( MjAccordionChild, MjAttributesChild, + MjSelector, MjBodyChild, MjCarouselChild, MjHeadChild, diff --git a/packages/mrml-core/src/prelude/render/header.rs b/packages/mrml-core/src/prelude/render/header.rs index 6778b27d..d6d0efbf 100644 --- a/packages/mrml-core/src/prelude/render/header.rs +++ b/packages/mrml-core/src/prelude/render/header.rs @@ -75,6 +75,7 @@ pub(crate) struct Header<'h> { attributes_all: Map<&'h str, &'h str>, attributes_class: Map<&'h str, Map<&'h str, &'h str>>, attributes_element: Map<&'h str, Map<&'h str, &'h str>>, + html_attributes: Map<&'h str, Map<&'h str, &'h str>>, breakpoint: Pixel, font_families: Map<&'h str, &'h str>, preview: Option<&'h str>, @@ -96,6 +97,10 @@ impl<'h> Header<'h> { .as_ref() .map(|h| h.build_attributes_element()) .unwrap_or_default(), + html_attributes: head + .as_ref() + .map(|h| h.build_html_attributes()) + .unwrap_or_default(), breakpoint: head .as_ref() .and_then(|h| h.breakpoint()) @@ -121,6 +126,10 @@ impl<'h> Header<'h> { .copied() } + pub fn html_attributes(&self) -> &Map<&str, Map<&str, &str>> { + &self.html_attributes + } + pub fn attribute_element(&self, name: &str, key: &str) -> Option<&str> { self.attributes_element .get(name) diff --git a/packages/mrml-core/src/prelude/render/tag.rs b/packages/mrml-core/src/prelude/render/tag.rs index 7f82407d..265bb4b5 100644 --- a/packages/mrml-core/src/prelude/render/tag.rs +++ b/packages/mrml-core/src/prelude/render/tag.rs @@ -33,6 +33,102 @@ impl std::fmt::Debug for Classes<'_> { } } +pub fn matches_selector(tag: &Tag, selector: &str) -> bool { + let mut tag_name: Option<&str> = None; + let mut class_selector: Option<&str> = None; + let mut id_selector: Option<&str> = None; + let mut attribute_selector: Option<&str> = None; + + let mut current = selector; + + if !current.starts_with('.') && !current.starts_with('#') && !current.starts_with('[') { + if let Some(first_special) = current.find(|c| c == '.' || c == '#' || c == '[') { + tag_name = Some(¤t[..first_special]); + current = ¤t[first_special..]; + } else { + tag_name = Some(current); + current = ""; + } + } + + while !current.is_empty() { + if current.starts_with('.') { + if class_selector.is_none() { + if let Some(next_special) = current[1..].find(|c| c == '.' || c == '#' || c == '[') + { + class_selector = Some(¤t[..next_special + 1]); + current = ¤t[next_special + 1..]; + } else { + class_selector = Some(current); + current = ""; + } + } else { + break; + } + } else if current.starts_with('#') { + if id_selector.is_none() { + if let Some(next_special) = current[1..].find(|c| c == '.' || c == '#' || c == '[') + { + id_selector = Some(¤t[..next_special + 1]); + current = ¤t[next_special + 1..]; + } else { + id_selector = Some(current); + current = ""; + } + } else { + break; + } + } else if current.starts_with('[') { + if attribute_selector.is_none() { + if let Some(end_bracket) = current.find(']') { + attribute_selector = Some(¤t[..end_bracket + 1]); + current = ¤t[end_bracket + 1..]; + } else { + break; + } + } else { + break; + } + } else { + break; + } + } + + let tag_name_matches = tag_name.map_or(true, |name| tag.name.as_ref() == name); + + let class_matches = class_selector.map_or(true, |class_sel| { + let class_name = &class_sel[1..]; + tag.classes.0.iter().any(|c| c.as_ref() == class_name) + }); + + let id_matches = id_selector.map_or(true, |id_sel| { + let id_name = &id_sel[1..]; + tag.attributes + .get("id") + .map(|id| id.as_ref() == id_name) + .unwrap_or(false) + }); + + let attribute_matches = attribute_selector.map_or(true, |attr_sel| { + if attr_sel.starts_with('[') && attr_sel.ends_with(']') { + let content = &attr_sel[1..attr_sel.len() - 1]; + if let Some((attr_name, attr_value)) = content.split_once('=') { + let value = attr_value.trim_matches('"').trim_matches('\''); + tag.attributes + .get(attr_name) + .map(|v| v.as_ref() == value) + .unwrap_or(false) + } else { + tag.attributes.contains_key(content) + } + } else { + true + } + }); + + tag_name_matches && class_matches && id_matches && attribute_matches +} + pub(crate) struct Tag<'a> { name: Cow<'a, str>, attributes: Map, Cow<'a, str>>, @@ -142,6 +238,23 @@ impl<'a> Tag<'a> { self } } + + pub fn set_html_attributes( + mut self, + html_attributes: &Map<&'a str, Map<&'a str, &'a str>>, + ) -> Self { + for (selector, attrs) in html_attributes.iter() { + if matches_selector(&self, selector) { + for (name, value) in attrs.iter() { + self.attributes.insert( + std::borrow::Cow::from(*name), + std::borrow::Cow::from(*value), + ); + } + } + } + self + } } impl Tag<'_> {