html-minifier 1.1.1 → 1.4.0

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.
@@ -1,838 +1,1108 @@
1
- /* global CleanCSS */
2
-
3
- (function(global) {
4
- 'use strict';
5
-
6
- var log, HTMLParser;
7
- if (global.console && global.console.log) {
8
- log = function(message) {
9
- // "preserving" `this`
10
- global.console.log(message);
11
- };
12
- }
13
- else {
14
- log = function() {};
15
- }
1
+ 'use strict';
2
+
3
+ var CleanCSS = require('clean-css');
4
+ var HTMLParser = require('./htmlparser').HTMLParser;
5
+ var RelateUrl = require('relateurl');
6
+ var UglifyJS = require('uglify-js');
7
+ var utils = require('./utils');
8
+
9
+ var log;
10
+ if (typeof window !== 'undefined' && typeof console !== 'undefined' && typeof console.log === 'function') {
11
+ log = function(message) {
12
+ // "preserving" `this`
13
+ console.log(message);
14
+ };
15
+ }
16
+ else {
17
+ log = function() {};
18
+ }
16
19
 
17
- if (global.HTMLParser) {
18
- HTMLParser = global.HTMLParser;
19
- }
20
- else if (typeof require === 'function') {
21
- HTMLParser = require('./htmlparser').HTMLParser;
20
+ var trimWhitespace = function(str) {
21
+ if (typeof str !== 'string') {
22
+ return str;
22
23
  }
23
-
24
- var trimWhitespace = function(str) {
24
+ return str.replace(/^\s+/, '').replace(/\s+$/, '');
25
+ };
26
+ if (String.prototype.trim) {
27
+ trimWhitespace = function(str) {
25
28
  if (typeof str !== 'string') {
26
29
  return str;
27
30
  }
28
- return str.replace(/^\s+/, '').replace(/\s+$/, '');
31
+ return str.trim();
29
32
  };
30
- if (String.prototype.trim) {
31
- trimWhitespace = function(str) {
32
- if (typeof str !== 'string') {
33
- return str;
34
- }
35
- return str.trim();
36
- };
37
- }
38
-
39
- function collapseWhitespace(str) {
40
- return str ? str.replace(/[\t\n\r ]+/g, ' ') : str;
41
- }
33
+ }
42
34
 
43
- function collapseWhitespaceSmart(str, prevTag, nextTag, options) {
35
+ function compressWhitespace(spaces) {
36
+ return spaces === '\t' ? spaces : ' ';
37
+ }
44
38
 
45
- // array of non-empty element tags that will maintain a single space outside of them
46
- var tags = [
47
- 'a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite',
48
- 'code', 'del', 'dfn', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'q',
49
- 'rt', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong',
50
- 'sub', 'sup', 'svg', 'time', 'tt', 'u', 'var'
51
- ],
52
- lineBreakBefore = /^[\t ]*[\n\r]+[\t\n\r ]*/,
53
- lineBreakAfter = /[\t\n\r ]*[\n\r]+[\t ]*$/,
54
- preserveBefore = lineBreakBefore.test(str) ? '\n' : ' ',
55
- preserveAfter = lineBreakAfter.test(str) ? '\n' : ' ',
56
- lineBreakStamp = 'htmlmincollapsedlinebreak';
57
-
58
- if (prevTag && prevTag !== 'img' && prevTag !== 'input' && (prevTag.substr(0, 1) !== '/'
59
- || (prevTag.substr(0, 1) === '/' && (options.collapseInlineTagWhitespace || tags.indexOf(prevTag.substr(1)) === -1)))) {
60
- str = str.replace(/^\s+/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveBefore : '');
61
- }
39
+ function collapseWhitespaceAll(str) {
40
+ return str ? str.replace(/[\t\n\r ]+/g, compressWhitespace) : str;
41
+ }
62
42
 
63
- if (nextTag && nextTag !== 'img' && nextTag !== 'input' && (nextTag.substr(0, 1) === '/'
64
- || (nextTag.substr(0, 1) !== '/' && (options.collapseInlineTagWhitespace || tags.indexOf(nextTag) === -1)))) {
65
- str = str.replace(/\s+$/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveAfter : '');
66
- }
43
+ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
44
+ var lineBreakBefore = '', lineBreakAfter = '';
67
45
 
68
- if (prevTag && nextTag) {
46
+ if (options.preserveLineBreaks) {
47
+ str = str.replace(/^[\t ]*[\n\r][\t\n\r ]*/, function() {
48
+ lineBreakBefore = '\n';
49
+ return '';
50
+ }).replace(/[\t\n\r ]*[\n\r][\t ]*$/, function() {
51
+ lineBreakAfter = '\n';
52
+ return '';
53
+ });
54
+ }
69
55
 
70
- if (options.preserveLineBreaks) {
71
- str = str
72
- .replace(lineBreakBefore, lineBreakStamp)
73
- .replace(lineBreakAfter, lineBreakStamp);
74
- }
75
- // strip non space whitespace then compress spaces to one
76
- return str
77
- .replace(/[\t\n\r]+/g, ' ').replace(/[ ]+/g, ' ')
78
- .replace(new RegExp(lineBreakStamp, 'g'), '\n');
79
- }
56
+ if (trimLeft) {
57
+ str = str.replace(/^\s+/, !lineBreakBefore && options.conservativeCollapse ? compressWhitespace : '');
58
+ }
80
59
 
81
- return str;
60
+ if (trimRight) {
61
+ str = str.replace(/\s+$/, !lineBreakAfter && options.conservativeCollapse ? compressWhitespace : '');
82
62
  }
83
63
 
84
- function isConditionalComment(text) {
85
- return ((/\[if[^\]]+\]/).test(text) || (/\s*((?:<!)?\[endif\])$/).test(text));
64
+ if (collapseAll) {
65
+ // strip non space whitespace then compress spaces to one
66
+ str = collapseWhitespaceAll(str);
86
67
  }
87
68
 
88
- function isIgnoredComment(text, options) {
89
- if ((/^!/).test(text) || /\s*htmlmin:temp/.test(text)) {
90
- return true;
91
- }
69
+ return lineBreakBefore + str + lineBreakAfter;
70
+ }
71
+
72
+ var createMapFromString = utils.createMapFromString;
73
+ // non-empty tags that will maintain whitespace around them
74
+ var inlineTags = createMapFromString('a,abbr,acronym,b,bdi,bdo,big,button,cite,code,del,dfn,em,font,i,ins,kbd,mark,math,q,rt,rp,s,samp,small,span,strike,strong,sub,sup,svg,time,tt,u,var');
75
+ // non-empty tags that will maintain whitespace within them
76
+ var inlineTextTags = createMapFromString('a,abbr,acronym,b,big,del,em,font,i,ins,kbd,mark,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var');
77
+ // self-closing tags that will maintain whitespace around them
78
+ var selfClosingInlineTags = createMapFromString('comment,img,input');
79
+
80
+ function collapseWhitespaceSmart(str, prevTag, nextTag, options) {
81
+ var trimLeft = prevTag && !selfClosingInlineTags(prevTag);
82
+ if (trimLeft && !options.collapseInlineTagWhitespace) {
83
+ trimLeft = prevTag.charAt(0) === '/' ? !inlineTags(prevTag.slice(1)) : !inlineTextTags(prevTag);
84
+ }
85
+ var trimRight = nextTag && !selfClosingInlineTags(nextTag);
86
+ if (trimRight && !options.collapseInlineTagWhitespace) {
87
+ trimRight = nextTag.charAt(0) === '/' ? !inlineTextTags(nextTag.slice(1)) : !inlineTags(nextTag);
88
+ }
89
+ return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
90
+ }
92
91
 
93
- if (options.ignoreCustomComments) {
94
- for (var i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
95
- if (options.ignoreCustomComments[i].test(text)) {
96
- return true;
97
- }
98
- }
99
- }
92
+ function isConditionalComment(text) {
93
+ return /^\[if\s[^\]]+\]|\[endif\]$/.test(text);
94
+ }
100
95
 
101
- return false;
96
+ function isIgnoredComment(text, options) {
97
+ if (/^!/.test(text)) {
98
+ return true;
102
99
  }
103
100
 
104
- function isEventAttribute(attrName) {
105
- return (/^on[a-z]+/).test(attrName);
101
+ if (options.ignoreCustomComments) {
102
+ for (var i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
103
+ if (options.ignoreCustomComments[i].test(text)) {
104
+ return true;
105
+ }
106
+ }
106
107
  }
107
108
 
108
- function canRemoveAttributeQuotes(value, isLast) {
109
- // http://mathiasbynens.be/notes/unquoted-attribute-values
110
- return (/^[^\x20\t\n\f\r"'`=<>]+$/).test(value) &&
111
- // make sure trailing slash is not interpreted as HTML self-closing tag
112
- !(isLast && (/\/$/).test(value));
113
- }
109
+ return false;
110
+ }
114
111
 
115
- function attributesInclude(attributes, attribute) {
116
- for (var i = attributes.length; i--;) {
117
- if (attributes[i].name.toLowerCase() === attribute) {
112
+ function isEventAttribute(attrName, options) {
113
+ var patterns = options.customEventAttributes;
114
+ if (patterns) {
115
+ for (var i = patterns.length; i--;) {
116
+ if (patterns[i].test(attrName)) {
118
117
  return true;
119
118
  }
120
119
  }
121
120
  return false;
122
121
  }
122
+ else {
123
+ return (/^on[a-z]{3,}$/).test(attrName);
124
+ }
125
+ }
123
126
 
124
- function isAttributeRedundant(tag, attrName, attrValue, attrs) {
125
- attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
126
-
127
- return (
128
- (tag === 'script' &&
129
- attrName === 'language' &&
130
- attrValue === 'javascript') ||
131
-
132
- (tag === 'form' &&
133
- attrName === 'method' &&
134
- attrValue === 'get') ||
127
+ function canRemoveAttributeQuotes(value) {
128
+ // http://mathiasbynens.be/notes/unquoted-attribute-values
129
+ return (/^[^\x20\t\n\f\r"'`=<>]+$/).test(value);
130
+ }
135
131
 
136
- (tag === 'input' &&
137
- attrName === 'type' &&
138
- attrValue === 'text') ||
132
+ function attributesInclude(attributes, attribute) {
133
+ for (var i = attributes.length; i--;) {
134
+ if (attributes[i].name.toLowerCase() === attribute) {
135
+ return true;
136
+ }
137
+ }
138
+ return false;
139
+ }
139
140
 
140
- (tag === 'script' &&
141
- attrName === 'charset' &&
142
- !attributesInclude(attrs, 'src')) ||
141
+ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
142
+ attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
143
143
 
144
- (tag === 'a' &&
145
- attrName === 'name' &&
146
- attributesInclude(attrs, 'id')) ||
144
+ return (
145
+ (tag === 'script' &&
146
+ attrName === 'language' &&
147
+ attrValue === 'javascript') ||
147
148
 
148
- (tag === 'area' &&
149
- attrName === 'shape' &&
150
- attrValue === 'rect')
151
- );
152
- }
149
+ (tag === 'form' &&
150
+ attrName === 'method' &&
151
+ attrValue === 'get') ||
153
152
 
154
- function isScriptTypeAttribute(tag, attrName, attrValue) {
155
- return (
156
- tag === 'script' &&
153
+ (tag === 'input' &&
157
154
  attrName === 'type' &&
158
- trimWhitespace(attrValue.toLowerCase()) === 'text/javascript'
159
- );
160
- }
161
-
162
- // https://mathiasbynens.be/demo/javascript-mime-type
163
- // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
164
- var executableScriptsMimetypes = {
165
- 'text/javascript': 1,
166
- 'text/ecmascript': 1,
167
- 'text/jscript': 1,
168
- 'application/javascript': 1,
169
- 'application/x-javascript': 1,
170
- 'application/ecmascript': 1
171
- };
172
-
173
- function isExecutableScript(tag, attrs) {
174
- if (tag !== 'script') {
175
- return false;
176
- }
177
- for (var i = 0, len = attrs.length; i < len; i++) {
178
- var attrName = attrs[i].name.toLowerCase();
179
- if (attrName === 'type') {
180
- return attrs[i].value === '' ||
181
- executableScriptsMimetypes[attrs[i].value] === 1;
182
- }
155
+ attrValue === 'text') ||
156
+
157
+ (tag === 'script' &&
158
+ attrName === 'charset' &&
159
+ !attributesInclude(attrs, 'src')) ||
160
+
161
+ (tag === 'a' &&
162
+ attrName === 'name' &&
163
+ attributesInclude(attrs, 'id')) ||
164
+
165
+ (tag === 'area' &&
166
+ attrName === 'shape' &&
167
+ attrValue === 'rect')
168
+ );
169
+ }
170
+
171
+ // https://mathiasbynens.be/demo/javascript-mime-type
172
+ // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
173
+ var executableScriptsMimetypes = utils.createMap([
174
+ 'text/javascript',
175
+ 'text/ecmascript',
176
+ 'text/jscript',
177
+ 'application/javascript',
178
+ 'application/x-javascript',
179
+ 'application/ecmascript'
180
+ ]);
181
+
182
+ function isScriptTypeAttribute(tag, attrName, attrValue) {
183
+ return (
184
+ tag === 'script' &&
185
+ attrName === 'type' &&
186
+ executableScriptsMimetypes(trimWhitespace(attrValue.toLowerCase()))
187
+ );
188
+ }
189
+
190
+ function isExecutableScript(tag, attrs) {
191
+ if (tag !== 'script') {
192
+ return false;
193
+ }
194
+ for (var i = 0, len = attrs.length; i < len; i++) {
195
+ var attrName = attrs[i].name.toLowerCase();
196
+ if (attrName === 'type') {
197
+ var attrValue = trimWhitespace(attrs[i].value.split(/;/, 2)[0]).toLowerCase();
198
+ return attrValue === '' || executableScriptsMimetypes(attrValue);
183
199
  }
184
- return true;
185
200
  }
201
+ return true;
202
+ }
186
203
 
187
- function isStyleLinkTypeAttribute(tag, attrName, attrValue) {
188
- return (
189
- (tag === 'style' || tag === 'link') &&
190
- attrName === 'type' &&
191
- trimWhitespace(attrValue.toLowerCase()) === 'text/css'
192
- );
204
+ function isStyleSheet(tag, attrs) {
205
+ if (tag !== 'style') {
206
+ return false;
193
207
  }
194
-
195
- var enumeratedAttributeValues = {
196
- draggable: ['true', 'false'] // defaults to 'auto'
197
- };
198
-
199
- function isBooleanAttribute(attrName, attrValue) {
200
- var isSimpleBoolean = (/^(?:allowfullscreen|async|autofocus|autoplay|checked|compact|controls|declare|default|defaultchecked|defaultmuted|defaultselected|defer|disabled|enabled|formnovalidate|hidden|indeterminate|inert|ismap|itemscope|loop|multiple|muted|nohref|noresize|noshade|novalidate|nowrap|open|pauseonexit|readonly|required|reversed|scoped|seamless|selected|sortable|spellcheck|truespeed|typemustmatch|visible)$/i).test(attrName);
201
- if (isSimpleBoolean) {
202
- return true;
203
- }
204
-
205
- var attrValueEnumeration = enumeratedAttributeValues[attrName.toLowerCase()];
206
- if (!attrValueEnumeration) {
207
- return false;
208
+ for (var i = 0, len = attrs.length; i < len; i++) {
209
+ var attrName = attrs[i].name.toLowerCase();
210
+ if (attrName === 'type') {
211
+ var attrValue = trimWhitespace(attrs[i].value).toLowerCase();
212
+ return attrValue === '' || attrValue === 'text/css';
208
213
  }
209
- else {
210
- return (-1 === attrValueEnumeration.indexOf(attrValue.toLowerCase()));
214
+ }
215
+ return true;
216
+ }
217
+
218
+ function isStyleLinkTypeAttribute(tag, attrName, attrValue) {
219
+ return (
220
+ (tag === 'style' || tag === 'link') &&
221
+ attrName === 'type' &&
222
+ trimWhitespace(attrValue.toLowerCase()) === 'text/css'
223
+ );
224
+ }
225
+
226
+ var isSimpleBoolean = createMapFromString('allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible');
227
+ var isBooleanValue = createMapFromString('true,false');
228
+
229
+ function isBooleanAttribute(attrName, attrValue) {
230
+ return isSimpleBoolean(attrName) || attrName === 'draggable' && !isBooleanValue(attrValue);
231
+ }
232
+
233
+ function isUriTypeAttribute(attrName, tag) {
234
+ return (
235
+ ((/^(?:a|area|link|base)$/).test(tag) && attrName === 'href') ||
236
+ (tag === 'img' && (/^(?:src|longdesc|usemap)$/).test(attrName)) ||
237
+ (tag === 'object' && (/^(?:classid|codebase|data|usemap)$/).test(attrName)) ||
238
+ (tag === 'q' && attrName === 'cite') ||
239
+ (tag === 'blockquote' && attrName === 'cite') ||
240
+ ((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
241
+ (tag === 'form' && attrName === 'action') ||
242
+ (tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
243
+ (tag === 'head' && attrName === 'profile') ||
244
+ (tag === 'script' && (attrName === 'src' || attrName === 'for'))
245
+ );
246
+ }
247
+
248
+ function isNumberTypeAttribute(attrName, tag) {
249
+ return (
250
+ ((/^(?:a|area|object|button)$/).test(tag) && attrName === 'tabindex') ||
251
+ (tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
252
+ (tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
253
+ (tag === 'textarea' && (/^(?:rows|cols|tabindex)$/).test(attrName)) ||
254
+ (tag === 'colgroup' && attrName === 'span') ||
255
+ (tag === 'col' && attrName === 'span') ||
256
+ ((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
257
+ );
258
+ }
259
+
260
+ function isCanonicalURL(tag, attrs) {
261
+ if (tag !== 'link') {
262
+ return false;
263
+ }
264
+ for (var i = 0, len = attrs.length; i < len; i++) {
265
+ if (attrs[i].name === 'rel' && attrs[i].value === 'canonical') {
266
+ return true;
211
267
  }
212
268
  }
269
+ }
213
270
 
214
- function isUriTypeAttribute(attrName, tag) {
215
- return (
216
- ((/^(?:a|area|link|base)$/).test(tag) && attrName === 'href') ||
217
- (tag === 'img' && (/^(?:src|longdesc|usemap)$/).test(attrName)) ||
218
- (tag === 'object' && (/^(?:classid|codebase|data|usemap)$/).test(attrName)) ||
219
- (tag === 'q' && attrName === 'cite') ||
220
- (tag === 'blockquote' && attrName === 'cite') ||
221
- ((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
222
- (tag === 'form' && attrName === 'action') ||
223
- (tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
224
- (tag === 'head' && attrName === 'profile') ||
225
- (tag === 'script' && (attrName === 'src' || attrName === 'for'))
226
- );
227
- }
228
-
229
- function isNumberTypeAttribute(attrName, tag) {
230
- return (
231
- ((/^(?:a|area|object|button)$/).test(tag) && attrName === 'tabindex') ||
232
- (tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
233
- (tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
234
- (tag === 'textarea' && (/^(?:rows|cols|tabindex)$/).test(attrName)) ||
235
- (tag === 'colgroup' && attrName === 'span') ||
236
- (tag === 'col' && attrName === 'span') ||
237
- ((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
238
- );
239
- }
271
+ var fnPrefix = '!function(){';
272
+ var fnSuffix = '}();';
240
273
 
241
- function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
242
- if (attrValue && isEventAttribute(attrName)) {
243
- attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '').replace(/\s*;$/, '');
244
- if (options.minifyJS) {
245
- var wrappedCode = '(function(){' + attrValue + '})()';
246
- var minified = minifyJS(wrappedCode, options.minifyJS);
247
- return minified.slice(12, minified.length - 4).replace(/"/g, '&quot;');
248
- }
249
- return attrValue;
250
- }
251
- else if (attrName === 'class') {
252
- return collapseWhitespace(trimWhitespace(attrValue));
253
- }
254
- else if (isUriTypeAttribute(attrName, tag)) {
255
- attrValue = trimWhitespace(attrValue);
256
- if (options.minifyURLs) {
257
- return minifyURLs(attrValue, options.minifyURLs);
258
- }
259
- return attrValue;
260
- }
261
- else if (isNumberTypeAttribute(attrName, tag)) {
262
- return trimWhitespace(attrValue);
263
- }
264
- else if (attrName === 'style') {
265
- attrValue = trimWhitespace(attrValue);
266
- if (attrValue) {
267
- attrValue = attrValue.replace(/\s*;\s*$/, '');
268
- }
269
- if (options.minifyCSS) {
270
- return minifyCSS(attrValue, options.minifyCSS, true);
271
- }
272
- return attrValue;
273
- }
274
- else if (isMetaViewport(tag, attrs) && attrName === 'content') {
275
- attrValue = attrValue.replace(/1\.0/g, '1').replace(/\s+/g, '');
276
- }
277
- else if (attrValue && options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
278
- attrValue = attrValue.replace(/\n+|\r+|\s{2,}/g, '');
274
+ function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
275
+ if (attrValue && isEventAttribute(attrName, options)) {
276
+ attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '').replace(/\s*;$/, '');
277
+ if (options.minifyJS) {
278
+ var minified = minifyJS(fnPrefix + attrValue + fnSuffix, options.minifyJS);
279
+ return minified.slice(fnPrefix.length, -fnSuffix.length);
279
280
  }
280
281
  return attrValue;
281
282
  }
282
-
283
- function isMetaViewport(tag, attrs) {
284
- if (tag !== 'meta') {
285
- return false;
286
- }
287
- for (var i = 0, len = attrs.length; i < len; i++) {
288
- if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
289
- return true;
290
- }
283
+ else if (attrName === 'class') {
284
+ return collapseWhitespaceAll(trimWhitespace(attrValue));
285
+ }
286
+ else if (isUriTypeAttribute(attrName, tag)) {
287
+ attrValue = trimWhitespace(attrValue);
288
+ if (options.minifyURLs && !isCanonicalURL(tag, attrs)) {
289
+ return minifyURLs(attrValue, options.minifyURLs);
291
290
  }
291
+ return attrValue;
292
292
  }
293
-
294
- // Wrap CSS declarations for CleanCSS > 3.x
295
- // See https://github.com/jakubpawlowicz/clean-css/issues/418
296
- function wrapCSS(text) {
297
- return '*{' + text + '}';
293
+ else if (isNumberTypeAttribute(attrName, tag)) {
294
+ return trimWhitespace(attrValue);
298
295
  }
299
-
300
- function unwrapCSS(text) {
301
- var matches = text.match(/^\*\{([\s\S]*)\}$/m);
302
- if (matches && matches[1]) {
303
- return matches[1];
296
+ else if (attrName === 'style') {
297
+ attrValue = trimWhitespace(attrValue);
298
+ if (attrValue) {
299
+ attrValue = attrValue.replace(/\s*;\s*$/, '');
304
300
  }
305
- else {
306
- return text;
301
+ if (options.minifyCSS) {
302
+ return minifyCSS(attrValue, options.minifyCSS, true);
307
303
  }
304
+ return attrValue;
308
305
  }
309
-
310
- function cleanConditionalComment(comment) {
311
- return comment
312
- .replace(/^(\[[^\]]+\]>)\s*/, '$1')
313
- .replace(/\s*(<!\[endif\])$/, '$1');
306
+ else if (isMetaViewport(tag, attrs) && attrName === 'content') {
307
+ attrValue = attrValue.replace(/\s+/g, '').replace(/[0-9]+\.[0-9]+/g, function(numString) {
308
+ // "0.90000" -> "0.9"
309
+ // "1.0" -> "1"
310
+ // "1.0001" -> "1.0001" (unchanged)
311
+ return (+numString).toString();
312
+ });
314
313
  }
315
-
316
- function removeCDATASections(text) {
317
- return text
318
- // "/* <![CDATA[ */" or "// <![CDATA["
319
- .replace(/^(?:\s*\/\*\s*<!\[CDATA\[\s*\*\/|\s*\/\/\s*<!\[CDATA\[.*)/, '')
320
- // "/* ]]> */" or "// ]]>"
321
- .replace(/(?:\/\*\s*\]\]>\s*\*\/|\/\/\s*\]\]>)\s*$/, '');
314
+ else if (attrValue && options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
315
+ attrValue = attrValue.replace(/\n+|\r+|\s{2,}/g, '');
316
+ }
317
+ else if (tag === 'script' && attrName === 'type') {
318
+ attrValue = trimWhitespace(attrValue.replace(/\s*;\s*/g, ';'));
322
319
  }
320
+ return attrValue;
321
+ }
323
322
 
324
- function processScript(text, options, currentAttrs) {
325
- for (var i = 0, len = currentAttrs.length; i < len; i++) {
326
- if (currentAttrs[i].name.toLowerCase() === 'type' &&
327
- options.processScripts.indexOf(currentAttrs[i].value) > -1) {
328
- return minify(text, options);
329
- }
323
+ function isMetaViewport(tag, attrs) {
324
+ if (tag !== 'meta') {
325
+ return false;
326
+ }
327
+ for (var i = 0, len = attrs.length; i < len; i++) {
328
+ if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
329
+ return true;
330
330
  }
331
- return text;
332
331
  }
333
-
334
- var reStartDelimiter = {
335
- // account for js + html comments (e.g.: //<!--)
336
- script: /^\s*(?:\/\/)?\s*<!--.*\n?/,
337
- style: /^\s*<!--\s*/
338
- };
339
- var reEndDelimiter = {
340
- script: /\s*(?:\/\/)?\s*-->\s*$/,
341
- style: /\s*-->\s*$/
342
- };
343
- function removeComments(text, tag) {
344
- return text.replace(reStartDelimiter[tag], '').replace(reEndDelimiter[tag], '');
332
+ }
333
+
334
+ // Wrap CSS declarations for CleanCSS > 3.x
335
+ // See https://github.com/jakubpawlowicz/clean-css/issues/418
336
+ function wrapCSS(text) {
337
+ return '*{' + text + '}';
338
+ }
339
+
340
+ function unwrapCSS(text) {
341
+ var matches = text.match(/^\*\{([\s\S]*)\}$/m);
342
+ if (matches && matches[1]) {
343
+ return matches[1];
345
344
  }
346
-
347
- function isOptionalTag(tag) {
348
- return (/^(?:html|t?body|t?head|tfoot|tr|td|th|dt|dd|option|colgroup|source|track)$/).test(tag);
345
+ else {
346
+ return text;
349
347
  }
350
-
351
- var reEmptyAttribute = new RegExp(
352
- '^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
353
- '?:down|up|over|move|out)|key(?:press|down|up)))$');
354
-
355
- function canDeleteEmptyAttribute(tag, attrName, attrValue) {
356
- var isValueEmpty = !attrValue || (/^\s*$/).test(attrValue);
357
- if (isValueEmpty) {
358
- return (
359
- (tag === 'input' && attrName === 'value') ||
360
- reEmptyAttribute.test(attrName));
348
+ }
349
+
350
+ function cleanConditionalComment(comment, options) {
351
+ return options.processConditionalComments ? comment.replace(/^(\[if\s[^\]]+\]>)([\s\S]*?)(<!\[endif\])$/, function(match, prefix, text, suffix) {
352
+ return prefix + minify(text, options, true) + suffix;
353
+ }) : comment;
354
+ }
355
+
356
+ function processScript(text, options, currentAttrs) {
357
+ for (var i = 0, len = currentAttrs.length; i < len; i++) {
358
+ if (currentAttrs[i].name.toLowerCase() === 'type' &&
359
+ options.processScripts.indexOf(currentAttrs[i].value) > -1) {
360
+ return minify(text, options);
361
361
  }
362
- return false;
363
362
  }
364
-
365
- function canRemoveElement(tag, attrs) {
366
- if (tag === 'textarea') {
367
- return false;
368
- }
369
-
370
- if (tag === 'script') {
371
- for (var i = attrs.length - 1; i >= 0; i--) {
372
- if (attrs[i].name === 'src') {
373
- return false;
374
- }
375
- }
376
- }
377
-
378
- return true;
363
+ return text;
364
+ }
365
+
366
+ // Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
367
+ // with the following deviations:
368
+ // - retain <body> if followed by <noscript>
369
+ // - </rb>, </rt>, </rtc>, </rp> & </tfoot> follow http://www.w3.org/TR/html5/syntax.html#optional-tags
370
+ // - retain all tags which are adjacent to non-standard HTML tags
371
+ var optionalStartTags = createMapFromString('html,head,body,colgroup,tbody');
372
+ var optionalEndTags = createMapFromString('html,head,body,li,dt,dd,p,rb,rt,rtc,rp,optgroup,option,colgroup,caption,thead,tbody,tfoot,tr,td,th');
373
+ var headerTags = createMapFromString('meta,link,script,style,template,noscript');
374
+ var descriptionTags = createMapFromString('dt,dd');
375
+ var pBlockTags = createMapFromString('address,article,aside,blockquote,details,div,dl,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,main,menu,nav,ol,p,pre,section,table,ul');
376
+ var pInlineTags = createMapFromString('a,audio,del,ins,map,noscript,video');
377
+ var rubyTags = createMapFromString('rb,rt,rtc,rp');
378
+ var rtcTag = createMapFromString('rb,rtc,rp');
379
+ var optionTag = createMapFromString('option,optgroup');
380
+ var tableContentTags = createMapFromString('tbody,tfoot');
381
+ var tableSectionTags = createMapFromString('thead,tbody,tfoot');
382
+ var cellTags = createMapFromString('td,th');
383
+ var topLevelTags = createMapFromString('html,head,body');
384
+ var compactTags = createMapFromString('html,body');
385
+ var looseTags = createMapFromString('head,colgroup,caption');
386
+ var trailingTags = createMapFromString('dt,thead');
387
+ var htmlTags = createMapFromString('a,abbr,acronym,address,applet,area,article,aside,audio,b,base,basefont,bdi,bdo,bgsound,big,blink,blockquote,body,br,button,canvas,caption,center,cite,code,col,colgroup,command,content,data,datalist,dd,del,details,dfn,dialog,dir,div,dl,dt,element,em,embed,fieldset,figcaption,figure,font,footer,form,frame,frameset,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,i,iframe,image,img,input,ins,isindex,kbd,keygen,label,legend,li,link,listing,main,map,mark,marquee,menu,menuitem,meta,meter,multicol,nav,nobr,noembed,noframes,noscript,object,ol,optgroup,option,output,p,param,picture,plaintext,pre,progress,q,rp,rt,rtc,ruby,s,samp,script,section,select,shadow,small,source,spacer,span,strike,strong,style,sub,summary,sup,table,tbody,td,template,textarea,tfoot,th,thead,time,title,tr,track,tt,u,ul,var,video,wbr,xmp');
388
+
389
+ function canRemoveParentTag(optionalStartTag, tag) {
390
+ switch (optionalStartTag) {
391
+ case 'html':
392
+ case 'head':
393
+ return true;
394
+ case 'body':
395
+ return !headerTags(tag);
396
+ case 'colgroup':
397
+ return tag === 'col';
398
+ case 'tbody':
399
+ return tag === 'tr';
379
400
  }
380
-
381
- function canCollapseWhitespace(tag) {
382
- return !(/^(?:script|style|pre|textarea)$/.test(tag));
401
+ return false;
402
+ }
403
+
404
+ function isStartTagMandatory(optionalEndTag, tag) {
405
+ switch (tag) {
406
+ case 'colgroup':
407
+ return optionalEndTag === 'colgroup';
408
+ case 'tbody':
409
+ return tableSectionTags(optionalEndTag);
383
410
  }
384
-
385
- function canTrimWhitespace(tag) {
386
- return !(/^(?:pre|textarea)$/.test(tag));
411
+ return false;
412
+ }
413
+
414
+ function canRemovePrecedingTag(optionalEndTag, tag) {
415
+ switch (optionalEndTag) {
416
+ case 'html':
417
+ case 'head':
418
+ case 'body':
419
+ case 'colgroup':
420
+ case 'caption':
421
+ return true;
422
+ case 'li':
423
+ case 'optgroup':
424
+ case 'tr':
425
+ return tag === optionalEndTag;
426
+ case 'dt':
427
+ case 'dd':
428
+ return descriptionTags(tag);
429
+ case 'p':
430
+ return pBlockTags(tag);
431
+ case 'rb':
432
+ case 'rt':
433
+ case 'rp':
434
+ return rubyTags(tag);
435
+ case 'rtc':
436
+ return rtcTag(tag);
437
+ case 'option':
438
+ return optionTag(tag);
439
+ case 'thead':
440
+ case 'tbody':
441
+ return tableContentTags(tag);
442
+ case 'tfoot':
443
+ return tag === 'tbody';
444
+ case 'td':
445
+ case 'th':
446
+ return cellTags(tag);
387
447
  }
448
+ return false;
449
+ }
388
450
 
389
- function normalizeAttribute(attr, attrs, tag, unarySlash, index, options, isLast) {
390
-
391
- var attrName = options.caseSensitive ? attr.name : attr.name.toLowerCase(),
392
- attrValue = options.preventAttributesEscaping ? attr.value : attr.escaped,
393
- attrQuote = options.preventAttributesEscaping ? attr.quote : (options.quoteCharacter === '\'' ? '\'' : '"'),
394
- attrFragment,
395
- emittedAttrValue,
396
- isTerminalOfUnarySlash = unarySlash && index === attrs.length - 1;
397
-
398
- if ((options.removeRedundantAttributes &&
399
- isAttributeRedundant(tag, attrName, attrValue, attrs))
400
- ||
401
- (options.removeScriptTypeAttributes &&
402
- isScriptTypeAttribute(tag, attrName, attrValue))
403
- ||
404
- (options.removeStyleLinkTypeAttributes &&
405
- isStyleLinkTypeAttribute(tag, attrName, attrValue))) {
406
- return '';
407
- }
408
-
409
- attrValue = cleanAttributeValue(tag, attrName, attrValue, options, attrs);
410
-
411
- if (attrValue !== undefined && !options.removeAttributeQuotes ||
412
- !canRemoveAttributeQuotes(attrValue, isLast) || isTerminalOfUnarySlash) {
413
- emittedAttrValue = attrQuote + attrValue + attrQuote;
414
- }
415
- else {
416
- emittedAttrValue = attrValue;
417
- }
451
+ var reEmptyAttribute = new RegExp(
452
+ '^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
453
+ '?:down|up|over|move|out)|key(?:press|down|up)))$');
418
454
 
419
- if (options.removeEmptyAttributes &&
420
- canDeleteEmptyAttribute(tag, attrName, attrValue)) {
421
- return '';
422
- }
455
+ function canDeleteEmptyAttribute(tag, attrName, attrValue) {
456
+ var isValueEmpty = !attrValue || (/^\s*$/).test(attrValue);
457
+ if (isValueEmpty) {
458
+ return (
459
+ (tag === 'input' && attrName === 'value') ||
460
+ reEmptyAttribute.test(attrName));
461
+ }
462
+ return false;
463
+ }
423
464
 
424
- if (attrValue === undefined || (options.collapseBooleanAttributes &&
425
- isBooleanAttribute(attrName, attrValue))) {
426
- attrFragment = attrName;
427
- }
428
- else {
429
- attrFragment = attrName + attr.customAssign + emittedAttrValue;
465
+ function hasAttrName(name, attrs) {
466
+ for (var i = attrs.length - 1; i >= 0; i--) {
467
+ if (attrs[i].name === name) {
468
+ return true;
430
469
  }
431
-
432
- return (' ' + attr.customOpen + attrFragment + attr.customClose);
433
470
  }
471
+ return false;
472
+ }
434
473
 
435
- function setDefaultTesters(options) {
436
-
437
- var defaultTesters = ['canCollapseWhitespace', 'canTrimWhitespace'];
438
-
439
- for (var i = 0, len = defaultTesters.length; i < len; i++) {
440
- if (!options[defaultTesters[i]]) {
441
- options[defaultTesters[i]] = function() {
442
- return false;
443
- };
474
+ function canRemoveElement(tag, attrs) {
475
+ switch (tag) {
476
+ case 'textarea':
477
+ return false;
478
+ case 'audio':
479
+ case 'script':
480
+ case 'video':
481
+ if (hasAttrName('src', attrs)) {
482
+ return false;
444
483
  }
445
- }
484
+ break;
485
+ case 'iframe':
486
+ if (hasAttrName('src', attrs) || hasAttrName('srcdoc', attrs)) {
487
+ return false;
488
+ }
489
+ break;
490
+ case 'object':
491
+ if (hasAttrName('data', attrs)) {
492
+ return false;
493
+ }
494
+ break;
495
+ case 'applet':
496
+ if (hasAttrName('code', attrs)) {
497
+ return false;
498
+ }
499
+ break;
500
+ }
501
+ return true;
502
+ }
503
+
504
+ function canCollapseWhitespace(tag) {
505
+ return !(/^(?:script|style|pre|textarea)$/.test(tag));
506
+ }
507
+
508
+ function canTrimWhitespace(tag) {
509
+ return !(/^(?:pre|textarea)$/.test(tag));
510
+ }
511
+
512
+ function normalizeAttribute(attr, attrs, tag, hasUnarySlash, index, options, isLast) {
513
+
514
+ var attrName = options.caseSensitive ? attr.name : attr.name.toLowerCase(),
515
+ attrValue = attr.value,
516
+ attrQuote = attr.quote,
517
+ attrFragment,
518
+ emittedAttrValue;
519
+
520
+ if ((options.removeRedundantAttributes &&
521
+ isAttributeRedundant(tag, attrName, attrValue, attrs))
522
+ ||
523
+ (options.removeScriptTypeAttributes &&
524
+ isScriptTypeAttribute(tag, attrName, attrValue))
525
+ ||
526
+ (options.removeStyleLinkTypeAttributes &&
527
+ isStyleLinkTypeAttribute(tag, attrName, attrValue))) {
528
+ return '';
446
529
  }
447
530
 
448
- function minifyURLs(text, options) {
449
- if (typeof options !== 'object') {
450
- options = { };
451
- }
531
+ attrValue = cleanAttributeValue(tag, attrName, attrValue, options, attrs);
452
532
 
453
- try {
454
- // try to get global reference first
455
- var __RelateUrl = global.RelateUrl;
533
+ if (options.removeEmptyAttributes &&
534
+ canDeleteEmptyAttribute(tag, attrName, attrValue)) {
535
+ return '';
536
+ }
456
537
 
457
- if (typeof __RelateUrl === 'undefined' && typeof require === 'function') {
458
- __RelateUrl = require('relateurl');
538
+ if (attrValue !== undefined && !options.removeAttributeQuotes ||
539
+ !canRemoveAttributeQuotes(attrValue)) {
540
+ if (!options.preventAttributesEscaping) {
541
+ if (options.quoteCharacter !== undefined) {
542
+ attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
459
543
  }
460
-
461
- // noop
462
- if (!__RelateUrl) {
463
- return text;
544
+ else {
545
+ var apos = (attrValue.match(/'/g) || []).length;
546
+ var quot = (attrValue.match(/"/g) || []).length;
547
+ attrQuote = apos < quot ? '\'' : '"';
464
548
  }
465
-
466
- if (__RelateUrl.relate) {
467
- return __RelateUrl.relate(text, options);
549
+ if (attrQuote === '"') {
550
+ attrValue = attrValue.replace(/"/g, '&#34;');
468
551
  }
469
552
  else {
470
- return text;
553
+ attrValue = attrValue.replace(/'/g, '&#39;');
471
554
  }
472
555
  }
473
- catch (err) {
474
- log(err);
556
+ emittedAttrValue = attrQuote + attrValue + attrQuote;
557
+ if (!isLast && !options.removeTagWhitespace) {
558
+ emittedAttrValue += ' ';
475
559
  }
476
- return text;
560
+ }
561
+ // make sure trailing slash is not interpreted as HTML self-closing tag
562
+ else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
563
+ emittedAttrValue = attrValue;
564
+ }
565
+ else {
566
+ emittedAttrValue = attrValue + ' ';
477
567
  }
478
568
 
479
- function minifyJS(text, options) {
480
- if (typeof options !== 'object') {
481
- options = { };
569
+ if (attrValue === undefined || (options.collapseBooleanAttributes &&
570
+ isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase()))) {
571
+ attrFragment = attrName;
572
+ if (!isLast) {
573
+ attrFragment += ' ';
482
574
  }
483
- options.fromString = true;
484
- var outputOptions = options.output || {};
485
- outputOptions.inline_script = true;
486
- options.output = outputOptions;
487
-
488
- try {
489
- // try to get global reference first
490
- var __UglifyJS = global.UglifyJS;
491
-
492
- if (typeof __UglifyJS === 'undefined' && typeof require === 'function') {
493
- __UglifyJS = require('uglify-js');
494
- }
495
-
496
- // noop
497
- if (!__UglifyJS) {
498
- return text;
499
- }
500
-
501
- if (__UglifyJS.minify) {
502
- return __UglifyJS.minify(text, options).code;
503
- }
504
- else if (__UglifyJS.parse) {
505
-
506
- var ast = __UglifyJS.parse(text);
507
- ast.figure_out_scope();
575
+ }
576
+ else {
577
+ attrFragment = attrName + attr.customAssign + emittedAttrValue;
578
+ }
508
579
 
509
- var compressor = __UglifyJS.Compressor();
510
- var compressedAst = ast.transform(compressor);
580
+ return attr.customOpen + attrFragment + attr.customClose;
581
+ }
511
582
 
512
- compressedAst.figure_out_scope();
513
- compressedAst.compute_char_frequency();
583
+ function processOptions(options) {
584
+ if (!('includeAutoGeneratedTags' in options)) {
585
+ options.includeAutoGeneratedTags = true;
586
+ }
514
587
 
515
- if (options.mangle !== false) {
516
- compressedAst.mangle_names();
517
- }
588
+ var defaultTesters = ['canCollapseWhitespace', 'canTrimWhitespace'];
589
+ for (var i = 0, len = defaultTesters.length; i < len; i++) {
590
+ if (!options[defaultTesters[i]]) {
591
+ options[defaultTesters[i]] = function() {
592
+ return false;
593
+ };
594
+ }
595
+ }
518
596
 
519
- var stream = __UglifyJS.OutputStream(options.output);
520
- compressedAst.print(stream);
597
+ if (options.minifyURLs && typeof options.minifyURLs !== 'object') {
598
+ options.minifyURLs = { };
599
+ }
521
600
 
522
- return stream.toString();
523
- }
524
- else {
525
- return text;
526
- }
601
+ if (options.minifyJS) {
602
+ if (typeof options.minifyJS !== 'object') {
603
+ options.minifyJS = { };
527
604
  }
528
- catch (err) {
529
- log(err);
530
- }
531
- return text;
605
+ options.minifyJS.fromString = true;
606
+ (options.minifyJS.output || (options.minifyJS.output = { })).inline_script = true;
532
607
  }
533
608
 
534
- function minifyCSS(text, options, inline) {
535
- if (typeof options !== 'object') {
536
- options = { };
609
+ if (options.minifyCSS) {
610
+ if (typeof options.minifyCSS !== 'object') {
611
+ options.minifyCSS = { };
537
612
  }
538
- if (typeof options.advanced === 'undefined') {
539
- options.advanced = false;
613
+ if (typeof options.minifyCSS.advanced === 'undefined') {
614
+ options.minifyCSS.advanced = false;
540
615
  }
541
- try {
542
- var cleanCSS;
616
+ }
617
+ }
543
618
 
544
- if (typeof CleanCSS !== 'undefined') {
545
- cleanCSS = new CleanCSS(options);
546
- }
547
- else if (typeof require === 'function') {
548
- var CleanCSSModule = require('clean-css');
549
- cleanCSS = new CleanCSSModule(options);
550
- }
551
- if (inline) {
552
- return unwrapCSS(cleanCSS.minify(wrapCSS(text)).styles);
553
- }
554
- else {
555
- return cleanCSS.minify(text).styles;
556
- }
619
+ function minifyURLs(text, options) {
620
+ try {
621
+ return RelateUrl.relate(text, options);
622
+ }
623
+ catch (err) {
624
+ log(err);
625
+ return text;
626
+ }
627
+ }
628
+
629
+ function minifyJS(text, options) {
630
+ var start = text.match(/^\s*<!--.*/);
631
+ var code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
632
+ try {
633
+ return UglifyJS.minify(code, options).code;
634
+ }
635
+ catch (err) {
636
+ log(err);
637
+ return text;
638
+ }
639
+ }
640
+
641
+ function minifyCSS(text, options, inline) {
642
+ var start = text.match(/^\s*<!--/);
643
+ var style = start ? text.slice(start[0].length).replace(/-->\s*$/, '') : text;
644
+ try {
645
+ var cleanCSS = new CleanCSS(options);
646
+ if (inline) {
647
+ return unwrapCSS(cleanCSS.minify(wrapCSS(style)).styles);
557
648
  }
558
- catch (err) {
559
- log(err);
649
+ else {
650
+ return cleanCSS.minify(style).styles;
560
651
  }
652
+ }
653
+ catch (err) {
654
+ log(err);
561
655
  return text;
562
656
  }
657
+ }
658
+
659
+ function uniqueId(value) {
660
+ var id;
661
+ do {
662
+ id = Math.random().toString(36).slice(2);
663
+ } while (~value.indexOf(id));
664
+ return id;
665
+ }
666
+
667
+ function minify(value, options, partialMarkup) {
668
+ options = options || {};
669
+ var optionsStack = [];
670
+ processOptions(options);
671
+ value = options.collapseWhitespace ? trimWhitespace(value) : value;
672
+
673
+ var buffer = [ ],
674
+ charsPrevTag,
675
+ currentChars = '',
676
+ hasChars,
677
+ currentTag = '',
678
+ currentAttrs = [],
679
+ stackNoTrimWhitespace = [],
680
+ stackNoCollapseWhitespace = [],
681
+ optionalStartTag = '',
682
+ optionalEndTag = '',
683
+ lint = options.lint,
684
+ t = Date.now(),
685
+ ignoredMarkupChunks = [ ],
686
+ ignoredCustomMarkupChunks = [ ],
687
+ uidIgnore,
688
+ uidAttr;
689
+
690
+ // temporarily replace ignored chunks with comments,
691
+ // so that we don't have to worry what's there.
692
+ // for all we care there might be
693
+ // completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
694
+ value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function(match, group1) {
695
+ if (!uidIgnore) {
696
+ uidIgnore = uniqueId(value);
697
+ }
698
+ var token = '<!--!' + uidIgnore + ignoredMarkupChunks.length + '-->';
699
+ ignoredMarkupChunks.push(group1);
700
+ return token;
701
+ });
702
+
703
+ var customFragments = (options.ignoreCustomFragments || [
704
+ /<%[\s\S]*?%>/,
705
+ /<\?[\s\S]*?\?>/
706
+ ]).map(function(re) {
707
+ return re.source;
708
+ });
709
+ if (customFragments.length) {
710
+ var reCustomIgnore = new RegExp('\\s*(?:' + customFragments.join('|') + ')+\\s*', 'g');
711
+ // temporarily replace custom ignored fragments with unique attributes
712
+ value = value.replace(reCustomIgnore, function(match) {
713
+ if (!uidAttr) {
714
+ uidAttr = uniqueId(value);
715
+ }
716
+ var token = uidAttr + ignoredCustomMarkupChunks.length;
717
+ ignoredCustomMarkupChunks.push(match);
718
+ return '\t' + token + '\t';
719
+ });
720
+ }
563
721
 
564
- function minify(value, options) {
565
-
566
- options = options || {};
567
- var optionsStack = [];
568
-
569
- value = trimWhitespace(value);
570
- setDefaultTesters(options);
571
-
572
- var results = [ ],
573
- buffer = [ ],
574
- currentChars = '',
575
- currentTag = '',
576
- currentAttrs = [],
577
- stackNoTrimWhitespace = [],
578
- stackNoCollapseWhitespace = [],
579
- lint = options.lint,
580
- t = new Date(),
581
- ignoredMarkupChunks = [ ],
582
- ignoredCustomMarkupChunks = [ ],
583
- reIgnore = /<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g,
584
- uidAttr = ' htmlmin' + (Math.random() + '').slice(2) + ' ',
585
- reCustomIgnore,
586
- customFragments;
587
-
588
- if (options.ignoreCustomFragments) {
722
+ function _canCollapseWhitespace(tag, attrs) {
723
+ return canCollapseWhitespace(tag) || options.canCollapseWhitespace(tag, attrs);
724
+ }
589
725
 
590
- customFragments = options.ignoreCustomFragments.map(function(re) {
591
- return re.source;
592
- });
593
- reCustomIgnore = new RegExp('(?:' + customFragments.join('|') + ')', 'g');
726
+ function _canTrimWhitespace(tag, attrs) {
727
+ return canTrimWhitespace(tag) || options.canTrimWhitespace(tag, attrs);
728
+ }
594
729
 
595
- // temporarily replace custom ignored fragments with unique attributes
596
- value = value.replace(reCustomIgnore, function(match) {
597
- ignoredCustomMarkupChunks.push(match);
598
- return uidAttr;
599
- });
730
+ function removeStartTag() {
731
+ var index = buffer.length - 1;
732
+ while (index > 0 && !/^<[^/!]/.test(buffer[index])) {
733
+ index--;
600
734
  }
735
+ buffer.length = Math.max(0, index);
736
+ }
601
737
 
602
- // temporarily replace ignored chunks with comments,
603
- // so that we don't have to worry what's there.
604
- // for all we care there might be
605
- // completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
606
- value = value.replace(reIgnore, function(match, group1) {
607
- ignoredMarkupChunks.push(group1);
608
- return '<!-- htmlmin:temp -->';
609
- });
610
-
611
- function _canCollapseWhitespace(tag, attrs) {
612
- return canCollapseWhitespace(tag) || options.canCollapseWhitespace(tag, attrs);
738
+ function removeEndTag() {
739
+ var index = buffer.length - 1;
740
+ while (index > 0 && !/^<\//.test(buffer[index])) {
741
+ index--;
613
742
  }
743
+ buffer.length = Math.max(0, index);
744
+ }
614
745
 
615
- function _canTrimWhitespace(tag, attrs) {
616
- return canTrimWhitespace(tag) || options.canTrimWhitespace(tag, attrs);
746
+ // look for trailing whitespaces from previously processed text
747
+ // which may not be trimmed due to a following comment or an empty
748
+ // element which has now been removed
749
+ function squashTrailingWhitespace(nextTag) {
750
+ var charsIndex;
751
+ if (buffer.length > 1 && /^(?:<!|$)/.test(buffer[buffer.length - 1]) &&
752
+ /\s$/.test(buffer[buffer.length - 2])) {
753
+ charsIndex = buffer.length - 2;
617
754
  }
755
+ else if (buffer.length > 0 && /\s$/.test(buffer[buffer.length - 1])) {
756
+ charsIndex = buffer.length - 1;
757
+ }
758
+ if (charsIndex > 0) {
759
+ buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function(text) {
760
+ return collapseWhitespaceSmart(text, 'comment', nextTag, options);
761
+ });
762
+ }
763
+ }
618
764
 
619
- new HTMLParser(value, {
620
- html5: typeof options.html5 !== 'undefined' ? options.html5 : true,
621
-
622
- start: function(tag, attrs, unary, unarySlash) {
765
+ new HTMLParser(value, {
766
+ partialMarkup: partialMarkup,
767
+ html5: typeof options.html5 !== 'undefined' ? options.html5 : true,
623
768
 
624
- var lowerTag = tag.toLowerCase();
769
+ start: function(tag, attrs, unary, unarySlash, autoGenerated) {
770
+ var lowerTag = tag.toLowerCase();
625
771
 
626
- if (lowerTag === 'svg') {
627
- optionsStack.push(options);
628
- var nextOptions = {};
629
- for (var key in options) {
630
- nextOptions[key] = options[key];
631
- }
632
- nextOptions.keepClosingSlash = true;
633
- nextOptions.caseSensitive = true;
634
- options = nextOptions;
772
+ if (lowerTag === 'svg') {
773
+ optionsStack.push(options);
774
+ var nextOptions = {};
775
+ for (var key in options) {
776
+ nextOptions[key] = options[key];
635
777
  }
778
+ nextOptions.keepClosingSlash = true;
779
+ nextOptions.caseSensitive = true;
780
+ options = nextOptions;
781
+ }
636
782
 
637
- tag = options.caseSensitive ? tag : lowerTag;
783
+ tag = options.caseSensitive ? tag : lowerTag;
638
784
 
639
- currentTag = tag;
785
+ currentTag = tag;
786
+ charsPrevTag = tag;
787
+ if (!inlineTextTags(tag)) {
640
788
  currentChars = '';
641
- currentAttrs = attrs;
642
-
643
- // set whitespace flags for nested tags (eg. <code> within a <pre>)
644
- if (options.collapseWhitespace) {
645
- if (!_canTrimWhitespace(tag, attrs)) {
646
- stackNoTrimWhitespace.push(tag);
647
- }
648
- if (!_canCollapseWhitespace(tag, attrs)) {
649
- stackNoCollapseWhitespace.push(tag);
650
- }
789
+ }
790
+ hasChars = false;
791
+ currentAttrs = attrs;
792
+
793
+ var optional = options.removeOptionalTags;
794
+ if (optional) {
795
+ var htmlTag = htmlTags(tag);
796
+ // <html> may be omitted if first thing inside is not comment
797
+ // <head> may be omitted if first thing inside is an element
798
+ // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
799
+ // <colgroup> may be omitted if first thing inside is <col>
800
+ // <tbody> may be omitted if first thing inside is <tr>
801
+ if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
802
+ removeStartTag();
803
+ }
804
+ optionalStartTag = '';
805
+ // end-tag-followed-by-start-tag omission rules
806
+ if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
807
+ removeEndTag();
808
+ // <colgroup> cannot be omitted if preceding </colgroup> is omitted
809
+ // <tbody> cannot be omitted if preceding </tbody>, </thead> or </tfoot> is omitted
810
+ optional = !isStartTagMandatory(optionalEndTag, tag);
651
811
  }
812
+ optionalEndTag = '';
813
+ }
652
814
 
653
- var openTag = '<' + tag;
654
- var closeTag = ((unarySlash && options.keepClosingSlash) ? '/' : '') + '>';
655
- if (attrs.length === 0) {
656
- openTag += closeTag;
815
+ // set whitespace flags for nested tags (eg. <code> within a <pre>)
816
+ if (options.collapseWhitespace) {
817
+ if (!stackNoTrimWhitespace.length) {
818
+ squashTrailingWhitespace(tag);
819
+ }
820
+ if (!_canTrimWhitespace(tag, attrs)) {
821
+ stackNoTrimWhitespace.push(tag);
822
+ }
823
+ if (!_canCollapseWhitespace(tag, attrs)) {
824
+ stackNoCollapseWhitespace.push(tag);
657
825
  }
826
+ }
827
+
828
+ var openTag = '<' + tag;
829
+ var hasUnarySlash = unarySlash && options.keepClosingSlash;
658
830
 
659
- buffer.push(openTag);
831
+ buffer.push(openTag);
832
+
833
+ if (lint) {
834
+ lint.testElement(tag);
835
+ }
660
836
 
837
+ var parts = [ ];
838
+ var token, isLast = true;
839
+ for (var i = attrs.length; --i >= 0; ) {
661
840
  if (lint) {
662
- lint.testElement(tag);
841
+ lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].value);
663
842
  }
664
-
665
- var token, isLast;
666
- for (var i = 0, len = attrs.length; i < len; i++) {
667
- isLast = i === len - 1;
668
- if (lint) {
669
- lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped);
670
- }
671
- token = normalizeAttribute(attrs[i], attrs, tag, unarySlash, i, options, isLast);
672
- if (isLast) {
673
- token += closeTag;
674
- }
675
- buffer.push(token);
843
+ token = normalizeAttribute(attrs[i], attrs, tag, hasUnarySlash, i, options, isLast);
844
+ if (token) {
845
+ isLast = false;
846
+ parts.unshift(token);
676
847
  }
677
- },
678
- end: function(tag, attrs) {
848
+ }
849
+ if (parts.length > 0) {
850
+ buffer.push(' ');
851
+ buffer.push.apply(buffer, parts);
852
+ }
853
+ // start tag must never be omitted if it has any attributes
854
+ else if (optional && optionalStartTags(tag)) {
855
+ optionalStartTag = tag;
856
+ }
679
857
 
680
- var lowerTag = tag.toLowerCase();
681
- if (lowerTag === 'svg') {
682
- options = optionsStack.pop();
683
- }
858
+ buffer.push(buffer.pop() + (hasUnarySlash ? '/' : '') + '>');
859
+
860
+ if (autoGenerated && !options.includeAutoGeneratedTags) {
861
+ removeStartTag();
862
+ }
863
+ },
864
+ end: function(tag, attrs, autoGenerated) {
865
+ var lowerTag = tag.toLowerCase();
866
+ if (lowerTag === 'svg') {
867
+ options = optionsStack.pop();
868
+ }
869
+ tag = options.caseSensitive ? tag : lowerTag;
684
870
 
685
- // check if current tag is in a whitespace stack
686
- if (options.collapseWhitespace) {
687
- if (stackNoTrimWhitespace.length &&
688
- tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
871
+ // check if current tag is in a whitespace stack
872
+ if (options.collapseWhitespace) {
873
+ if (stackNoTrimWhitespace.length) {
874
+ if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
689
875
  stackNoTrimWhitespace.pop();
690
876
  }
691
- if (stackNoCollapseWhitespace.length &&
692
- tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
693
- stackNoCollapseWhitespace.pop();
694
- }
695
877
  }
878
+ else {
879
+ squashTrailingWhitespace('/' + tag);
880
+ }
881
+ if (stackNoCollapseWhitespace.length &&
882
+ tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
883
+ stackNoCollapseWhitespace.pop();
884
+ }
885
+ }
696
886
 
697
- var isElementEmpty = currentChars === '' && tag === currentTag;
698
- if ((options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs))) {
699
- // remove last "element" from buffer, return
700
- for (var i = buffer.length - 1; i >= 0; i--) {
701
- if (/^<[^\/!]/.test(buffer[i])) {
702
- buffer.splice(i);
703
- break;
704
- }
705
- }
706
- return;
887
+ var isElementEmpty = false;
888
+ if (tag === currentTag) {
889
+ currentTag = '';
890
+ isElementEmpty = !hasChars;
891
+ }
892
+
893
+ if (options.removeOptionalTags) {
894
+ // <html>, <head> or <body> may be omitted if the element is empty
895
+ if (isElementEmpty && topLevelTags(optionalStartTag)) {
896
+ removeStartTag();
707
897
  }
708
- else if (options.removeOptionalTags && isOptionalTag(tag)) {
709
- // noop, leave start tag in buffer
710
- return;
898
+ optionalStartTag = '';
899
+ // </html> or </body> may be omitted if not followed by comment
900
+ // </head> may be omitted if not followed by space or comment
901
+ // </p> may be omitted if no more content in non-</a> parent
902
+ // except for </dt> or </thead>, end tags may be omitted if no more content in parent element
903
+ if (htmlTags(tag) && optionalEndTag && !trailingTags(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineTags(tag))) {
904
+ removeEndTag();
711
905
  }
712
- else {
713
- // push end tag to buffer
714
- buffer.push('</' + (options.caseSensitive ? tag : lowerTag) + '>');
715
- results.push.apply(results, buffer);
906
+ optionalEndTag = optionalEndTags(tag) ? tag : '';
907
+ }
908
+
909
+ if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
910
+ // remove last "element" from buffer
911
+ removeStartTag();
912
+ optionalStartTag = '';
913
+ optionalEndTag = '';
914
+ }
915
+ else {
916
+ if (options.includeAutoGeneratedTags || !autoGenerated) {
917
+ buffer.push('</' + tag + '>');
716
918
  }
717
- // flush buffer
718
- buffer.length = 0;
719
- currentChars = '';
720
- },
721
- chars: function(text, prevTag, nextTag) {
722
- prevTag = prevTag === '' ? 'comment' : prevTag;
723
- nextTag = nextTag === '' ? 'comment' : nextTag;
724
-
725
- if (currentTag === 'script' || currentTag === 'style') {
726
- if (options.removeCommentsFromCDATA) {
727
- text = removeComments(text, currentTag);
919
+ charsPrevTag = '/' + tag;
920
+ if (!inlineTags(tag)) {
921
+ currentChars = '';
922
+ }
923
+ else if (isElementEmpty) {
924
+ currentChars += '|';
925
+ }
926
+ }
927
+ },
928
+ chars: function(text, prevTag, nextTag) {
929
+ prevTag = prevTag === '' ? 'comment' : prevTag;
930
+ nextTag = nextTag === '' ? 'comment' : nextTag;
931
+ if (options.collapseWhitespace) {
932
+ if (!stackNoTrimWhitespace.length) {
933
+ if (prevTag === 'comment') {
934
+ var removed = buffer[buffer.length - 1] === '';
935
+ if (removed) {
936
+ prevTag = charsPrevTag;
937
+ }
938
+ if (buffer.length > 1 && (removed || / $/.test(currentChars))) {
939
+ var charsIndex = buffer.length - 2;
940
+ buffer[charsIndex] = buffer[charsIndex].replace(/\s+$/, function(trailingSpaces) {
941
+ text = trailingSpaces + text;
942
+ return '';
943
+ });
944
+ }
728
945
  }
729
- if (options.removeCDATASectionsFromCDATA) {
730
- text = removeCDATASections(text);
946
+ if (prevTag && inlineTextTags(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
947
+ text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
731
948
  }
732
- if (options.processScripts) {
733
- text = processScript(text, options, currentAttrs);
949
+ text = prevTag || nextTag ? collapseWhitespaceSmart(text, prevTag, nextTag, options) : trimWhitespace(text);
950
+ if (!text && /\s$/.test(currentChars) && prevTag && prevTag.charAt(0) === '/') {
951
+ for (var index = buffer.length - 2, endTag = prevTag.slice(1); index >= 0 && _canTrimWhitespace(endTag); index--) {
952
+ var str = buffer[index];
953
+ var match = str.match(/^<\/([\w:-]+)>$/);
954
+ if (match) {
955
+ endTag = match[1];
956
+ }
957
+ else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options))) {
958
+ break;
959
+ }
960
+ }
734
961
  }
735
962
  }
736
- if (options.minifyJS && isExecutableScript(currentTag, currentAttrs)) {
737
- text = minifyJS(text, options.minifyJS);
963
+ if (!stackNoCollapseWhitespace.length) {
964
+ text = prevTag && nextTag || nextTag === 'html' ? text : collapseWhitespaceAll(text);
738
965
  }
739
- if (currentTag === 'style' && options.minifyCSS) {
740
- text = minifyCSS(text, options.minifyCSS);
966
+ }
967
+ if (options.processScripts && (currentTag === 'script' || currentTag === 'style')) {
968
+ text = processScript(text, options, currentAttrs);
969
+ }
970
+ if (options.minifyJS && isExecutableScript(currentTag, currentAttrs)) {
971
+ text = minifyJS(text, options.minifyJS);
972
+ if (text.charAt(text.length - 1) === ';') {
973
+ text = text.slice(0, -1);
741
974
  }
742
- if (options.collapseWhitespace) {
743
- if (!stackNoTrimWhitespace.length) {
744
- text = ((prevTag && prevTag !== 'comment') || (nextTag && nextTag !== 'comment')) ?
745
- collapseWhitespaceSmart(text, prevTag, nextTag, options)
746
- : trimWhitespace(text);
747
- }
748
- if (!stackNoCollapseWhitespace.length) {
749
- text = !(prevTag && nextTag || nextTag === 'html') ? collapseWhitespace(text) : text;
750
- }
975
+ }
976
+ if (options.minifyCSS && isStyleSheet(currentTag, currentAttrs)) {
977
+ text = minifyCSS(text, options.minifyCSS);
978
+ }
979
+ if (options.removeOptionalTags && text) {
980
+ // <html> may be omitted if first thing inside is not comment
981
+ // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
982
+ if (optionalStartTag === 'html' || optionalStartTag === 'body' && !/^\s/.test(text)) {
983
+ removeStartTag();
751
984
  }
752
- currentChars = text;
753
- if (lint) {
754
- lint.testChars(text);
985
+ optionalStartTag = '';
986
+ // </html> or </body> may be omitted if not followed by comment
987
+ // </head>, </colgroup> or </caption> may be omitted if not followed by space or comment
988
+ if (compactTags(optionalEndTag) || looseTags(optionalEndTag) && !/^\s/.test(text)) {
989
+ removeEndTag();
755
990
  }
756
- buffer.push(text);
757
- },
758
- comment: function(text, nonStandard) {
759
-
760
- var prefix = nonStandard ? '<!' : '<!--';
761
- var suffix = nonStandard ? '>' : '-->';
762
-
763
- if (options.removeComments) {
764
- if (isConditionalComment(text)) {
765
- text = prefix + cleanConditionalComment(text) + suffix;
766
- }
767
- else if (isIgnoredComment(text, options)) {
768
- text = '<!--' + text + '-->';
769
- }
770
- else {
771
- text = '';
772
- }
991
+ optionalEndTag = '';
992
+ }
993
+ charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
994
+ currentChars += text;
995
+ if (text) {
996
+ hasChars = true;
997
+ }
998
+ if (lint) {
999
+ lint.testChars(text);
1000
+ }
1001
+ buffer.push(text);
1002
+ },
1003
+ comment: function(text, nonStandard) {
1004
+ var prefix = nonStandard ? '<!' : '<!--';
1005
+ var suffix = nonStandard ? '>' : '-->';
1006
+ if (isConditionalComment(text)) {
1007
+ text = prefix + cleanConditionalComment(text, options) + suffix;
1008
+ }
1009
+ else if (options.removeComments) {
1010
+ if (isIgnoredComment(text, options)) {
1011
+ text = '<!--' + text + '-->';
773
1012
  }
774
1013
  else {
775
- text = prefix + text + suffix;
1014
+ text = '';
776
1015
  }
777
- buffer.push(text);
778
- },
779
- doctype: function(doctype) {
780
- buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
781
- },
782
- customAttrAssign: options.customAttrAssign,
783
- customAttrSurround: options.customAttrSurround
784
- });
785
-
786
- results.push.apply(results, buffer);
787
- var str = joinResultSegments(results, options);
1016
+ }
1017
+ else {
1018
+ text = prefix + text + suffix;
1019
+ }
1020
+ if (options.removeOptionalTags && text) {
1021
+ // preceding comments suppress tag omissions
1022
+ optionalStartTag = '';
1023
+ optionalEndTag = '';
1024
+ }
1025
+ buffer.push(text);
1026
+ },
1027
+ doctype: function(doctype) {
1028
+ buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespaceAll(doctype));
1029
+ },
1030
+ customAttrAssign: options.customAttrAssign,
1031
+ customAttrSurround: options.customAttrSurround
1032
+ });
1033
+
1034
+ if (options.removeOptionalTags) {
1035
+ // <html> may be omitted if first thing inside is not comment
1036
+ // <head> or <body> may be omitted if empty
1037
+ if (topLevelTags(optionalStartTag)) {
1038
+ removeStartTag();
1039
+ }
1040
+ // except for </dt> or </thead>, end tags may be omitted if no more content in parent element
1041
+ if (optionalEndTag && !trailingTags(optionalEndTag)) {
1042
+ removeEndTag();
1043
+ }
1044
+ }
788
1045
 
789
- var customIgnoreRe = new RegExp('\\s*' + trimWhitespace(uidAttr) + '\\s*', 'g');
1046
+ var str = joinResultSegments(buffer, options);
790
1047
 
791
- str = str.replace(customIgnoreRe, function() {
792
- return ignoredCustomMarkupChunks.shift();
1048
+ if (uidAttr) {
1049
+ str = str.replace(new RegExp('(\\s*)' + uidAttr + '([0-9]+)(\\s*)', 'g'), function(match, prefix, index, suffix) {
1050
+ var chunk = ignoredCustomMarkupChunks[+index];
1051
+ if (options.collapseWhitespace) {
1052
+ if (prefix !== '\t') {
1053
+ chunk = prefix + chunk;
1054
+ }
1055
+ if (suffix !== '\t') {
1056
+ chunk += suffix;
1057
+ }
1058
+ return collapseWhitespace(chunk, {
1059
+ preserveLineBreaks: options.preserveLineBreaks,
1060
+ conservativeCollapse: true
1061
+ }, /^\s/.test(chunk), /\s$/.test(chunk));
1062
+ }
1063
+ else {
1064
+ return chunk;
1065
+ }
793
1066
  });
794
- str = str.replace(/<!-- htmlmin:temp -->/g, function() {
795
- return ignoredMarkupChunks.shift();
1067
+ }
1068
+ if (uidIgnore) {
1069
+ str = str.replace(new RegExp('<!--!' + uidIgnore + '([0-9]+)-->', 'g'), function(match, index) {
1070
+ return ignoredMarkupChunks[+index];
796
1071
  });
797
-
798
- log('minified in: ' + (new Date() - t) + 'ms');
799
- return str;
800
1072
  }
801
1073
 
802
- function joinResultSegments(results, options) {
803
- var str;
804
- var maxLineLength = options.maxLineLength;
805
- if (maxLineLength) {
806
- var token;
807
- var lines = [];
808
- var line = '';
809
- for (var i = 0, len = results.length; i < len; i++) {
810
- token = results[i];
811
- if (line.length + token.length < maxLineLength) {
812
- line += token;
813
- }
814
- else {
815
- lines.push(line.replace(/^\n/, ''));
816
- line = token;
817
- }
1074
+ log('minified in: ' + (Date.now() - t) + 'ms');
1075
+ return str;
1076
+ }
1077
+
1078
+ function joinResultSegments(results, options) {
1079
+ var str;
1080
+ var maxLineLength = options.maxLineLength;
1081
+ if (maxLineLength) {
1082
+ var token;
1083
+ var lines = [];
1084
+ var line = '';
1085
+ for (var i = 0, len = results.length; i < len; i++) {
1086
+ token = results[i];
1087
+ if (line.length + token.length < maxLineLength) {
1088
+ line += token;
1089
+ }
1090
+ else {
1091
+ lines.push(line.replace(/^\n/, ''));
1092
+ line = token;
818
1093
  }
819
- lines.push(line);
820
-
821
- str = lines.join('\n');
822
- }
823
- else {
824
- str = results.join('');
825
1094
  }
1095
+ lines.push(line);
826
1096
 
827
- return str;
828
- }
829
-
830
- // for CommonJS enviroments, export everything
831
- if (typeof exports !== 'undefined') {
832
- exports.minify = minify;
1097
+ str = lines.join('\n');
833
1098
  }
834
1099
  else {
835
- global.minify = minify;
1100
+ str = results.join('');
836
1101
  }
837
1102
 
838
- }(typeof exports === 'undefined' ? this : exports));
1103
+ return options.collapseWhitespace ? trimWhitespace(str) : str;
1104
+ }
1105
+
1106
+ exports.minify = function(value, options) {
1107
+ return minify(value, options);
1108
+ };