Repository: jdrouet/mrml (language: python)

Issue:
mj-wrapper rendering issues on Window Desktop Outlook Client
Hello! It's me again. We're noticing a layout issues specific only to Window Desktop Outlook client platform (macOs works fine). We're using `<mj-wrapper>` to wrap multiple sections, but on these platform, the sections appears to be stacking in the single row. Example:

![image](https://user-images.githubusercontent.com/31352203/156802763-a2600975-5d85-4e4d-9fc8-b6a677b2d954.png)

After diving down deeper, I realized the diff between mrml generated html vs mjml generated html are the following (for each sections within a wrapper):

MJML 

```
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><![endif]-->
```

MRML

```
<!--[if mso | IE]></td></tr></table><![endif]-->
<!--[if mso | IE]></td><td width="600px"><![endif]-->
```

Notice that there's a missing `</tr><tr>` in between `</td><td width="600px">` in the MRML version. Once I replaced all of these differences, the layout works correctly for all platforms.

Let me know what you think, if it's a simple fix I can help put a PR, but your guidance might be required cause I'm still new to Rust :D 



Discussion / hints:
I'll take a look as soon as possible 😉 
@nguyenquannnn could you give me a MRML template that's causing this issue?
Yes!, I removed the irrelevant stuff but this should reproduce the issue.

```
<mjml lang="en">
  <mj-head>
    <mj-title>Air Astana</mj-title>
    <mj-attributes>
      <mj-class name="nav-link" line-height="24px" font-size="11px" />
      <mj-class name="color-primary" color="#A5965D" />
      <mj-class name="bold" font-weight="700" color="black" text-align="left" />
      <mj-text color="#626262" font-weight="400" font-size="16px" />
      <mj-all font-family="'Proxima Nova', Helvetica, Arial, sans-serif" />
    </mj-attributes>
    <mj-style>
      @media {
        @font-face {
          font-family: SourceSans-Bold;
          src: url('https://upgrade-stage-cdn.plusgrade.com/offer/fonts/source-sans/SourceSansPro-Bold.otf');
          font-weight: bold;
          font-style: normal;
        }

        @font-face {
          font-family: SourceSans;
          src: url('https://upgrade-stage-cdn.plusgrade.com/offer/fonts/source-sans/SourceSansPro-Regular.otf'),
            url('https://upgrade-stage-cdn.plusgrade.com/offer/fonts/source-sans/SourceSansPro-Regular.woff') format('woff'),
            url('https://upgrade-stage-cdn.plusgrade.com/offer/fonts/source-sans/SourceSansPro-Regular.ttf') format('truetype');
          font-weight: normal;
          font-style: normal;
        }

        @font-face {
          font-family: Varela Round;
          src: url('https://upgrade-stage-cdn.plusgrade.com/offer/fonts/varela/Varela-Regular.otf');
          font-weight: normal;
          font-style: normal;
        }

        @font-face {
          font-family: 'Proxima Nova';
          src: url('https://upgrade-cdn.plusgrade.com/offer/fonts/ProximaNova/ProximaNova.otf') format('opentype');
          font-weight: normal;
          font-style: normal;
        }
      }

      #outlook a {
        padding: 0;
      }

      body {
        margin: 0;
        padding: 0;
        -webkit-text-size-adjust: 100%;
        -ms-text-size-adjust: 100%;
      }

      table,
      td {
        border-collapse: collapse;
        mso-table-lspace: 0pt;
        mso-table-rspace: 0pt;
      }

      img {
        border: 0;
        height: auto;
        line-height: 100%;
        outline: none;
        text-decoration: none;
        -ms-interpolation-mode: bicubic;
      }

      p {
        display: block;
        margin: 13px 0;
      }

      a {
        color: #A5965D;
      }
    </mj-style>
  </mj-head>

  <mj-body background-color="#f0f0f0">
    <mj-section padding="0px 20px" padding-top="5px">
     
    </mj-section>
    <mj-wrapper background-color="white" padding="20px 25px">
      <mj-section border="2px solid #A5965D">
        <mj-column>
          <mj-text padding="15px" line-height="1.5">Пожалуйста, выберите желаемый язык вверху этой страницы.<br />Бас парақшасының жоғарғы жағында өзіңізге ыңғайлы тілді таңдауға болады.<br />Please select your preferred language at the top of this page. </mj-text>
        </mj-column>
      </mj-section>

      <mj-section padding="0">
        <mj-column>
          <mj-text line-height="1" padding="0" padding-top="24px">Dear Guests</mj-text>
          <mj-text line-height="1" padding="0" padding-top="24px">Booking reference: ABC123</mj-text>
        </mj-column>
      </mj-section>
    </mj-wrapper>
  </mj-body>
</mjml>
```

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 93ad1d8c..9548c764 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -340,15 +340,6 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "ansi_term"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
-dependencies = [
- "winapi 0.3.9",
-]
-
 [[package]]
 name = "async-trait"
 version = "0.1.50"
@@ -427,27 +418,6 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
-[[package]]
-name = "block-buffer"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
-dependencies = [
- "block-padding",
- "byte-tools",
- "byteorder",
- "generic-array",
-]
-
-[[package]]
-name = "block-padding"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
-dependencies = [
- "byte-tools",
-]
-
 [[package]]
 name = "brotli-sys"
 version = "0.3.2"
@@ -486,12 +456,6 @@ version = "3.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
 
-[[package]]
-name = "byte-tools"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
-
 [[package]]
 name = "byteorder"
 version = "1.4.3"
@@ -565,13 +529,9 @@ version = "2.33.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
 dependencies = [
- "ansi_term",
- "atty",
  "bitflags",
- "strsim 0.8.0",
  "textwrap 0.11.0",
  "unicode-width",
- "vec_map",
 ]
 
 [[package]]
@@ -586,7 +546,7 @@ dependencies = [
  "indexmap",
  "lazy_static",
  "os_str_bytes",
- "strsim 0.10.0",
+ "strsim",
  "termcolor",
  "textwrap 0.12.1",
  "unicode-width",
@@ -774,15 +734,6 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "digest"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
-dependencies = [
- "generic-array",
-]
-
 [[package]]
 name = "dtoa"
 version = "0.4.8"
@@ -864,12 +815,6 @@ dependencies = [
  "synstructure",
 ]
 
-[[package]]
-name = "fake-simd"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
-
 [[package]]
 name = "flate2"
 version = "1.0.20"
@@ -1021,15 +966,6 @@ dependencies = [
  "byteorder",
 ]
 
-[[package]]
-name = "generic-array"
-version = "0.12.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
-dependencies = [
- "typenum",
-]
-
 [[package]]
 name = "getrandom"
 version = "0.1.16"
@@ -1121,21 +1057,6 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
-[[package]]
-name = "html_parser"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58112d7b68ff61447bd22a489d7877a79ee688f6f34d57d3857842776d42dd0c"
-dependencies = [
- "pest",
- "pest_derive",
- "serde",
- "serde_derive",
- "serde_json",
- "structopt",
- "thiserror",
-]
-
 [[package]]
 name = "http"
 version = "0.2.4"
@@ -1314,12 +1235,6 @@ dependencies = [
  "linked-hash-map",
 ]
 
-[[package]]
-name = "maplit"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
-
 [[package]]
 name = "match_cfg"
 version = "0.1.0"
@@ -1410,7 +1325,6 @@ name = "mrml"
 version = "1.2.7"
 dependencies = [
  "criterion",
- "html_parser",
  "indexmap",
  "rand 0.8.3",
  "rustc-hash",
@@ -1512,12 +1426,6 @@ version = "11.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
 
-[[package]]
-name = "opaque-debug"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
-
 [[package]]
 name = "os_str_bytes"
 version = "2.4.0"
@@ -1555,49 +1463,6 @@ version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
 
-[[package]]
-name = "pest"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
-dependencies = [
- "ucd-trie",
-]
-
-[[package]]
-name = "pest_derive"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
-dependencies = [
- "pest",
- "pest_generator",
-]
-
-[[package]]
-name = "pest_generator"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
-dependencies = [
- "pest",
- "pest_meta",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "pest_meta"
-version = "2.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
-dependencies = [
- "maplit",
- "pest",
- "sha-1",
-]
-
 [[package]]
 name = "pin-project"
 version = "0.4.28"
@@ -2045,18 +1910,6 @@ dependencies = [
  "url",
 ]
 
-[[package]]
-name = "sha-1"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
-dependencies = [
- "block-buffer",
- "digest",
- "fake-simd",
- "opaque-debug",
-]
-
 [[package]]
 name = "sha1"
 version = "0.6.0"
@@ -2095,42 +1948,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
-[[package]]
-name = "strsim"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-
 [[package]]
 name = "strsim"
 version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
-[[package]]
-name = "structopt"
-version = "0.3.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c"
-dependencies = [
- "clap 2.33.3",
- "lazy_static",
- "structopt-derive",
-]
-
-[[package]]
-name = "structopt-derive"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
-dependencies = [
- "heck",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "syn"
 version = "1.0.72"
@@ -2181,26 +2004,6 @@ dependencies = [
  "unicode-width",
 ]
 
-[[package]]
-name = "thiserror"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "threadpool"
 version = "1.8.1"
@@ -2374,18 +2177,6 @@ dependencies = [
  "unchecked-index",
 ]
 
-[[package]]
-name = "typenum"
-version = "1.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
-
-[[package]]
-name = "ucd-trie"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
-
 [[package]]
 name = "unchecked-index"
 version = "0.2.2"
diff --git a/packages/mrml-core/Cargo.toml b/packages/mrml-core/Cargo.toml
index ba83b9e6..7b62a8f7 100644
--- a/packages/mrml-core/Cargo.toml
+++ b/packages/mrml-core/Cargo.toml
@@ -26,7 +26,7 @@ orderedmap = ["indexmap", "rustc-hash"]
 
 [dependencies]
 rand = { default-features = false, features = ["std", "std_rng"], version = "0.8", optional = true }
-serde = { version = "1.0", optional = true }
+serde = { version = "1.0", optional = true, features = ["derive"] }
 serde_json = { version = "1.0", optional = true }
 xmlparser = { version = "0.13", optional = true }
 indexmap = { version = "1.7", features = ["serde-1"], optional = true }
@@ -34,7 +34,6 @@ rustc-hash = { version = "1.1", optional = true }
 
 [dev-dependencies]
 criterion = "0.3"
-html_parser = "0.6.2"
 
 [[bench]]
 name = "basic"
diff --git a/packages/mrml-core/src/helper/condition.rs b/packages/mrml-core/src/helper/condition.rs
index bb8952b3..d9b9dcf2 100644
--- a/packages/mrml-core/src/helper/condition.rs
+++ b/packages/mrml-core/src/helper/condition.rs
@@ -5,6 +5,9 @@ pub const START_NEGATION_CONDITIONAL_TAG: &str = "<!--[if !mso | IE]><!-->";
 pub const START_MSO_NEGATION_CONDITIONAL_TAG: &str = "<!--[if !mso]><!-->";
 pub const END_NEGATION_CONDITIONAL_TAG: &str = "<!--<![endif]-->";
 
+#[cfg(test)]
+pub const START_IE11_CONDITIONAL_TAG: &str = "<!--[if lte mso 11]>";
+
 pub fn conditional_tag<T: AsRef<str>>(input: T) -> String {
     START_CONDITIONAL_TAG.to_string() + input.as_ref() + END_CONDITIONAL_TAG
 }
diff --git a/packages/mrml-core/src/mj_body/render.rs b/packages/mrml-core/src/mj_body/render.rs
index f3494cb3..5bd53f9c 100644
--- a/packages/mrml-core/src/mj_body/render.rs
+++ b/packages/mrml-core/src/mj_body/render.rs
@@ -89,7 +89,8 @@ impl<'e, 'h> Render<'h> for MJBodyRender<'e, 'h> {
 
     fn render(&self, opts: &Options) -> Result<String, Error> {
         let body = self.get_body_tag();
-        Ok(body.render(self.render_preview() + &self.render_content(opts)?))
+        let result = body.render(self.render_preview() + &self.render_content(opts)?);
+        Ok(result)
     }
 }
 
diff --git a/packages/mrml-core/src/mj_head/render.rs b/packages/mrml-core/src/mj_head/render.rs
index 9df74d9d..5976eda5 100644
--- a/packages/mrml-core/src/mj_head/render.rs
+++ b/packages/mrml-core/src/mj_head/render.rs
@@ -1,4 +1,5 @@
 use super::MJHead;
+use crate::helper::condition::{END_NEGATION_CONDITIONAL_TAG, START_MSO_NEGATION_CONDITIONAL_TAG};
 use crate::helper::sort::sort_by_key;
 use crate::prelude::hash::Map;
 use crate::prelude::render::{Error, Header, Options, Render, Renderable};
@@ -164,14 +165,14 @@ impl<'e, 'h> MJHeadRender<'e, 'h> {
         if links.is_empty() && imports.is_empty() {
             String::default()
         } else {
-            let mut buf = String::from("<!--[if !mso]><!-->");
+            let mut buf = String::from(START_MSO_NEGATION_CONDITIONAL_TAG);
             buf.push_str(&links);
             if !imports.is_empty() {
                 buf.push_str("<style type=\"text/css\">");
                 buf.push_str(&imports);
                 buf.push_str("</style>");
             }
-            buf.push_str("<!--<![endif]-->");
+            buf.push_str(END_NEGATION_CONDITIONAL_TAG);
             buf
         }
     }
@@ -287,9 +288,9 @@ impl<'e, 'h> Render<'h> for MJHeadRender<'e, 'h> {
                 .unwrap_or_default(),
         );
         buf.push_str("</title>");
-        buf.push_str("<!--[if !mso]><!-- -->");
+        buf.push_str(START_MSO_NEGATION_CONDITIONAL_TAG);
         buf.push_str("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">");
-        buf.push_str("<!--<![endif]-->");
+        buf.push_str(END_NEGATION_CONDITIONAL_TAG);
         buf.push_str("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">");
         buf.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
         buf.push_str(STYLE_BASE);
diff --git a/packages/mrml-core/src/mj_section/mod.rs b/packages/mrml-core/src/mj_section/mod.rs
index 942ae6a4..7ab3f94e 100644
--- a/packages/mrml-core/src/mj_section/mod.rs
+++ b/packages/mrml-core/src/mj_section/mod.rs
@@ -11,7 +11,7 @@ mod print;
 mod render;
 
 #[cfg(feature = "render")]
-pub use render::WithMJSectionBackground;
+pub use render::{SectionLikeRender, WithMJSectionBackground};
 
 pub const NAME: &str = "mj-section";
 
diff --git a/packages/mrml-core/src/mj_section/render.rs b/packages/mrml-core/src/mj_section/render.rs
index 6b1d4998..b0250f02 100644
--- a/packages/mrml-core/src/mj_section/render.rs
+++ b/packages/mrml-core/src/mj_section/render.rs
@@ -1,5 +1,5 @@
 use super::{MJSection, NAME};
-use crate::helper::condition::{conditional_tag, END_CONDITIONAL_TAG, START_CONDITIONAL_TAG};
+use crate::helper::condition::conditional_tag;
 use crate::helper::size::{Percent, Pixel};
 use crate::helper::tag::Tag;
 use crate::prelude::hash::Map;
@@ -195,15 +195,11 @@ pub trait WithMJSectionBackground<'h>: Render<'h> {
     }
 }
 
-struct MJSectionRender<'e, 'h> {
-    header: Rc<RefCell<Header<'h>>>,
-    element: &'e MJSection,
-    container_width: Option<Pixel>,
-}
-
-impl<'e, 'h> WithMJSectionBackground<'h> for MJSectionRender<'e, 'h> {}
+pub trait SectionLikeRender<'h>: WithMJSectionBackground<'h> {
+    fn clone_header(&self) -> Rc<RefCell<Header<'h>>>;
+    fn container_width(&self) -> &Option<Pixel>;
+    fn children(&self) -> &Vec<crate::mj_body::MJBodyChild>;
 
-impl<'e, 'h> MJSectionRender<'e, 'h> {
     fn is_full_width(&self) -> bool {
         self.attribute_exists("full-width")
     }
@@ -220,7 +216,7 @@ impl<'e, 'h> MJSectionRender<'e, 'h> {
                 if full_width {
                     None
                 } else {
-                    self.container_width.as_ref().map(|v| v.to_string())
+                    self.container_width().as_ref().map(|v| v.to_string())
                 },
             )
             .add_attribute("xmlns:v", "urn:schemas-microsoft-com:vml")
@@ -245,7 +241,7 @@ impl<'e, 'h> MJSectionRender<'e, 'h> {
             .maybe_add_style("border-radius", self.attribute("border-radius"))
             .maybe_add_style(
                 "max-width",
-                self.container_width.as_ref().map(|item| item.to_string()),
+                self.container_width().as_ref().map(|item| item.to_string()),
             )
     }
 
@@ -255,11 +251,13 @@ impl<'e, 'h> MJSectionRender<'e, 'h> {
             .add_attribute("align", "center")
             .maybe_add_attribute(
                 "width",
-                self.container_width.as_ref().map(|p| p.value().to_string()),
+                self.container_width()
+                    .as_ref()
+                    .map(|p| p.value().to_string()),
             )
             .maybe_add_style(
                 "width",
-                self.container_width.as_ref().map(|v| v.to_string()),
+                self.container_width().as_ref().map(|v| v.to_string()),
             )
             .maybe_add_suffixed_class(self.attribute("css-class"), "outlook");
         let tr = Tag::tr();
@@ -273,46 +271,37 @@ impl<'e, 'h> MJSectionRender<'e, 'h> {
     }
 
     fn get_siblings(&self) -> usize {
-        self.element.children.len()
+        self.children().len()
     }
 
     fn get_raw_siblings(&self) -> usize {
-        self.element
-            .children
-            .iter()
-            .filter(|elt| elt.is_raw())
-            .count()
+        self.children().iter().filter(|elt| elt.is_raw()).count()
     }
 
     fn render_wrapped_children(&self, opts: &Options) -> Result<String, Error> {
-        let tr = Tag::tr();
         let siblings = self.get_siblings();
         let raw_siblings = self.get_raw_siblings();
-        let content = self
-            .element
-            .children
-            .iter()
-            .try_fold(String::default(), |res, child| {
-                let mut renderer = child.renderer(Rc::clone(&self.header));
-                renderer.set_siblings(siblings);
-                renderer.set_raw_siblings(raw_siblings);
-                renderer.set_container_width(self.container_width.clone());
-                if child.is_raw() {
-                    Ok(res + END_CONDITIONAL_TAG + &renderer.render(opts)? + START_CONDITIONAL_TAG)
-                } else {
-                    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");
-                    Ok(res
-                        + &td.open()
-                        + END_CONDITIONAL_TAG
-                        + &renderer.render(opts)?
-                        + START_CONDITIONAL_TAG
-                        + &td.close())
-                }
-            })?;
-        Ok(tr.render(content))
+        let tr = Tag::tr();
+        let mut result = conditional_tag(tr.open());
+        for child in self.children().iter() {
+            let mut renderer = child.renderer(self.clone_header());
+            renderer.set_siblings(siblings);
+            renderer.set_raw_siblings(raw_siblings);
+            renderer.set_container_width(self.container_width().clone());
+            if child.is_raw() {
+                result.push_str(&renderer.render(opts)?);
+            } else {
+                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");
+                result.push_str(&conditional_tag(td.open()));
+                result.push_str(&renderer.render(opts)?);
+                result.push_str(&conditional_tag(td.close()));
+            }
+        }
+        result.push_str(&conditional_tag(tr.close()));
+        Ok(result)
     }
 
     fn set_style_section_inner_div(&self, tag: Tag) -> Tag {
@@ -372,8 +361,14 @@ impl<'e, 'h> MJSectionRender<'e, 'h> {
         let tr = Tag::tr();
         let td = self.set_style_section_td(Tag::td());
         let inner_table = Tag::table_presentation();
-        let content = conditional_tag(inner_table.render(self.render_wrapped_children(opts)?));
-        let content = table.render(tbody.render(tr.render(td.render(content))));
+
+        let content = self.render_wrapped_children(opts)?;
+        let content =
+            conditional_tag(inner_table.open()) + &content + &conditional_tag(inner_table.close());
+        let content = td.render(content);
+        let content = tr.render(content);
+        let content = tbody.render(content);
+        let content = table.render(content);
         Ok(div.render(if self.has_background() {
             inner_div.render(content)
         } else {
@@ -424,6 +419,27 @@ impl<'e, 'h> MJSectionRender<'e, 'h> {
     }
 }
 
+struct MJSectionRender<'e, 'h> {
+    header: Rc<RefCell<Header<'h>>>,
+    element: &'e MJSection,
+    container_width: Option<Pixel>,
+}
+
+impl<'e, 'h> WithMJSectionBackground<'h> for MJSectionRender<'e, 'h> {}
+impl<'e, 'h> SectionLikeRender<'h> for MJSectionRender<'e, 'h> {
+    fn clone_header(&self) -> Rc<RefCell<Header<'h>>> {
+        Rc::clone(&self.header)
+    }
+
+    fn children(&self) -> &Vec<crate::mj_body::MJBodyChild> {
+        &self.element.children
+    }
+
+    fn container_width(&self) -> &Option<Pixel> {
+        &self.container_width
+    }
+}
+
 impl<'e, 'h> Render<'h> for MJSectionRender<'e, 'h> {
     fn default_attribute(&self, name: &str) -> Option<&str> {
         match name {
diff --git a/packages/mrml-core/src/mj_text/render.rs b/packages/mrml-core/src/mj_text/render.rs
index 62048391..05878769 100644
--- a/packages/mrml-core/src/mj_text/render.rs
+++ b/packages/mrml-core/src/mj_text/render.rs
@@ -45,9 +45,9 @@ impl<'e, 'h> MJTextRender<'e, 'h> {
             .add_attribute("height", height)
             .add_style("vertical-align", "top")
             .add_style("height", height);
-        Ok(conditional_tag(
-            table.render(tr.render(td.render(self.render_content(opts)?))),
-        ))
+        Ok(conditional_tag(table.open() + &tr.open() + &td.open())
+            + &self.render_content(opts)?
+            + &conditional_tag(td.close() + &tr.close() + &table.close()))
     }
 }
 
diff --git a/packages/mrml-core/src/mj_wrapper/render.rs b/packages/mrml-core/src/mj_wrapper/render.rs
index f17fac17..1cc4a13e 100644
--- a/packages/mrml-core/src/mj_wrapper/render.rs
+++ b/packages/mrml-core/src/mj_wrapper/render.rs
@@ -1,8 +1,8 @@
 use super::{MJWrapper, NAME};
-use crate::helper::condition::{conditional_tag, END_CONDITIONAL_TAG, START_CONDITIONAL_TAG};
+use crate::helper::condition::{END_CONDITIONAL_TAG, START_CONDITIONAL_TAG};
 use crate::helper::size::Pixel;
 use crate::helper::tag::Tag;
-use crate::mj_section::WithMJSectionBackground;
+use crate::mj_section::{SectionLikeRender, WithMJSectionBackground};
 use crate::prelude::hash::Map;
 use crate::prelude::render::{Error, Header, Options, Render, Renderable};
 use std::cell::{Ref, RefCell};
@@ -14,8 +14,6 @@ struct MJWrapperRender<'e, 'h> {
     container_width: Option<Pixel>,
 }
 
-impl<'e, 'h> WithMJSectionBackground<'h> for MJWrapperRender<'e, 'h> {}
-
 impl<'e, 'h> MJWrapperRender<'e, 'h> {
     fn current_width(&self) -> Option<Pixel> {
         self.container_width.as_ref().map(|width| {
@@ -24,85 +22,21 @@ impl<'e, 'h> MJWrapperRender<'e, 'h> {
             Pixel::new(width.value() - hborder.value() - hpadding.value())
         })
     }
+}
 
-    fn is_full_width(&self) -> bool {
-        self.attribute_exists("full-width")
-    }
-
-    fn render_with_background<T: AsRef<str>>(&self, content: T) -> String {
-        let full_width = self.is_full_width();
-        let vrect = Tag::new("v:rect")
-            .maybe_add_attribute(
-                "mso-width-percent",
-                if full_width { Some("1000") } else { None },
-            )
-            .maybe_add_style(
-                "width",
-                if full_width {
-                    None
-                } else {
-                    self.container_width.as_ref().map(|v| v.to_string())
-                },
-            )
-            .add_attribute("xmlns:v", "urn:schemas-microsoft-com:vml")
-            .add_attribute("fill", "true")
-            .add_attribute("stroke", "false");
-        let vfill = self.get_vfill_tag();
-        let vtextbox = Tag::new("v:textbox")
-            .add_attribute("inset", "0,0,0,0")
-            .add_style("mso-fit-shape-to-text", "true");
-        let before = conditional_tag(vrect.open() + &vfill.closed() + &vtextbox.open());
-        let after = conditional_tag(vtextbox.close() + &vrect.close());
-        before + content.as_ref() + &after
-    }
-
-    fn set_style_section_div(&self, tag: Tag) -> Tag {
-        let base = if self.is_full_width() {
-            tag
-        } else {
-            self.set_background_style(tag)
-        };
-        base.add_style("margin", "0px auto")
-            .maybe_add_style("border-radius", self.attribute("border-radius"))
-            .maybe_add_style(
-                "max-width",
-                self.container_width.as_ref().map(|item| item.to_string()),
-            )
-    }
+impl<'e, 'h> WithMJSectionBackground<'h> for MJWrapperRender<'e, 'h> {}
 
-    fn render_wrap<T: AsRef<str>>(&self, content: T) -> String {
-        let table = Tag::table_presentation()
-            .add_attribute("align", "center")
-            .maybe_add_attribute("bgcolor", self.attribute("background-color"))
-            .maybe_add_attribute(
-                "width",
-                self.container_width.as_ref().map(|p| p.value().to_string()),
-            )
-            .maybe_add_style(
-                "width",
-                self.container_width.as_ref().map(|v| v.to_string()),
-            )
-            .maybe_add_suffixed_class(self.attribute("css-class"), "outlook");
-        let tr = Tag::tr();
-        let td = Tag::td()
-            .add_style("line-height", "0px")
-            .add_style("font-size", "0px")
-            .add_style("mso-line-height-rule", "exactly");
-        let before = conditional_tag(table.open() + &tr.open() + &td.open());
-        let after = conditional_tag(td.close() + &tr.close() + &table.close());
-        before + content.as_ref() + &after
+impl<'e, 'h> SectionLikeRender<'h> for MJWrapperRender<'e, 'h> {
+    fn clone_header(&self) -> Rc<RefCell<Header<'h>>> {
+        Rc::clone(&self.header)
     }
 
-    fn get_siblings(&self) -> usize {
-        self.element.children.len()
+    fn children(&self) -> &Vec<crate::mj_body::MJBodyChild> {
+        &self.element.children
     }
 
-    fn get_raw_siblings(&self) -> usize {
-        self.element
-            .children
-            .iter()
-            .filter(|elt| elt.is_raw())
-            .count()
+    fn container_width(&self) -> &Option<Pixel> {
+        &self.container_width
     }
 
     fn render_wrapped_children(&self, opts: &Options) -> Result<String, Error> {
@@ -111,140 +45,32 @@ impl<'e, 'h> MJWrapperRender<'e, 'h> {
         let raw_siblings = self.get_raw_siblings();
         let current_width = self.current_width();
         let container_width = self.container_width.as_ref().map(|v| v.to_string());
-        let content = self
-            .element
-            .children
-            .iter()
-            .try_fold(String::default(), |res, child| {
-                let mut renderer = child.renderer(Rc::clone(&self.header));
-                renderer.set_siblings(siblings);
-                renderer.set_raw_siblings(raw_siblings);
-                renderer.set_container_width(current_width.clone());
-                if child.is_raw() {
-                    Ok(res + END_CONDITIONAL_TAG + &renderer.render(opts)? + START_CONDITIONAL_TAG)
-                } else {
-                    let td = renderer
-                        .set_style("td-outlook", Tag::td())
-                        .maybe_add_attribute("align", renderer.attribute("align"))
-                        .maybe_add_attribute("width", container_width.as_ref())
-                        .maybe_add_suffixed_class(renderer.attribute("css-class"), "outlook");
-                    Ok(res
-                        + &td.open()
-                        + END_CONDITIONAL_TAG
-                        + &renderer.render(opts)?
-                        + START_CONDITIONAL_TAG
-                        + &td.close())
-                }
-            })?;
-        Ok(tr.render(content))
-    }
-
-    fn set_style_section_inner_div(&self, tag: Tag) -> Tag {
-        tag.add_style("line-height", "0")
-            .add_style("font-size", "0")
-    }
-
-    fn set_style_section_table(&self, tag: Tag) -> Tag {
-        let base = if self.is_full_width() {
-            tag
-        } else {
-            self.set_background_style(tag)
-        };
-        base.add_style("width", "100%")
-            .maybe_add_style("border-radius", self.attribute("border-radius"))
-    }
-
-    fn set_style_section_td(&self, tag: Tag) -> Tag {
-        tag.maybe_add_style("border", self.attribute("border"))
-            .maybe_add_style("border-bottom", self.attribute("border-bottom"))
-            .maybe_add_style("border-left", self.attribute("border-left"))
-            .maybe_add_style("border-right", self.attribute("border-right"))
-            .maybe_add_style("border-top", self.attribute("border-top"))
-            .maybe_add_style("direction", self.attribute("direction"))
-            .add_style("font-size", "0px")
-            .maybe_add_style("padding", self.attribute("padding"))
-            .maybe_add_style("padding-bottom", self.attribute("padding-bottom"))
-            .maybe_add_style("padding-left", self.attribute("padding-left"))
-            .maybe_add_style("padding-right", self.attribute("padding-right"))
-            .maybe_add_style("padding-top", self.attribute("padding-top"))
-            .maybe_add_style("text-align", self.attribute("text-align"))
-    }
-
-    fn render_section(&self, opts: &Options) -> Result<String, Error> {
-        let is_full_width = self.is_full_width();
-        let div = self
-            .set_style_section_div(Tag::div())
-            .maybe_add_class(if is_full_width {
-                None
+        let mut result = String::default();
+        for child in self.children().iter() {
+            let mut renderer = child.renderer(Rc::clone(&self.header));
+            renderer.set_siblings(siblings);
+            renderer.set_raw_siblings(raw_siblings);
+            renderer.set_container_width(current_width.clone());
+            if child.is_raw() {
+                result.push_str(&renderer.render(opts)?);
             } else {
-                self.attribute("css-class")
-            });
-        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 content = conditional_tag(inner_table.render(self.render_wrapped_children(opts)?));
-        let content = table.render(tbody.render(tr.render(td.render(content))));
-        Ok(div.render(if self.has_background() {
-            inner_div.render(content)
-        } else {
-            content
-        }))
-    }
-
-    fn set_style_table_full_width(&self, tag: Tag) -> Tag {
-        let base = if self.is_full_width() {
-            self.set_background_style(tag)
-        } else {
-            tag
-        };
-        base.maybe_add_style("border-radius", self.attribute("border-radius"))
-            .add_style("width", "100%")
-    }
-
-    fn get_full_width_table(&self) -> Tag {
-        self.set_style_table_full_width(Tag::table_presentation())
-            .add_attribute("align", "center")
-            .maybe_add_class(self.attribute("css-class"))
-            .maybe_add_attribute("background", self.attribute("background-url"))
-    }
-
-    fn render_full_width(&self, opts: &Options) -> Result<String, Error> {
-        let table = self.get_full_width_table();
-        let tbody = Tag::tbody();
-        let tr = Tag::tr();
-        let td = Tag::td();
-        let content = self.render_wrap(self.render_section(opts)?);
-        let content = if self.has_background() {
-            self.render_with_background(content)
-        } else {
-            content
-        };
-        Ok(table.render(tbody.render(tr.render(td.render(content)))))
-    }
-
-    fn render_simple(&self, opts: &Options) -> Result<String, Error> {
-        let section = self.render_section(opts)?;
-
-        let section = if self.has_background() {
-            self.render_with_background(section)
-        } else {
-            section
-        };
-        Ok(self.render_wrap(section))
+                let td = renderer
+                    .set_style("td-outlook", Tag::td())
+                    .maybe_add_attribute("align", renderer.attribute("align"))
+                    .maybe_add_attribute("width", container_width.as_ref())
+                    .maybe_add_suffixed_class(renderer.attribute("css-class"), "outlook");
+                result.push_str(START_CONDITIONAL_TAG);
+                result.push_str(&tr.open());
+                result.push_str(&td.open());
+                result.push_str(END_CONDITIONAL_TAG);
+                result.push_str(&renderer.render(opts)?);
+                result.push_str(START_CONDITIONAL_TAG);
+                result.push_str(&td.close());
+                result.push_str(&tr.close());
+                result.push_str(END_CONDITIONAL_TAG);
+            }
+        }
+        Ok(result)
     }
 }
 
diff --git a/packages/mrml-core/src/mjml/render.rs b/packages/mrml-core/src/mjml/render.rs
index 483e622d..ae09d356 100644
--- a/packages/mrml-core/src/mjml/render.rs
+++ b/packages/mrml-core/src/mjml/render.rs
@@ -99,6 +99,15 @@ mod tests {
         assert!(root.render(&opts).is_ok());
     }
 
+    #[test]
+    fn template_air_astana() {
+        let opts = Options::default();
+        let template = include_str!("../../resources/template/air-astana.mjml");
+        let expected = include_str!("../../resources/template/air-astana.html");
+        let root = MJML::parse(template.to_string()).unwrap();
+        compare(expected, root.render(&opts).unwrap().as_str());
+    }
+
     #[test]
     #[cfg(feature = "orderedmap")]
     fn stable_output() {
diff --git a/resources/compare/success/mj-raw-head.html b/resources/compare/success/mj-raw-head.html
index df81cf9c..6a14d11b 100644
--- a/resources/compare/success/mj-raw-head.html
+++ b/resources/compare/success/mj-raw-head.html
@@ -15,4 +15,4 @@
         <style type="text/css">
           .mj-outlook-group-fix { width:100% !important; }
         </style>
-        <![endif]--><meta name="description" content="This should stay."></head><body><div></div></body></html>
+        <![endif]--><meta name="description" content="This should stay."/></head><body><div></div></body></html>
diff --git a/resources/compare/success/mj-raw.html b/resources/compare/success/mj-raw.html
index 49c75065..bf755cfc 100644
--- a/resources/compare/success/mj-raw.html
+++ b/resources/compare/success/mj-raw.html
@@ -68,7 +68,7 @@
   <div style="">
     <div id="whatever">
       <span>Hello</span>
-    </div> Something <canvas></canvas>
+    </div> Something <canvas />
     <!-- some comment -->
     <img src="some_url" />
   </div>
diff --git a/resources/template/air-astana.html b/resources/template/air-astana.html
new file mode 100644
index 00000000..ec0242e2
--- /dev/null
+++ b/resources/template/air-astana.html
@@ -0,0 +1,209 @@
+<!doctype html>
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
+
+<head>
+  <title> Air Astana </title>
+  <!--[if !mso]><!-->
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <!--<![endif]-->
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <style type="text/css">
+    #outlook a {
+      padding: 0;
+    }
+
+    body {
+      margin: 0;
+      padding: 0;
+      -webkit-text-size-adjust: 100%;
+      -ms-text-size-adjust: 100%;
+    }
+
+    table,
+    td {
+      border-collapse: collapse;
+      mso-table-lspace: 0pt;
+      mso-table-rspace: 0pt;
+    }
+
+    img {
+      border: 0;
+      height: auto;
+      line-height: 100%;
+      outline: none;
+      text-decoration: none;
+      -ms-interpolation-mode: bicubic;
+    }
+
+    p {
+      display: block;
+      margin: 13px 0;
+    }
+
+  </style>
+  <!--[if mso]>
+    <noscript>
+    <xml>
+    <o:OfficeDocumentSettings>
+      <o:AllowPNG/>
+      <o:PixelsPerInch>96</o:PixelsPerInch>
+    </o:OfficeDocumentSettings>
+    </xml>
+    </noscript>
+    <![endif]-->
+  <!--[if lte mso 11]>
+    <style type="text/css">
+      .mj-outlook-group-fix { width:100% !important; }
+    </style>
+    <![endif]-->
+  <style type="text/css">
+    @media only screen and (min-width:480px) {
+      .mj-column-per-100 {
+        width: 100% !important;
+        max-width: 100%;
+      }
+    }
+
+  </style>
+  <style media="screen and (min-width:480px)">
+    .moz-text-html .mj-column-per-100 {
+      width: 100% !important;
+      max-width: 100%;
+    }
+
+  </style>
+  <style type="text/css">
+  </style>
+  <style type="text/css">
+    @media {
+      @font-face {
+        font-family: SourceSans-Bold;
+        src: url('https://somewhere.com/offer/fonts/source-sans/SourceSansPro-Bold.otf');
+        font-weight: bold;
+        font-style: normal;
+      }
+
+      @font-face {
+        font-family: SourceSans;
+        src: url('https://somewhere.com/offer/fonts/source-sans/SourceSansPro-Regular.otf'),
+          url('https://somewhere.com/offer/fonts/source-sans/SourceSansPro-Regular.woff') format('woff'),
+          url('https://somewhere.com/offer/fonts/source-sans/SourceSansPro-Regular.ttf') format('truetype');
+        font-weight: normal;
+        font-style: normal;
+      }
+
+      @font-face {
+        font-family: Varela Round;
+        src: url('https://somewhere.com/offer/fonts/varela/Varela-Regular.otf');
+        font-weight: normal;
+        font-style: normal;
+      }
+
+      @font-face {
+        font-family: 'Proxima Nova';
+        src: url('htsomewhere.com/offer/fonts/ProximaNova/ProximaNova.otf') format('opentype');
+        font-weight: normal;
+        font-style: normal;
+      }
+    }
+
+    #outlook a {
+      padding: 0;
+    }
+
+    body {
+      margin: 0;
+      padding: 0;
+      -webkit-text-size-adjust: 100%;
+      -ms-text-size-adjust: 100%;
+    }
+
+    table,
+    td {
+      border-collapse: collapse;
+      mso-table-lspace: 0pt;
+      mso-table-rspace: 0pt;
+    }
+
+    img {
+      border: 0;
+      height: auto;
+      line-height: 100%;
+      outline: none;
+      text-decoration: none;
+      -ms-interpolation-mode: bicubic;
+    }
+
+    p {
+      display: block;
+      margin: 13px 0;
+    }
+
+    a {
+      color: #A5965D;
+    }
+
+  </style>
+</head>
+
+<body style="word-spacing:normal;background-color:#f0f0f0;">
+  <div style="background-color:#f0f0f0;">
+    <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+    <div style="margin:0px auto;max-width:600px;">
+      <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
+        <tbody>
+          <tr>
+            <td style="direction:ltr;font-size:0px;padding:0px 20px;padding-top:5px;text-align:center;">
+              <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="white" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+    <div style="background:white;background-color:white;margin:0px auto;max-width:600px;">
+      <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:white;background-color:white;width:100%;">
+        <tbody>
+          <tr>
+            <td style="direction:ltr;font-size:0px;padding:20px 25px;text-align:center;">
+              <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:550px;" width="550" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+              <div style="margin:0px auto;max-width:550px;">
+                <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
+                  <tbody>
+                    <tr>
+                      <td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
+                        <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:550px;" ><![endif]-->
+                        <div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
+                          <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
+                            <tbody>
+                              <tr>
+                                <td align="left" style="font-size:0px;padding:0;padding-top:24px;word-break:break-word;">
+                                  <div style="font-family:'Proxima Nova', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:1;text-align:left;color:#626262;">Dear Guests</div>
+                                </td>
+                              </tr>
+                              <tr>
+                                <td align="left" style="font-size:0px;padding:0;padding-top:24px;word-break:break-word;">
+                                  <div style="font-family:'Proxima Nova', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:1;text-align:left;color:#626262;">Booking reference: ABC123</div>
+                                </td>
+                              </tr>
+                            </tbody>
+                          </table>
+                        </div>
+                        <!--[if mso | IE]></td></tr></table><![endif]-->
+                      </td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+              <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    <!--[if mso | IE]></td></tr></table><![endif]-->
+  </div>
+</body>
+
+</html>
diff --git a/resources/template/air-astana.mjml b/resources/template/air-astana.mjml
new file mode 100644
index 00000000..74082087
--- /dev/null
+++ b/resources/template/air-astana.mjml
@@ -0,0 +1,96 @@
+<mjml lang="en">
+  <mj-head>
+    <mj-title>Air Astana</mj-title>
+    <mj-attributes>
+      <mj-class name="nav-link" line-height="24px" font-size="11px" />
+      <mj-class name="color-primary" color="#A5965D" />
+      <mj-class name="bold" font-weight="700" color="black" text-align="left" />
+      <mj-text color="#626262" font-weight="400" font-size="16px" />
+      <mj-all font-family="'Proxima Nova', Helvetica, Arial, sans-serif" />
+    </mj-attributes>
+    <mj-style>
+      @media {
+        @font-face {
+          font-family: SourceSans-Bold;
+          src: url('https://somewhere.com/offer/fonts/source-sans/SourceSansPro-Bold.otf');
+          font-weight: bold;
+          font-style: normal;
+        }
+
+        @font-face {
+          font-family: SourceSans;
+          src: url('https://somewhere.com/offer/fonts/source-sans/SourceSansPro-Regular.otf'),
+            url('https://somewhere.com/offer/fonts/source-sans/SourceSansPro-Regular.woff') format('woff'),
+            url('https://somewhere.com/offer/fonts/source-sans/SourceSansPro-Regular.ttf') format('truetype');
+          font-weight: normal;
+          font-style: normal;
+        }
+
+        @font-face {
+          font-family: Varela Round;
+          src: url('https://somewhere.com/offer/fonts/varela/Varela-Regular.otf');
+          font-weight: normal;
+          font-style: normal;
+        }
+
+        @font-face {
+          font-family: 'Proxima Nova';
+          src: url('htsomewhere.com/offer/fonts/ProximaNova/ProximaNova.otf') format('opentype');
+          font-weight: normal;
+          font-style: normal;
+        }
+      }
+
+      #outlook a {
+        padding: 0;
+      }
+
+      body {
+        margin: 0;
+        padding: 0;
+        -webkit-text-size-adjust: 100%;
+        -ms-text-size-adjust: 100%;
+      }
+
+      table,
+      td {
+        border-collapse: collapse;
+        mso-table-lspace: 0pt;
+        mso-table-rspace: 0pt;
+      }
+
+      img {
+        border: 0;
+        height: auto;
+        line-height: 100%;
+        outline: none;
+        text-decoration: none;
+        -ms-interpolation-mode: bicubic;
+      }
+
+      p {
+        display: block;
+        margin: 13px 0;
+      }
+
+      a {
+        color: #A5965D;
+      }
+    </mj-style>
+  </mj-head>
+
+  <mj-body background-color="#f0f0f0">
+    <mj-section padding="0px 20px" padding-top="5px">
+     
+    </mj-section>
+    <mj-wrapper background-color="white" padding="20px 25px">
+
+      <mj-section padding="0">
+        <mj-column>
+          <mj-text line-height="1" padding="0" padding-top="24px">Dear Guests</mj-text>
+          <mj-text line-height="1" padding="0" padding-top="24px">Booking reference: ABC123</mj-text>
+        </mj-column>
+      </mj-section>
+    </mj-wrapper>
+  </mj-body>
+</mjml>


Test patch:
diff --git a/packages/mrml-core/src/helper/test.rs b/packages/mrml-core/src/helper/test.rs
index ca5e226c..7c48fcbc 100644
--- a/packages/mrml-core/src/helper/test.rs
+++ b/packages/mrml-core/src/helper/test.rs
@@ -1,4 +1,6 @@
-use html_parser::{Dom, Element, Node};
+use crate::helper::condition;
+use std::collections::{BTreeMap, BTreeSet, LinkedList};
+use xmlparser::{ElementEnd, Token, Tokenizer};
 
 fn trim_header_comment(input: &str) -> String {
     if input.starts_with("<!-- FILE:") {
@@ -12,18 +14,21 @@ fn trim_header_comment(input: &str) -> String {
 fn cleanup(input: &str) -> String {
     trim_header_comment(input)
         // conditions and comments
-        .replace("<!--[if !mso]><!-- -->", "")
-        .replace("<!--[if !mso><!-->", "")
-        .replace("<!--[if !mso]><!-->", "")
-        .replace("<!--<![endif]-->", "")
-        .replace("<!--[if mso | IE]>", "")
-        .replace("<!--[if !mso | IE]>", "")
-        .replace("<!--[if mso]>", "")
-        .replace("<!--[if !mso]>", "")
-        .replace("<!--[if lte mso 11]>", "")
-        .replace("<!--<![endif]-->", "")
-        .replace("<![endif]-->", "")
-        .replace("<!-->", "")
+        .replace(condition::END_NEGATION_CONDITIONAL_TAG, " ENDIF_NEGATION ")
+        .replace(condition::END_CONDITIONAL_TAG, " ENDIF ")
+        .replace(condition::START_MSO_NEGATION_CONDITIONAL_TAG, " IF_NO_MSO ")
+        .replace(
+            condition::START_NEGATION_CONDITIONAL_TAG,
+            " IF_NO_MSO_OR_IE ",
+        )
+        .replace("<!--[if !mso><!-->", " IF_NO_MSO ")
+        .replace("<!--[if !mso]><!-- -->", " IF_NO_MSO ")
+        .replace(condition::START_MSO_CONDITIONAL_TAG, " IF_MSO ")
+        .replace(condition::START_IE11_CONDITIONAL_TAG, " IF_LTE_MSO_11 ")
+        .replace(condition::START_CONDITIONAL_TAG, " IF_MSO_OR_IE ")
+        // TODO handle removing closing and opening conditions
+        .replace(" ENDIF  IF_MSO ", "")
+        .replace(" ENDIF  IF_MSO_OR_IE ", "")
         // empty style header blocks
         .replace("<style type=\"text/css\">\n  </style>", "")
         // empty style attributes
@@ -32,13 +37,16 @@ fn cleanup(input: &str) -> String {
         .replace("class=\"\"", "")
         // empty divs
         .replace("<div></div>", "")
+        //
+        .replace("<!doctype html>\n", "")
+        .replace("<!doctype html>", "")
 }
 
 pub fn cleanup_text(input: &str) -> String {
-    input.replace(" ", "").replace("\n", "")
+    input.replace(" ", "").replace("\t", "").replace("\n", "")
 }
 
-fn compare_style(path: &str, expected: &str, result: &str) {
+fn compare_style<'a>(path: &mut LinkedList<&'a str>, expected: &str, result: &str) {
     let expected_set = expected
         .split(';')
         .filter(|item| !item.is_empty())
@@ -49,101 +57,207 @@ fn compare_style(path: &str, expected: &str, result: &str) {
         .filter(|item| !item.is_empty())
         .map(|item| item.trim().to_string())
         .collect::<Vec<String>>();
-    assert_eq!(expected_set, result_set, "different styles in {}", path);
+    assert_eq!(expected_set, result_set, "different styles in {:?}", path);
 }
 
-fn compare_attribute(path: &str, key: &str, expected: Option<&String>, result: Option<&String>) {
-    let expected = expected.cloned().unwrap_or_default();
-    let result = result.cloned().unwrap_or_default();
-    if key == "style" {
-        compare_style(path, &expected, &result);
-    } else {
-        assert_eq!(expected, result, "different attribute {} in {}", key, path);
+fn compare_text<'a>(path: &mut LinkedList<&'a str>, expected: &str, result: &str) {
+    assert_eq!(
+        cleanup_text(expected),
+        cleanup_text(result),
+        "different text in {:?}",
+        path
+    );
+}
+
+fn read_attributes<'a>(
+    path: &mut LinkedList<&'a str>,
+    tokenizer: &mut Tokenizer<'a>,
+) -> Result<(BTreeMap<&'a str, &'a str>, ElementEnd<'a>), String> {
+    let mut result = BTreeMap::<&'a str, &'a str>::new();
+    loop {
+        match tokenizer.next() {
+            Some(Ok(Token::Attribute { local, value, .. })) => {
+                result.insert(local.as_str(), value.as_str());
+            }
+            Some(Ok(Token::ElementEnd { end, .. })) => return Ok((result, end)),
+            _ => return Err(format!("invalid token in attributes: {:?}", path)),
+        }
     }
 }
 
-fn compare_element(path: &str, expected: &Element, result: &Element) {
-    assert_eq!(expected.name, result.name, "different element in {}", path);
-    let current_path = format!("{} {}.{}", path, result.name, result.classes.join("."));
-    assert_eq!(expected.id, result.id, "different id in {}", current_path);
-    for (key, value) in expected.attributes.iter() {
-        compare_attribute(
-            current_path.as_str(),
-            key,
-            value.as_ref(),
-            result.attributes.get(key).and_then(|v| v.as_ref()),
+fn compare_attributes<'a>(
+    path: &mut LinkedList<&'a str>,
+    expected: &mut Tokenizer<'a>,
+    result: &mut Tokenizer<'a>,
+) -> bool {
+    let (exp_attrs, exp_end) = read_attributes(path, expected).unwrap();
+    let (res_attrs, res_end) = read_attributes(path, result).unwrap();
+
+    if !exp_end.eq(&res_end) {
+        panic!(
+            "invalid end of element at path {:?}: {:?} <> {:?}",
+            path, exp_end, res_end
         );
     }
-    assert_eq!(
-        expected.classes.len(),
-        result.classes.len(),
-        "classes mismatch in {}",
-        current_path
-    );
-    for classname in expected.classes.iter() {
-        assert!(
-            result.classes.contains(classname),
-            "cannot find classname {} in {}",
-            classname,
-            current_path
+
+    let exp_keys = exp_attrs.keys().copied().collect::<BTreeSet<_>>();
+    let res_keys = res_attrs.keys().copied().collect::<BTreeSet<_>>();
+
+    let diff = exp_keys.difference(&res_keys).copied().collect::<Vec<_>>();
+    if !diff.is_empty() {
+        panic!(
+            "expected attributes {:?} but do not exist in generated: {:?}",
+            diff, path
         );
     }
-    if expected.children.len() != result.children.len() {
-        println!("expected: {:#?}", expected.children);
-        println!("result: {:#?}", result.children);
+    let diff = res_keys.difference(&exp_keys).copied().collect::<Vec<_>>();
+    if !diff.is_empty() {
+        panic!(
+            "generated attributes {:?} but do not exist in origin: {:?}",
+            diff, path
+        );
     }
-    assert_eq!(
-        expected.children.len(),
-        result.children.len(),
-        "children count mismatch in {}",
-        current_path
-    );
-    for (index, (expected_child, result_child)) in expected
-        .children
-        .iter()
-        .zip(result.children.iter())
-        .enumerate()
-    {
-        let iterate_path = format!("{}[{}]", current_path, index);
-        compare_node(iterate_path.as_str(), expected_child, result_child);
+
+    for (key, exp_value) in exp_attrs.iter() {
+        if let Some(res_value) = res_attrs.get(key) {
+            if key == &"style" {
+                compare_style(path, exp_value, res_value);
+            } else if key == &"class" {
+                let exp_values = exp_value
+                    .split(' ')
+                    .filter(|item| !item.is_empty())
+                    .collect::<BTreeSet<_>>();
+                let res_values = res_value
+                    .split(' ')
+                    .filter(|item| !item.is_empty())
+                    .collect::<BTreeSet<_>>();
+
+                let diff = exp_values
+                    .difference(&res_values)
+                    .copied()
+                    .collect::<BTreeSet<_>>();
+                if !diff.is_empty() {
+                    panic!(
+                        "expected classes {:?} but do not exist in generated: {:?}",
+                        diff, path
+                    );
+                }
+
+                let diff = res_values
+                    .difference(&exp_values)
+                    .copied()
+                    .collect::<BTreeSet<_>>();
+                if !diff.is_empty() {
+                    panic!(
+                        "generated classes {:?} but do not exist in origin: {:?}",
+                        diff, path
+                    );
+                }
+            } else {
+                assert_eq!(exp_value, res_value);
+            }
+        } else {
+            panic!(
+                "expected attribute {:?} in generated at path {:?}",
+                key, path
+            );
+        }
     }
-}
 
-fn compare_text(path: &str, expected: &str, result: &str) {
-    assert_eq!(
-        cleanup_text(expected),
-        cleanup_text(result),
-        "different text in {}",
-        path
-    );
+    matches!(exp_end, ElementEnd::Open)
 }
 
-fn compare_node(path: &str, expected: &Node, result: &Node) {
-    if let Node::Element(exp) = expected {
-        if let Node::Element(res) = result {
-            return compare_element(path, &exp, &res);
-        }
+fn compare_element<'a>(
+    path: &mut LinkedList<&'a str>,
+    expected: &mut Tokenizer<'a>,
+    result: &mut Tokenizer<'a>,
+) {
+    let open = compare_attributes(path, expected, result);
+    if open && path.back() != Some(&"meta") {
+        compare_node(path, expected, result);
+    } else {
+        path.pop_back();
     }
-    if let Node::Text(exp) = expected {
-        if let Node::Text(res) = result {
-            return compare_text(path, &exp, &res);
+}
+
+fn next_element<'a>(tokenizer: &mut Tokenizer<'a>) -> Option<Token<'a>> {
+    if let Some(token) = tokenizer.next() {
+        let token = token.expect("unable to get next token");
+        match token {
+            Token::Text { text } => {
+                if cleanup_text(text.as_str()).is_empty() {
+                    next_element(tokenizer)
+                } else {
+                    Some(token)
+                }
+            }
+            _ => Some(token),
         }
+    } else {
+        None
     }
-    assert_eq!(expected, result);
 }
 
-fn compare_dom(expected: &Dom, result: &Dom) {
-    assert_eq!(expected.tree_type, result.tree_type);
-    assert_eq!(expected.children.len(), result.children.len());
-    for (expected_child, result_child) in expected.children.iter().zip(result.children.iter()) {
-        compare_node("", expected_child, result_child);
+fn compare_node<'a>(
+    path: &mut LinkedList<&'a str>,
+    expected: &mut Tokenizer<'a>,
+    result: &mut Tokenizer<'a>,
+) {
+    loop {
+        match (next_element(expected), next_element(result)) {
+            (
+                Some(Token::Comment { text: exp_text, .. }),
+                Some(Token::Comment { text: res_text, .. }),
+            ) => {
+                compare_text(path, &exp_text, &res_text);
+            }
+            (Some(Token::Text { text: exp_text }), Some(Token::Text { text: res_text })) => {
+                compare_text(path, &exp_text, &res_text);
+            }
+            (
+                Some(Token::ElementStart { local: exp_tag, .. }),
+                Some(Token::ElementStart { local: res_tag, .. }),
+            ) => {
+                assert_eq!(
+                    exp_tag.as_str(),
+                    res_tag.as_str(),
+                    "tags mismatch at path {:?}",
+                    path
+                );
+                path.push_back(exp_tag.as_str());
+                compare_element(path, expected, result);
+            }
+            (
+                Some(Token::ElementEnd { end: _exp_end, .. }),
+                Some(Token::ElementEnd { end: _res_end, .. }),
+            ) => {
+                path.pop_back();
+                // END OF ELEMENT
+                return;
+            }
+            (Some(exp), Some(res)) => {
+                panic!("mismatch at path {:?}: {:?} <> {:?}", path, exp, res);
+            }
+            (None, None) => {
+                // nothing to do
+                return;
+            }
+            (Some(_), None) => {
+                panic!("expected element at path: {:?}", path);
+            }
+            (None, Some(_)) => {
+                panic!("didn't expect element at path: {:?}", path);
+            }
+        };
     }
 }
 
 pub fn compare(expected: &str, result: &str) {
     let expected = cleanup(expected);
     let result = cleanup(result);
-    let expected_dom = Dom::parse(&expected).unwrap();
-    let result_dom = Dom::parse(&result).unwrap();
-    compare_dom(&expected_dom, &result_dom);
+    println!("# expected: {}\n\n#result: {}\n", expected, result);
+    let mut expected_tokenizer = Tokenizer::from(expected.as_str());
+    let mut result_tokenizer = Tokenizer::from(result.as_str());
+    let mut path = LinkedList::new();
+    compare_node(&mut path, &mut expected_tokenizer, &mut result_tokenizer);
 }