CssClassCollectorProcessor Pennington.MonorailCss
Extracts CSS class names from HTML and JSON responses and registers them with CssClassCollector so MonorailCSS can generate the correct stylesheet. This processor only observes — it never modifies the response body.
Constructors
.ctor
#public partial class CssClassCollectorProcessor(
CssClassCollector collector,
ILogger<CssClassCollectorProcessor> logger) : IResponseProcessor
{
int IResponseProcessor.Order => 100;
bool IResponseProcessor.ShouldProcess(HttpContext context)
{
var contentType = context.Response.ContentType;
if (string.IsNullOrEmpty(contentType))
return false;
return contentType.Contains("text/html", StringComparison.OrdinalIgnoreCase)
|| contentType.Contains("application/json", StringComparison.OrdinalIgnoreCase);
}
Task<string> IResponseProcessor.ProcessAsync(string responseBody, HttpContext context)
{
var url = context.Request.Path;
var contentType = context.Response.ContentType;
var isJson = contentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true;
LogGatheringCssFromContentTypeResponseForUrl(isJson ? "JSON" : "HTML", url);
// JSON responses contain HTML with escaped quotes. JavaScriptEncoder.Default
// encodes " as \u0022 and may also produce \". Unescape both forms so the
// class-attribute regex can match.
var textToScan = isJson
? JsonUnescapeRegex().Replace(responseBody, m => ((char)Convert.ToInt32(m.Groups[1].Value, 16)).ToString())
.Replace("\\\"", "\"")
: responseBody;
var classMatches = CssClassGatherRegex().Matches(textToScan);
var allClasses = classMatches
.SelectMany(m => m.Groups[1].Value.Split([' ', '\n', '\r', '\t'], StringSplitOptions.RemoveEmptyEntries))
.Select(WebUtility.HtmlDecode)
.Where(c => c is not null)
.Select(c => c!)
.Distinct()
.ToList();
if (allClasses.Count > 0)
{
collector.BeginProcessing();
try
{
LogGatheredCountCssClasses(allClasses.Count);
collector.AddClasses(url, allClasses);
}
finally
{
collector.EndProcessing();
}
}
// Never modify the response body — just observe.
return Task.FromResult(responseBody);
}
[GeneratedRegex("""class\s*=\s*["']([^"']+)["']""", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex CssClassGatherRegex();
[GeneratedRegex("""\\u([0-9a-fA-F]{4})""")]
private static partial Regex JsonUnescapeRegex();
[LoggerMessage(LogLevel.Trace, "Gathering CSS from {ContentType} response for {Url}")]
partial void LogGatheringCssFromContentTypeResponseForUrl(string contentType, PathString url);
[LoggerMessage(LogLevel.Trace, "Gathered {Count} CSS classes")]
partial void LogGatheredCountCssClasses(int count);
}Extracts CSS class names from HTML and JSON responses and registers them with CssClassCollector so MonorailCSS can generate the correct stylesheet. This processor only observes — it never modifies the response body.
Parameters
collectorCssClassCollectorloggerILogger<CssClassCollectorProcessor>
Pennington.MonorailCss.CssClassCollectorProcessor
namespace Pennington.MonorailCss;
/// Extracts CSS class names from HTML and JSON responses and registers them with CssClassCollector so MonorailCSS can generate the correct stylesheet. This processor only observes — it never modifies the response body.
public class CssClassCollectorProcessor
{
/// Extracts CSS class names from HTML and JSON responses and registers them with CssClassCollector so MonorailCSS can generate the correct stylesheet. This processor only observes — it never modifies the response body.
public partial class CssClassCollectorProcessor(
CssClassCollector collector,
ILogger<CssClassCollectorProcessor> logger) : IResponseProcessor
{
int IResponseProcessor.Order => 100;
bool IResponseProcessor.ShouldProcess(HttpContext context)
{
var contentType = context.Response.ContentType;
if (string.IsNullOrEmpty(contentType))
return false;
return contentType.Contains("text/html", StringComparison.OrdinalIgnoreCase)
|| contentType.Contains("application/json", StringComparison.OrdinalIgnoreCase);
}
Task<string> IResponseProcessor.ProcessAsync(string responseBody, HttpContext context)
{
var url = context.Request.Path;
var contentType = context.Response.ContentType;
var isJson = contentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true;
LogGatheringCssFromContentTypeResponseForUrl(isJson ? "JSON" : "HTML", url);
// JSON responses contain HTML with escaped quotes. JavaScriptEncoder.Default
// encodes " as \u0022 and may also produce \". Unescape both forms so the
// class-attribute regex can match.
var textToScan = isJson
? JsonUnescapeRegex().Replace(responseBody, m => ((char)Convert.ToInt32(m.Groups[1].Value, 16)).ToString())
.Replace("\\\"", "\"")
: responseBody;
var classMatches = CssClassGatherRegex().Matches(textToScan);
var allClasses = classMatches
.SelectMany(m => m.Groups[1].Value.Split([' ', '\n', '\r', '\t'], StringSplitOptions.RemoveEmptyEntries))
.Select(WebUtility.HtmlDecode)
.Where(c => c is not null)
.Select(c => c!)
.Distinct()
.ToList();
if (allClasses.Count > 0)
{
collector.BeginProcessing();
try
{
LogGatheredCountCssClasses(allClasses.Count);
collector.AddClasses(url, allClasses);
}
finally
{
collector.EndProcessing();
}
}
// Never modify the response body — just observe.
return Task.FromResult(responseBody);
}
[GeneratedRegex("""class\s*=\s*["']([^"']+)["']""", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex CssClassGatherRegex();
[GeneratedRegex("""\\u([0-9a-fA-F]{4})""")]
private static partial Regex JsonUnescapeRegex();
[LoggerMessage(LogLevel.Trace, "Gathering CSS from {ContentType} response for {Url}")]
partial void LogGatheringCssFromContentTypeResponseForUrl(string contentType, PathString url);
[LoggerMessage(LogLevel.Trace, "Gathered {Count} CSS classes")]
partial void LogGatheredCountCssClasses(int count);
}
}