Skip to content

Commit

Permalink
Fix strictness.
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastianStehle committed Sep 18, 2024
1 parent be97935 commit b6566ab
Show file tree
Hide file tree
Showing 60 changed files with 616 additions and 196 deletions.
24 changes: 24 additions & 0 deletions Mjml.Net.PostProcessors/AngleSharpExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using AngleSharp.Dom;

namespace Mjml.Net;

public static class AngleSharpExtensions
{
public static void Traverse(this INode node, Action<IElement> action)
{
foreach (var child in node.ChildNodes.ToList())
{
Traverse(child, action);
}

if (node is IElement element)
{
action(element);
}
}

public static IEnumerable<IElement> Children(this IElement node, string tagName)
{
return node.Children.Where(x => string.Equals(x.NodeName, tagName, StringComparison.OrdinalIgnoreCase));
}
}
49 changes: 49 additions & 0 deletions Mjml.Net.PostProcessors/AngleSharpPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using AngleSharp;
using AngleSharp.Css;
using AngleSharp.Dom;

namespace Mjml.Net;

public sealed class AngleSharpPostProcessor : IPostProcessor, INestingPostProcessor
{
private static readonly IConfiguration HtmlConfiguration =
Configuration.Default
.WithCss()
.Without<ICssDefaultStyleSheetProvider>();

public static readonly IPostProcessor Default = new AngleSharpPostProcessor(new InlineCssPostProcessor(), new AttributesPostProcessor());

private readonly IAngleSharpPostProcessor[] inner;

public bool Has<T>()
{
return inner.Any(x => x is T);
}

public AngleSharpPostProcessor(params IAngleSharpPostProcessor[] inner)
{
this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
}

public async ValueTask<string> PostProcessAsync(string html, MjmlOptions options,
CancellationToken ct)
{
var document = await ParseAsync(html, ct);

foreach (var processor in inner)
{
await processor.ProcessAsync(document, options, ct);
}

var result = document.ToHtml();

return result;
}

private static async Task<IDocument> ParseAsync(string html, CancellationToken ct)
{
var context = BrowsingContext.New(HtmlConfiguration);

return await context.OpenAsync(req => req.Content(html), ct);
}
}
57 changes: 57 additions & 0 deletions Mjml.Net.PostProcessors/AttributesPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using AngleSharp.Dom;

namespace Mjml.Net;

public sealed class AttributesPostProcessor : IAngleSharpPostProcessor
{
public static readonly IPostProcessor Instance = new AngleSharpPostProcessor(new AttributesPostProcessor());

public ValueTask ProcessAsync(IDocument document, MjmlOptions options, CancellationToken ct)
{
foreach (var attributes in document.QuerySelectorAll("mj-html-attributes"))
{
foreach (var selector in attributes.Children("mj-selector"))
{
var path = selector.GetAttribute("path");

if (string.IsNullOrEmpty(path))
{
continue;
}

var attributeValues = selector.Children("mj-html-attribute")
.Select(x =>
{
var attributeName = x.GetAttribute("name")!;
var attributeValue = x.TextContent;
return (Name: attributeName, Value: attributeValue);
})
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
.ToList();

foreach (var target in document.QuerySelectorAll(path))
{
foreach (var (name, value) in attributeValues)
{
target.SetAttribute(name, value?.Trim());
}
}
}
}

RemoveAll(document, "mj-html-attributes");
RemoveAll(document, "mj-html-attribute");
RemoveAll(document, "mj-selector");

return default;
}

private static void RemoveAll(IDocument document, string selector)
{
foreach (var element in document.QuerySelectorAll(selector))
{
element.Remove();
}
}
}
30 changes: 30 additions & 0 deletions Mjml.Net.PostProcessors/Components/AttributeSelectorComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Mjml.Net;

public partial class SelectorComponent : Component
{
private static readonly AllowedParents Parents =
[
"mj-html-attributes"
];

public override AllowedParents? AllowedParents => Parents;

public override ContentType ContentType => ContentType.Complex;

public override string ComponentName => "mj-selector";

[Bind("path", BindType.RequiredString)]
public string Path;

public override void Render(IHtmlRenderer renderer, GlobalContext context)
{
if (!context.Options.HasProcessor<AttributesPostProcessor>())
{
return;
}

renderer.StartElement(ComponentName).Attr("path", Path);
RenderChildren(renderer, context);
renderer.EndElement(ComponentName);
}
}
33 changes: 33 additions & 0 deletions Mjml.Net.PostProcessors/Components/HtmlAttributeComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Mjml.Net;

public partial class HtmlAttributeComponent : Component
{
private static readonly AllowedParents Parents =
[
"mj-selector"
];

public override AllowedParents? AllowedParents => Parents;

public override ContentType ContentType => ContentType.Text;

public override string ComponentName => "mj-html-attribute";

[Bind("name", BindType.RequiredString)]
public string Name;

[BindText]
public InnerTextOrHtml? Text;

public override void Render(IHtmlRenderer renderer, GlobalContext context)
{
if (!context.Options.HasProcessor<AttributesPostProcessor>())
{
return;
}

renderer.StartElement(ComponentName).Attr("name", Name);
renderer.Content(Text);
renderer.EndElement(ComponentName);
}
}
27 changes: 27 additions & 0 deletions Mjml.Net.PostProcessors/Components/HtmlAttributesComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Mjml.Net.Components;

public partial class HtmlAttributesComponent : Component
{
private static readonly AllowedParents Parents =
[
"mj-head"
];

public override AllowedParents? AllowedParents => Parents;

public override ContentType ContentType => ContentType.Complex;

public override string ComponentName => "mj-html-attributes";

public override void Render(IHtmlRenderer renderer, GlobalContext context)
{
if (!context.Options.HasProcessor<AttributesPostProcessor>())
{
return;
}

renderer.StartElement(ComponentName);
RenderChildren(renderer, context);
renderer.EndElement(ComponentName);
}
}
9 changes: 9 additions & 0 deletions Mjml.Net.PostProcessors/IAngleSharpPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using AngleSharp.Dom;

namespace Mjml.Net;

public interface IAngleSharpPostProcessor
{
ValueTask ProcessAsync(IDocument document, MjmlOptions options,
CancellationToken ct);
}
6 changes: 6 additions & 0 deletions Mjml.Net.PostProcessors/INestingPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Mjml.Net;

public interface INestingPostProcessor
{
bool Has<T>();
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,21 @@
using AngleSharp;
using AngleSharp.Css;
using AngleSharp.Dom;
using Mjml.Net;

namespace Html.Net;
namespace Mjml.Net;

public sealed class InlineCssPostProcessor : IPostProcessor
public sealed class InlineCssPostProcessor : IAngleSharpPostProcessor
{
private const string FallbackStyle = "non_inline_style";
private static readonly IConfiguration HtmlConfiguration =
Configuration.Default
.WithCss()
.Without<ICssDefaultStyleSheetProvider>();

public static readonly InlineCssPostProcessor Instance = new InlineCssPostProcessor();
public static readonly IPostProcessor Instance = new AngleSharpPostProcessor(new InlineCssPostProcessor());

private InlineCssPostProcessor()
{
}

public async ValueTask<string> PostProcessAsync(string html, MjmlOptions options,
public ValueTask ProcessAsync(IDocument document, MjmlOptions options,
CancellationToken ct)
{
var context = BrowsingContext.New(HtmlConfiguration);

var document = await context.OpenAsync(req => req.Content(html), ct);

Traverse(document, a => RenameNonInline(a, document));
Traverse(document, InlineStyle);
Traverse(document, a => RestoreNonInline(a, document));

var result = document.ToHtml();

return result;
return default;
}

private static void Traverse(INode node, Action<IElement> action)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591</NoWarn>
<NeutralLanguage>en</NeutralLanguage>
<LangVersion>latest</LangVersion>
<RootNamespace>Mjml.Net</RootNamespace>
Expand Down Expand Up @@ -41,6 +44,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Mjml.Net.Generator\Mjml.Net.Generator.csproj" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Mjml.Net\Mjml.Net.csproj" />
</ItemGroup>

Expand Down
43 changes: 43 additions & 0 deletions Mjml.Net.PostProcessors/PostProcessorExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Mjml.Net.Components;

namespace Mjml.Net;

public static class PostProcessorExtensions
{
public static MjmlOptions WithPostProcessors(this MjmlOptions options)
{
options.PostProcessors = [AngleSharpPostProcessor.Default];
return options;
}

public static IMjmlRenderer AddHtmlAttributes(this IMjmlRenderer renderer)
{
renderer.Add<HtmlAttributeComponent>();
renderer.Add<HtmlAttributesComponent>();
renderer.Add<SelectorComponent>();
return renderer;
}

public static bool HasProcessor<T>(this MjmlOptions options)
{
if (options.PostProcessors == null || options.PostProcessors.Length == 0)
{
return false;
}

foreach (var processor in options.PostProcessors)
{
if (processor is T)
{
return true;
}

if (processor is INestingPostProcessor nesting && nesting.Has<T>())
{
return true;
}
}

return false;
}
}
File renamed without changes
2 changes: 1 addition & 1 deletion Mjml.Net.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tools", "Tools\Tools.csproj
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mjml.Net.Benchmark", "Mjml.Net.Benchmark\Mjml.Net.Benchmark.csproj", "{3969326A-B528-4120-8E30-4127F66B638E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Html.Net.PostProcessors", "Html.Net.PostProcessors\Html.Net.PostProcessors.csproj", "{BADECB72-90D2-44F3-AA93-27B0247C8FA9}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mjml.Net.PostProcessors", "Mjml.Net.PostProcessors\Mjml.Net.PostProcessors.csproj", "{BADECB72-90D2-44F3-AA93-27B0247C8FA9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ You can also specify options to the MJML parser.
| Head | `mj-all` | :white_check_mark: | :white_check_mark: | Feature Complete |
| Head | [mj-breakpoint](https://documentation.mjml.io/#mj-breakpoint) | :white_check_mark: | :white_check_mark: | Feature Complete |
| Head | [mj-font](https://documentation.mjml.io/#mj-font) | :white_check_mark: | :white_check_mark: | Feature Complete |
| Head | [mj-html-attributes](https://documentation.mjml.io/#mj-html-attributes) | :x: | :x: | Not Planned |
| Head | [mj-html-attributes](https://documentation.mjml.io/#mj-html-attributes) | :white_check_mark: | :white_check_mark: | Feature Complete |
| Head | [mj-preview](https://documentation.mjml.io/#mj-preview) | :white_check_mark: | :white_check_mark: | Feature Complete |
| Head | [mj-style](https://documentation.mjml.io/#mj-style) | :white_check_mark: | :white_check_mark: | Feature Complete |
| Head | [mj-title](https://documentation.mjml.io/#mj-title) | :white_check_mark: | :white_check_mark: | Feature Complete |
Expand All @@ -163,9 +163,9 @@ You can also specify options to the MJML parser.
| Body | [mj-text](https://documentation.mjml.io/#mj-text) | :white_check_mark: | :white_check_mark: | Feature Complete |
| Body | [mj-wrapper](https://documentation.mjml.io/#mj-wrapper) | :white_check_mark: | :white_check_mark: | Feature Complete |

## Inline Styles
## Inline Styles and Html Attributes

MJML supports inline styles (see: https://documentation.mjml.io/#mj-style). This is an expensive feature and can only be done after rendering. Therefore we have introduced a new post process step, that applies inline styles using the AngleSharp library. To reduce the dependencies this has been moved to a separate nuget package.
MJML supports inline styles (see: https://documentation.mjml.io/#mj-style) and html attributes (see: https://documentation.mjml.io/#mj-html-attributes). These are an expensive feature and can only be done after rendering. Therefore we have introduced a new post processing step, that applies inline styles and attributes using the AngleSharp library. To reduce the dependencies this has been moved to a separate nuget package.

```cmd
PM > Install-Package Mjml.Net.PostProcessors
Expand All @@ -176,9 +176,11 @@ You have to add the postprocessor to the options:
```csharp
var options = new MjmlOptions
{
PostProcessors = [InlineCssPostProcessor]
PostProcessors = [AngleSharpPostProcessor.Default]
};

var mjmlRenderer = new MjmlRenderer();
mjmlRenderer.AddHtmlAttributes();
var (html, errors) = await mjmlRenderer.RenderAsync(text, options);
```

Expand Down
1 change: 0 additions & 1 deletion Tests/ComplexTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Mjml.Net;
using Mjml.Net.Validators;
using Tests.Internal;
using Xunit;

namespace Tests;

Expand Down
Loading

0 comments on commit b6566ab

Please sign in to comment.