html-minifier 0.7.2 → 0.8.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.
@@ -0,0 +1,834 @@
1
+ /* global CleanCSS */
2
+
3
+ (function(global) {
4
+ 'use strict';
5
+
6
+ var log, HTMLParser;
7
+ if (global.console && global.console.log) {
8
+ log = function(message) {
9
+ // "preserving" `this`
10
+ global.console.log(message);
11
+ };
12
+ }
13
+ else {
14
+ log = function() {};
15
+ }
16
+
17
+ if (global.HTMLParser) {
18
+ HTMLParser = global.HTMLParser;
19
+ }
20
+ else if (typeof require === 'function') {
21
+ HTMLParser = require('./htmlparser').HTMLParser;
22
+ }
23
+
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
+
39
+ function collapseWhitespace(str) {
40
+ return str ? str.replace(/[\t\n\r ]+/g, ' ') : str;
41
+ }
42
+
43
+ function collapseWhitespaceSmart(str, prevTag, nextTag, options) {
44
+
45
+ // array of non-empty element tags that will maintain a single space outside of them
46
+ var tags = [
47
+ 'a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite',
48
+ 'code', 'del', 'dfn', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'q',
49
+ 'rt', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong',
50
+ 'sub', 'sup', 'svg', 'time', 'tt', 'u', 'var'
51
+ ],
52
+ lineBreakBefore = /^[\t ]*[\n\r]+[\t\n\r ]*/,
53
+ lineBreakAfter = /[\t\n\r ]*[\n\r]+[\t ]*$/,
54
+ preserveBefore = lineBreakBefore.test(str) ? '\n' : ' ',
55
+ preserveAfter = lineBreakAfter.test(str) ? '\n' : ' ',
56
+ lineBreakStamp = 'htmlmincollapsedlinebreak';
57
+
58
+ if (prevTag && prevTag !== 'img' && prevTag !== 'input' && (prevTag.substr(0, 1) !== '/'
59
+ || (prevTag.substr(0, 1) === '/' && tags.indexOf(prevTag.substr(1)) === -1))) {
60
+ str = str.replace(/^\s+/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveBefore : '');
61
+ }
62
+
63
+ if (nextTag && nextTag !== 'img' && nextTag !== 'input' && (nextTag.substr(0, 1) === '/'
64
+ || (nextTag.substr(0, 1) !== '/' && tags.indexOf(nextTag) === -1))) {
65
+ str = str.replace(/\s+$/, options.conservativeCollapse ? ' ' : options.preserveLineBreaks ? preserveAfter : '');
66
+ }
67
+
68
+ if (prevTag && nextTag) {
69
+
70
+ if (options.preserveLineBreaks) {
71
+ str = str
72
+ .replace(lineBreakBefore, lineBreakStamp)
73
+ .replace(lineBreakAfter, lineBreakStamp);
74
+ }
75
+ // strip non space whitespace then compress spaces to one
76
+ return str
77
+ .replace(/[\t\n\r]+/g, ' ').replace(/[ ]+/g, ' ')
78
+ .replace(new RegExp(lineBreakStamp, 'g'), '\n');
79
+ }
80
+
81
+ return str;
82
+ }
83
+
84
+ function isConditionalComment(text) {
85
+ return ((/\[if[^\]]+\]/).test(text) || (/\s*((?:<!)?\[endif\])$/).test(text));
86
+ }
87
+
88
+ function isIgnoredComment(text, options) {
89
+ if ((/^!/).test(text)) {
90
+ return true;
91
+ }
92
+
93
+ if (options.ignoreCustomComments) {
94
+ for (var i = 0, len = options.ignoreCustomComments.length; i < len; i++) {
95
+ if (options.ignoreCustomComments[i].test(text)) {
96
+ return true;
97
+ }
98
+ }
99
+ }
100
+
101
+ return false;
102
+ }
103
+
104
+ function isEventAttribute(attrName) {
105
+ return (/^on[a-z]+/).test(attrName);
106
+ }
107
+
108
+ function canRemoveAttributeQuotes(value) {
109
+ // http://mathiasbynens.be/notes/unquoted-attribute-values
110
+ return (/^[^\x20\t\n\f\r"'`=<>]+$/).test(value) && !(/\/$/).test(value) &&
111
+ // make sure trailing slash is not interpreted as HTML self-closing tag
112
+ !(/\/$/).test(value);
113
+ }
114
+
115
+ function attributesInclude(attributes, attribute) {
116
+ for (var i = attributes.length; i--;) {
117
+ if (attributes[i].name.toLowerCase() === attribute) {
118
+ return true;
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+
124
+ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
125
+ attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
126
+
127
+ return (
128
+ (tag === 'script' &&
129
+ attrName === 'language' &&
130
+ attrValue === 'javascript') ||
131
+
132
+ (tag === 'form' &&
133
+ attrName === 'method' &&
134
+ attrValue === 'get') ||
135
+
136
+ (tag === 'input' &&
137
+ attrName === 'type' &&
138
+ attrValue === 'text') ||
139
+
140
+ (tag === 'script' &&
141
+ attrName === 'charset' &&
142
+ !attributesInclude(attrs, 'src')) ||
143
+
144
+ (tag === 'a' &&
145
+ attrName === 'name' &&
146
+ attributesInclude(attrs, 'id')) ||
147
+
148
+ (tag === 'area' &&
149
+ attrName === 'shape' &&
150
+ attrValue === 'rect')
151
+ );
152
+ }
153
+
154
+ function isScriptTypeAttribute(tag, attrName, attrValue) {
155
+ return (
156
+ tag === 'script' &&
157
+ attrName === 'type' &&
158
+ trimWhitespace(attrValue.toLowerCase()) === 'text/javascript'
159
+ );
160
+ }
161
+
162
+ // https://mathiasbynens.be/demo/javascript-mime-type
163
+ // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
164
+ var executableScriptsMimetypes = {
165
+ 'text/javascript': 1,
166
+ 'text/ecmascript': 1,
167
+ 'text/jscript': 1,
168
+ 'application/javascript': 1,
169
+ 'application/x-javascript': 1,
170
+ 'application/ecmascript': 1
171
+ };
172
+
173
+ function isExecutableScript(tag, attrs) {
174
+ if (tag !== 'script') {
175
+ return false;
176
+ }
177
+ for (var i = 0, len = attrs.length; i < len; i++) {
178
+ var attrName = attrs[i].name.toLowerCase();
179
+ if (attrName === 'type') {
180
+ return attrs[i].value === '' ||
181
+ executableScriptsMimetypes[attrs[i].value] === 1;
182
+ }
183
+ }
184
+ return true;
185
+ }
186
+
187
+ function isStyleLinkTypeAttribute(tag, attrName, attrValue) {
188
+ return (
189
+ (tag === 'style' || tag === 'link') &&
190
+ attrName === 'type' &&
191
+ trimWhitespace(attrValue.toLowerCase()) === 'text/css'
192
+ );
193
+ }
194
+
195
+ var enumeratedAttributeValues = {
196
+ draggable: ['true', 'false'] // defaults to 'auto'
197
+ };
198
+
199
+ function isBooleanAttribute(attrName, attrValue) {
200
+ var isSimpleBoolean = (/^(?:allowfullscreen|async|autofocus|autoplay|checked|compact|controls|declare|default|defaultchecked|defaultmuted|defaultselected|defer|disabled|enabled|formnovalidate|hidden|indeterminate|inert|ismap|itemscope|loop|multiple|muted|nohref|noresize|noshade|novalidate|nowrap|open|pauseonexit|readonly|required|reversed|scoped|seamless|selected|sortable|spellcheck|truespeed|typemustmatch|visible)$/i).test(attrName);
201
+ if (isSimpleBoolean) {
202
+ return true;
203
+ }
204
+
205
+ var attrValueEnumeration = enumeratedAttributeValues[attrName.toLowerCase()];
206
+ if (!attrValueEnumeration) {
207
+ return false;
208
+ }
209
+ else {
210
+ return (-1 === attrValueEnumeration.indexOf(attrValue.toLowerCase()));
211
+ }
212
+ }
213
+
214
+ function isUriTypeAttribute(attrName, tag) {
215
+ return (
216
+ ((/^(?:a|area|link|base)$/).test(tag) && attrName === 'href') ||
217
+ (tag === 'img' && (/^(?:src|longdesc|usemap)$/).test(attrName)) ||
218
+ (tag === 'object' && (/^(?:classid|codebase|data|usemap)$/).test(attrName)) ||
219
+ (tag === 'q' && attrName === 'cite') ||
220
+ (tag === 'blockquote' && attrName === 'cite') ||
221
+ ((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
222
+ (tag === 'form' && attrName === 'action') ||
223
+ (tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
224
+ (tag === 'head' && attrName === 'profile') ||
225
+ (tag === 'script' && (attrName === 'src' || attrName === 'for'))
226
+ );
227
+ }
228
+
229
+ function isNumberTypeAttribute(attrName, tag) {
230
+ return (
231
+ ((/^(?:a|area|object|button)$/).test(tag) && attrName === 'tabindex') ||
232
+ (tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
233
+ (tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
234
+ (tag === 'textarea' && (/^(?:rows|cols|tabindex)$/).test(attrName)) ||
235
+ (tag === 'colgroup' && attrName === 'span') ||
236
+ (tag === 'col' && attrName === 'span') ||
237
+ ((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
238
+ );
239
+ }
240
+
241
+ function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
242
+ if (attrValue && isEventAttribute(attrName)) {
243
+ attrValue = trimWhitespace(attrValue).replace(/^javascript:\s*/i, '').replace(/\s*;$/, '');
244
+ if (options.minifyJS) {
245
+ var wrappedCode = '(function(){' + attrValue + '})()';
246
+ var minified = minifyJS(wrappedCode, options.minifyJS);
247
+ return minified.slice(12, minified.length - 4).replace(/"/g, '&quot;');
248
+ }
249
+ return attrValue;
250
+ }
251
+ else if (attrName === 'class') {
252
+ return collapseWhitespace(trimWhitespace(attrValue));
253
+ }
254
+ else if (isUriTypeAttribute(attrName, tag)) {
255
+ attrValue = trimWhitespace(attrValue);
256
+ if (options.minifyURLs) {
257
+ return minifyURLs(attrValue, options.minifyURLs);
258
+ }
259
+ return attrValue;
260
+ }
261
+ else if (isNumberTypeAttribute(attrName, tag)) {
262
+ return trimWhitespace(attrValue);
263
+ }
264
+ else if (attrName === 'style') {
265
+ attrValue = trimWhitespace(attrValue);
266
+ if (attrValue) {
267
+ attrValue = attrValue.replace(/\s*;\s*$/, '');
268
+ }
269
+ if (options.minifyCSS) {
270
+ return minifyCSS(attrValue, options.minifyCSS, true);
271
+ }
272
+ return attrValue;
273
+ }
274
+ else if (isMetaViewport(tag, attrs) && attrName === 'content') {
275
+ attrValue = attrValue.replace(/1\.0/g, '1').replace(/\s+/g, '');
276
+ }
277
+ else if (attrValue && options.customAttrCollapse && options.customAttrCollapse.test(attrName)) {
278
+ attrValue = attrValue.replace(/\n+/g, '');
279
+ }
280
+ return attrValue;
281
+ }
282
+
283
+ function isMetaViewport(tag, attrs) {
284
+ if (tag !== 'meta') {
285
+ return false;
286
+ }
287
+ for (var i = 0, len = attrs.length; i < len; i++) {
288
+ if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
289
+ return true;
290
+ }
291
+ }
292
+ }
293
+
294
+ // Wrap CSS declarations for CleanCSS > 3.x
295
+ // See https://github.com/jakubpawlowicz/clean-css/issues/418
296
+ function wrapCSS(text) {
297
+ return '*{' + text + '}';
298
+ }
299
+
300
+ function unwrapCSS(text) {
301
+ var matches = text.match(/^\*\{([\s\S]*)\}$/m);
302
+ if (matches && matches[1]) {
303
+ return matches[1];
304
+ }
305
+ else {
306
+ return text;
307
+ }
308
+ }
309
+
310
+ function cleanConditionalComment(comment) {
311
+ return comment
312
+ .replace(/^(\[[^\]]+\]>)\s*/, '$1')
313
+ .replace(/\s*(<!\[endif\])$/, '$1');
314
+ }
315
+
316
+ function removeCDATASections(text) {
317
+ return text
318
+ // "/* <![CDATA[ */" or "// <![CDATA["
319
+ .replace(/^(?:\s*\/\*\s*<!\[CDATA\[\s*\*\/|\s*\/\/\s*<!\[CDATA\[.*)/, '')
320
+ // "/* ]]> */" or "// ]]>"
321
+ .replace(/(?:\/\*\s*\]\]>\s*\*\/|\/\/\s*\]\]>)\s*$/, '');
322
+ }
323
+
324
+ function processScript(text, options, currentAttrs) {
325
+ for (var i = 0, len = currentAttrs.length; i < len; i++) {
326
+ if (currentAttrs[i].name.toLowerCase() === 'type' &&
327
+ options.processScripts.indexOf(currentAttrs[i].value) > -1) {
328
+ return minify(text, options);
329
+ }
330
+ }
331
+ return text;
332
+ }
333
+
334
+ var reStartDelimiter = {
335
+ // account for js + html comments (e.g.: //<!--)
336
+ script: /^\s*(?:\/\/)?\s*<!--.*\n?/,
337
+ style: /^\s*<!--\s*/
338
+ };
339
+ var reEndDelimiter = {
340
+ script: /\s*(?:\/\/)?\s*-->\s*$/,
341
+ style: /\s*-->\s*$/
342
+ };
343
+ function removeComments(text, tag) {
344
+ return text.replace(reStartDelimiter[tag], '').replace(reEndDelimiter[tag], '');
345
+ }
346
+
347
+ function isOptionalTag(tag) {
348
+ return (/^(?:html|t?body|t?head|tfoot|tr|td|th|dt|dd|option|colgroup|source|track)$/).test(tag);
349
+ }
350
+
351
+ var reEmptyAttribute = new RegExp(
352
+ '^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
353
+ '?:down|up|over|move|out)|key(?:press|down|up)))$');
354
+
355
+ function canDeleteEmptyAttribute(tag, attrName, attrValue) {
356
+ var isValueEmpty = !attrValue || (/^\s*$/).test(attrValue);
357
+ if (isValueEmpty) {
358
+ return (
359
+ (tag === 'input' && attrName === 'value') ||
360
+ reEmptyAttribute.test(attrName));
361
+ }
362
+ return false;
363
+ }
364
+
365
+ function canRemoveElement(tag, attrs) {
366
+ if (tag === 'textarea') {
367
+ return false;
368
+ }
369
+
370
+ if (tag === 'script') {
371
+ for (var i = attrs.length - 1; i >= 0; i--) {
372
+ if (attrs[i].name === 'src') {
373
+ return false;
374
+ }
375
+ }
376
+ }
377
+
378
+ return true;
379
+ }
380
+
381
+ function canCollapseWhitespace(tag) {
382
+ return !(/^(?:script|style|pre|textarea)$/.test(tag));
383
+ }
384
+
385
+ function canTrimWhitespace(tag) {
386
+ return !(/^(?:pre|textarea)$/.test(tag));
387
+ }
388
+
389
+ function attrsToMarkup(attrs) {
390
+ var markup = '';
391
+ for (var i = 0, len = attrs.length; i < len; i++) {
392
+ markup += (' ' + attrs[i].name + (isBooleanAttribute(attrs[i].value) ? '' : ('="' + attrs[i].value + '"')));
393
+ }
394
+ return markup;
395
+ }
396
+
397
+ function normalizeAttribute(attr, attrs, tag, unarySlash, index, options) {
398
+
399
+ var attrName = options.caseSensitive ? attr.name : attr.name.toLowerCase(),
400
+ attrValue = options.preventAttributesEscaping ? attr.value : attr.escaped,
401
+ attrQuote = options.preventAttributesEscaping ? attr.quote : (options.quoteCharacter === '\'' ? '\'' : '"'),
402
+ attrFragment,
403
+ emittedAttrValue,
404
+ isTerminalOfUnarySlash = unarySlash && index === attrs.length - 1;
405
+
406
+ if ((options.removeRedundantAttributes &&
407
+ isAttributeRedundant(tag, attrName, attrValue, attrs))
408
+ ||
409
+ (options.removeScriptTypeAttributes &&
410
+ isScriptTypeAttribute(tag, attrName, attrValue))
411
+ ||
412
+ (options.removeStyleLinkTypeAttributes &&
413
+ isStyleLinkTypeAttribute(tag, attrName, attrValue))) {
414
+ return '';
415
+ }
416
+
417
+ attrValue = cleanAttributeValue(tag, attrName, attrValue, options, attrs);
418
+
419
+ if (attrValue !== undefined && !options.removeAttributeQuotes ||
420
+ !canRemoveAttributeQuotes(attrValue) || isTerminalOfUnarySlash) {
421
+ emittedAttrValue = attrQuote + attrValue + attrQuote;
422
+ }
423
+ else {
424
+ emittedAttrValue = attrValue;
425
+ }
426
+
427
+ if (options.removeEmptyAttributes &&
428
+ canDeleteEmptyAttribute(tag, attrName, attrValue)) {
429
+ return '';
430
+ }
431
+
432
+ if (attrValue === undefined || (options.collapseBooleanAttributes &&
433
+ isBooleanAttribute(attrName, attrValue))) {
434
+ attrFragment = attrName;
435
+ }
436
+ else {
437
+ attrFragment = attrName + attr.customAssign + emittedAttrValue;
438
+ }
439
+
440
+ return (' ' + attr.customOpen + attrFragment + attr.customClose);
441
+ }
442
+
443
+ function setDefaultTesters(options) {
444
+
445
+ var defaultTesters = ['canCollapseWhitespace', 'canTrimWhitespace'];
446
+
447
+ for (var i = 0, len = defaultTesters.length; i < len; i++) {
448
+ if (!options[defaultTesters[i]]) {
449
+ options[defaultTesters[i]] = function() {
450
+ return false;
451
+ };
452
+ }
453
+ }
454
+ }
455
+
456
+ function minifyURLs(text, options) {
457
+ if (typeof options !== 'object') {
458
+ options = { };
459
+ }
460
+
461
+ try {
462
+ // try to get global reference first
463
+ var __RelateUrl = global.RelateUrl;
464
+
465
+ if (typeof __RelateUrl === 'undefined' && typeof require === 'function') {
466
+ __RelateUrl = require('relateurl');
467
+ }
468
+
469
+ // noop
470
+ if (!__RelateUrl) {
471
+ return text;
472
+ }
473
+
474
+ if (__RelateUrl.relate) {
475
+ return __RelateUrl.relate(text, options);
476
+ }
477
+ else {
478
+ return text;
479
+ }
480
+ }
481
+ catch (err) {
482
+ log(err);
483
+ }
484
+ return text;
485
+ }
486
+
487
+ function minifyJS(text, options) {
488
+ if (typeof options !== 'object') {
489
+ options = { };
490
+ }
491
+ options.fromString = true;
492
+ var outputOptions = options.output || {};
493
+ outputOptions.inline_script = true;
494
+ options.output = outputOptions;
495
+
496
+ try {
497
+ // try to get global reference first
498
+ var __UglifyJS = global.UglifyJS;
499
+
500
+ if (typeof __UglifyJS === 'undefined' && typeof require === 'function') {
501
+ __UglifyJS = require('uglify-js');
502
+ }
503
+
504
+ // noop
505
+ if (!__UglifyJS) {
506
+ return text;
507
+ }
508
+
509
+ if (__UglifyJS.minify) {
510
+ return __UglifyJS.minify(text, options).code;
511
+ }
512
+ else if (__UglifyJS.parse) {
513
+
514
+ var ast = __UglifyJS.parse(text);
515
+ ast.figure_out_scope();
516
+
517
+ var compressor = __UglifyJS.Compressor();
518
+ var compressedAst = ast.transform(compressor);
519
+
520
+ compressedAst.figure_out_scope();
521
+ compressedAst.compute_char_frequency();
522
+
523
+ if (options.mangle !== false) {
524
+ compressedAst.mangle_names();
525
+ }
526
+
527
+ var stream = __UglifyJS.OutputStream(options.output);
528
+ compressedAst.print(stream);
529
+
530
+ return stream.toString();
531
+ }
532
+ else {
533
+ return text;
534
+ }
535
+ }
536
+ catch (err) {
537
+ log(err);
538
+ }
539
+ return text;
540
+ }
541
+
542
+ function minifyCSS(text, options, inline) {
543
+ if (typeof options !== 'object') {
544
+ options = { };
545
+ }
546
+ if (typeof options.advanced === 'undefined') {
547
+ options.advanced = false;
548
+ }
549
+ try {
550
+ var cleanCSS;
551
+
552
+ if (typeof CleanCSS !== 'undefined') {
553
+ cleanCSS = new CleanCSS(options);
554
+ }
555
+ else if (typeof require === 'function') {
556
+ var CleanCSSModule = require('clean-css');
557
+ cleanCSS = new CleanCSSModule(options);
558
+ }
559
+ if (inline) {
560
+ return unwrapCSS(cleanCSS.minify(wrapCSS(text)).styles);
561
+ }
562
+ else {
563
+ return cleanCSS.minify(text).styles;
564
+ }
565
+ }
566
+ catch (err) {
567
+ log(err);
568
+ }
569
+ return text;
570
+ }
571
+
572
+ function minify(value, options) {
573
+
574
+ options = options || {};
575
+ var optionsStack = [];
576
+
577
+ value = trimWhitespace(value);
578
+ setDefaultTesters(options);
579
+
580
+ var results = [ ],
581
+ buffer = [ ],
582
+ currentChars = '',
583
+ currentTag = '',
584
+ currentAttrs = [],
585
+ stackNoTrimWhitespace = [],
586
+ stackNoCollapseWhitespace = [],
587
+ lint = options.lint,
588
+ isIgnoring = false,
589
+ t = new Date();
590
+
591
+ if (options.removeIgnored) {
592
+ value = value
593
+ .replace(/<\?[^\?]+\?>/g, '')
594
+ .replace(/<%[^%]+%>/g, '');
595
+ }
596
+
597
+ function _canCollapseWhitespace(tag, attrs) {
598
+ return canCollapseWhitespace(tag) || options.canCollapseWhitespace(tag, attrs);
599
+ }
600
+
601
+ function _canTrimWhitespace(tag, attrs) {
602
+ return canTrimWhitespace(tag) || options.canTrimWhitespace(tag, attrs);
603
+ }
604
+
605
+ new HTMLParser(value, {
606
+ html5: typeof options.html5 !== 'undefined' ? options.html5 : true,
607
+
608
+ start: function(tag, attrs, unary, unarySlash) {
609
+ if (isIgnoring) {
610
+ buffer.push('<' + tag, attrsToMarkup(attrs), unarySlash ? '/' : '', '>');
611
+ return;
612
+ }
613
+
614
+ var lowerTag = tag.toLowerCase();
615
+
616
+ if (lowerTag === 'svg') {
617
+ optionsStack.push(options);
618
+ var nextOptions = {};
619
+ for (var key in options) {
620
+ nextOptions[key] = options[key];
621
+ }
622
+ nextOptions.keepClosingSlash = true;
623
+ nextOptions.caseSensitive = true;
624
+ options = nextOptions;
625
+ }
626
+
627
+ tag = options.caseSensitive ? tag : lowerTag;
628
+
629
+ currentTag = tag;
630
+ currentChars = '';
631
+ currentAttrs = attrs;
632
+
633
+ // set whitespace flags for nested tags (eg. <code> within a <pre>)
634
+ if (options.collapseWhitespace) {
635
+ if (!_canTrimWhitespace(tag, attrs)) {
636
+ stackNoTrimWhitespace.push(tag);
637
+ }
638
+ if (!_canCollapseWhitespace(tag, attrs)) {
639
+ stackNoCollapseWhitespace.push(tag);
640
+ }
641
+ }
642
+
643
+ var openTag = '<' + tag;
644
+ var closeTag = ((unarySlash && options.keepClosingSlash) ? '/' : '') + '>';
645
+ if (attrs.length === 0) {
646
+ openTag += closeTag;
647
+ }
648
+
649
+ buffer.push(openTag);
650
+
651
+ if (lint) {
652
+ lint.testElement(tag);
653
+ }
654
+
655
+ var token;
656
+ for (var i = 0, len = attrs.length; i < len; i++) {
657
+ if (lint) {
658
+ lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].escaped);
659
+ }
660
+ token = normalizeAttribute(attrs[i], attrs, tag, unarySlash, i, options);
661
+ if (i === len - 1) {
662
+ token += closeTag;
663
+ }
664
+ buffer.push(token);
665
+ }
666
+ },
667
+ end: function(tag, attrs) {
668
+
669
+ if (isIgnoring) {
670
+ buffer.push('</' + tag + '>');
671
+ return;
672
+ }
673
+
674
+ var lowerTag = tag.toLowerCase();
675
+ if (lowerTag === 'svg') {
676
+ options = optionsStack.pop();
677
+ }
678
+
679
+ // check if current tag is in a whitespace stack
680
+ if (options.collapseWhitespace) {
681
+ if (stackNoTrimWhitespace.length &&
682
+ tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
683
+ stackNoTrimWhitespace.pop();
684
+ }
685
+ if (stackNoCollapseWhitespace.length &&
686
+ tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
687
+ stackNoCollapseWhitespace.pop();
688
+ }
689
+ }
690
+
691
+ var isElementEmpty = currentChars === '' && tag === currentTag;
692
+ if ((options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs))) {
693
+ // remove last "element" from buffer, return
694
+ for (var i = buffer.length - 1; i >= 0; i--) {
695
+ if (/^<[^\/!]/.test(buffer[i])) {
696
+ buffer.splice(i);
697
+ break;
698
+ }
699
+ }
700
+ return;
701
+ }
702
+ else if (options.removeOptionalTags && isOptionalTag(tag)) {
703
+ // noop, leave start tag in buffer
704
+ return;
705
+ }
706
+ else {
707
+ // push end tag to buffer
708
+ buffer.push('</' + (options.caseSensitive ? tag : lowerTag) + '>');
709
+ results.push.apply(results, buffer);
710
+ }
711
+ // flush buffer
712
+ buffer.length = 0;
713
+ currentChars = '';
714
+ },
715
+ chars: function(text, prevTag, nextTag) {
716
+ prevTag = prevTag === '' ? 'comment' : prevTag;
717
+ nextTag = nextTag === '' ? 'comment' : nextTag;
718
+
719
+ if (isIgnoring) {
720
+ buffer.push(text);
721
+ return;
722
+ }
723
+
724
+ if (currentTag === 'script' || currentTag === 'style') {
725
+ if (options.removeCommentsFromCDATA) {
726
+ text = removeComments(text, currentTag);
727
+ }
728
+ if (options.removeCDATASectionsFromCDATA) {
729
+ text = removeCDATASections(text);
730
+ }
731
+ if (options.processScripts) {
732
+ text = processScript(text, options, currentAttrs);
733
+ }
734
+ }
735
+ if (options.minifyJS && isExecutableScript(currentTag, currentAttrs)) {
736
+ text = minifyJS(text, options.minifyJS);
737
+ }
738
+ if (currentTag === 'style' && options.minifyCSS) {
739
+ text = minifyCSS(text, options.minifyCSS);
740
+ }
741
+ if (options.collapseWhitespace) {
742
+ if (!stackNoTrimWhitespace.length) {
743
+ text = ((prevTag && prevTag !== 'comment') || (nextTag && nextTag !== 'comment')) ?
744
+ collapseWhitespaceSmart(text, prevTag, nextTag, options)
745
+ : trimWhitespace(text);
746
+ }
747
+ if (!stackNoCollapseWhitespace.length) {
748
+ text = !(prevTag && nextTag || nextTag === 'html') ? collapseWhitespace(text) : text;
749
+ }
750
+ }
751
+ currentChars = text;
752
+ if (lint) {
753
+ lint.testChars(text);
754
+ }
755
+ buffer.push(text);
756
+ },
757
+ comment: function(text, nonStandard) {
758
+
759
+ var prefix = nonStandard ? '<!' : '<!--';
760
+ var suffix = nonStandard ? '>' : '-->';
761
+
762
+ if (/^\s*htmlmin:ignore/.test(text)) {
763
+ isIgnoring = !isIgnoring;
764
+ if (!options.removeComments) {
765
+ buffer.push('<!--' + text + '-->');
766
+ }
767
+ return;
768
+ }
769
+ if (options.removeComments) {
770
+ if (isConditionalComment(text)) {
771
+ text = prefix + cleanConditionalComment(text) + suffix;
772
+ }
773
+ else if (isIgnoredComment(text, options)) {
774
+ text = '<!--' + text + '-->';
775
+ }
776
+ else {
777
+ text = '';
778
+ }
779
+ }
780
+ else {
781
+ text = prefix + text + suffix;
782
+ }
783
+ buffer.push(text);
784
+ },
785
+ doctype: function(doctype) {
786
+ buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespace(doctype));
787
+ },
788
+ customAttrAssign: options.customAttrAssign,
789
+ customAttrSurround: options.customAttrSurround
790
+ });
791
+
792
+ results.push.apply(results, buffer);
793
+ var str = joinResultSegments(results, options);
794
+ log('minified in: ' + (new Date() - t) + 'ms');
795
+ return str;
796
+ }
797
+
798
+ function joinResultSegments(results, options) {
799
+ var str;
800
+ var maxLineLength = options.maxLineLength;
801
+ if (maxLineLength) {
802
+ var token;
803
+ var lines = [];
804
+ var line = '';
805
+ for (var i = 0, len = results.length; i < len; i++) {
806
+ token = results[i];
807
+ if (line.length + token.length < maxLineLength) {
808
+ line += token;
809
+ }
810
+ else {
811
+ lines.push(line.replace(/^\n/, ''));
812
+ line = token;
813
+ }
814
+ }
815
+ lines.push(line);
816
+
817
+ str = lines.join('\n');
818
+ }
819
+ else {
820
+ str = results.join('');
821
+ }
822
+
823
+ return str;
824
+ }
825
+
826
+ // for CommonJS enviroments, export everything
827
+ if (typeof exports !== 'undefined') {
828
+ exports.minify = minify;
829
+ }
830
+ else {
831
+ global.minify = minify;
832
+ }
833
+
834
+ }(typeof exports === 'undefined' ? this : exports));