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 CHANGED
@@ -1,7 +1,7 @@
1
- [![NPM version](https://img.shields.io/npm/v/html-minifier.svg?style=flat)](https://www.npmjs.com/package/html-minifier)
2
- [![Build Status](https://img.shields.io/travis/kangax/html-minifier.svg?style=flat)](https://travis-ci.org/kangax/html-minifier)
3
- [![Dependency Status](https://img.shields.io/david/kangax/html-minifier.svg?style=flat)](https://david-dm.org/kangax/html-minifier)
4
- [![devDependency Status](https://img.shields.io/david/dev/kangax/html-minifier.svg?style=flat)](https://david-dm.org/kangax/html-minifier#info=devDependencies)
1
+ [![NPM version](https://img.shields.io/npm/v/html-minifier.svg)](https://www.npmjs.com/package/html-minifier)
2
+ [![Build Status](https://img.shields.io/travis/kangax/html-minifier.svg)](https://travis-ci.org/kangax/html-minifier)
3
+ [![Dependency Status](https://img.shields.io/david/kangax/html-minifier.svg)](https://david-dm.org/kangax/html-minifier)
4
+ [![devDependency Status](https://img.shields.io/david/dev/kangax/html-minifier.svg)](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
- How does HTMLMinifier compare to [another solution](http://www.willpeavy.com/minifier/) — HTML Minifier from Will Peavy (1st result in [google search for "html minifier"](https://www.google.com/#q=html+minifier)) as well as htmlcompressor.com?
14
+ ## Minification comparison
15
15
 
16
- | Site | Original size _(KB)_ | HTMLMinifier _(KB)_ | Will Peavy _(KB)_ | htmlcompressor.com _(KB)_ |
17
- | --------------------------------------------------------------------------- |:-----------:| ----------------:| ------------:| ----------------:|
18
- | [HTMLMinifier page](https://github.com/kangax/html-minifier) | 48.8 | <b>37.3</b> | 43.3 | 41.9 |
19
- | [ES6 table](http://kangax.github.io/es5-compat-table/es6/) | 117.9 | <b>79.9</b> | 92 | 91.9 |
20
- | [MSN](http://msn.com) | 156.6 | <b>133</b> | 145 | 138.3 |
21
- | [Stackoverflow](http://stackoverflow.com) | 200.4 | <b>159.5</b> | 168.3 | 163.3 |
22
- | [Amazon](http://amazon.com) | 245.9 | <b>206.3</b> | 225 | 218.5 |
23
- | [Wikipedia](http://en.wikipedia.org/wiki/President_of_the_United_States) | 401.4 | <b>380.6</b> | 396.3 | n/a |
24
- | [Eloquent Javascript](http://eloquentjavascript.net/print.html) | 869.5 | <b>830</b> | 872 | n/a |
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'], 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-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 parseJSONOption(value) {
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
- process.stderr.write('Error: Minification error');
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
- process.stderr.write('Error: Cannot write to output');
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
- process.stderr.write(appName + ' v' + appVersion);
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
- var fileOptions = JSON.parse(fs.readFileSync(path.resolve(options['config-file']), 'utf8'));
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
- process.stderr.write('Error: Cannot read the specified config file');
170
- cli.exit(1);
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
- process.stderr.write('Error: Cannot read file ' + afile);
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
  });
@@ -1,7 +1,7 @@
1
1
  /*!
2
- * HTMLMinifier v0.7.2 (http://kangax.github.io/html-minifier/)
3
- * Copyright 2010-2015 Juriy "kangax" Zaytsev
4
- * Licensed under MIT (https://github.com/kangax/html-minifier/blob/gh-pages/LICENSE)
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,noscript');
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) && !(/\/$/).test(value) &&
677
- // make sure trailing slash is not interpreted as HTML self-closing tag
678
- !(/\/$/).test(value);
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+/g, '');
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 attrsToMarkup(attrs) {
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
- isIgnoring = false,
1155
- t = new Date();
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
- if (options.removeIgnored) {
1158
- value = value
1159
- .replace(/<\?[^\?]+\?>/g, '')
1160
- .replace(/<%[^%]+%>/g, '');
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 (i === len - 1) {
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)