3
\$\begingroup\$

The last part of my HTML genrator requries a few styles. Because all styles are inline and the styling is for emails there won't be any fancy selectors, just by element, id or class.

The most complex css might look like this:

var css = @"
h1 { 
    font-family: Sans-Serif; color: green; font-weight: normal; margin-top: 1.5em; 
}
h2 { 
    font-family: Sans-Serif; color: green; font-weight: normal; margin-top: 1.5em; 
}
#foo, p { 
    font-family: Sans-Serif; color: green; 
}
hr, .foo { 
    border: 0; border-bottom: 1px solid #ccc; background: #ccc; 
}";

I read it into a dictionary that the StyleVisitor will consume and applay.

static class Css
{   
    public static IDictionary<string, string> Parse(string css)
    {
        return          
            Regex
                // Match "selector { declarations }"
                .Matches(css.Minify(), @"(?<selectors>[a-z0-9_\-\.,\s#]+)\s*{(?<declarations>.+?)}", RegexOptions.IgnoreCase)
                .Cast<Match>()
                .Select(m => Regex
                    // Split selectors separated by ",".
                    .Split(m.Groups["selectors"].Value, @",")
                    // Skip empty results.
                    .Where(s => !string.IsNullOrEmpty(s))
                    .Select(selector => new KeyValuePair<string, string>(
                        key: selector.Trim().ToLowerInvariant(),
                        value: m.Groups["declarations"].Value.Trim())))
                .SelectMany(x => x)
                .ToDictionary(x => x.Key, x => x.Value);
    }

    private static string Minify(this string css) => Regex.Replace(css, @"(\r\n|\r|\n)", string.Empty);
}

I'm assuming the css does not require any vaidation because it's been created and validated with an editor like jsfiddle etc.

The result of Css.Parse(css) is

h1    font-family: Sans-Serif; color: green; font-weight: normal; margin-top: 1.5em;
h2    font-family: Sans-Serif; color: green; font-weight: normal; margin-top: 1.5em;
#foo  font-family: Sans-Serif; color: green;
p     font-family: Sans-Serif; color: green;
hr    border: 0; border-bottom: 1px solid #ccc; background: #ccc
.foo  border: 0; border-bottom: 1px solid #ccc; background: #ccc
\$\endgroup\$
1
  • 1
    \$\begingroup\$ What a sweet LINQ expression :) \$\endgroup\$
    – Denis
    Jun 15, 2017 at 16:44

2 Answers 2

2
\$\begingroup\$

Some helper functions can make the query look a lot cleaner. LINQ is quite readable on it's own but in your case it has gotten a bit more complex and it's not exactly easy to modify something.

Example of how the main method can look:

public static IDictionary<string, string> Parse(string css)
{
    return GetSelectorMatches(css.Minify())
        .Select(match => new {match, selectors = ExtractSelectorsByPattern(match, ",")})
        .SelectMany(t => SelectorToDeclarationValue(t.selectors, t.match))
        .ToDictionary(pair => pair.Key, pair => pair.Value);
}

Where the methods have similar implementation to this one.

private static IEnumerable<Match> GetSelectorMatches(string css)
{
    return Regex.Matches(css, @"(?<selectors>[a-z0-9_\-\.,\s#]+)\s*{(?<declarations>.+?)}",
        RegexOptions.IgnoreCase).Cast<Match>();
}

private static IEnumerable<string> ExtractSelectorsByPattern(Match match, string pattern)
{
    return Regex.Split(match.Groups["selectors"].Value, pattern).Where(s => !string.IsNullOrEmpty(s));
}

private static IEnumerable<KeyValuePair<string, string>> SelectorToDeclarationValue(
    IEnumerable<string> selectors, Match match)
{
    return selectors.Select(selector => new KeyValuePair<string, string>(
        selector.Trim().ToLowerInvariant(),
        match.Groups["declarations"].Value.Trim()));
}

I'm not sure if it makes sense to have .Where(s => !string.IsNullOrEmpty(s)) in the method like this maybe additional bool parameter removeEmpty can decide that.

Besides that you can use some Dependency Injection to modify the delegates passed in most of the methods as they're quite specific I really cant provide an example of that as I'm not aware what makes sense to your project to be modifiable and what not.

You can even go one step further and implement some sort of class hierarchy and pass interfaces instead.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ I should definitelly extract more methods and I was also thinking of unstaticfying it for DI - if there is even such a word ;-) \$\endgroup\$
    – t3chb0t
    Jun 15, 2017 at 17:39
  • \$\begingroup\$ Hooray, 5k new privileges! ;-D oh, the lame wiki-tag approval ;-] \$\endgroup\$
    – t3chb0t
    Jun 15, 2017 at 17:41
  • \$\begingroup\$ I'm interested in seeing a DI version of this, especially with classes and interfaces, if you end up doing that consider posting a follow-up maybe? \$\endgroup\$
    – Denis
    Jun 15, 2017 at 17:41
  • 1
    \$\begingroup\$ Haha yeah just got it today, ehh the privilege is fine, it's not like I'm paying for it (oh wait in a way I'm..) \$\endgroup\$
    – Denis
    Jun 15, 2017 at 17:42
  • \$\begingroup\$ We'll see if it won't become to large for question :-) I'm still designing it and not all pieces fit entirely together yet. \$\endgroup\$
    – t3chb0t
    Jun 15, 2017 at 17:45
1
\$\begingroup\$

This is what I turned it into:

  • extracted an interface for DI
  • added a few null checks
  • made the methods smaller

The main interface and class:

public interface ICssParser
{
    [NotNull]
    Css Parse([NotNull] string css);
}

public class SimpleCssParser : ICssParser
{
    public Css Parse(string css)
    {
        if (css == null) throw new ArgumentNullException(nameof(css));
        return new Css(MatchRules(css));
    }

    [NotNull]
    [ItemNotNull]
    private static IEnumerable<CssRule> MatchRules(string css)
    {
        return
            Regex
                .Matches(css.Minify(), @"(?<selectors>[a-z0-9_\-\.,\s#]+)\s*{(?<declarations>.+?)}", RegexOptions.IgnoreCase)
                .Cast<Match>()
                .Select(m => new CssRule(
                        selectors: SplitSelectors(m.Groups["selectors"].Value),
                        declarations: m.Groups["declarations"].Value.Trim()));
    }

    [NotNull]
    [ItemNotNull]
    private static IEnumerable<string> SplitSelectors(string selectors)
    {
        return
            Regex
                .Split(selectors, @",")
                .Where(Conditional.IsNotNullOrEmpty)
                .Select(selector => selector.Trim().ToLowerInvariant());
    }
}

Supporting data structures:

[PublicAPI]
public class CssRule
{
    private ISet<string> _selectors = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

    public CssRule() { }

    public CssRule([NotNull] IEnumerable<string> selectors, [NotNull] string declarations)
    {
        if (selectors == null) throw new ArgumentNullException(nameof(selectors));

        Selectors = new HashSet<string>(selectors, StringComparer.OrdinalIgnoreCase);
        Declarations = declarations ?? throw new ArgumentNullException(nameof(declarations));
    }

    [NotNull]
    [ItemNotNull]
    public ISet<string> Selectors
    {
        get => _selectors;
        set => _selectors = value ?? throw new ArgumentNullException(nameof(Selectors));
    }

    [CanBeNull]
    public string Declarations { get; set; }
}

Simple css:

[PublicAPI]
public class Css : IEnumerable<CssRule>
{
    private readonly List<CssRule> _cssRules = new List<CssRule>();

    public Css() { }

    public Css([NotNull] IEnumerable<CssRule> cssRules)
    {
        if (cssRules == null) throw new ArgumentNullException(nameof(cssRules));
        _cssRules.AddRange(cssRules);
    }

    public void Add([NotNull] CssRule cssRule)
    {
        if (cssRule == null) throw new ArgumentNullException(nameof(cssRule));
        _cssRules.Add(cssRule);
    }

    public IEnumerator<CssRule> GetEnumerator() => _cssRules.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public static implicit operator Dictionary<string, string>(Css css)
    {
        return
            css
                .SelectMany(rule => rule
                    .Selectors
                    .Select(selector => new KeyValuePair<string, string>(selector, rule.Declarations)))
                .ToDictionary(x => x.Key, x => x.Value);
    }
}

Some helpers:

internal static class StringExtensions
{
    /// <summary>
    /// Removes line breakes form a string.
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public static string Minify(this string value) => Regex.Replace(value, @"(\r\n|\r|\n)", string.Empty);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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