A T4 Template To Get All CSS Class Names

Metaprogramming For Fun And Profit

Published on Friday, October 10, 2014

This post combines two of my favorite things: meta-programming and the elimination of magic strings. The goal is to automatically generate a class with static const strings containing the name of all the CSS classes in your CSS files. Why would you want to do this? There are a number of reasons. First, it helps eliminate magic strings from your view code. Instead of writing <p class="my-class"> you can write <p class="@Css.MyClass">. It also helps when writing view code because you'll have access to IntelliSense data for all of your CSS classes, making it easier to remember their names and avoid mistakes. Finally, it improves analysis and refactoring because you can now rely on code engines to locate and operate on uses of a particular const string instead of just plain-text searching.

To accomplish this, we're going to use a T4 template. If you're not familiar with T4 templates, they're a technology that was added to Visual Studio and are "a mixture of text blocks and control logic that can generate a text file". In short, they let you create code or other content for your application before compile time. They're very powerful, but are also a little like salting your food: a little bit goes a long way and you don't want to use too much.

This particular T4 template iterates over all the .css files in your web project, extracts all the CSS classes, and then spits them out into a series of const strings within a static class called Css. To use it, just create a new file at the root of your project called Css.tt and paste in the code below. Typically, T4 templates are only executed in Visual Studio when they change, not when other files in the project change. Since this template is actually looking at the .css files in your project, you may want a way to run it on every build. For that, I recommend installing AutoT4, which will automatically run all of your T4 templates on every build.

Here is the code for the T4 template. If it looks a little funny, don't worry. The T4 syntax is outside the scope of this blog post, but there are plenty of resources if you'd like to learn more.

<#@ template language="C#" hostSpecific="true" #>
<#@ assembly name="System.Core" #> 
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<# Process(); #>
<#+
	// Regex for CSS classes from http://paul.kinlan.me/regex-to-get-class-names-from-css-2-0/
	string cssClassRegex = @"\.[-]?[_a-zA-Z][_a-zA-Z0-9-]*|[^\0-\177]*\\[0-9a-f]{1,6}(\r\n[ \n\r\t\f])?|\\[^\n\r\f0-9a-f]*";
	// Regexes for removing comments from http://stackoverflow.com/questions/3524317/regex-to-strip-line-comments-from-c-sharp/3524689#3524689
	string blockComments = @"/\*(.*?)\*/";
	string lineComments = @"//(.*?)\r?\n";
	string strings = @"""((\\[^\n]|[^""\n])*)""";
	string verbatimStrings = @"@(""[^""]*"")+";
	public void Process()
	{
		WriteLine("public static class Css");
		WriteLine("{");
		// Iterate all CSS files in the solution
		HashSet<string> cssClasses = new HashSet<string>();
		foreach(string fileName in Directory.GetFiles(Host.ResolvePath(@"."), "*.css", SearchOption.AllDirectories))
		{
			// Read the CSS file and strip comments
			string css = System.IO.File.ReadAllText(fileName);
			css = css.Replace("\r\n", "\n");
			css = Regex.Replace(css,
				blockComments + "|" + lineComments + "|" + strings + "|" + verbatimStrings,
				me => {
					if (me.Value.StartsWith("/*") || me.Value.StartsWith("//"))
						return me.Value.StartsWith("//") ? Environment.NewLine : "";
					// Keep the literal strings
					return me.Value;
				},
				RegexOptions.Singleline);
			// Get all CSS classes in the file
			foreach (Match match in Regex.Matches(css, cssClassRegex))
			{
				if(match.Success && !string.IsNullOrWhiteSpace(match.Groups[0].Value) && !match.Groups[0].Value.StartsWith(@"\"))
				{
					cssClasses.Add(match.Groups[0].Value.Substring(1));	
				}
			}
		}
		// Generate the const strings
		HashSet<string> constNames = new HashSet<string>();
		foreach(string cssClass in cssClasses.OrderBy(x => x))
		{
			string constName = String.Join(null, cssClass.Split(new char[]{'-'}, StringSplitOptions.RemoveEmptyEntries)
				.Select(x => char.ToUpper(x[0]).ToString() + x.Substring(1)));
			string uniqueConstName = constName;
			int i = 1;
			while(!constNames.Add(uniqueConstName))
			{
				uniqueConstName = constName + "_" + i++;
			}
			WriteLine("\tpublic const string " + uniqueConstName + " = \"" + cssClass + "\";");	
		}
		WriteLine("}");
	}
#>
comments powered by Disqus