From 9e264dc1a2eb51d0cc9efa2f848cc9d40c4ed85f Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 4 Feb 2024 22:57:43 +0100 Subject: [PATCH] Improve include handling. (#191) * Improve include handling. * Fixes * Some comments. --- Mjml.Net/Component.cs | 9 ++- Mjml.Net/Components/Body/BodyComponent.cs | 2 + Mjml.Net/Components/Body/CarouselComponent.cs | 2 +- Mjml.Net/Components/Body/ColumnComponent.cs | 1 - Mjml.Net/Components/Body/NavbarComponent.cs | 2 +- Mjml.Net/Components/IncludeComponent.cs | 70 ++++++++++++++++++- Mjml.Net/Components/RootComponent.cs | 13 +++- Mjml.Net/Components/RootData.cs | 6 ++ Mjml.Net/GlobalContext.cs | 2 - Mjml.Net/MjmlRenderContext.cs | 2 + Tests/ComplexTests.cs | 1 - Tests/Components/IncludeTests.cs | 29 ++++++++ Tests/Components/Outputs/Carousel.html | 2 +- .../Components/Outputs/CarouselIconWidth.html | 2 +- .../Outputs/CarouselImageWithHref.html | 2 +- .../Outputs/CarouselImagesFive.html | 2 +- .../Components/Outputs/CarouselImagesOne.html | 2 +- .../Components/Outputs/CarouselImagesTwo.html | 2 +- .../Outputs/CarouselThumbnailWidth.html | 2 +- Tests/Components/Outputs/List.html | 8 +-- Tests/Components/Outputs/Navbar.html | 2 +- Tests/Components/Outputs/NavbarWithLinks.html | 2 +- Tests/Components/Outputs/TextInclude.html | 9 +++ Tests/HtmlSpecialCaseTests.cs | 3 +- Tests/Internal/AssertHelpers.cs | 4 +- Tests/Tests.csproj | 2 + 26 files changed, 153 insertions(+), 30 deletions(-) create mode 100644 Tests/Components/Outputs/TextInclude.html diff --git a/Mjml.Net/Component.cs b/Mjml.Net/Component.cs index 424b387..919d4b8 100644 --- a/Mjml.Net/Component.cs +++ b/Mjml.Net/Component.cs @@ -1,6 +1,4 @@ -using Mjml.Net.Internal; - -namespace Mjml.Net; +namespace Mjml.Net; public abstract class Component : IComponent { @@ -100,6 +98,11 @@ public virtual void Read(IHtmlReader htmlReader, IMjmlReader mjmlReader, GlobalC { } + protected virtual void ClearChildren() + { + childNodes?.Clear(); + } + protected virtual void BeforeBind(GlobalContext context) { } diff --git a/Mjml.Net/Components/Body/BodyComponent.cs b/Mjml.Net/Components/Body/BodyComponent.cs index ed37e2b..948879e 100644 --- a/Mjml.Net/Components/Body/BodyComponent.cs +++ b/Mjml.Net/Components/Body/BodyComponent.cs @@ -37,6 +37,8 @@ public override void Render(IHtmlRenderer renderer, GlobalContext context) renderer.StartBuffer(); renderer.StartElement("div") + .Attr("lang", context.GlobalData.Values.OfType().FirstOrDefault()?.Value) + .Attr("dir", context.GlobalData.Values.OfType().FirstOrDefault()?.Value) .Class(CssClass) .Style("background-color", BackgroundColor); diff --git a/Mjml.Net/Components/Body/CarouselComponent.cs b/Mjml.Net/Components/Body/CarouselComponent.cs index 3d3bf16..07ec2ed 100644 --- a/Mjml.Net/Components/Body/CarouselComponent.cs +++ b/Mjml.Net/Components/Body/CarouselComponent.cs @@ -80,7 +80,7 @@ public override void Render(IHtmlRenderer renderer, GlobalContext context) context.SetGlobalData("mj-carousel", new Style(HeadStyle)); - renderer.StartConditional(""); + renderer.StartConditional(""); { renderer.StartElement("div") .Class("mj-carousel"); diff --git a/Mjml.Net/Components/Body/ColumnComponent.cs b/Mjml.Net/Components/Body/ColumnComponent.cs index 49d08e7..a83d465 100644 --- a/Mjml.Net/Components/Body/ColumnComponent.cs +++ b/Mjml.Net/Components/Body/ColumnComponent.cs @@ -212,7 +212,6 @@ private void RenderColumn(IHtmlRenderer renderer, GlobalContext context) renderer.StartElement("tr"); renderer.StartElement("td") .Attr("align", child.GetAttribute("align")) - .Attr("vertical-align", child.GetAttribute("vertical-align")) .Class(child.GetAttribute("css-class")) .Style("background", child.GetAttribute("container-background-color")) .Style("font-size", "0px") diff --git a/Mjml.Net/Components/Body/NavbarComponent.cs b/Mjml.Net/Components/Body/NavbarComponent.cs index 816281a..e1e300f 100644 --- a/Mjml.Net/Components/Body/NavbarComponent.cs +++ b/Mjml.Net/Components/Body/NavbarComponent.cs @@ -123,7 +123,7 @@ private void RenderHamburger(IHtmlRenderer renderer, GlobalContext context) { var key = context.Options.IdGenerator.Next(); - renderer.StartConditional(""); + renderer.StartConditional(""); { renderer.StartElement("input", true) .Attr("id", key) diff --git a/Mjml.Net/Components/IncludeComponent.cs b/Mjml.Net/Components/IncludeComponent.cs index cdf61d7..785c438 100644 --- a/Mjml.Net/Components/IncludeComponent.cs +++ b/Mjml.Net/Components/IncludeComponent.cs @@ -1,4 +1,6 @@ -using Mjml.Net.Helpers; +using Mjml.Net.Components.Body; +using Mjml.Net.Components.Head; +using Mjml.Net.Helpers; using Mjml.Net.Types; namespace Mjml.Net.Components; @@ -59,10 +61,74 @@ public override void Read(IHtmlReader htmlReader, IMjmlReader mjmlReader, Global if (!string.IsNullOrWhiteSpace(content)) { - mjmlReader.ReadFragment(content, actualPath, Parent!); + // Add the new element to the include parent, so actually after the include to have a correct parent-child relationship. + mjmlReader.ReadFragment(content, actualPath, this); + + static void AddToHead(IComponent component, IComponent parent) + { + // Go to the root element to find the parent. + while (parent.Parent != null) + { + parent = parent.Parent; + } + + if (parent is not RootComponent root) + { + return; + } + + var head = root.ChildNodes.OfType().FirstOrDefault(); + + // If there is no head element in the current tree, add one. + if (head == null) + { + head = new HeadComponent(); + + root.InsertChild(head, 0); + } + + foreach (var child in component.ChildNodes) + { + Add(child, head); + } + } + + static void Add(IComponent component, IComponent parent) + { + if (component is HeadComponent) + { + // Add head children to the root head. + AddToHead(component, parent); + } + else if (component is RootComponent or BodyComponent or IncludeComponent) + { + // Just ignore these component and add the children to the parent. + foreach (var child in component.ChildNodes) + { + Add(child, parent); + } + } + else + { + parent.AddChild(component); + } + } + + if (Parent != null) + { + Add(this, Parent); + } + + // The children have been added to our parent or to the head element. + ClearChildren(); } } + public override void Measure(GlobalContext context, double parentWidth, int numSiblings, int numNonRawSiblings) + { + base.Measure(context, parentWidth, numSiblings, numNonRawSiblings); + } + public override void Render(IHtmlRenderer renderer, GlobalContext context) { if (ActualType == IncludeType.Mjml || string.IsNullOrWhiteSpace(Path)) diff --git a/Mjml.Net/Components/RootComponent.cs b/Mjml.Net/Components/RootComponent.cs index 341727b..d56c6b7 100644 --- a/Mjml.Net/Components/RootComponent.cs +++ b/Mjml.Net/Components/RootComponent.cs @@ -2,7 +2,7 @@ namespace Mjml.Net.Components; -public sealed class RootComponent : Component +public partial class RootComponent : Component { private static readonly string DefaultMeta = Resources.DefaultMeta; private static readonly string DefaultStyles = Resources.DefaultStyles; @@ -10,8 +10,17 @@ public sealed class RootComponent : Component public override string ComponentName => "mjml"; + [Bind("lang", BindType.RequiredString)] + public string Lang = "und"; + + [Bind("dir", BindType.RequiredString)] + public string Dir = "auto"; + public override void Render(IHtmlRenderer renderer, GlobalContext context) { + context.SetGlobalData("lang", new Language(Lang)); + context.SetGlobalData("dir", new Direction(Dir)); + RenderChildren(renderer, context); if (Parent != null) @@ -20,7 +29,7 @@ public override void Render(IHtmlRenderer renderer, GlobalContext context) } renderer.Content(""); - renderer.Content(""); + renderer.Content($""); renderer.Content(string.Empty); RenderHead(renderer, context); diff --git a/Mjml.Net/Components/RootData.cs b/Mjml.Net/Components/RootData.cs index fd5c1dc..a79d18c 100644 --- a/Mjml.Net/Components/RootData.cs +++ b/Mjml.Net/Components/RootData.cs @@ -9,3 +9,9 @@ public sealed record BodyBuffer(StringBuilder? Buffer) : GlobalData; public sealed record Background(string Color) : GlobalData; public sealed record HeadBuffer(StringBuilder? Buffer) : GlobalData; + +public sealed record Language(string Value) : GlobalData; + +public sealed record Direction(string Value) : GlobalData; + +public sealed record ForceOWADesktop(bool Value) : GlobalData; diff --git a/Mjml.Net/GlobalContext.cs b/Mjml.Net/GlobalContext.cs index 15b6002..2f683ec 100644 --- a/Mjml.Net/GlobalContext.cs +++ b/Mjml.Net/GlobalContext.cs @@ -29,11 +29,9 @@ public void SetOptions(MjmlOptions options) public void Clear() { GlobalData.Clear(); - fileLoader = null; attributesByClass.Clear(); attributesByName.Clear(); - Options = null!; } diff --git a/Mjml.Net/MjmlRenderContext.cs b/Mjml.Net/MjmlRenderContext.cs index 74699b4..ddd2f07 100644 --- a/Mjml.Net/MjmlRenderContext.cs +++ b/Mjml.Net/MjmlRenderContext.cs @@ -105,6 +105,7 @@ private void ReadElement(string name, IHtmlReader reader, IComponent? parent, st var binder = DefaultPools.Binders.Get().Setup(context, parent, component.ComponentName); + // Add all binders to list, so that we can return them later to the pool. allBinders.Add(binder); for (var i = 0; i < reader.AttributeCount; i++) @@ -140,6 +141,7 @@ private void ReadElement(string name, IHtmlReader reader, IComponent? parent, st Read(reader, component, file); } + // If there is no parent, we handle the root and we can render everything top to bottom. if (parent == null) { component.Bind(context); diff --git a/Tests/ComplexTests.cs b/Tests/ComplexTests.cs index 9fe878a..ada4cab 100644 --- a/Tests/ComplexTests.cs +++ b/Tests/ComplexTests.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using System.Diagnostics; -using Castle.Components.DictionaryAdapter; using Mjml.Net; using Mjml.Net.Validators; using Tests.Internal; diff --git a/Tests/Components/IncludeTests.cs b/Tests/Components/IncludeTests.cs index e77dd06..d91abfd 100644 --- a/Tests/Components/IncludeTests.cs +++ b/Tests/Components/IncludeTests.cs @@ -86,6 +86,35 @@ public void Should_include_mjml() AssertHelpers.HtmlFileAssert("Components.Outputs.TextWhitespace.html", result); } + [Fact] + public void Should_include_mjml_with_dummy_body() + { + var files = new Dictionary + { + ["./text.mjml"] = @" + + + Hello MJML + + " + }; + + const string source = @" + + Before Include + + After Include + +"; + + var result = TestHelper.Render(source, new MjmlOptions + { + FileLoader = () => new InMemoryFileLoader(files) + }); + + AssertHelpers.HtmlFileAssert("Components.Outputs.TextInclude.html", result); + } + [Fact] public void Should_include_mjml_nested() { diff --git a/Tests/Components/Outputs/Carousel.html b/Tests/Components/Outputs/Carousel.html index de3eb4f..18b4413 100644 --- a/Tests/Components/Outputs/Carousel.html +++ b/Tests/Components/Outputs/Carousel.html @@ -1,4 +1,4 @@ - +