html-minifier 0.7.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -15
- package/cli.js +147 -23
- package/dist/htmlminifier.js +59 -55
- package/dist/htmlminifier.min.js +4 -4
- package/package.json +18 -21
- package/src/htmllint.js +168 -0
- package/src/htmlminifier.js +838 -0
- package/src/htmlparser.js +560 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
[](https://www.npmjs.com/package/html-minifier)
|
|
2
|
+
[](https://travis-ci.org/kangax/html-minifier)
|
|
3
|
+
[](https://david-dm.org/kangax/html-minifier)
|
|
4
|
+
[](https://david-dm.org/kangax/html-minifier#info=devDependencies)
|
|
5
5
|
|
|
6
6
|
[HTMLMinifier](http://kangax.github.io/html-minifier/) is a highly **configurable**, **well-tested**, Javascript-based HTML minifier, with lint-like capabilities.
|
|
7
7
|
|
|
@@ -11,17 +11,19 @@ See [corresponding blog post](http://perfectionkills.com/experimenting-with-html
|
|
|
11
11
|
|
|
12
12
|
Also see corresponding [Ruby wrapper](https://github.com/stereobooster/html_minifier), and for Node.js, [Grunt plugin](https://github.com/gruntjs/grunt-contrib-htmlmin), [Gulp module](https://github.com/jonschlinkert/gulp-htmlmin), and [Koa middleware wrapper](https://github.com/koajs/html-minifier).
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## Minification comparison
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
| [
|
|
21
|
-
| [
|
|
22
|
-
| [
|
|
23
|
-
| [
|
|
24
|
-
| [
|
|
16
|
+
How does HTMLMinifier compare to other solutions — [HTML Minifier from Will Peavy](http://www.willpeavy.com/minifier/) (1st result in [google search for "html minifier"](https://www.google.com/#q=html+minifier)) as well as [htmlcompressor.com](http://htmlcompressor.com) and [minimize](https://github.com/Swaagie/minimize)?
|
|
17
|
+
|
|
18
|
+
| Site | Original size _(KB)_ | HTMLMinifier | minimize | Will Peavy | htmlcompressor.com |
|
|
19
|
+
| --------------------------------------------------------------------------- |:-----------:| ----------------:| ----------------:| ------------:| ----------------:|
|
|
20
|
+
| [HTMLMinifier page](https://github.com/kangax/html-minifier) | 48.8 | <b>37.3</b> | 41.8 | 43.3 | 41.9 |
|
|
21
|
+
| [ES6 table](http://kangax.github.io/es5-compat-table/es6/) | 117.9 | <b>79.9</b> | 94.1 | 92 | 91.9 |
|
|
22
|
+
| [MSN](http://msn.com) | 156.6 | <b>133</b> | 137.7 | 145 | 138.3 |
|
|
23
|
+
| [Stackoverflow](http://stackoverflow.com) | 200.4 | <b>159.5</b> | 165.1 | 168.3 | 163.3 |
|
|
24
|
+
| [Amazon](http://amazon.com) | 245.9 | <b>206.3</b> | 234.1 | 225 | 218.5 |
|
|
25
|
+
| [Wikipedia](http://en.wikipedia.org/wiki/President_of_the_United_States) | 401.4 | <b>380.6</b> | 386.6 | 396.3 | n/a |
|
|
26
|
+
| [Eloquent Javascript](http://eloquentjavascript.net/print.html) | 869.5 | <b>830</b> | 838 | 872 | n/a |
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
|
|
@@ -44,7 +46,6 @@ How does HTMLMinifier compare to [another solution](http://www.willpeavy.com/min
|
|
|
44
46
|
| `removeScriptTypeAttributes` | Remove `type="text/javascript"` from `script` tags. Other `type` attribute values are left intact. | `false` |
|
|
45
47
|
| `removeStyleLinkTypeAttributes`| Remove `type="text/css"` from `style` and `link` tags. Other `type` attribute values are left intact. | `false` |
|
|
46
48
|
| `removeOptionalTags` | [Remove unrequired tags](http://perfectionkills.com/experimenting-with-html-minifier/#remove_optional_tags) | `false` |
|
|
47
|
-
| `removeIgnored` | Remove all tags starting and ending with `<%`, `%>`, `<?`, `?>` | `false` |
|
|
48
49
|
| `removeEmptyElements` | [Remove all elements with empty contents](http://perfectionkills.com/experimenting-with-html-minifier/#remove_empty_elements) | `false` |
|
|
49
50
|
| `lint` | [Toggle linting](http://perfectionkills.com/experimenting-with-html-minifier/#validate_input_through_html_lint) | `false` |
|
|
50
51
|
| `keepClosingSlash` | Keep the trailing slash on singleton elements | `false` |
|
|
@@ -53,11 +54,13 @@ How does HTMLMinifier compare to [another solution](http://www.willpeavy.com/min
|
|
|
53
54
|
| `minifyCSS` | Minify CSS in style elements and style attributes (uses [clean-css](https://github.com/GoalSmashers/clean-css)) | `false` (could be `true`, `false`, `Object` (options)) |
|
|
54
55
|
| `minifyURLs` | Minify URLs in various attributes (uses [relateurl](https://github.com/stevenvachon/relateurl)) | `false` (could be `Object` (options)) |
|
|
55
56
|
| `ignoreCustomComments` | Array of regex'es that allow to ignore certain comments, when matched | `[ ]` |
|
|
57
|
+
| `ignoreCustomFragments` | Array of regex'es that allow to ignore certain fragments, when matched (e.g. `<?php ... ?>`, `{{ ... }}`, etc.) | `[ ]` |
|
|
56
58
|
| `processScripts` | Array of strings corresponding to types of script elements to process through minifier (e.g. `text/ng-template`, `text/x-handlebars-template`, etc.) | `[ ]` |
|
|
57
59
|
| `maxLineLength` | Specify a maximum line length. Compressed output will be split by newlines at valid HTML split-points. |
|
|
58
60
|
| `customAttrAssign` | Arrays of regex'es that allow to support custom attribute assign expressions (e.g. `'<div flex?="{{mode != cover}}"></div>'`) | `[ ]` |
|
|
59
61
|
| `customAttrSurround` | Arrays of regex'es that allow to support custom attribute surround expressions (e.g. `<input {{#if value}}checked="checked"{{/if}}>`) | `[ ]` |
|
|
60
62
|
| `customAttrCollapse` | Regex that specifies custom attribute to strip newlines from (e.g. `/ng\-class/`) | |
|
|
63
|
+
| `quoteCharacter` | Type of quote to use for attribute values (' or ") | " |
|
|
61
64
|
|
|
62
65
|
## Special cases
|
|
63
66
|
|
|
@@ -69,6 +72,26 @@ If you have chunks of markup you would like preserved, you can wrap them `<!-- h
|
|
|
69
72
|
|
|
70
73
|
SVG tags are automatically recognized, and when they are minified, both case-sensitivity and closing-slashes are preserved, regardless of the minification settings used for the rest of the file.
|
|
71
74
|
|
|
75
|
+
### Working with invalid markup
|
|
76
|
+
|
|
77
|
+
HTMLMinifier **can't work with invalid or partial chunks of markup**. This is because it parses markup into a tree structure, then modifies it (removing anything that was specified for removal, ignoring anything that was specified to be ingored, etc.), then it creates a markup out of that tree and returns it.
|
|
78
|
+
|
|
79
|
+
Input markup (e.g. `<p id="">foo`)
|
|
80
|
+
|
|
81
|
+
↓
|
|
82
|
+
|
|
83
|
+
Internal representation of markup in a form of tree (e.g. `{tag: "p", attr: "id", children: ["foo"] }`)
|
|
84
|
+
|
|
85
|
+
↓
|
|
86
|
+
|
|
87
|
+
Transformation of internal representation (e.g. removal of "id" attribute)
|
|
88
|
+
|
|
89
|
+
↓
|
|
90
|
+
|
|
91
|
+
Output of resulting markup (e.g. `<p>foo</p>`)
|
|
92
|
+
|
|
93
|
+
HTMLMinifier can't know that original markup was only half of the tree; it does its best to try to parse it as a full tree and it loses information about tree being malformed or partial in the beginning. As a result, it can't create a partial/malformed tree at the time of the output.
|
|
94
|
+
|
|
72
95
|
## Installation Instructions
|
|
73
96
|
|
|
74
97
|
From NPM for use as a command line app:
|
package/cli.js
CHANGED
|
@@ -63,6 +63,7 @@ var mainOptions = {
|
|
|
63
63
|
collapseWhitespace: [[false, 'Collapse white space that contributes to text nodes in a document tree.']],
|
|
64
64
|
conservativeCollapse: [[false, 'Always collapse to 1 space (never remove it entirely)']],
|
|
65
65
|
preserveLineBreaks: [[false, 'Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break.']],
|
|
66
|
+
collapseInlineTagWhitespace: [[false, 'Collapse white space around inline tag']],
|
|
66
67
|
collapseBooleanAttributes: [[false, 'Omit attribute values from boolean attributes']],
|
|
67
68
|
removeAttributeQuotes: [[false, 'Remove quotes around attributes when possible.']],
|
|
68
69
|
removeRedundantAttributes: [[false, 'Remove attributes when value matches default.']],
|
|
@@ -72,7 +73,6 @@ var mainOptions = {
|
|
|
72
73
|
removeScriptTypeAttributes: [[false, 'Remove type="text/javascript" from script tags. Other type attribute values are left intact.']],
|
|
73
74
|
removeStyleLinkTypeAttributes: [[false, 'Remove type="text/css" from style and link tags. Other type attribute values are left intact.']],
|
|
74
75
|
removeOptionalTags: [[false, 'Remove unrequired tags']],
|
|
75
|
-
removeIgnored: [[false, 'Remove all tags starting and ending with <%, %>, <?, ?>']],
|
|
76
76
|
removeEmptyElements: [[false, 'Remove all elements with empty contents']],
|
|
77
77
|
lint: [[false, 'Toggle linting']],
|
|
78
78
|
keepClosingSlash: [[false, 'Keep the trailing slash on singleton elements']],
|
|
@@ -80,18 +80,20 @@ var mainOptions = {
|
|
|
80
80
|
minifyJS: [[false, 'Minify Javascript in script elements and on* attributes (uses UglifyJS)']],
|
|
81
81
|
minifyCSS: [[false, 'Minify CSS in style elements and style attributes (uses clean-css)']],
|
|
82
82
|
minifyURLs: [[false, 'Minify URLs in various attributes (uses relateurl)']],
|
|
83
|
-
ignoreCustomComments: [[false, 'Array of regex\'es that allow to ignore certain comments, when matched', 'string'], 'json'],
|
|
84
|
-
processScripts: [[false, 'Array of strings corresponding to types of script elements to process through minifier (e.g. "text/ng-template", "text/x-handlebars-template", etc.)', 'string'], 'json'],
|
|
83
|
+
ignoreCustomComments: [[false, 'Array of regex\'es that allow to ignore certain comments, when matched', 'string'], 'json-regex'],
|
|
84
|
+
processScripts: [[false, 'Array of strings corresponding to types of script elements to process through minifier (e.g. "text/ng-template", "text/x-handlebars-template", etc.)', 'string'], 'json-regex'],
|
|
85
85
|
maxLineLength: [[false, 'Max line length', 'number'], true],
|
|
86
|
-
customAttrAssign: [[false, 'Arrays of regex\'es that allow to support custom attribute assign expressions (e.g. \'<div flex?="{{mode != cover}}"></div>\')', 'string'], 'json'],
|
|
87
|
-
customAttrSurround: [[false, 'Arrays of regex\'es that allow to support custom attribute surround expressions (e.g. <input {{#if value}}checked="checked"{{/if}}>)', 'string'], 'json'],
|
|
88
|
-
customAttrCollapse: [[false, 'Regex that specifies custom attribute to strip newlines from (e.g. /ng\-class/)', 'string'],
|
|
86
|
+
customAttrAssign: [[false, 'Arrays of regex\'es that allow to support custom attribute assign expressions (e.g. \'<div flex?="{{mode != cover}}"></div>\')', 'string'], 'json-regex'],
|
|
87
|
+
customAttrSurround: [[false, 'Arrays of regex\'es that allow to support custom attribute surround expressions (e.g. <input {{#if value}}checked="checked"{{/if}}>)', 'string'], 'json-regex'],
|
|
88
|
+
customAttrCollapse: [[false, 'Regex that specifies custom attribute to strip newlines from (e.g. /ng\-class/)', 'string'], 'string-regex']
|
|
89
89
|
};
|
|
90
90
|
|
|
91
91
|
var cliOptions = {
|
|
92
92
|
version: ['v', 'Version information'],
|
|
93
93
|
output: ['o', 'Specify output file (if not specified STDOUT will be used for output)', 'file'],
|
|
94
|
-
'config-file': ['c', 'Use config file', 'file']
|
|
94
|
+
'config-file': ['c', 'Use config file', 'file'],
|
|
95
|
+
'input-dir': [false, 'Specify an input directory', 'dir'],
|
|
96
|
+
'output-dir': [false, 'Specify an output directory', 'dir']
|
|
95
97
|
};
|
|
96
98
|
|
|
97
99
|
var mainOptionKeys = Object.keys(mainOptions);
|
|
@@ -103,13 +105,48 @@ cli.parse(cliOptions);
|
|
|
103
105
|
|
|
104
106
|
cli.main(function(args, options) {
|
|
105
107
|
|
|
106
|
-
function
|
|
108
|
+
function stringToRegExp(value) {
|
|
109
|
+
// JSON does not support regexes, so, e.g., JSON.parse() will not create
|
|
110
|
+
// a RegExp from the JSON value `[ "/matchString/" ]`, which is
|
|
111
|
+
// technically just an array containing a string that begins and end with
|
|
112
|
+
// a forward slash. To get a RegExp from a JSON string, it must be
|
|
113
|
+
// constructed explicitly in JavaScript.
|
|
114
|
+
//
|
|
115
|
+
// The likelihood of actually wanting to match text that is enclosed in
|
|
116
|
+
// forward slashes is probably quite rare, so if forward slashes were
|
|
117
|
+
// included in an argument that requires a regex, the user most likely
|
|
118
|
+
// thought they were part of the syntax for specifying a regex.
|
|
119
|
+
//
|
|
120
|
+
// In the unlikely case that forward slashes are indeed desired in the
|
|
121
|
+
// search string, the user would need to enclose the expression in a
|
|
122
|
+
// second set of slashes:
|
|
123
|
+
//
|
|
124
|
+
// --customAttrSrround "[\"//matchString//\"]"
|
|
125
|
+
//
|
|
126
|
+
if (value) {
|
|
127
|
+
var stripSlashes = /^\/(.*)\/$/.exec(value);
|
|
128
|
+
if (stripSlashes) {
|
|
129
|
+
value = stripSlashes[1];
|
|
130
|
+
}
|
|
131
|
+
return new RegExp(value);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseJSONOption(value, options) {
|
|
136
|
+
var opts = options || {};
|
|
107
137
|
if (value !== null) {
|
|
108
138
|
var jsonArray;
|
|
109
139
|
try {
|
|
110
140
|
jsonArray = JSON.parse(value);
|
|
141
|
+
if (opts.regexArray) {
|
|
142
|
+
jsonArray = jsonArray.map(function (regexString) {
|
|
143
|
+
return stringToRegExp(regexString);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
cli.fatal('Could not parse JSON value \'' + value + '\'');
|
|
111
149
|
}
|
|
112
|
-
catch (e) {}
|
|
113
150
|
if (jsonArray instanceof Array) {
|
|
114
151
|
return jsonArray;
|
|
115
152
|
}
|
|
@@ -119,7 +156,7 @@ cli.main(function(args, options) {
|
|
|
119
156
|
}
|
|
120
157
|
}
|
|
121
158
|
|
|
122
|
-
function runMinify(original) {
|
|
159
|
+
function runMinify(original, output) {
|
|
123
160
|
var status = 0;
|
|
124
161
|
var minified = null;
|
|
125
162
|
try {
|
|
@@ -127,7 +164,8 @@ cli.main(function(args, options) {
|
|
|
127
164
|
}
|
|
128
165
|
catch (e) {
|
|
129
166
|
status = 3;
|
|
130
|
-
|
|
167
|
+
cli.error('Minification error');
|
|
168
|
+
process.stderr.write(e);
|
|
131
169
|
}
|
|
132
170
|
|
|
133
171
|
if (minifyOptions.lint) {
|
|
@@ -146,31 +184,76 @@ cli.main(function(args, options) {
|
|
|
146
184
|
}
|
|
147
185
|
catch (e) {
|
|
148
186
|
status = 4;
|
|
149
|
-
|
|
187
|
+
console.log(output);
|
|
188
|
+
cli.error('Cannot write to output');
|
|
150
189
|
}
|
|
151
190
|
}
|
|
152
191
|
|
|
153
192
|
cli.exit(status);
|
|
154
193
|
}
|
|
155
194
|
|
|
195
|
+
function createDirectory(path) {
|
|
196
|
+
try {
|
|
197
|
+
fs.mkdirSync(path);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
if (e.code === 'EEXIST') {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
cli.fatal('Can not create directory ' + path);
|
|
204
|
+
cli.exit(3);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function processDirectory(inputDir, outputDir) {
|
|
209
|
+
createDirectory(outputDir);
|
|
210
|
+
var fileList = fs.readdirSync(inputDir);
|
|
211
|
+
|
|
212
|
+
fileList.forEach(function(fileName) {
|
|
213
|
+
var inputFilePath = inputDir + '/' + fileName;
|
|
214
|
+
var outputFilePath = outputDir + '/' + fileName;
|
|
215
|
+
|
|
216
|
+
var stat = fs.statSync(inputFilePath);
|
|
217
|
+
if (stat.isDirectory()) {
|
|
218
|
+
processDirectory(inputFilePath, outputFilePath);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var originalContent = fs.readFileSync(inputFilePath, 'utf8');
|
|
223
|
+
runMinify(originalContent, outputFilePath);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
156
227
|
if (options.version) {
|
|
157
|
-
|
|
228
|
+
cli.output(appName + ' v' + appVersion);
|
|
158
229
|
cli.exit(0);
|
|
159
230
|
}
|
|
160
231
|
|
|
161
232
|
if (options['config-file']) {
|
|
233
|
+
var fileOptions;
|
|
234
|
+
var fileOptionsPath = path.resolve(options['config-file']);
|
|
162
235
|
try {
|
|
163
|
-
|
|
164
|
-
if ((fileOptions !== null) && (typeof fileOptions === 'object')) {
|
|
165
|
-
minifyOptions = fileOptions;
|
|
166
|
-
}
|
|
236
|
+
fs.accessSync(fileOptionsPath, fs.R_OK);
|
|
167
237
|
}
|
|
168
238
|
catch (e) {
|
|
169
|
-
|
|
170
|
-
|
|
239
|
+
cli.fatal('The specified config file doesn’t exist or is unreadable:\n' + fileOptionsPath);
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
fileOptions = JSON.parse(fs.readFileSync(fileOptionsPath), 'utf8');
|
|
243
|
+
}
|
|
244
|
+
catch (je) {
|
|
245
|
+
try {
|
|
246
|
+
fileOptions = require(fileOptionsPath);
|
|
247
|
+
}
|
|
248
|
+
catch (ne) {
|
|
249
|
+
cli.fatal('Cannot read the specified config file. \nAs JSON: ' + je.message + '\nAs module: ' + ne.message);
|
|
250
|
+
}
|
|
171
251
|
}
|
|
172
|
-
}
|
|
173
252
|
|
|
253
|
+
if (fileOptions && typeof fileOptions === 'object') {
|
|
254
|
+
minifyOptions = fileOptions;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
174
257
|
mainOptionKeys.forEach(function(key) {
|
|
175
258
|
var paramKey = changeCase.paramCase(key);
|
|
176
259
|
var value = options[paramKey];
|
|
@@ -179,6 +262,12 @@ cli.main(function(args, options) {
|
|
|
179
262
|
case 'json':
|
|
180
263
|
minifyOptions[key] = parseJSONOption(value);
|
|
181
264
|
break;
|
|
265
|
+
case 'json-regex':
|
|
266
|
+
minifyOptions[key] = parseJSONOption(value, { regexArray: true });
|
|
267
|
+
break;
|
|
268
|
+
case 'string-regex':
|
|
269
|
+
minifyOptions[key] = stringToRegExp(value);
|
|
270
|
+
break;
|
|
182
271
|
case true:
|
|
183
272
|
minifyOptions[key] = value;
|
|
184
273
|
break;
|
|
@@ -196,6 +285,42 @@ cli.main(function(args, options) {
|
|
|
196
285
|
input = args;
|
|
197
286
|
}
|
|
198
287
|
|
|
288
|
+
if (options['input-dir'] || options['output-dir']) {
|
|
289
|
+
var inputDir = options['input-dir'];
|
|
290
|
+
var outputDir = options['output-dir'];
|
|
291
|
+
|
|
292
|
+
if (!inputDir) {
|
|
293
|
+
cli.error('The option output-dir need to be use with the option include-dir. If you are working with only 1 file, use -o.');
|
|
294
|
+
cli.exit(2);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
fs.statSync(inputDir);
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
cli.error('The input directory does not exits');
|
|
302
|
+
cli.exit(2);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if (!outputDir) {
|
|
307
|
+
cli.error('You need to specify where to write the output files with the option --output-dir');
|
|
308
|
+
cli.exit(2);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
processDirectory(inputDir, outputDir);
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
cli.error('Something wrong happened');
|
|
316
|
+
cli.error(e);
|
|
317
|
+
cli.exit(3);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
cli.exit(0);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
199
324
|
if (options.output) {
|
|
200
325
|
output = options.output;
|
|
201
326
|
}
|
|
@@ -209,16 +334,15 @@ cli.main(function(args, options) {
|
|
|
209
334
|
original += fs.readFileSync(afile, 'utf8');
|
|
210
335
|
}
|
|
211
336
|
catch (e) {
|
|
212
|
-
|
|
337
|
+
cli.error('Cannot read file ' + afile);
|
|
213
338
|
cli.exit(2);
|
|
214
339
|
}
|
|
215
340
|
});
|
|
216
341
|
|
|
217
|
-
runMinify(original);
|
|
342
|
+
runMinify(original, output);
|
|
218
343
|
|
|
219
344
|
}
|
|
220
345
|
else { // Minifying input coming from STDIN
|
|
221
346
|
process.stdin.pipe(concat({ encoding: 'string' }, runMinify));
|
|
222
347
|
}
|
|
223
|
-
|
|
224
348
|
});
|
package/dist/htmlminifier.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* HTMLMinifier
|
|
3
|
-
* Copyright 2010-
|
|
4
|
-
* Licensed under MIT
|
|
2
|
+
* HTMLMinifier v1.1.1 (http://kangax.github.io/html-minifier/)
|
|
3
|
+
* Copyright 2010-2016 Juriy "kangax" Zaytsev
|
|
4
|
+
* Licensed under the MIT license
|
|
5
5
|
*/
|
|
6
6
|
/*!
|
|
7
7
|
* HTML Parser By John Resig (ejohn.org)
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected');
|
|
74
74
|
|
|
75
75
|
// Special Elements (can contain anything)
|
|
76
|
-
var special = makeMap('script,style
|
|
76
|
+
var special = makeMap('script,style');
|
|
77
77
|
|
|
78
78
|
var reCache = {}, stackedTag, reStackedTag, tagMatch;
|
|
79
79
|
|
|
@@ -622,12 +622,12 @@
|
|
|
622
622
|
lineBreakStamp = 'htmlmincollapsedlinebreak';
|
|
623
623
|
|
|
624
624
|
if (prevTag && prevTag !== 'img' && prevTag !== 'input' && (prevTag.substr(0, 1) !== '/'
|
|
625
|
-
|| (prevTag.substr(0, 1) === '/' && tags.indexOf(prevTag.substr(1)) === -1))) {
|
|
625
|
+
|| (prevTag.substr(0, 1) === '/' && (options.collapseInlineTagWhitespace || tags.indexOf(prevTag.substr(1)) === -1)))) {
|
|
626
626
|
str = str.replace(/^\s+/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveBefore : '');
|
|
627
627
|
}
|
|
628
628
|
|
|
629
629
|
if (nextTag && nextTag !== 'img' && nextTag !== 'input' && (nextTag.substr(0, 1) === '/'
|
|
630
|
-
|| (nextTag.substr(0, 1) !== '/' && tags.indexOf(nextTag) === -1))) {
|
|
630
|
+
|| (nextTag.substr(0, 1) !== '/' && (options.collapseInlineTagWhitespace || tags.indexOf(nextTag) === -1)))) {
|
|
631
631
|
str = str.replace(/\s+$/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveAfter : '');
|
|
632
632
|
}
|
|
633
633
|
|
|
@@ -652,7 +652,7 @@
|
|
|
652
652
|
}
|
|
653
653
|
|
|
654
654
|
function isIgnoredComment(text, options) {
|
|
655
|
-
if ((/^!/).test(text)) {
|
|
655
|
+
if ((/^!/).test(text) || /\s*htmlmin:temp/.test(text)) {
|
|
656
656
|
return true;
|
|
657
657
|
}
|
|
658
658
|
|
|
@@ -671,11 +671,11 @@
|
|
|
671
671
|
return (/^on[a-z]+/).test(attrName);
|
|
672
672
|
}
|
|
673
673
|
|
|
674
|
-
function canRemoveAttributeQuotes(value) {
|
|
674
|
+
function canRemoveAttributeQuotes(value, isLast) {
|
|
675
675
|
// http://mathiasbynens.be/notes/unquoted-attribute-values
|
|
676
|
-
return (/^[^\x20\t\n\f\r"'`=<>]+$/).test(value) &&
|
|
677
|
-
|
|
678
|
-
|
|
676
|
+
return (/^[^\x20\t\n\f\r"'`=<>]+$/).test(value) &&
|
|
677
|
+
// make sure trailing slash is not interpreted as HTML self-closing tag
|
|
678
|
+
!(isLast && (/\/$/).test(value));
|
|
679
679
|
}
|
|
680
680
|
|
|
681
681
|
function attributesInclude(attributes, attribute) {
|
|
@@ -841,7 +841,7 @@
|
|
|
841
841
|
attrValue = attrValue.replace(/1\.0/g, '1').replace(/\s+/g, '');
|
|
842
842
|
}
|
|
843
843
|
else if (attrValue && options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
|
|
844
|
-
attrValue = attrValue.replace(/\n
|
|
844
|
+
attrValue = attrValue.replace(/\n+|\r+|\s{2,}/g, '');
|
|
845
845
|
}
|
|
846
846
|
return attrValue;
|
|
847
847
|
}
|
|
@@ -911,7 +911,7 @@
|
|
|
911
911
|
}
|
|
912
912
|
|
|
913
913
|
function isOptionalTag(tag) {
|
|
914
|
-
return (/^(?:html|t?body|t?head|tfoot|tr|td|th|dt|dd|option|colgroup|source)$/).test(tag);
|
|
914
|
+
return (/^(?:html|t?body|t?head|tfoot|tr|td|th|dt|dd|option|colgroup|source|track)$/).test(tag);
|
|
915
915
|
}
|
|
916
916
|
|
|
917
917
|
var reEmptyAttribute = new RegExp(
|
|
@@ -952,19 +952,11 @@
|
|
|
952
952
|
return !(/^(?:pre|textarea)$/.test(tag));
|
|
953
953
|
}
|
|
954
954
|
|
|
955
|
-
function
|
|
956
|
-
var markup = '';
|
|
957
|
-
for (var i = 0, len = attrs.length; i < len; i++) {
|
|
958
|
-
markup += (' ' + attrs[i].name + (isBooleanAttribute(attrs[i].value) ? '' : ('="' + attrs[i].value + '"')));
|
|
959
|
-
}
|
|
960
|
-
return markup;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function normalizeAttribute(attr, attrs, tag, unarySlash, index, options) {
|
|
955
|
+
function normalizeAttribute(attr, attrs, tag, unarySlash, index, options, isLast) {
|
|
964
956
|
|
|
965
957
|
var attrName = options.caseSensitive ? attr.name : attr.name.toLowerCase(),
|
|
966
958
|
attrValue = options.preventAttributesEscaping ? attr.value : attr.escaped,
|
|
967
|
-
attrQuote = options.preventAttributesEscaping ? attr.quote : '"',
|
|
959
|
+
attrQuote = options.preventAttributesEscaping ? attr.quote : (options.quoteCharacter === '\'' ? '\'' : '"'),
|
|
968
960
|
attrFragment,
|
|
969
961
|
emittedAttrValue,
|
|
970
962
|
isTerminalOfUnarySlash = unarySlash && index === attrs.length - 1;
|
|
@@ -983,7 +975,7 @@
|
|
|
983
975
|
attrValue = cleanAttributeValue(tag, attrName, attrValue, options, attrs);
|
|
984
976
|
|
|
985
977
|
if (attrValue !== undefined && !options.removeAttributeQuotes ||
|
|
986
|
-
!canRemoveAttributeQuotes(attrValue) || isTerminalOfUnarySlash) {
|
|
978
|
+
!canRemoveAttributeQuotes(attrValue, isLast) || isTerminalOfUnarySlash) {
|
|
987
979
|
emittedAttrValue = attrQuote + attrValue + attrQuote;
|
|
988
980
|
}
|
|
989
981
|
else {
|
|
@@ -1151,15 +1143,37 @@
|
|
|
1151
1143
|
stackNoTrimWhitespace = [],
|
|
1152
1144
|
stackNoCollapseWhitespace = [],
|
|
1153
1145
|
lint = options.lint,
|
|
1154
|
-
|
|
1155
|
-
|
|
1146
|
+
t = new Date(),
|
|
1147
|
+
ignoredMarkupChunks = [ ],
|
|
1148
|
+
ignoredCustomMarkupChunks = [ ],
|
|
1149
|
+
reIgnore = /<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g,
|
|
1150
|
+
uidAttr = ' htmlmin' + (Math.random() + '').slice(2) + ' ',
|
|
1151
|
+
reCustomIgnore,
|
|
1152
|
+
customFragments;
|
|
1153
|
+
|
|
1154
|
+
if (options.ignoreCustomFragments) {
|
|
1155
|
+
|
|
1156
|
+
customFragments = options.ignoreCustomFragments.map(function(re) {
|
|
1157
|
+
return re.source;
|
|
1158
|
+
});
|
|
1159
|
+
reCustomIgnore = new RegExp('(?:' + customFragments.join('|') + ')', 'g');
|
|
1156
1160
|
|
|
1157
|
-
|
|
1158
|
-
value = value
|
|
1159
|
-
.
|
|
1160
|
-
|
|
1161
|
+
// temporarily replace custom ignored fragments with unique attributes
|
|
1162
|
+
value = value.replace(reCustomIgnore, function(match) {
|
|
1163
|
+
ignoredCustomMarkupChunks.push(match);
|
|
1164
|
+
return uidAttr;
|
|
1165
|
+
});
|
|
1161
1166
|
}
|
|
1162
1167
|
|
|
1168
|
+
// temporarily replace ignored chunks with comments,
|
|
1169
|
+
// so that we don't have to worry what's there.
|
|
1170
|
+
// for all we care there might be
|
|
1171
|
+
// completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
|
|
1172
|
+
value = value.replace(reIgnore, function(match, group1) {
|
|
1173
|
+
ignoredMarkupChunks.push(group1);
|
|
1174
|
+
return '<!-- htmlmin:temp -->';
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1163
1177
|
function _canCollapseWhitespace(tag, attrs) {
|
|
1164
1178
|
return canCollapseWhitespace(tag) || options.canCollapseWhitespace(tag, attrs);
|
|
1165
1179
|
}
|
|
@@ -1172,10 +1186,6 @@
|
|
|
1172
1186
|
html5: typeof options.html5 !== 'undefined' ? options.html5 : true,
|
|
1173
1187
|
|
|
1174
1188
|
start: function(tag, attrs, unary, unarySlash) {
|
|
1175
|
-
if (isIgnoring) {
|
|
1176
|
-
buffer.push('<' + tag, attrsToMarkup(attrs), unarySlash ? '/' : '', '>');
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
1189
|
|
|
1180
1190
|
var lowerTag = tag.toLowerCase();
|
|
1181
1191
|
|
|
@@ -1218,13 +1228,14 @@
|
|
|
1218
1228
|
lint.testElement(tag);
|
|
1219
1229
|
}
|
|
1220
1230
|
|
|
1221
|
-
var token;
|
|
1231
|
+
var token, isLast;
|
|
1222
1232
|
for (var i = 0, len = attrs.length; i < len; i++) {
|
|
1233
|
+
isLast = i === len - 1;
|
|
1223
1234
|
if (lint) {
|
|
1224
1235
|
lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped);
|
|
1225
1236
|
}
|
|
1226
|
-
token = normalizeAttribute(attrs[i], attrs, tag, unarySlash, i, options);
|
|
1227
|
-
if (
|
|
1237
|
+
token = normalizeAttribute(attrs[i], attrs, tag, unarySlash, i, options, isLast);
|
|
1238
|
+
if (isLast) {
|
|
1228
1239
|
token += closeTag;
|
|
1229
1240
|
}
|
|
1230
1241
|
buffer.push(token);
|
|
@@ -1232,11 +1243,6 @@
|
|
|
1232
1243
|
},
|
|
1233
1244
|
end: function(tag, attrs) {
|
|
1234
1245
|
|
|
1235
|
-
if (isIgnoring) {
|
|
1236
|
-
buffer.push('</' + tag + '>');
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
1246
|
var lowerTag = tag.toLowerCase();
|
|
1241
1247
|
if (lowerTag === 'svg') {
|
|
1242
1248
|
options = optionsStack.pop();
|
|
@@ -1282,11 +1288,6 @@
|
|
|
1282
1288
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
1283
1289
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
1284
1290
|
|
|
1285
|
-
if (isIgnoring) {
|
|
1286
|
-
buffer.push(text);
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
1291
|
if (currentTag === 'script' || currentTag === 'style') {
|
|
1291
1292
|
if (options.removeCommentsFromCDATA) {
|
|
1292
1293
|
text = removeComments(text, currentTag);
|
|
@@ -1325,13 +1326,6 @@
|
|
|
1325
1326
|
var prefix = nonStandard ? '<!' : '<!--';
|
|
1326
1327
|
var suffix = nonStandard ? '>' : '-->';
|
|
1327
1328
|
|
|
1328
|
-
if (/^\s*htmlmin:ignore/.test(text)) {
|
|
1329
|
-
isIgnoring = !isIgnoring;
|
|
1330
|
-
if (!options.removeComments) {
|
|
1331
|
-
buffer.push('<!--' + text + '-->');
|
|
1332
|
-
}
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
1329
|
if (options.removeComments) {
|
|
1336
1330
|
if (isConditionalComment(text)) {
|
|
1337
1331
|
text = prefix + cleanConditionalComment(text) + suffix;
|
|
@@ -1357,6 +1351,16 @@
|
|
|
1357
1351
|
|
|
1358
1352
|
results.push.apply(results, buffer);
|
|
1359
1353
|
var str = joinResultSegments(results, options);
|
|
1354
|
+
|
|
1355
|
+
var customIgnoreRe = new RegExp('\\s*' + trimWhitespace(uidAttr) + '\\s*', 'g');
|
|
1356
|
+
|
|
1357
|
+
str = str.replace(customIgnoreRe, function() {
|
|
1358
|
+
return ignoredCustomMarkupChunks.shift();
|
|
1359
|
+
});
|
|
1360
|
+
str = str.replace(/<!-- htmlmin:temp -->/g, function() {
|
|
1361
|
+
return ignoredMarkupChunks.shift();
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1360
1364
|
log('minified in: ' + (new Date() - t) + 'ms');
|
|
1361
1365
|
return str;
|
|
1362
1366
|
}
|
|
@@ -1397,7 +1401,7 @@
|
|
|
1397
1401
|
global.minify = minify;
|
|
1398
1402
|
}
|
|
1399
1403
|
|
|
1400
|
-
}(this));
|
|
1404
|
+
}(typeof exports === 'undefined' ? this : exports));
|
|
1401
1405
|
|
|
1402
1406
|
/*!
|
|
1403
1407
|
* HTMLLint (to be used in conjunction with HTMLMinifier)
|