Skip to content

Instantly share code, notes, and snippets.

@guardrex
Last active June 28, 2016 05:29
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save guardrex/d3c08c8c337f646b8b60 to your computer and use it in GitHub Desktop.
Save guardrex/d3c08c8c337f646b8b60 to your computer and use it in GitHub Desktop.
Razor Markup Minfier
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Directives;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Chunks;
using Microsoft.AspNetCore.Razor.CodeGenerators;
using Microsoft.AspNetCore.Razor.Parser;
using Microsoft.AspNetCore.Razor.CodeGenerators.Visitors;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using System.Text;
using System.Text.RegularExpressions;
namespace CustomMvcRazorHost
{
public class MinifyingMvcRazorHost_ChunkVisitor : IMvcRazorHost
{
private readonly MvcRazorHost _host;
public static bool HasOnlySeenOpenBracket = false;
public string DefaultNamespace => _host.DefaultNamespace;
public string MainClassNamePrefix => _host.DefaultClassName;
private ChunkInheritanceUtility _chunkInheritanceUtility;
private readonly IChunkTreeCache _chunkTreeCache;
private const string ModelExpressionProviderProperty = "ModelExpressionProvider";
private const string ViewDataProperty = "ViewData";
public MinifyingMvcRazorHost_ChunkVisitor(IChunkTreeCache chunkTreeCache, ITagHelperDescriptorResolver descriptorResolver)
{
_chunkTreeCache = chunkTreeCache;
_host = new MvcRazorHost(chunkTreeCache, descriptorResolver);
}
public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream)
{
var className = MainClassNamePrefix + ParserHelpers.SanitizeClassName(rootRelativePath);
var engine = new CustomRazorTemplateEngine(this, _host);
return engine.GenerateCode(inputStream, className, DefaultNamespace, rootRelativePath);
}
public CodeGenerator DecorateCodeGenerator(CodeGenerator incomingGenerator, CodeGeneratorContext context)
{
var inheritedChunkTrees = GetInheritedChunkTrees(context.SourceFile);
ChunkInheritanceUtility.MergeInheritedChunkTrees(
context.ChunkTreeBuilder.Root,
inheritedChunkTrees,
_host.DefaultModel);
return new CustomCSharpCodeGenerator(
context,
_host.DefaultModel,
_host.InjectAttribute,
new GeneratedTagHelperAttributeContext
{
ModelExpressionTypeName = _host.ModelExpressionType,
CreateModelExpressionMethodName = _host.CreateModelExpressionMethod,
ModelExpressionProviderPropertyName = ModelExpressionProviderProperty,
ViewDataPropertyName = ViewDataProperty
});
}
public virtual string ModelExpressionProvider
{
get { return ModelExpressionProviderProperty; }
}
public virtual string ViewDataPropertyName
{
get { return ViewDataProperty; }
}
internal ChunkInheritanceUtility ChunkInheritanceUtility
{
get
{
if (_chunkInheritanceUtility == null)
{
_chunkInheritanceUtility = new ChunkInheritanceUtility(_host, _chunkTreeCache, _host.DefaultInheritedChunks);
}
return _chunkInheritanceUtility;
}
set
{
_chunkInheritanceUtility = value;
}
}
private IReadOnlyList<ChunkTree> GetInheritedChunkTrees(string sourceFileName)
{
var inheritedChunkTrees = _host.GetInheritedChunkTreeResults(sourceFileName)
.Select(result => result.ChunkTree)
.ToList();
return inheritedChunkTrees;
}
}
public class CustomRazorTemplateEngine : RazorTemplateEngine
{
private MinifyingMvcRazorHost_ChunkVisitor _myHost;
public CustomRazorTemplateEngine(MinifyingMvcRazorHost_ChunkVisitor myHost, MvcRazorHost host) : base(host)
{
_myHost = myHost;
}
protected override CodeGenerator CreateCodeGenerator(CodeGeneratorContext context)
{
return _myHost.DecorateCodeGenerator(Host.CodeLanguage.CreateCodeGenerator(context), context);
}
}
public class CustomCSharpCodeGenerator : MvcCSharpCodeGenerator
{
private GeneratedTagHelperAttributeContext _tagHelperAttributeContext;
public CustomCSharpCodeGenerator(CodeGeneratorContext context, string defaultModel, string injectAttribute, GeneratedTagHelperAttributeContext tagHelperAttributeContext) : base(context, defaultModel, injectAttribute, tagHelperAttributeContext)
{
_tagHelperAttributeContext = tagHelperAttributeContext;
}
protected override CSharpCodeVisitor CreateCSharpCodeVisitor(CSharpCodeWriter writer, CodeGeneratorContext context)
{
var csharpCodeVisitor = new CustomCSharpCodeVisitor(writer, context);
csharpCodeVisitor.TagHelperRenderer.AttributeValueCodeRenderer = new MvcTagHelperAttributeValueCodeRenderer(_tagHelperAttributeContext);
return csharpCodeVisitor;
}
}
public class CustomCSharpCodeVisitor : CSharpCodeVisitor
{
public CustomCSharpCodeVisitor(CSharpCodeWriter writer, CodeGeneratorContext context) : base(writer, context)
{ }
private bool _outsideOfElement = true;
private bool _comingOffHyperlink = false;
private StringBuilder chunkStringBuilder = new StringBuilder();
private StringBuilder chunkCharactersStringBuilder = new StringBuilder();
private const int MaxStringLiteralLength = 1024;
protected override void Visit(ParentLiteralChunk chunk)
{
if (Context.Host.DesignTimeMode)
{
// Skip generating the chunk if we're in design time or if the chunk is empty.
return;
}
var text = chunk.GetText();
if (Context.Host.EnableInstrumentation)
{
var start = chunk.Start.AbsoluteIndex;
Writer.WriteStartInstrumentationContext(Context, start, text.Length, isLiteral: true);
}
RenderStartWriteLiteral(MinifyChunk(text));
if (Context.Host.EnableInstrumentation)
{
Writer.WriteEndInstrumentationContext(Context);
}
}
protected override void Visit(LiteralChunk chunk)
{
if (Context.Host.DesignTimeMode || string.IsNullOrEmpty(chunk.Text))
{
// Skip generating the chunk if we're in design time or if the chunk is empty.
return;
}
if (Context.Host.EnableInstrumentation)
{
Writer.WriteStartInstrumentationContext(Context, chunk.Association, isLiteral: true);
}
RenderStartWriteLiteral(MinifyChunk(chunk.Text));
if (Context.Host.EnableInstrumentation)
{
Writer.WriteEndInstrumentationContext(Context);
}
}
protected override void Visit(TagHelperChunk chunk)
{
if (chunk.TagName == "a")
{
Writer.Write(@"WriteLiteral("" "");");
TagHelperRenderer.RenderTagHelper(chunk);
_comingOffHyperlink = true;
}
else
{
TagHelperRenderer.RenderTagHelper(chunk);
}
}
private void RenderStartWriteLiteral(string text)
{
var charactersRendered = 0;
// Render the string in pieces to avoid Roslyn OOM exceptions at compile time:
// https://github.com/aspnet/External/issues/54
while (charactersRendered < text.Length)
{
if (Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
{
if (!string.IsNullOrEmpty(Context.TargetWriterName))
{
Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.WriteLiteralToMethodName)
.Write(Context.TargetWriterName)
.WriteParameterSeparator();
}
else
{
Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.WriteLiteralMethodName);
}
}
string textToRender;
if (text.Length <= MaxStringLiteralLength)
{
textToRender = text;
}
else
{
int charRemaining = text.Length - charactersRendered;
if (charRemaining < MaxStringLiteralLength)
{
textToRender = text.Substring(charactersRendered, charRemaining);
}
else
{
textToRender = text.Substring(charactersRendered, MaxStringLiteralLength);
}
}
Writer.WriteStringLiteral(textToRender);
charactersRendered += textToRender.Length;
if (Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput)
{
Writer.WriteEndMethodInvocation();
}
}
}
private string MinifyChunk(string chunkText)
{
chunkStringBuilder.Clear();
chunkCharactersStringBuilder.Clear();
foreach (char c in chunkText)
{
if (_outsideOfElement)
{
if (!c.Equals('<'))
{
// Collect this inter-element character for processing later
chunkCharactersStringBuilder.Append(c);
}
else
{
// We just came to the end of an inter-element sequence of characters ... process these characters now
_outsideOfElement = false;
chunkStringBuilder.Append(MinifyCharacters(chunkCharactersStringBuilder.ToString()) + c);
chunkCharactersStringBuilder.Clear();
}
}
else
{
// Accumulate this element character to the output chunk
chunkStringBuilder.Append(c);
if (c.Equals('>'))
{
// Time to start collecting and processing characters again
_outsideOfElement = true;
}
}
}
// Minify and add any leftover characters b/c we didn't reach the end of an inter-element sequence in this chunk
chunkStringBuilder.Append(MinifyCharacters(chunkCharactersStringBuilder.ToString()));
// Look around here and see if we can add a space back around inline elements that are inside the chunk
// This assumes the inline element is next to the text IN THE SAME chunk. [That might be a bad assumption!]
if (_comingOffHyperlink)
{
_comingOffHyperlink = false;
var chunkStringBuilderVal = chunkStringBuilder.ToString();
if (chunkStringBuilderVal.Length > 0 && Regex.IsMatch(chunkStringBuilderVal.Substring(0, 1), "[A-Za-z0-9]"))
{
return " " + AddSpaceBack(chunkStringBuilderVal);
}
}
return AddSpaceBack(chunkStringBuilder.ToString());
}
private string MinifyCharacters(string inputString)
{
return inputString.Replace("\t", string.Empty).Replace("\r", string.Empty).Replace("\n", string.Empty).Trim(' ');
}
private string AddSpaceBack(string inputString)
{
return Regex.Replace(inputString, "((?<=\\S)(?=(<a|<b|<big|<i|<small|<tt|<abbr|<acronym|<cite|<code|<dfn|<em|<kbd|<strong|<samp|<time|<var|<bdo|<br|<img|<map|<object|<q|<span|<sub|<sup|<button|<input|<label|<select|<textarea)))|((?<=(/a>|/b>|/big>|/i>|/small>|/tt>|/abbr>|/acronym>|/cite>|/code>|/dfn>|/em>|/kbd>|/strong>|/samp>|/time>|/var>|/bdo>|/br>|/map>|/object>|/q>|/span>|/sub>|/sup>|/button>|/input>|/label>|/select>|/textarea>))(?=[A-Za-z0-9]))", " ");
}
}
}
@guardrex
Copy link
Author

This has been updated to only add the space back to the backside of the closing tag if the next character is within [A-Za-z0-9]. I used to have \\S there, but that was being too greedy and adding the space back when a mark of punctuation came after the closing tag.

An outstanding issue is with links ... it's still adding a space back after a link and before a mark of punctuation. This occurs because adding a space around a link is actually handled by the TagHelperChunk visitor, and I'll need to get some kind of logic in there to assess the next character behind the link before injecting that second Writer.Write(@"WriteLiteral("" "");"); there.

@guardrex
Copy link
Author

Ok, I updated it to keep track of when it's coming off a hyperlink from the TagHelperChunk visitor. When it gets to the next chunk after a hyperlink, MinifyChunk will check the first character. If that character is within [A-Za-z0-9], it will add a space after the hyperlink. Otherwise (e.g., a punctuation mark is there), it will not add a space at the start of that chunk.

@guardrex
Copy link
Author

This has been updated to work with the latest updates by @NTaylorMullen to the Razor Engine. aspnet/Razor@4db32ec

@guardrex
Copy link
Author

guardrex commented Apr 3, 2016

This has been updated to work with the latest updates that included adding the ITagHelperDescriptorResolver to the engine ctor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment