Skip to content

Instantly share code, notes, and snippets.

@Grinderofl
Created December 19, 2019 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Grinderofl/7a71411034c6b3e624fc259fe8a28fd1 to your computer and use it in GitHub Desktop.
Save Grinderofl/7a71411034c6b3e624fc259fe8a28fd1 to your computer and use it in GitHub Desktop.
UnCSS PoC
services.AddWebOptimizer(x =>
{
x.AddBundle("/css/site.css", "text/css;charset=UTF-8", "static/css/styles.css")
.UnCss()
.MinifyCss()
.AutoPrefixCss()
.Concatenate()
.FingerprintUrls();
});
using WebOptimizer;
public static class UnCssAssetPipelineExtensions
{
public static IAsset UnCss(this IAsset bundle)
{
bundle.Processors.Add(new UnCssProcessor());
return bundle;
}
}
public static class UnCssConstants
{
public const string QueryParameter = "uc";
}
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Tokens;
public static class UnCssHelpers
{
internal static readonly Regex RefererRegex = new Regex(@"([0-9])");
public static string GetPath(this Uri uri)
{
// Remove numeric type parameters (id's etc)
var referer = RefererRegex.Replace(uri.AbsolutePath, "");
var isIdArgument = referer.EndsWith('/');
// Find first encounter of equal sign
var equalsIndex = referer.IndexOf('=');
if (equalsIndex > -1)
{
referer = referer.Substring(0, equalsIndex);
// Remove everything before equal sign but after last '/'
var paramIndex = referer.LastIndexOf('/');
if (paramIndex > -1 && !isIdArgument)
{
referer = referer.Substring(0, paramIndex);
}
}
return referer.TrimEnd('/');
}
public static string ToSHA512Hash(this string source)
{
using (var hasher = SHA512.Create())
{
var bytes = Encoding.UTF8.GetBytes(source);
var hash = hasher.ComputeHash(bytes);
return Base64UrlEncoder.Encode(hash);
}
}
public static string GetSelectorsCacheKey(this HttpContext httpContext)
{
var path = httpContext.Request.GetUri().GetPath();
var hash = path.ToSHA512Hash();
return CreateSelectorsCacheKey(hash);
}
public static string CreateSelectorsCacheKey(string hash)
{
return $"Selectors[{hash}]";
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using NUglify.Html;
[HtmlTargetElement("html")]
public class UnCssHtmlTagHelper : TagHelper
{
private readonly IDistributedCache cache;
private readonly IHttpContextAccessor httpContextAccessor;
public UnCssHtmlTagHelper(IDistributedCache cache, IHttpContextAccessor httpContextAccessor)
{
this.cache = cache;
this.httpContextAccessor = httpContextAccessor;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var cacheKey = httpContextAccessor.HttpContext.GetSelectorsCacheKey();
var cacheValue = await cache.GetStringAsync(cacheKey);
if (string.IsNullOrWhiteSpace(cacheValue))
{
var selectors = await ProcessSelectors(output);
cacheValue = JsonConvert.SerializeObject(selectors);
await cache.SetStringAsync(cacheKey, cacheValue);
}
}
private async Task<List<string>> ProcessSelectors(TagHelperOutput output)
{
var selectors = new List<string> {"*"};
void AddSelector(string selector)
{
selector = selector.ToLowerInvariant();
if (!selectors.Contains(selector))
{
selectors.Add(selector);
}
}
var childContent = await output.GetChildContentAsync(true);
var content = childContent.GetContent();
var parser = new HtmlParser(content);
var descendants = parser.Parse();
foreach (var htmlElement in descendants.FindAllDescendants().OfType<HtmlElement>())
{
AddSelector(htmlElement.Name);
if (htmlElement.Attributes == null)
{
continue;
}
foreach (var attribute in htmlElement.Attributes)
{
if (attribute.Name.Equals("class", StringComparison.OrdinalIgnoreCase))
{
var classNames = attribute.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
foreach (var className in classNames)
{
AddSelector($".{className}");
}
continue;
}
if (attribute.Name.Equals("id", StringComparison.OrdinalIgnoreCase))
{
AddSelector($"#{attribute.Value}");
continue;
}
if (attribute.Value == null)
{
continue;
}
var values = attribute.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
foreach (var value in values)
{
AddSelector($"[{attribute.Name}=\"{value}\"]");
}
}
}
return selectors;
}
}
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using WebOptimizer;
using WebOptimizer.Taghelpers;
[HtmlTargetElement("link")]
public class UnCssLinkTagHelper : LinkTagHelper
{
private readonly IHttpContextAccessor httpContextAccessor;
public UnCssLinkTagHelper(IHttpContextAccessor httpContextAccessor,
IHostingEnvironment env, IMemoryCache memoryCache, IAssetPipeline pipeline,
IOptionsMonitor<WebOptimizerOptions> options)
: base(env, memoryCache, pipeline, options)
{
this.httpContextAccessor = httpContextAccessor;
}
[HtmlAttributeName("href")]
public string Href { get; set; }
public override int Order => 20;
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var cacheKey = httpContextAccessor.HttpContext.GetSelectorsCacheKey();
var href = context.AllAttributes.TryGetAttribute("href", out var attribute)
? (string) attribute.Value
: Href;
if (href.Contains("?"))
{
href += "&";
}
else
{
href += "?";
}
href += $"un={cacheKey}";
output.Attributes.SetAttribute("href", href);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ExCSS;
public class UnCssParser : IDisposable
{
private StylesheetParser parser;
public UnCssParser()
{
parser = new StylesheetParser();
}
public async Task<string> ParseAsync(string source, IList<string> selectors)
{
var parsed = await parser.ParseAsync(source);
var stringBuilder = new StringBuilder();
foreach (var rule in ProcessUsedRules(parsed.StyleRules.OfType<StyleRule>(), selectors))
{
stringBuilder.AppendLine(rule.ToCss());
}
var output = stringBuilder.ToString();
return output;
}
private IEnumerable<StyleRule> ProcessUsedRules(IEnumerable<StyleRule> allRules, IList<string> selectors)
{
var newRules = new List<StyleRule>();
foreach (var rule in allRules)
{
var newRule = new StyleRule(parser);
foreach (var child in rule.Children)
{
if (!(child is StyleRule style) || !selectors.Contains(style.SelectorText))
{
continue;
}
newRule.AppendChild(child);
}
if (!newRule.Children.Any())
{
continue;
}
newRules.Add(newRule);
}
return newRules;
}
public void Dispose()
{
parser = null;
}
}
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using WebOptimizer;
public class UnCssProcessor : IProcessor
{
public async Task ExecuteAsync(IAssetContext context)
{
var content = new Dictionary<string, byte[]>();
var cache = context.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
using (var parser = new UnCssParser())
{
foreach (var key in context.Content.Keys)
{
var cacheKey = CacheKey(context.HttpContext);
if (cacheKey == null)
{
return;
}
var output = await cache.GetAsync(cacheKey);
if (output == null)
{
var selectorsKey = UnCssHelpers.CreateSelectorsCacheKey(cacheKey);
var selectorsString = await cache.GetStringAsync(selectorsKey);
if (string.IsNullOrWhiteSpace(selectorsString))
{
return;
}
var selectors = JsonConvert.DeserializeObject<List<string>>(selectorsString);
var input = context.Content[key].AsString();
var result = await parser.ParseAsync(input, selectors);
output = result.AsByteArray();
await cache.SetAsync(cacheKey, output);
}
content[key] = output;
}
}
context.Content = content;
}
public string CacheKey(HttpContext context)
{
var queryString = context.Request
.GetUri()
.ParseQueryString();
return queryString.AllKeys.Contains(UnCssConstants.QueryParameter)
? queryString[UnCssConstants.QueryParameter]
: null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment