I need to apply some css styles to an IMarkupElement
. The generated HTML is only for emails so all styles are inline styles and the selectors are very simple, just element names, ids or class names can be used. I'm not sure whether a Visitor
is the right tool to apply styles to elements but it seemed to fit for the task.
This is the IMarkupElment
:
public interface IMarkupElement : ICollection<object>, IFormattable
{
string Name { get; }
IDictionary<string, string> Attributes { get; }
IMarkupElement Parent { get; set; }
int Depth { get; }
}
The StyleVisitor
works recursively and finds all IMarkupElement
nodes and redirects the style creations to the Element
method. Styles are provided via a Dictionary<string, string>
where the key is a single selector like element name, #id or .class. I have another module that parses a very simple css file into such a dictionary.
public class StyleVisitor
{
private readonly IDictionary<string, string> _styles;
public StyleVisitor([NotNull] IDictionary<string, string> styles)
{
_styles = styles ?? throw new ArgumentNullException(nameof(styles));
}
public IMarkupElement Visit([NotNull] IMarkupElement element)
{
if (element == null) throw new ArgumentNullException(nameof(element));
var selectors = GetSelectors(element).Distinct(StringComparer.OrdinalIgnoreCase);
var style = Element(selectors);
if (string.IsNullOrEmpty(style))
{
element.Attributes.Remove("style");
}
else
{
element.Attributes["style"] = style;
}
foreach (var child in element)
{
if (child is IMarkupElement e)
{
Visit(e);
}
}
return element;
}
// Gets element, id and class selectors.
private static IEnumerable<string> GetSelectors(IMarkupElement element)
{
yield return element.Name;
if (element.Attributes.TryGetValue("id", out var id))
{
yield return $"#{id}";
}
if (element.Attributes.TryGetValue("class", out var classes))
{
foreach (var className in Regex.Split(classes, @"\s+").Select(className => className.Trim()))
{
yield return $".{className}";
}
}
}
private string Element(IEnumerable<string> selectors)
{
var styles = new StringBuilder();
foreach (var selector in selectors)
{
if (_styles.TryGetValue(selector, out var style))
{
// Fix the ";" but trim it first in case there is already one to avoid an "if".
styles
.Append(style.Trim().TrimEnd(';'))
.Append(";");
}
}
return styles.ToString();
}
}
This is the test that passes that I wrote for it so far:
[TestClass]
public class StyleVisitorTest
{
private static readonly IMarkupElement Html = MarkupElement.Builder;
[TestMethod]
public void Visit_WithStyles_Applied()
{
var html = Html
.Element("p", p => p
.Append("foo ")
.Element("span", span => span
.Attribute("class", "qux")
.Append("bar"))
.Append(" baz"));
Assert.AreEqual(@"<p>foo <span class=""qux"">bar</span> baz</p>", html.ToHtml());
var styleVisitor = new StyleVisitor(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
[".qux"] = "font-family: sans-serif;"
});
html = styleVisitor.Visit(html);
Assert.AreEqual(@"<p>foo <span class=""qux"" style=""font-family: sans-serif;"">bar</span> baz</p>", html.ToHtml());
}
}
Before I write more complex html and tests I wanted to ask you what do you think about the StyleVisitor
?
(Just for reference: this is build on top of an improved version of the Functional Html builder)