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