Repository: jdrouet/mrml (language: rust)

Issue:
Self-closing empty nodes in mj-raw results in invalid HTML
MRML rewrites empty HTML nodes `<node></node>` within an `<mj-raw>` section into a self closing format `<node />`. But not all HTML elements are allowed to be self closed, e.q. the `<div>` element, as mentioned in https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div

"Tag omission: None, both the starting and ending tag are mandatory."

This behavior leads to broken results, as shown in the following example:

```
<mj-raw>
  <div style="display: none"></div>
  <div>Foo</div>
<mj-raw>
```

parsed by mrml into

```
<div>
  <div style="display: none" />
  <div>Foo</div>
</div>
```

resulting in the following DOM rendered by chrome browser

```
<div>
  <div style="display: none">
    <div>Foo</div>
  </div>
</div>
```

The original MJML parser does not self close empty nodes and renders output in a proper way.


Discussion / hints:

Fix patch:
diff --git a/packages/mrml-core/lib/html-compare/src/lib.rs b/packages/mrml-core/lib/html-compare/src/lib.rs
index 9064c720..ee52f81b 100644
--- a/packages/mrml-core/lib/html-compare/src/lib.rs
+++ b/packages/mrml-core/lib/html-compare/src/lib.rs
@@ -248,7 +248,7 @@ fn compare_elements<'a>(
     let ending = compare_attributes(cursor, expected.clone(), generated.clone())?;
 
     if matches!(ending.end, HtmlElementEnd::Open)
-        && !["br", "meta"].contains(&expected.local.as_str())
+        && !matches!(expected.local.as_str(), "br" | "meta")
     {
         compare_all(cursor, &expected.local, expected.span, generated.span)?;
     }
diff --git a/packages/mrml-core/resources/compare/success/mj-raw.html b/packages/mrml-core/resources/compare/success/mj-raw.html
index dfcc6cae..ec6061e4 100644
--- a/packages/mrml-core/resources/compare/success/mj-raw.html
+++ b/packages/mrml-core/resources/compare/success/mj-raw.html
@@ -64,10 +64,10 @@
         <span>Hello</span>
       </div>
       Something
-      <canvas />
+      <canvas></canvas>
       <!-- some comment -->
       <img src="some_url" />
     </div>
   </body>
 
-</html>
\ No newline at end of file
+</html>
diff --git a/packages/mrml-core/src/mj_body/parse.rs b/packages/mrml-core/src/mj_body/parse.rs
index ee5cd843..53a32699 100644
--- a/packages/mrml-core/src/mj_body/parse.rs
+++ b/packages/mrml-core/src/mj_body/parse.rs
@@ -20,9 +20,9 @@ use crate::mj_table::NAME as MJ_TABLE;
 use crate::mj_text::NAME as MJ_TEXT;
 use crate::mj_wrapper::NAME as MJ_WRAPPER;
 use crate::node::Node;
+use crate::prelude::is_void_element;
 use crate::prelude::parser::{
-    parse_attributes_map, should_ignore_children, Error, MrmlCursor, MrmlParser, MrmlToken,
-    ParseChildren, ParseElement,
+    parse_attributes_map, Error, MrmlCursor, MrmlParser, MrmlToken, ParseChildren, ParseElement,
 };
 #[cfg(feature = "async")]
 use crate::prelude::parser::{AsyncMrmlParser, AsyncParseChildren, AsyncParseElement};
@@ -37,7 +37,7 @@ impl<'opts> ParseElement<Node<MjBodyChild>> for MrmlParser<'opts> {
         let tag = tag.to_string();
         let attributes = parse_attributes_map(cursor)?;
         let ending = cursor.assert_element_end()?;
-        if ending.empty || should_ignore_children(tag.as_str()) {
+        if ending.empty || is_void_element(tag.as_str()) {
             return Ok(Node {
                 tag,
                 attributes,
@@ -68,7 +68,7 @@ impl AsyncParseElement<Node<MjBodyChild>> for AsyncMrmlParser {
         let tag = tag.to_string();
         let attributes = parse_attributes_map(cursor)?;
         let ending = cursor.assert_element_end()?;
-        if ending.empty || should_ignore_children(tag.as_str()) {
+        if ending.empty || is_void_element(tag.as_str()) {
             return Ok(Node {
                 tag,
                 attributes,
diff --git a/packages/mrml-core/src/mj_raw/parse.rs b/packages/mrml-core/src/mj_raw/parse.rs
index 1a167296..bb2d4d47 100644
--- a/packages/mrml-core/src/mj_raw/parse.rs
+++ b/packages/mrml-core/src/mj_raw/parse.rs
@@ -3,12 +3,12 @@ use xmlparser::StrSpan;
 use super::{MjRaw, MjRawChild};
 use crate::comment::Comment;
 use crate::node::Node;
-use crate::prelude::parser::{
-    should_ignore_children, Error, MrmlCursor, MrmlParser, MrmlToken, ParseAttributes,
-    ParseChildren, ParseElement,
-};
+use crate::prelude::is_void_element;
 #[cfg(feature = "async")]
 use crate::prelude::parser::{AsyncMrmlParser, AsyncParseChildren, AsyncParseElement};
+use crate::prelude::parser::{
+    Error, MrmlCursor, MrmlParser, MrmlToken, ParseAttributes, ParseChildren, ParseElement,
+};
 use crate::text::Text;
 
 impl<'opts> ParseElement<Node<MjRawChild>> for MrmlParser<'opts> {
@@ -19,7 +19,7 @@ impl<'opts> ParseElement<Node<MjRawChild>> for MrmlParser<'opts> {
     ) -> Result<Node<MjRawChild>, Error> {
         let attributes = self.parse_attributes(cursor)?;
         let ending = cursor.assert_element_end()?;
-        if ending.empty || should_ignore_children(tag.as_str()) {
+        if ending.empty || is_void_element(tag.as_str()) {
             return Ok(Node {
                 tag: tag.to_string(),
                 attributes,
@@ -49,7 +49,7 @@ impl AsyncParseElement<Node<MjRawChild>> for AsyncMrmlParser {
     ) -> Result<Node<MjRawChild>, Error> {
         let attributes = self.parse_attributes(cursor)?;
         let ending = cursor.assert_element_end()?;
-        if ending.empty || should_ignore_children(tag.as_str()) {
+        if ending.empty || is_void_element(tag.as_str()) {
             return Ok(Node {
                 tag: tag.to_string(),
                 attributes,
diff --git a/packages/mrml-core/src/node/render.rs b/packages/mrml-core/src/node/render.rs
index 27c25a27..2f5fa399 100644
--- a/packages/mrml-core/src/node/render.rs
+++ b/packages/mrml-core/src/node/render.rs
@@ -2,10 +2,9 @@ use std::cell::{Ref, RefCell};
 use std::rc::Rc;
 
 use super::Node;
+use crate::prelude::is_void_element;
 use crate::prelude::render::{Error, Header, Render, RenderOptions, Renderable};
 
-const SHOULD_SELF_CLOSE: [&str; 1] = ["script"];
-
 struct NodeRender<'e, 'h, T> {
     header: Rc<RefCell<Header<'h>>>,
     element: &'e Node<T>,
@@ -33,10 +32,14 @@ where
             buf.push_str(value);
             buf.push('"');
         }
-        if self.element.children.is_empty()
-            && !SHOULD_SELF_CLOSE.contains(&self.element.tag.as_str())
-        {
-            buf.push_str(" />");
+        if self.element.children.is_empty() {
+            if is_void_element(self.element.tag.as_str()) {
+                buf.push_str(" />");
+            } else {
+                buf.push_str("></");
+                buf.push_str(&self.element.tag);
+                buf.push('>');
+            }
         } else {
             buf.push('>');
             for (index, child) in self.element.children.iter().enumerate() {
diff --git a/packages/mrml-core/src/prelude/mod.rs b/packages/mrml-core/src/prelude/mod.rs
index e1611aa6..63945a5f 100644
--- a/packages/mrml-core/src/prelude/mod.rs
+++ b/packages/mrml-core/src/prelude/mod.rs
@@ -6,3 +6,25 @@ pub mod print;
 pub mod render;
 
 pub mod hash;
+
+// see https://developer.mozilla.org/en-US/docs/Glossary/Void_element
+#[cfg(any(feature = "parser", feature = "render"))]
+pub(crate) fn is_void_element(tag: &str) -> bool {
+    matches!(
+        tag,
+        "area"
+            | "base"
+            | "br"
+            | "col"
+            | "embed"
+            | "hr"
+            | "img"
+            | "input"
+            | "link"
+            | "meta"
+            | "param"
+            | "source"
+            | "track"
+            | "wbr"
+    )
+}
diff --git a/packages/mrml-core/src/prelude/parser/mod.rs b/packages/mrml-core/src/prelude/parser/mod.rs
index 29c8c292..00f55008 100644
--- a/packages/mrml-core/src/prelude/parser/mod.rs
+++ b/packages/mrml-core/src/prelude/parser/mod.rs
@@ -15,26 +15,6 @@ pub mod memory_loader;
 pub mod multi_loader;
 pub mod noop_loader;
 
-pub(crate) fn should_ignore_children(tag: &str) -> bool {
-    matches!(
-        tag,
-        "area"
-            | "base"
-            | "br"
-            | "col"
-            | "embed"
-            | "hr"
-            | "img"
-            | "input"
-            | "link"
-            | "meta"
-            | "param"
-            | "source"
-            | "track"
-            | "wbr"
-    )
-}
-
 #[derive(Clone, Debug, Default)]
 pub struct Span {
     pub start: usize,


Test patch: