PreMailer.Net icon indicating copy to clipboard operation
PreMailer.Net copied to clipboard

Resolve css var's with their actual values?

Open jmalatia opened this issue 2 years ago • 2 comments

Is there a way to resolve css variables into their actual values?

css

:root {
    --my-css-color-var: blue;
}

.my-class {
    color: var(--my-css-color-var);
}

html

<html>
    <body>
        <div class="my-class">Hello</div>
    </body>
</html>

html > after inlining with PreMailer.net

<html style="--my-css-color-var: blue;">
    <body>
        <div style="color: var(--my-css-color-var)">Hello</div>
    </body>
</html>

Which doesn't work with web mail (at least Gmail because they strip html attributes).

Also current Bootstrap versions are now predominantly using css variables which only adds to this issue and a need to resolve the variables into their actual values.

Any guidance would be appreciated.

jmalatia avatar Feb 02 '23 21:02 jmalatia

I was able to inline the CSS variables with the following code - it's not perfect (expect issues if you mix var() with other CSS functions), so please let me know if you make improvements, but otherwise you can use the following under the MIT license. All the assemblies you need should be included with the latest PreMailer.NET release.


using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using AngleSharp.Html;
using PreMailer.Net;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
using AngleSharp.Html.Dom;

partial class EmailGenerationService
{
	private readonly HtmlParser _htmlParser = new ();
	private readonly CssParser _cssParser = new ();

	public async Task<Stream> ToEmailReadyHtml(string htmlContent)
	{
		var result = PreMailer.Net.PreMailer.MoveCssInline(htmlContent, removeStyleElements: true, stripIdAndClassAttributes: true, removeComments: true);

		var ms = new MemoryStream();
		var streamWriter = new StreamWriter(ms);
		InlineCssVariables(result.Html).ToHtml(streamWriter, HtmlMarkupFormatter.Instance);
		await streamWriter.FlushAsync();
		ms.Position = 0;
		return ms;
	}

	private IHtmlDocument InlineCssVariables(string html)
	{
		var doc = _htmlParser.ParseDocument(html);

		InlineCssVariables(doc.DocumentElement, ImmutableDictionary<string, string?>.Empty);

		return doc;
	}

	private void InlineCssVariables(IElement documentElement, ImmutableDictionary<string, string?> cssVariables)
	{
		if (documentElement.GetAttribute("style") is string styles)
		{
			var styleProps = _cssParser.ParseStyleClass("inline", styles);
			cssVariables = cssVariables.AddRange(
				from cssStyle in styleProps.Attributes
				where cssStyle.Style.StartsWith("--")
				select new KeyValuePair<string, string?>(cssStyle.Style, Evaluate(cssStyle.Value))
			);

			foreach (var cssStyle in styleProps.Attributes.ToArray())
			{
				if (cssStyle.Style.StartsWith("--"))
				{
					styleProps.Attributes.Remove(cssStyle.Style);
					continue;
				}
				var value = Evaluate(cssStyle.Value);
				if (value == null)
					styleProps.Attributes.Remove(cssStyle.Style);
				else if (cssStyle.Value != value)
					cssStyle.Value = value;
			}
			documentElement.SetAttribute("style", styleProps.ToString());
		}

		foreach (var elem in documentElement.Children)
			InlineCssVariables(elem, cssVariables);

		string? Evaluate(string value)
		{
			var match = CssVariableFunction().Match(value);
			if (!match.Success)
				return value;

			return cssVariables.TryGetValue(match.Groups["varName"].Value, out var variableValue)
				? variableValue
				: match.Groups.TryGetValue("defaultValue", out var group) ? group.Value : null;
		}
	}

	[GeneratedRegex("""var\((?<varName>--[^ ,]+)(, (?<defaultValue>.*))?\)""")]
	private static partial Regex CssVariableFunction();
}

mdekrey avatar Dec 24 '24 18:12 mdekrey

Successfuly implemented the solution in our PreMailer wrapper class. Since we only use simple variables (not the more complex functions) for email layouts, I don't expect to run into issues (yet). Thanks for the code.

whorchner avatar Jan 19 '25 09:01 whorchner