Skip to content

Commit

Permalink
Fixes to classes. Closes #210 (#212)
Browse files Browse the repository at this point in the history
* Fixes to classes. Closes #210

* Update version.
  • Loading branch information
SebastianStehle authored Jan 1, 2025
1 parent 242cd7b commit 681cff6
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 178 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
<RepositoryUrl>https://github.com/SebastianStehle/mjml-net.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Version>4.5.0</Version>
<Version>4.6.0</Version>
</PropertyGroup>
</Project>
39 changes: 31 additions & 8 deletions Mjml.Net/Components/Head/AttributesComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,45 @@ public override void Read(IHtmlReader htmlReader, IMjmlReader mjmlReader, Global
return;
case HtmlTokenKind.Tag when htmlReader.Name == "mj-class":
var className = htmlReader.GetAttribute("name");
if (string.IsNullOrWhiteSpace(className))
{
htmlReader.Read();
break;
}

if (className != null)
for (var i = 0; i < htmlReader.AttributeCount; i++)
{
for (var i = 0; i < htmlReader.AttributeCount; i++)
var attributeName = htmlReader.GetAttributeName(i);
var attributeValue = htmlReader.GetAttribute(i);

if (attributeName != "name")
{
var attributeName = htmlReader.GetAttributeName(i);
var attributeValue = htmlReader.GetAttribute(i);
context.SetClassAttribute(attributeName, className, attributeValue);
}
}

if (attributeName != "name")
if (htmlReader.SelfClosingElement)
{
htmlReader.Read();
break;
}

var subTree = htmlReader.ReadSubtree();
while (subTree.Read())
{
if (subTree.TokenKind == HtmlTokenKind.Tag)
{
var childTagName = htmlReader.Name;

for (var i = 0; i < subTree.AttributeCount; i++)
{
context.SetClassAttribute(attributeName, className, attributeValue);
var attributeName = subTree.GetAttributeName(i);
var attributeValue = subTree.GetAttribute(i);

context.SetParentClassAttribute(attributeName, className, childTagName, attributeValue);
}
}
}

htmlReader.Read();
break;

case HtmlTokenKind.Tag:
Expand Down
64 changes: 15 additions & 49 deletions Mjml.Net/GlobalContext.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
using Mjml.Net.Internal;
namespace Mjml.Net;

namespace Mjml.Net;
public record struct AttributeKey(string ClassOrType, string Name);

public record struct AttributeParentKey(string ParentClass, string ClassOrType, string Name);

public sealed class GlobalContext
{
private readonly Dictionary<string, Dictionary<string, string>> attributesByName = new Dictionary<string, Dictionary<string, string>>(10);
private readonly Dictionary<string, Dictionary<string, string>> attributesByClass = new Dictionary<string, Dictionary<string, string>>(10);
private readonly Dictionary<AttributeKey, string> attributesByName = new Dictionary<AttributeKey, string>(10);
private readonly Dictionary<AttributeKey, string> attributesByClass = new Dictionary<AttributeKey, string>(10);
private readonly Dictionary<AttributeParentKey, string> attributesByParentClass = new Dictionary<AttributeParentKey, string>(10);
private IFileLoader? fileLoader;

public Dictionary<(Type Type, object Identifier), GlobalData> GlobalData { get; } = [];

public Dictionary<string, Dictionary<string, string>> AttributesByClass => attributesByClass;
public IReadOnlyDictionary<AttributeKey, string> AttributesByClass => attributesByClass;

public IReadOnlyDictionary<AttributeParentKey, string> AttributesByParentClass => attributesByParentClass;

public Dictionary<string, Dictionary<string, string>> AttributesByName => attributesByName;
public IReadOnlyDictionary<AttributeKey, string> AttributesByName => attributesByName;

public MjmlOptions Options { get; set; }

Expand Down Expand Up @@ -53,55 +58,16 @@ public void AddGlobalData<T>(T value) where T : GlobalData

public void SetTypeAttribute(string name, string type, string value)
{
if (!attributesByName.TryGetValue(name, out var attributes))
{
attributes = [];
attributesByName[name] = attributes;
}

attributes[type] = value;
attributesByName[new AttributeKey(type, name)] = value;
}

public void SetClassAttribute(string name, string className, string value)
{
if (!attributesByClass.TryGetValue(className, out var attributes))
{
attributes = [];
attributesByClass[className] = attributes;
}

attributes[name] = value;
attributesByClass[new AttributeKey(className, name)] = value;
}

public string? GetAttribute(string elementName, string[]? classes)
public void SetParentClassAttribute(string name, string parentClassName, string type, string value)
{
if (attributesByName.TryGetValue(elementName, out var byType))
{
if (byType.TryGetValue(elementName, out var attribute))
{
return attribute;
}

if (byType.TryGetValue(Constants.All, out attribute))
{
return attribute;
}
}

if (attributesByClass.Count > 0 && classes != null)
{
foreach (var className in classes)
{
if (attributesByClass.TryGetValue(className, out var byName))
{
if (byName.TryGetValue(elementName, out var attribute))
{
return attribute;
}
}
}
}

return null;
attributesByParentClass[new AttributeParentKey(parentClassName, type, name)] = value;
}
}
5 changes: 5 additions & 0 deletions Mjml.Net/IBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public interface IBinder
/// </returns>
string? GetAttribute(string name);

/// <summary>
/// Gets the class names.
/// </summary>
string[] ClassNames { get; }

/// <summary>
/// Get the text content of the node.
/// </summary>
Expand Down
100 changes: 72 additions & 28 deletions Mjml.Net/Internal/Binder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace Mjml.Net.Internal;
using System.Runtime.Intrinsics.X86;
using System.Xml.Linq;

namespace Mjml.Net.Internal;

internal sealed class Binder : IBinder
{
Expand All @@ -9,6 +12,26 @@ internal sealed class Binder : IBinder
private string elementName;
private string[]? currentClasses;

public string[] ClassNames
{
get
{
if (currentClasses == null)
{
if (attributes.TryGetValue(Constants.MjClass, out var classNames))
{
currentClasses = classNames.Split(' ');
}
else
{
currentClasses = Array.Empty<string>();
}
}

return currentClasses;
}
}

public Binder Setup(GlobalContext newContext, IComponent? newParent, string? newElementName = null)
{
context = newContext;
Expand Down Expand Up @@ -39,66 +62,87 @@ public void SetText(InnerTextOrHtml text)

public string? GetAttribute(string name)
{
if (attributes.TryGetValue(name, out var attribute))
if (attributes.TryGetValue(name, out var a1))
{
return attribute;
return a1;
}

var inherited = elementParent?.GetInheritingAttribute(name);

if (inherited != null)
{
return inherited;
}

if (context.AttributesByClass.Count > 0)
{
if (currentClasses == null)
var classNames = ClassNames;
if (classNames.Length > 0)
{
if (attributes.TryGetValue(Constants.MjClass, out var classNames))
string? classAttribute = null;
// Loop over all classes and use the last match.
foreach (var className in classNames)
{
currentClasses = classNames.Split(' ');
if (context.AttributesByClass.TryGetValue(new AttributeKey(className, name), out var a2))
{
classAttribute = a2;
}
}
else

if (classAttribute != null)
{
currentClasses = Array.Empty<string>();
return classAttribute;
}
}
}

string? classAttribute = null;

// Loop over all classes and use the last match.
foreach (var className in currentClasses)
if (context.AttributesByParentClass.Count > 0 && elementParent != null)
{
var classNames = elementParent.Binder.ClassNames;
if (classNames.Length > 0)
{
if (context.AttributesByClass.TryGetValue(className, out var byName))
string? classAttribute = null;
// Loop over all classes and use the last match.
foreach (var className in classNames)
{
if (byName.TryGetValue(name, out attribute))
if (context.AttributesByParentClass.TryGetValue(new AttributeParentKey(className, elementName, name), out var a3))
{
classAttribute = attribute;
classAttribute = a3;
}
}
}

if (classAttribute != null)
{
return classAttribute;
if (classAttribute != null)
{
return classAttribute;
}
}
}

if (context.AttributesByName.TryGetValue(name, out var byType))
if (context.AttributesByName.TryGetValue(new AttributeKey(elementName, name), out var a4))
{
if (byType.TryGetValue(elementName, out attribute))
{
return attribute;
}
return a4;
}

if (context.AttributesByName.TryGetValue(new AttributeKey(Constants.All, name), out var a5))
{
return a5;
}

if (byType.TryGetValue(Constants.All, out attribute))
return null;
}

private static string? GetByClass(IReadOnlyDictionary<AttributeKey, string> attributes, string[] classNames, string name)
{
string? result = null;
// Loop over all classes and use the last match.
foreach (var className in classNames)
{
if (attributes.TryGetValue(new AttributeKey(className, name), out var a))
{
return attribute;
result = a;
}
}

return null;
return result;
}

public InnerTextOrHtml? GetText()
Expand Down
12 changes: 6 additions & 6 deletions Mjml.Net/MjmlRenderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,22 @@ public void Read(IHtmlReader reader, IComponent? parent, string? file)

try
{
var substree = reader.ReadSubtree();
var subTree = reader.ReadSubtree();

// Only add the text error once per parent.
var hasAddedError = false;

while (substree.Read())
while (subTree.Read())
{
switch (substree.TokenKind)
switch (subTree.TokenKind)
{
case HtmlTokenKind.Tag:
ReadElement(substree.Name, substree, parent, file);
ReadElement(subTree.Name, subTree, parent, file);
break;
case HtmlTokenKind.Comment when mjmlOptions.KeepComments && parent != null:
ReadComment(substree, parent);
ReadComment(subTree, parent);
break;
case HtmlTokenKind.Text when !hasAddedError && substree.TextAsSpan.Trim().Length > 0:
case HtmlTokenKind.Text when !hasAddedError && subTree.TextAsSpan.Trim().Length > 0:
errors.Add(
"Unexpected text content.",
ValidationErrorType.UnexpectedText,
Expand Down
25 changes: 25 additions & 0 deletions Tests/Components/AttributesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,29 @@ public void Should_render_font_with_multiple_attributes()

AssertHelpers.HtmlFileAssert("Components.Outputs.Font.html", result);
}

[Fact]
public void Should_render_nested_child_class_names()
{
var source = @"
<mjml-test head=""false"">
<mj-head>
<mj-attributes>
<mj-class name=""mjexampleclass"" align=""center"" icon-size=""200px"">
<mj-social-element padding=""0 10px 0 0"" alt=""christmas tree"" src=""https://cdn.pixabay.com/photo/2023/12/14/20/24/christmas-balls-8449615_1280.jpg"" />
</mj-class>
</mj-attributes>
</mj-head>
<mj-body>
<mj-social mj-class=""mjexampleclass"">
<mj-social-element href=""#"">This image is not displayed when complied with mjml-net</mj-social-element>
</mj-social>
</mj-body>
</mjml-test>
";

var (result, _) = TestHelper.Render(source, helpers: [new FontHelper()]);

AssertHelpers.HtmlFileAssert("Components.Outputs.ChildClasses.html", result);
}
}
22 changes: 22 additions & 0 deletions Tests/Components/Outputs/ChildClasses.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tr>
<td style="padding:0 10px 0 0;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:200px;">
<tr>
<td style="font-size:0;height:200px;vertical-align:middle;width:200px;">
<a href="#" target="_blank">
<img alt="christmas tree" height="200" src="https://cdn.pixabay.com/photo/2023/12/14/20/24/christmas-balls-8449615_1280.jpg" style="border-radius:3px;display:block;" width="200" />
</a>
</td>
</tr>
</table>
</td>
<td style="vertical-align:middle;padding:4px 4px 4px 0;">
<a href="#" style="color:#333333;font-size:13px;font-family:Ubuntu, Helvetica, Arial, sans-serif;line-height:22px;text-decoration:none;" target="_blank"> This image is not displayed when complied with mjml-net </a>
</td>
</tr>
</table>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
Loading

0 comments on commit 681cff6

Please sign in to comment.