Minifying Inline Css, Js and Html using Tag Helpers in ASP.NET Core
Introduction:
I think Tag Helpers is one of coolest feature in ASP.NET Core. Tag helpers allows our server side code to participate in generating final html response using normal (or custom) html tags. In this article, I will show you how we can leverage tag helpers to minify inline css and inline javascript. I will also show you how we can minify the html.
Description:
Note that I am using RC1 at the time of writing. First we need the css, javascript and html minifiers. You can use any minifier library which work for you but here I will use the minifiers available in ServiceStack source. Here are the html, css and js minifiers,
01 |
public
class
BasicHtmlMinifier
|
02 |
{
|
03 |
static
Regex BetweenScriptTagsRegEx = new
Regex(@"<script[^>]*>[\w|\t|\r|\W]*?</script>", RegexOptions.Compiled);
|
04 |
static
Regex BetweenTagsRegex = new
Regex(@"(?<=[^])\t{2,}|(?<=[>])\s{2,}(?=[<])|(?<=[>])\s{2,11}(?=[<])|(?=[\n])\s{2,}|(?=[\r])\s{2,}", RegexOptions.Compiled);
|
05 |
static
Regex MatchBodyRegEx = new
Regex(@"</body>", RegexOptions.Compiled);
|
06 |
07 |
public
static
string
MinifyHtml(string
html)
|
08 |
{
|
09 |
if
(html == null)
|
10 |
return
html;
|
11 |
12 |
var mymatch =
BetweenScriptTagsRegEx.Matches(html);
|
13 |
html = BetweenScriptTagsRegEx.Replace(html, string.Empty);
|
14 |
html = BetweenTagsRegex.Replace(html, string.Empty);
|
15 |
16 |
var str = string.Empty;
|
17 |
foreach
(Match match in
mymatch)
|
18 |
{
|
19 |
str += match.ToString();
|
20 |
}
|
21 |
22 |
html = MatchBodyRegEx.Replace(html, str + "</body>");
|
23 |
return
html;
|
24 |
}
|
25 |
}
|
001 |
public
class
JSMinifier
|
002 |
{
|
003 |
const
int
EOF = -1;
|
004 |
005 |
TextReader sr;
|
006 |
StringBuilder sb;
|
007 |
int
theA;
|
008 |
int
theB;
|
009 |
int
theLookahead = EOF;
|
010 |
011 |
public
string
Compress(string
js)
|
012 |
{
|
013 |
using
(sr = new
StringReader(js))
|
014 |
{
|
015 |
sb = new
StringBuilder();
|
016 |
jsmin();
|
017 |
return
sb.ToString(); // return the minified string
|
018 |
}
|
019 |
}
|
020 |
021 |
public
static
string
MinifyJs(string
js) //removed the out file path
|
022 |
{
|
023 |
return
new
JSMinifier().Compress(js);
|
024 |
}
|
025 |
026 |
/* jsmin -- Copy the input to the output,
deleting the characters which are
|
027 |
insignificant to JavaScript. Comments will be
removed. Tabs will be
|
028 |
replaced with spaces. Carriage returns will
be replaced with linefeeds.
|
029 |
Most spaces and linefeeds will be removed.
|
030 |
*/
|
031 |
void
jsmin()
|
032 |
{
|
033 |
theA = '\n';
|
034 |
action(3);
|
035 |
while
(theA != EOF)
|
036 |
{
|
037 |
switch
(theA)
|
038 |
{
|
039 |
case
' ':
|
040 |
{
|
041 |
if
(isAlphanum(theB))
|
042 |
{
|
043 |
action(1);
|
044 |
}
|
045 |
else
|
046 |
{
|
047 |
action(2);
|
048 |
}
|
049 |
break;
|
050 |
}
|
051 |
case
'\n':
|
052 |
{
|
053 |
switch
(theB)
|
054 |
{
|
055 |
case
'{':
|
056 |
case
'[':
|
057 |
case
'(':
|
058 |
case
'+':
|
059 |
case
'-':
|
060 |
{
|
061 |
action(1);
|
062 |
break;
|
063 |
}
|
064 |
case
' ':
|
065 |
{
|
066 |
action(3);
|
067 |
break;
|
068 |
}
|
069 |
default:
|
070 |
{
|
071 |
if
(isAlphanum(theB))
|
072 |
{
|
073 |
action(1);
|
074 |
}
|
075 |
else
|
076 |
{
|
077 |
action(2);
|
078 |
}
|
079 |
break;
|
080 |
}
|
081 |
}
|
082 |
break;
|
083 |
}
|
084 |
default:
|
085 |
{
|
086 |
switch
(theB)
|
087 |
{
|
088 |
case
' ':
|
089 |
{
|
090 |
if
(isAlphanum(theA))
|
091 |
{
|
092 |
action(1);
|
093 |
break;
|
094 |
}
|
095 |
action(3);
|
096 |
break;
|
097 |
}
|
098 |
case
'\n':
|
099 |
{
|
100 |
switch
(theA)
|
101 |
{
|
102 |
case
'}':
|
103 |
case
']':
|
104 |
case
')':
|
105 |
case
'+':
|
106 |
case
'-':
|
107 |
case
'"':
|
108 |
case
'\'':
|
109 |
{
|
110 |
action(1);
|
111 |
break;
|
112 |
}
|
113 |
default:
|
114 |
{
|
115 |
if
(isAlphanum(theA))
|
116 |
{
|
117 |
action(1);
|
118 |
}
|
119 |
else
|
120 |
{
|
121 |
action(3);
|
122 |
}
|
123 |
break;
|
124 |
}
|
125 |
}
|
126 |
break;
|
127 |
}
|
128 |
default:
|
129 |
{
|
130 |
action(1);
|
131 |
break;
|
132 |
}
|
133 |
}
|
134 |
break;
|
135 |
}
|
136 |
}
|
137 |
}
|
138 |
}
|
139 |
/* action -- do something! What you do is
determined by the argument:
|
140 |
1 Output A. Copy B to A. Get the next B.
|
141 |
2 Copy B to A. Get the next B. (Delete A).
|
142 |
3 Get the next B. (Delete B).
|
143 |
action treats a string as a single character.
Wow!
|
144 |
action recognizes a regular expression if it
is preceded by ( or , or =.
|
145 |
*/
|
146 |
void
action(int
d)
|
147 |
{
|
148 |
if
(d <= 1)
|
149 |
{
|
150 |
put(theA);
|
151 |
}
|
152 |
if
(d <= 2)
|
153 |
{
|
154 |
theA = theB;
|
155 |
if
(theA == '\''
|| theA == '"')
|
156 |
{
|
157 |
for
(;;)
|
158 |
{
|
159 |
put(theA);
|
160 |
theA = get();
|
161 |
if
(theA == theB)
|
162 |
{
|
163 |
break;
|
164 |
}
|
165 |
if
(theA <= '\n')
|
166 |
{
|
167 |
throw
new
Exception(string.Format("Error: JSMIN unterminated string literal:
{0}\n", theA));
|
168 |
}
|
169 |
if
(theA == '\\')
|
170 |
{
|
171 |
put(theA);
|
172 |
theA = get();
|
173 |
}
|
174 |
}
|
175 |
}
|
176 |
}
|
177 |
if
(d <= 3)
|
178 |
{
|
179 |
theB = next();
|
180 |
if
(theB == '/'
&& (theA == '('
|| theA == ','
|| theA == '='
||
|
181 |
theA == '['
|| theA == '!'
|| theA == ':'
||
|
182 |
theA == '&'
|| theA == '|'
|| theA == '?'
||
|
183 |
theA == '{'
|| theA == '}'
|| theA == ';'
||
|
184 |
theA == '\n'))
|
185 |
{
|
186 |
put(theA);
|
187 |
put(theB);
|
188 |
for
(;;)
|
189 |
{
|
190 |
theA = get();
|
191 |
if
(theA == '/')
|
192 |
{
|
193 |
break;
|
194 |
}
|
195 |
else
if
(theA == '\\')
|
196 |
{
|
197 |
put(theA);
|
198 |
theA = get();
|
199 |
}
|
200 |
else
if
(theA <= '\n')
|
201 |
{
|
202 |
throw
new
Exception(string.Format("Error: JSMIN unterminated Regular Expression
literal : {0}.\n", theA));
|
203 |
}
|
204 |
put(theA);
|
205 |
}
|
206 |
theB = next();
|
207 |
}
|
208 |
}
|
209 |
}
|
210 |
/* next -- get the next character, excluding
comments. peek() is used to see
|
211 |
if a '/' is followed by a '/' or '*'.
|
212 |
*/
|
213 |
int
next()
|
214 |
{
|
215 |
int
c = get();
|
216 |
if
(c == '/')
|
217 |
{
|
218 |
switch
(peek())
|
219 |
{
|
220 |
case
'/':
|
221 |
{
|
222 |
for
(;;)
|
223 |
{
|
224 |
c = get();
|
225 |
if
(c <= '\n')
|
226 |
{
|
227 |
return
c;
|
228 |
}
|
229 |
}
|
230 |
}
|
231 |
case
'*':
|
232 |
{
|
233 |
get();
|
234 |
for
(;;)
|
235 |
{
|
236 |
switch
(get())
|
237 |
{
|
238 |
case
'*':
|
239 |
{
|
240 |
if
(peek() == '/')
|
241 |
{
|
242 |
get();
|
243 |
return
' ';
|
244 |
}
|
245 |
break;
|
246 |
}
|
247 |
case
EOF:
|
248 |
{
|
249 |
throw
new
Exception("Error: JSMIN Unterminated comment.\n");
|
250 |
}
|
251 |
}
|
252 |
}
|
253 |
}
|
254 |
default:
|
255 |
{
|
256 |
return
c;
|
257 |
}
|
258 |
}
|
259 |
}
|
260 |
return
c;
|
261 |
}
|
262 |
/* peek -- get the next character without
getting it.
|
263 |
*/
|
264 |
int
peek()
|
265 |
{
|
266 |
theLookahead = get();
|
267 |
return
theLookahead;
|
268 |
}
|
269 |
/* get -- return the next character from
stdin. Watch out for lookahead. If
|
270 |
the character is a control character,
translate it to a space or
|
271 |
linefeed.
|
272 |
*/
|
273 |
int
get()
|
274 |
{
|
275 |
int
c = theLookahead;
|
276 |
theLookahead = EOF;
|
277 |
if
(c == EOF)
|
278 |
{
|
279 |
c = sr.Read();
|
280 |
}
|
281 |
if
(c >= ' '
|| c == '\n'
|| c == EOF)
|
282 |
{
|
283 |
return
c;
|
284 |
}
|
285 |
if
(c == '\r')
|
286 |
{
|
287 |
return
'\n';
|
288 |
}
|
289 |
return
' ';
|
290 |
}
|
291 |
292 |
void
put(int
c)
|
293 |
{
|
294 |
sb.Append((char)c);
|
295 |
}
|
296 |
/* isAlphanum -- return true if the character
is a letter, digit, underscore,
|
297 |
dollar sign, or non-ASCII character.
|
298 |
*/
|
299 |
bool
isAlphanum(int
c)
|
300 |
{
|
301 |
return
((c >= 'a'
&& c <= 'z') || (c >= '0'
&& c <= '9') ||
|
302 |
(c >= 'A'
&& c <= 'Z') || c == '_'
|| c == '$'
|| c == '\\'
||
|
303 |
c > 126);
|
304 |
}
|
305 |
}
|
01 |
public
class
CssMinifier
|
02 |
{
|
03 |
public
static
string
MinifyCss(string
css)
|
04 |
{
|
05 |
css = Regex.Replace(css, @"[a-zA-Z]+#", "#");
|
06 |
css = Regex.Replace(css, @"[\n\r]+\s*", String.Empty);
|
07 |
css = Regex.Replace(css, @"\s+", " ");
|
08 |
css = Regex.Replace(css, @"\s?([:,;{}])\s?", "$1");
|
09 |
css = css.Replace(";}", "}");
|
10 |
css = Regex.Replace(css, @"([\s:]0)(px|pt|%|em)", "$1");
|
11 |
12 |
// Remove comments from CSS
|
13 |
css = Regex.Replace(css, @"/\[\d\D]?\*/", String.Empty);
|
14 |
15 |
return
css;
|
16 |
}
|
17 |
}
|
BasicHtmlMinifier.MinifyHtml will minify the html, JsMinifier.MinifyJs will minify javascript and CssMinifier.MinifyCss will minify css. Now let's add our html, script and style tag helpers,
01 |
public
class
HtmlTagHelper : TagHelper
|
02 |
{
|
03 |
public
override
async Task ProcessAsync(TagHelperContext
context, TagHelperOutput output)
|
04 |
{
|
05 |
if
(context == null)
|
06 |
{
|
07 |
throw
new
ArgumentNullException(nameof(context));
|
08 |
}
|
09 |
10 |
if
(output == null)
|
11 |
{
|
12 |
throw
new
ArgumentNullException(nameof(output));
|
13 |
}
|
14 |
15 |
var html = await
output.GetChildContentAsync();
|
16 |
var minifiedHtml =
BasicHtmlMinifier.MinifyHtml(html.GetContent());
|
17 |
output.Content.SetHtmlContent(minifiedHtml);
|
18 |
}
|
19 |
}
|
01 |
public
class
ScriptTagHelper : TagHelper
|
02 |
{
|
03 |
public
override
async Task ProcessAsync(TagHelperContext
context, TagHelperOutput output)
|
04 |
{
|
05 |
if
(context == null)
|
06 |
{
|
07 |
throw
new
ArgumentNullException(nameof(context));
|
08 |
}
|
09 |
10 |
if
(output == null)
|
11 |
{
|
12 |
throw
new
ArgumentNullException(nameof(output));
|
13 |
}
|
14 |
15 |
var js = (await
output.GetChildContentAsync()).GetContent();
|
16 |
if
(!string.IsNullOrWhiteSpace(js))
|
17 |
{
|
18 |
var minifiedJs =
JSMinifier.MinifyJs(js);
|
19 |
output.Content.SetHtmlContent(minifiedJs);
|
20 |
}
|
21 |
}
|
22 |
}
|
01 |
public
class
StyleTagHelper : TagHelper
|
02 |
{
|
03 |
public
override
async Task ProcessAsync(TagHelperContext
context, TagHelperOutput output)
|
04 |
{
|
05 |
if
(context == null)
|
06 |
{
|
07 |
throw
new
ArgumentNullException(nameof(context));
|
08 |
}
|
09 |
10 |
if
(output == null)
|
11 |
{
|
12 |
throw
new
ArgumentNullException(nameof(output));
|
13 |
}
|
14 |
15 |
var css = await
output.GetChildContentAsync();
|
16 |
var minifiedCss =
CssMinifier.MinifyCss(css.GetContent());
|
17 |
output.Content.SetHtmlContent(minifiedCss);
|
18 |
}
|
19 |
}
|
These tag helpers simply get the inner/child contents of the tag, then minify and set the minified contents. Finally just add these tag helpers inside your razor/cshtml view(s) where you need to minify,
1 |
@addTagHelper "YourNameSpace.HtmlTagHelper,
YourProject"
|
2 |
@addTagHelper "YourNameSpace.StyleTagHelper,
YourProject"
|
3 |
@addTagHelper "YourNameSpace.ScriptTagHelper,
YourProject"
|
Now just run your application and see the view source, you will see the html, inline css and inline js are minified.
Summary:
In this article, I showed you how easily we can minify html, inline css and inline javascript using tag helpers (which makes this task very easy). I have used ServiceStack helpers to minify but you can use any minifier you like.