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