3
\$\begingroup\$

As part of a larger project I've created recently a simple CssBuilder and CssParser. It's composed of three classes and can parse/build inline and block CSS. Its main purpose was to render CSS for inline styles for email formatting (which is the rest of the project (maybe another time)). I also tried to design a fluent API here.


CssBuilder

public class CssBuilder
{
    public string Selector { get; set; }

    public IList<CssDeclaration> Declarations { get; internal set; } = new List<CssDeclaration>();

    public static CssBuilder Create()
    {
        return new CssBuilder();
    }

    public static CssBuilder CreateFor(string selectror)
    {
        return new CssBuilder { Selector = selectror };
    }

    public static CssBuilder CreateFrom(string css)
    {
        return CssParser.ParseCss(css);
    }

    public CssBuilder Declare(string property, string value)
    {
        Declarations.Add(new CssDeclaration
        {
            Property = property,
            Value = value.TrimEnd(';')
        });
        return this;
    }

    public string BuildCss()
    {
        return string.IsNullOrEmpty(Selector) ? BuildInlineCss(this) : BuildBlockCss(this);
    }

    internal string BuildInlineCss(CssBuilder builder)
    {
        return builder.Declarations.Aggregate(string.Empty, (current, next) => string.IsNullOrEmpty(current) ? next.ToString() : $"{current} {next}");
    }

    internal string BuildBlockCss(CssBuilder builder)
    {
        const int indentWidth = 3;

        var block = new StringBuilder();
        block.Append(builder.Selector);
        block.AppendLine(" {");
        foreach (var declaration in builder.Declarations)
        {
            block.Append(string.Empty.PadLeft(indentWidth));
            block.AppendLine(declaration.ToString());
        }
        block.Append("}");

        return block.ToString();
    }
}

CssParser

public class CssParser
{
    public static CssBuilder ParseCss(string css)
    {
        var builder = new CssBuilder();

        const string selectorPattern = @"(?<Selector>.+?)(?=\s*{)";
        var selectorMatch = Regex.Match(css, selectorPattern, RegexOptions.ExplicitCapture);
        if (selectorMatch.Success)
        {
            builder.Selector = selectorMatch.Groups["Selector"].Value;

            const string declarationsPattern = @"{\s*(?<Declarations>.+)\s*}";
            var declarationsMatch = Regex.Match(css, declarationsPattern, RegexOptions.ExplicitCapture | RegexOptions.Singleline);
            builder.Declarations = ParseDeclarations(declarationsMatch.Groups["Declarations"].Value).ToList();
        }
        else
        {
            builder.Declarations = ParseDeclarations(css).ToList();
        }

        return builder;
    }

    internal static IEnumerable<CssDeclaration> ParseDeclarations(string declarations)
    {
        const string declarationPattern = @"\s*(?<Property>.+?):\s*(?<Value>.+?);";
        var declarationMatches = Regex.Matches(declarations, declarationPattern, RegexOptions.ExplicitCapture);
        var result = declarationMatches.Cast<Match>()
            .Select(m => new CssDeclaration
            {
                Property = m.Groups["Property"].Value,
                Value = m.Groups["Value"].Value
            });
        return result;
    }
}

CssDeclaration

public class CssDeclaration
{
    public string Property { get; set; }
    public string Value { get; set; }

    public override string ToString()
    {
        return $"{Property}: {Value};";
    }
}

CssParserTests & Usage

[TestMethod]
public void ParseCss_Inline()
{
    var builder = CssParser.ParseCss("color: blue; font-family: sans-serif;");
    Assert.AreEqual(2, builder.Declarations.Count);
    Assert.AreEqual("color: blue;", builder.Declarations[0].ToString());
    Assert.AreEqual("font-family: sans-serif;", builder.Declarations[1].ToString());
}

CssBuilderTests & Usage

[TestMethod]
public void BuildInlineCss()
{
    var css = CssBuilder.Create().Declare("color", "blue").Declare("font-family", "sans-serif").BuildCss();
    Assert.AreEqual("color: blue; font-family: sans-serif;", css);
}

[TestMethod]
public void BuildBlockCss()
{
    var css = CssBuilder.CreateFor("p").Declare("color", "blue").Declare("font-family", "sans-serif").BuildCss();
    var block =
@"p {
color: blue;
font-family: sans-serif;
}";
    Assert.AreEqual(block, css);
}
\$\endgroup\$
  • \$\begingroup\$ I don't know what's your scenario, but I've had a little experience with building CSS/JS from the server dynamically and it ended up being a mess. Be cautious :) \$\endgroup\$ – IEatBagels Nov 2 '15 at 17:54
  • \$\begingroup\$ @TopinFrassi it wasn't anything big just adding some styles to autogenerated emails so the main goal was to create inline css. I've added blocks later just for sport and for later use \$\endgroup\$ – t3chb0t Nov 2 '15 at 18:04
2
\$\begingroup\$

CssBuilder

internal string BuildBlockCss(CssBuilder builder)
{
    const int indentWidth = 3;

    var block = new StringBuilder();
    block.Append(builder.Selector);
    block.AppendLine(" {");
    foreach (var declaration in builder.Declarations)
    {
        block.Append(string.Empty.PadLeft(indentWidth));
        block.AppendLine(declaration.ToString());
    }
    block.Append("}");

    return block.ToString();
}  

Instead of having a const int indentWidth I would prefer to have either a const string of just string. In this way you wouldn't need to call string.Empty.PadLeft(indentWidth) for each iteration of the loop.

Starting the "Data section" with block.AppendLine(" {"); and ending it with block.Append("}"); (missing leading space) will look odd in the css block.

The StringBuilder's methods are implemented fluently so you could use the returned instance of the StringBuilder.
Using the overloaded constructor of the StringBuilder which takes a capacity as an argument is better if you know that the content of the StringBuilder will exceed 16 characters, because if this initial capacity is reached the current capacity is doubled.

internal string BuildBlockCss(CssBuilder builder)
{
    const int indentWidth = 3;
    string indentValue = string.Empty.PadLeft(indentWidth);

    var block = new StringBuilder(1024);
    block.Append(builder.Selector)
         .AppendLine(" {")

    foreach (var declaration in builder.Declarations)
    {
        block.Append(indentValue)
             .AppendLine(declaration.ToString());
    }

    block.Append("}");

    return block.ToString();
}  

you should also think about wether it would be good to have an optional parameter defining the indentWidth instead of hardcoding it.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.