In my library for styling the Console
I need to be able to add or update two styles: color
and background-color
.
The use case is very simple. There is a string with styles that I’m building and that I update to change the color if necessary.
I divided this task into three steps:
- explode the string into declarations
- manipulate the style
- reconstruct the style
Example Each of these steps has its own extension:
void Main() { var style = "color: red;"; style .ToDeclarations() .AddOrUpdate( ("color", "black"), ("background-color", "blue") ) .ToStyle() .Dump(); }
These extensions are used by higher-level extensions that call them with const string
for property names. I just used simple strings in LINQPad.
Code Their implementation is very straightforward. They use only LINQ and tuples.
public static class CssExtensions { public static IEnumerable<(string property, string value)> ToDeclarations( this string style) { const string declarationPattern = @"(?<property>[a-z-]+):\s*(?<value>.+?)(;|$ )"; return Regex .Matches(style, declarationPattern, RegexOptions.IgnoreCase) .Cast<Match>() .Select(m => ( property: m.Value("property").Trim(), value: m.Value("value").Trim()) ); } public static IEnumerable<(string property, string value)> AddOrUpdate( this IEnumerable<(string property, string value)> declarations, params (string property, string value)[] updates) { return declarations .Concat(updates) .GroupBy(t => t.property) .Select(t => t.Last()) .Skip(t => string.IsNullOrEmpty(t.value)); } public static string ToStyle( this IEnumerable<(string property, string value)> declarations) { return declarations .Select(t => $ "{t.property}: {t.value};") .Join(" "); } }
As the use case is very simple and I also don’t need to parse any complex styles so are the extensions, just a simple regex with some string manipulation.
I update the style by grouping by the property
and pick the last one if there are mutliple ones, if there is none yet, then a new style is added. If the value happens to be null
or Empty
then it’s removed.
There are three more extensions: one that I use to Join
declarations, one Value
shortcut for regex and a Skip
that I use as an inverse of Where
to use positive conditions if I want to exclude some cases.
public static class EnumerableExtensions { public static string Join<T>(this IEnumerable<T> values, string separator) { return string.Join(separator, values); } public static IEnumerable<T> Skip<T>(this IEnumerable<T> source, Func<T, bool> predicate) { return source.Where(x => !predicate(x)); } } public static class MatchExtensions { public static string Value(this Match match, string groupName) { return match.Groups[groupName].Value; } }
Should I still improve it in any way?