Skip to content

Commit

Permalink
Inline css (#200)
Browse files Browse the repository at this point in the history
* Fix tests.

* Temp

* CSS Inline. Close #111

* Fix tests

* Small refactoring.

* Fix build.
  • Loading branch information
SebastianStehle authored Sep 18, 2024
1 parent 103bd37 commit be97935
Show file tree
Hide file tree
Showing 25 changed files with 465 additions and 45 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>3.14.0</Version>
<Version>3.15.0</Version>
</PropertyGroup>
</Project>
47 changes: 47 additions & 0 deletions Html.Net.PostProcessors/Html.Net.PostProcessors.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NeutralLanguage>en</NeutralLanguage>
<LangVersion>latest</LangVersion>
<RootNamespace>Mjml.Net</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.2" />
<PackageReference Include="AngleSharp.Css" Version="1.0.0-beta.139" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.145">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" />
</ItemGroup>

<ItemGroup>
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>

<None Include="icon.png">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Mjml.Net\Mjml.Net.csproj" />
</ItemGroup>

</Project>
105 changes: 105 additions & 0 deletions Html.Net.PostProcessors/InlineCssPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using AngleSharp;
using AngleSharp.Css;
using AngleSharp.Dom;
using Mjml.Net;

namespace Html.Net;

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

public static readonly InlineCssPostProcessor Instance = new InlineCssPostProcessor();

private InlineCssPostProcessor()
{
}

public async ValueTask<string> PostProcessAsync(string html, 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;
}

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

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

private static void InlineStyle(IElement element)
{
var currentStyle = element.ComputeCurrentStyle();

if (currentStyle.Any())
{
var css = currentStyle.ToCss();

element.SetAttribute(TagNames.Style, css);
}
}

private static void RenameNonInline(IElement element, IDocument document)
{
if (string.Equals(element.TagName, TagNames.Style, StringComparison.OrdinalIgnoreCase) && !IsInline(element))
{
RenameTag(element, FallbackStyle, document);
}
}

private static void RestoreNonInline(IElement element, IDocument document)
{
if (string.Equals(element.TagName, FallbackStyle, StringComparison.OrdinalIgnoreCase))
{
RenameTag(element, TagNames.Style, document);
}

if (string.Equals(element.TagName, TagNames.Style, StringComparison.OrdinalIgnoreCase) && IsInline(element))
{
element.Remove();
}
}

private static bool IsInline(IElement element)
{
return element.HasAttribute("inline");
}

private static void RenameTag(IElement node, string tagName, IDocument document)
{
var clone = document.CreateElement(tagName);

foreach (var attribute in node.Attributes)
{
clone.SetAttribute(attribute.NamespaceUri, attribute.Name, attribute.Value);
}

var parent = node.Parent!;

clone.InnerHtml = node.InnerHtml;

parent.InsertBefore(clone, node);
parent.RemoveChild(node);
}
}
Binary file added Html.Net.PostProcessors/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 0 additions & 7 deletions Mjml.Net.Benchmark/TemplateBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ namespace Mjml.Net.Benchmarking;
public class TemplateBenchmarks
{
private static readonly MjmlOptions WithBeautify = new MjmlOptions { Beautify = true };
private static readonly MjmlOptions WithMinify = new MjmlOptions { Minify = true };
private readonly MjmlRenderer MjmlRenderer;

[ParamsSource(nameof(MjmlTemplates))]
Expand Down Expand Up @@ -61,10 +60,4 @@ public string Render_Template_Beautify()
{
return MjmlRenderer.Render(MjmlTemplate, WithBeautify).Html;
}

[Benchmark]
public string Render_Template_Minify()
{
return MjmlRenderer.Render(MjmlTemplate, WithMinify).Html;
}
}
6 changes: 6 additions & 0 deletions Mjml.Net.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ 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}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -39,6 +41,10 @@ Global
{3969326A-B528-4120-8E30-4127F66B638E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3969326A-B528-4120-8E30-4127F66B638E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3969326A-B528-4120-8E30-4127F66B638E}.Release|Any CPU.Build.0 = Release|Any CPU
{BADECB72-90D2-44F3-AA93-27B0247C8FA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BADECB72-90D2-44F3-AA93-27B0247C8FA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BADECB72-90D2-44F3-AA93-27B0247C8FA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BADECB72-90D2-44F3-AA93-27B0247C8FA9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 2 additions & 0 deletions Mjml.Net/AttributeTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public static class AttributeTypes

public static readonly IType IncludeType = new EnumType(true, "mjml", "html", "css");

public static readonly IType Inline = new EnumType(true, "inline");

public static readonly IType Pixels = new NumberType(Unit.Pixels);

public static readonly IType PixelsOrAuto = new OneOfType(new EnumType(false, "auto"), Pixels);
Expand Down
1 change: 1 addition & 0 deletions Mjml.Net/BindType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum BindType
File,
FourPixelsOrPercent,
LeftRight,
Inline,
Pixels,
PixelsOrAuto,
PixelsOrEm,
Expand Down
6 changes: 4 additions & 2 deletions Mjml.Net/Components/Head/StyleComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public partial class StyleComponent : HeadComponentBase

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

[Bind("inline")]
[Bind("inline", BindType.Inline)]
public string? Inline;

[BindText]
Expand All @@ -19,7 +19,9 @@ public override void Render(IHtmlRenderer renderer, GlobalContext context)
// Just in case that validation is disabled.
if (Text != null)
{
var style = Style.Static(Text);
var isInline = string.Equals(Inline, "inline", StringComparison.OrdinalIgnoreCase);

var style = Style.Static(Text, isInline);

// Allow multiple styles.
context.AddGlobalData(style);
Expand Down
7 changes: 6 additions & 1 deletion Mjml.Net/Components/IncludeComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public sealed partial class IncludeComponent : Component
[Bind("path", BindType.RequiredString)]
public string Path;

[Bind("css-inline", BindType.Inline)]
public string? CssInline;

[Bind("type", typeof(TypeValidator))]
public string Type;

Expand Down Expand Up @@ -153,8 +156,10 @@ public override void Render(IHtmlRenderer renderer, GlobalContext context)
}
else if (ActualType == IncludeType.Css)
{
var isInline = string.Equals(CssInline, "inline", StringComparison.OrdinalIgnoreCase);

// Allow multiple styles and render them later.
context.AddGlobalData(Style.Static(new InnerTextOrHtml(content)));
context.AddGlobalData(Style.Static(new InnerTextOrHtml(content), isInline));
}
}

Expand Down
20 changes: 16 additions & 4 deletions Mjml.Net/Helpers/Style.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

namespace Mjml.Net.Helpers;

public sealed record Style(Action<IHtmlRenderer, GlobalContext> Renderer) : GlobalData
public sealed record Style(Action<IHtmlRenderer, GlobalContext> Renderer, bool Inline = false) : GlobalData
{
public static Style Static(InnerTextOrHtml text)
public static Style Static(InnerTextOrHtml text, bool inline)
{
return new Style((renderer, _) => renderer.Content(text));
return new Style((renderer, _) => renderer.Content(text), inline);
}
}

Expand Down Expand Up @@ -65,11 +65,23 @@ private static void WriteMediaQueriesThunderbird(IHtmlRenderer renderer, GlobalC
}

private static void WriteStyles(IHtmlRenderer renderer, GlobalContext context)
{
WriteStyles(renderer, context, context.GlobalData.Values.OfType<Style>().Where(x => !x.Inline), null);

var inlineStyles = context.GlobalData.Values.OfType<Style>().Where(x => x.Inline).ToList();
if (inlineStyles.Count > 0)
{
WriteStyles(renderer, context, inlineStyles, "inline");
}
}

private static void WriteStyles(IHtmlRenderer renderer, GlobalContext context, IEnumerable<Style> inline, string? inlineAttr)
{
renderer.StartElement("style")
.Attr("inline", inlineAttr)
.Attr("type", "text/css");

foreach (var style in context.GlobalData.Values.OfType<Style>())
foreach (var style in inline)
{
style.Renderer(renderer, context);
}
Expand Down
49 changes: 47 additions & 2 deletions Mjml.Net/IMjmlRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public interface IMjmlRenderer
/// The result of rendering, including the HTML and validation errors.
/// </returns>
/// <remarks>
/// This method is thread safe.
/// This method is thread safe. Post processors are not run in the sync method.
/// </remarks>
RenderResult Render(Stream mjml, MjmlOptions? options = null);

Expand All @@ -105,7 +105,52 @@ public interface IMjmlRenderer
/// The result of rendering, including the HTML and validation errors.
/// </returns>
/// <remarks>
/// This method is thread safe.
/// This method is thread safe. Post processors are not run in the sync method.
/// </remarks>
RenderResult Render(TextReader mjml, MjmlOptions? options = null);

/// <summary>
/// Renders MJML from a string.
/// </summary>
/// <param name="mjml">The MJML as string.</param>
/// <param name="options">Optional options.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// The result of rendering, including the HTML and validation errors.
/// </returns>
/// <remarks>
/// This method is thread safe. Post processors are not run in the sync method.
/// </remarks>
ValueTask<RenderResult> RenderAsync(string mjml, MjmlOptions? options = null,
CancellationToken ct = default);

/// <summary>
/// Renders MJML from a stream.
/// </summary>
/// <param name="mjml">The MJML as stream.</param>
/// <param name="options">Optional options.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// The result of rendering, including the HTML and validation errors.
/// </returns>
/// <remarks>
/// This method is thread safe.
/// </remarks>
ValueTask<RenderResult> RenderAsync(Stream mjml, MjmlOptions? options = null,
CancellationToken ct = default);

/// <summary>
/// Renders MJML from a text renderer.
/// </summary>
/// <param name="mjml">The MJML as text renderer.</param>
/// <param name="options">Optional options.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>
/// The result of rendering, including the HTML and validation errors.
/// </returns>
/// <remarks>
/// This method is thread safe.
/// </remarks>
ValueTask<RenderResult> RenderAsync(TextReader mjml, MjmlOptions? options = null,
CancellationToken ct = default);
}
7 changes: 7 additions & 0 deletions Mjml.Net/IPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Mjml.Net;

public interface IPostProcessor
{
ValueTask<string> PostProcessAsync(string html, MjmlOptions options,
CancellationToken ct);
}
1 change: 0 additions & 1 deletion Mjml.Net/Internal/HtmlReaderWrapper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using HtmlPerformanceKit;
using System;
using HtmlReaderImpl = HtmlPerformanceKit.HtmlReader;

namespace Mjml.Net.Internal;
Expand Down
Loading

0 comments on commit be97935

Please sign in to comment.