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);
}