Created
December 19, 2019 14:13
-
-
Save Grinderofl/7a71411034c6b3e624fc259fe8a28fd1 to your computer and use it in GitHub Desktop.
UnCSS PoC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
services.AddWebOptimizer(x => | |
{ | |
x.AddBundle("/css/site.css", "text/css;charset=UTF-8", "static/css/styles.css") | |
.UnCss() | |
.MinifyCss() | |
.AutoPrefixCss() | |
.Concatenate() | |
.FingerprintUrls(); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using WebOptimizer; | |
public static class UnCssAssetPipelineExtensions | |
{ | |
public static IAsset UnCss(this IAsset bundle) | |
{ | |
bundle.Processors.Add(new UnCssProcessor()); | |
return bundle; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class UnCssConstants | |
{ | |
public const string QueryParameter = "uc"; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}]"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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