html-minifier 1.4.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,8 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  var CleanCSS = require('clean-css');
4
+ var decode = require('he').decode;
4
5
  var HTMLParser = require('./htmlparser').HTMLParser;
5
6
  var RelateUrl = require('relateurl');
7
+ var TokenChain = require('./tokenchain');
6
8
  var UglifyJS = require('uglify-js');
7
9
  var utils = require('./utils');
8
10
 
@@ -17,20 +19,17 @@ else {
17
19
  log = function() {};
18
20
  }
19
21
 
20
- var trimWhitespace = function(str) {
22
+ var trimWhitespace = String.prototype.trim ? function(str) {
23
+ if (typeof str !== 'string') {
24
+ return str;
25
+ }
26
+ return str.trim();
27
+ } : function(str) {
21
28
  if (typeof str !== 'string') {
22
29
  return str;
23
30
  }
24
31
  return str.replace(/^\s+/, '').replace(/\s+$/, '');
25
32
  };
26
- if (String.prototype.trim) {
27
- trimWhitespace = function(str) {
28
- if (typeof str !== 'string') {
29
- return str;
30
- }
31
- return str.trim();
32
- };
33
- }
34
33
 
35
34
  function compressWhitespace(spaces) {
36
35
  return spaces === '\t' ? spaces : ' ';
@@ -119,14 +118,12 @@ function isEventAttribute(attrName, options) {
119
118
  }
120
119
  return false;
121
120
  }
122
- else {
123
- return (/^on[a-z]{3,}$/).test(attrName);
124
- }
121
+ return /^on[a-z]{3,}$/.test(attrName);
125
122
  }
126
123
 
127
124
  function canRemoveAttributeQuotes(value) {
128
125
  // http://mathiasbynens.be/notes/unquoted-attribute-values
129
- return (/^[^\x20\t\n\f\r"'`=<>]+$/).test(value);
126
+ return /^[^ \t\n\f\r"'`=<>]+$/.test(value);
130
127
  }
131
128
 
132
129
  function attributesInclude(attributes, attribute) {
@@ -142,29 +139,29 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
142
139
  attrValue = attrValue ? trimWhitespace(attrValue.toLowerCase()) : '';
143
140
 
144
141
  return (
145
- (tag === 'script' &&
142
+ tag === 'script' &&
146
143
  attrName === 'language' &&
147
- attrValue === 'javascript') ||
144
+ attrValue === 'javascript' ||
148
145
 
149
- (tag === 'form' &&
146
+ tag === 'form' &&
150
147
  attrName === 'method' &&
151
- attrValue === 'get') ||
148
+ attrValue === 'get' ||
152
149
 
153
- (tag === 'input' &&
150
+ tag === 'input' &&
154
151
  attrName === 'type' &&
155
- attrValue === 'text') ||
152
+ attrValue === 'text' ||
156
153
 
157
- (tag === 'script' &&
154
+ tag === 'script' &&
158
155
  attrName === 'charset' &&
159
- !attributesInclude(attrs, 'src')) ||
156
+ !attributesInclude(attrs, 'src') ||
160
157
 
161
- (tag === 'a' &&
158
+ tag === 'a' &&
162
159
  attrName === 'name' &&
163
- attributesInclude(attrs, 'id')) ||
160
+ attributesInclude(attrs, 'id') ||
164
161
 
165
- (tag === 'area' &&
162
+ tag === 'area' &&
166
163
  attrName === 'shape' &&
167
- attrValue === 'rect')
164
+ attrValue === 'rect'
168
165
  );
169
166
  }
170
167
 
@@ -179,12 +176,9 @@ var executableScriptsMimetypes = utils.createMap([
179
176
  'application/ecmascript'
180
177
  ]);
181
178
 
182
- function isScriptTypeAttribute(tag, attrName, attrValue) {
183
- return (
184
- tag === 'script' &&
185
- attrName === 'type' &&
186
- executableScriptsMimetypes(trimWhitespace(attrValue.toLowerCase()))
187
- );
179
+ function isScriptTypeAttribute(attrValue) {
180
+ attrValue = trimWhitespace(attrValue.split(/;/, 2)[0]).toLowerCase();
181
+ return attrValue === '' || executableScriptsMimetypes(attrValue);
188
182
  }
189
183
 
190
184
  function isExecutableScript(tag, attrs) {
@@ -194,13 +188,17 @@ function isExecutableScript(tag, attrs) {
194
188
  for (var i = 0, len = attrs.length; i < len; i++) {
195
189
  var attrName = attrs[i].name.toLowerCase();
196
190
  if (attrName === 'type') {
197
- var attrValue = trimWhitespace(attrs[i].value.split(/;/, 2)[0]).toLowerCase();
198
- return attrValue === '' || executableScriptsMimetypes(attrValue);
191
+ return isScriptTypeAttribute(attrs[i].value);
199
192
  }
200
193
  }
201
194
  return true;
202
195
  }
203
196
 
197
+ function isStyleLinkTypeAttribute(attrValue) {
198
+ attrValue = trimWhitespace(attrValue).toLowerCase();
199
+ return attrValue === '' || attrValue === 'text/css';
200
+ }
201
+
204
202
  function isStyleSheet(tag, attrs) {
205
203
  if (tag !== 'style') {
206
204
  return false;
@@ -208,21 +206,12 @@ function isStyleSheet(tag, attrs) {
208
206
  for (var i = 0, len = attrs.length; i < len; i++) {
209
207
  var attrName = attrs[i].name.toLowerCase();
210
208
  if (attrName === 'type') {
211
- var attrValue = trimWhitespace(attrs[i].value).toLowerCase();
212
- return attrValue === '' || attrValue === 'text/css';
209
+ return isStyleLinkTypeAttribute(attrs[i].value);
213
210
  }
214
211
  }
215
212
  return true;
216
213
  }
217
214
 
218
- function isStyleLinkTypeAttribute(tag, attrName, attrValue) {
219
- return (
220
- (tag === 'style' || tag === 'link') &&
221
- attrName === 'type' &&
222
- trimWhitespace(attrValue.toLowerCase()) === 'text/css'
223
- );
224
- }
225
-
226
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');
227
216
  var isBooleanValue = createMapFromString('true,false');
228
217
 
@@ -232,28 +221,28 @@ function isBooleanAttribute(attrName, attrValue) {
232
221
 
233
222
  function isUriTypeAttribute(attrName, tag) {
234
223
  return (
235
- ((/^(?:a|area|link|base)$/).test(tag) && attrName === 'href') ||
236
- (tag === 'img' && (/^(?:src|longdesc|usemap)$/).test(attrName)) ||
237
- (tag === 'object' && (/^(?:classid|codebase|data|usemap)$/).test(attrName)) ||
238
- (tag === 'q' && attrName === 'cite') ||
239
- (tag === 'blockquote' && attrName === 'cite') ||
240
- ((tag === 'ins' || tag === 'del') && attrName === 'cite') ||
241
- (tag === 'form' && attrName === 'action') ||
242
- (tag === 'input' && (attrName === 'src' || attrName === 'usemap')) ||
243
- (tag === 'head' && attrName === 'profile') ||
244
- (tag === 'script' && (attrName === 'src' || attrName === 'for'))
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')
245
234
  );
246
235
  }
247
236
 
248
237
  function isNumberTypeAttribute(attrName, tag) {
249
238
  return (
250
- ((/^(?:a|area|object|button)$/).test(tag) && attrName === 'tabindex') ||
251
- (tag === 'input' && (attrName === 'maxlength' || attrName === 'tabindex')) ||
252
- (tag === 'select' && (attrName === 'size' || attrName === 'tabindex')) ||
253
- (tag === 'textarea' && (/^(?:rows|cols|tabindex)$/).test(attrName)) ||
254
- (tag === 'colgroup' && attrName === 'span') ||
255
- (tag === 'col' && attrName === 'span') ||
256
- ((tag === 'th' || tag === 'td') && (attrName === 'rowspan' || attrName === 'colspan'))
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')
257
246
  );
258
247
  }
259
248
 
@@ -281,7 +270,14 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
281
270
  return attrValue;
282
271
  }
283
272
  else if (attrName === 'class') {
284
- return collapseWhitespaceAll(trimWhitespace(attrValue));
273
+ attrValue = trimWhitespace(attrValue);
274
+ if (options.sortClassName) {
275
+ attrValue = options.sortClassName(attrValue);
276
+ }
277
+ else {
278
+ attrValue = collapseWhitespaceAll(attrValue);
279
+ }
280
+ return attrValue;
285
281
  }
286
282
  else if (isUriTypeAttribute(attrName, tag)) {
287
283
  attrValue = trimWhitespace(attrValue);
@@ -295,8 +291,8 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs) {
295
291
  }
296
292
  else if (attrName === 'style') {
297
293
  attrValue = trimWhitespace(attrValue);
298
- if (attrValue) {
299
- attrValue = attrValue.replace(/\s*;\s*$/, '');
294
+ if (attrValue && /;$/.test(attrValue) && !/&#?[0-9a-zA-Z]+;$/.test(attrValue)) {
295
+ attrValue = attrValue.replace(/\s*;$/, '');
300
296
  }
301
297
  if (options.minifyCSS) {
302
298
  return minifyCSS(attrValue, options.minifyCSS, true);
@@ -342,9 +338,7 @@ function unwrapCSS(text) {
342
338
  if (matches && matches[1]) {
343
339
  return matches[1];
344
340
  }
345
- else {
346
- return text;
347
- }
341
+ return text;
348
342
  }
349
343
 
350
344
  function cleanConditionalComment(comment, options) {
@@ -453,11 +447,9 @@ var reEmptyAttribute = new RegExp(
453
447
  '?:down|up|over|move|out)|key(?:press|down|up)))$');
454
448
 
455
449
  function canDeleteEmptyAttribute(tag, attrName, attrValue) {
456
- var isValueEmpty = !attrValue || (/^\s*$/).test(attrValue);
450
+ var isValueEmpty = !attrValue || /^\s*$/.test(attrValue);
457
451
  if (isValueEmpty) {
458
- return (
459
- (tag === 'input' && attrName === 'value') ||
460
- reEmptyAttribute.test(attrName));
452
+ return tag === 'input' && attrName === 'value' || reEmptyAttribute.test(attrName);
461
453
  }
462
454
  return false;
463
455
  }
@@ -502,50 +494,67 @@ function canRemoveElement(tag, attrs) {
502
494
  }
503
495
 
504
496
  function canCollapseWhitespace(tag) {
505
- return !(/^(?:script|style|pre|textarea)$/.test(tag));
497
+ return !/^(?:script|style|pre|textarea)$/.test(tag);
506
498
  }
507
499
 
508
500
  function canTrimWhitespace(tag) {
509
- return !(/^(?:pre|textarea)$/.test(tag));
501
+ return !/^(?:pre|textarea)$/.test(tag);
510
502
  }
511
503
 
512
- function normalizeAttribute(attr, attrs, tag, hasUnarySlash, index, options, isLast) {
513
-
504
+ function normalizeAttr(attr, attrs, tag, options) {
514
505
  var attrName = options.caseSensitive ? attr.name : attr.name.toLowerCase(),
515
- attrValue = attr.value,
516
- attrQuote = attr.quote,
517
- attrFragment,
518
- emittedAttrValue;
506
+ attrValue = attr.value;
519
507
 
520
- if ((options.removeRedundantAttributes &&
521
- isAttributeRedundant(tag, attrName, attrValue, attrs))
522
- ||
523
- (options.removeScriptTypeAttributes &&
524
- isScriptTypeAttribute(tag, attrName, attrValue))
525
- ||
526
- (options.removeStyleLinkTypeAttributes &&
527
- isStyleLinkTypeAttribute(tag, attrName, attrValue))) {
528
- return '';
508
+ if (options.decodeEntities && attrValue) {
509
+ attrValue = decode(attrValue, { isAttributeValue: true });
510
+ }
511
+
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;
529
519
  }
530
520
 
531
521
  attrValue = cleanAttributeValue(tag, attrName, attrValue, options, attrs);
532
522
 
533
523
  if (options.removeEmptyAttributes &&
534
524
  canDeleteEmptyAttribute(tag, attrName, attrValue)) {
535
- return '';
525
+ return;
536
526
  }
537
527
 
538
- if (attrValue !== undefined && !options.removeAttributeQuotes ||
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 ||
539
548
  !canRemoveAttributeQuotes(attrValue)) {
540
549
  if (!options.preventAttributesEscaping) {
541
- if (options.quoteCharacter !== undefined) {
542
- attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
543
- }
544
- else {
550
+ if (typeof options.quoteCharacter === 'undefined') {
545
551
  var apos = (attrValue.match(/'/g) || []).length;
546
552
  var quot = (attrValue.match(/"/g) || []).length;
547
553
  attrQuote = apos < quot ? '\'' : '"';
548
554
  }
555
+ else {
556
+ attrQuote = options.quoteCharacter === '\'' ? '\'' : '"';
557
+ }
549
558
  if (attrQuote === '"') {
550
559
  attrValue = attrValue.replace(/"/g, '&#34;');
551
560
  }
@@ -566,8 +575,8 @@ function normalizeAttribute(attr, attrs, tag, hasUnarySlash, index, options, isL
566
575
  emittedAttrValue = attrValue + ' ';
567
576
  }
568
577
 
569
- if (attrValue === undefined || (options.collapseBooleanAttributes &&
570
- isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase()))) {
578
+ if (typeof attrValue === 'undefined' || options.collapseBooleanAttributes &&
579
+ isBooleanAttribute(attrName.toLowerCase(), attrValue.toLowerCase())) {
571
580
  attrFragment = attrName;
572
581
  if (!isLast) {
573
582
  attrFragment += ' ';
@@ -646,9 +655,7 @@ function minifyCSS(text, options, inline) {
646
655
  if (inline) {
647
656
  return unwrapCSS(cleanCSS.minify(wrapCSS(style)).styles);
648
657
  }
649
- else {
650
- return cleanCSS.minify(style).styles;
651
- }
658
+ return cleanCSS.minify(style).styles;
652
659
  }
653
660
  catch (err) {
654
661
  log(err);
@@ -664,13 +671,96 @@ function uniqueId(value) {
664
671
  return id;
665
672
  }
666
673
 
674
+ var specialContentTags = createMapFromString('script,style');
675
+
676
+ function createSortFns(value, options, uidIgnore, uidAttr) {
677
+ var attrChains = options.sortAttributes && Object.create(null);
678
+ var classChain = options.sortClassName && new TokenChain();
679
+
680
+ function attrNames(attrs) {
681
+ return attrs.map(function(attr) {
682
+ return options.caseSensitive ? attr.name : attr.name.toLowerCase();
683
+ });
684
+ }
685
+
686
+ function shouldSkipUID(token, uid) {
687
+ return !uid || token.indexOf(uid) === -1;
688
+ }
689
+
690
+ function shouldSkipUIDs(token) {
691
+ return shouldSkipUID(token, uidIgnore) && shouldSkipUID(token, uidAttr);
692
+ }
693
+
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);
722
+ }
723
+ }
724
+ });
725
+ }
726
+
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
+ });
746
+ }
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
+
667
757
  function minify(value, options, partialMarkup) {
668
758
  options = options || {};
669
759
  var optionsStack = [];
670
760
  processOptions(options);
671
761
  value = options.collapseWhitespace ? trimWhitespace(value) : value;
672
762
 
673
- var buffer = [ ],
763
+ var buffer = [],
674
764
  charsPrevTag,
675
765
  currentChars = '',
676
766
  hasChars,
@@ -682,8 +772,8 @@ function minify(value, options, partialMarkup) {
682
772
  optionalEndTag = '',
683
773
  lint = options.lint,
684
774
  t = Date.now(),
685
- ignoredMarkupChunks = [ ],
686
- ignoredCustomMarkupChunks = [ ],
775
+ ignoredMarkupChunks = [],
776
+ ignoredCustomMarkupChunks = [],
687
777
  uidIgnore,
688
778
  uidAttr;
689
779
 
@@ -719,6 +809,12 @@ function minify(value, options, partialMarkup) {
719
809
  });
720
810
  }
721
811
 
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
+ }
817
+
722
818
  function _canCollapseWhitespace(tag, attrs) {
723
819
  return canCollapseWhitespace(tag) || options.canCollapseWhitespace(tag, attrs);
724
820
  }
@@ -764,7 +860,7 @@ function minify(value, options, partialMarkup) {
764
860
 
765
861
  new HTMLParser(value, {
766
862
  partialMarkup: partialMarkup,
767
- html5: typeof options.html5 !== 'undefined' ? options.html5 : true,
863
+ html5: typeof options.html5 === 'undefined' ? true : options.html5,
768
864
 
769
865
  start: function(tag, attrs, unary, unarySlash, autoGenerated) {
770
866
  var lowerTag = tag.toLowerCase();
@@ -834,16 +930,20 @@ function minify(value, options, partialMarkup) {
834
930
  lint.testElement(tag);
835
931
  }
836
932
 
837
- var parts = [ ];
838
- var token, isLast = true;
839
- for (var i = attrs.length; --i >= 0; ) {
933
+ if (options.sortAttributes) {
934
+ options.sortAttributes(tag, attrs);
935
+ }
936
+
937
+ var parts = [];
938
+ for (var i = attrs.length, isLast = true; --i >= 0;) {
939
+ var attr = attrs[i];
840
940
  if (lint) {
841
- lint.testAttribute(tag, attrs[i].name.toLowerCase(), attrs[i].value);
941
+ lint.testAttribute(tag, attr.name.toLowerCase(), attr.value);
842
942
  }
843
- token = normalizeAttribute(attrs[i], attrs, tag, hasUnarySlash, i, options, isLast);
844
- if (token) {
943
+ var normalized = normalizeAttr(attr, attrs, tag, options);
944
+ if (normalized) {
945
+ parts.unshift(buildAttr(normalized, hasUnarySlash, options, isLast));
845
946
  isLast = false;
846
- parts.unshift(token);
847
947
  }
848
948
  }
849
949
  if (parts.length > 0) {
@@ -928,6 +1028,9 @@ function minify(value, options, partialMarkup) {
928
1028
  chars: function(text, prevTag, nextTag) {
929
1029
  prevTag = prevTag === '' ? 'comment' : prevTag;
930
1030
  nextTag = nextTag === '' ? 'comment' : nextTag;
1031
+ if (options.decodeEntities && text && !specialContentTags(currentTag)) {
1032
+ text = decode(text);
1033
+ }
931
1034
  if (options.collapseWhitespace) {
932
1035
  if (!stackNoTrimWhitespace.length) {
933
1036
  if (prevTag === 'comment') {
@@ -964,12 +1067,12 @@ function minify(value, options, partialMarkup) {
964
1067
  text = prevTag && nextTag || nextTag === 'html' ? text : collapseWhitespaceAll(text);
965
1068
  }
966
1069
  }
967
- if (options.processScripts && (currentTag === 'script' || currentTag === 'style')) {
1070
+ if (options.processScripts && specialContentTags(currentTag)) {
968
1071
  text = processScript(text, options, currentAttrs);
969
1072
  }
970
1073
  if (options.minifyJS && isExecutableScript(currentTag, currentAttrs)) {
971
1074
  text = minifyJS(text, options.minifyJS);
972
- if (text.charAt(text.length - 1) === ';') {
1075
+ if (/;$/.test(text)) {
973
1076
  text = text.slice(0, -1);
974
1077
  }
975
1078
  }
@@ -991,6 +1094,11 @@ function minify(value, options, partialMarkup) {
991
1094
  optionalEndTag = '';
992
1095
  }
993
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
+ }
994
1102
  currentChars += text;
995
1103
  if (text) {
996
1104
  hasChars = true;
@@ -1025,6 +1133,9 @@ function minify(value, options, partialMarkup) {
1025
1133
  buffer.push(text);
1026
1134
  },
1027
1135
  doctype: function(doctype) {
1136
+ if (lint) {
1137
+ lint.testDoctype(doctype);
1138
+ }
1028
1139
  buffer.push(options.useShortDoctype ? '<!DOCTYPE html>' : collapseWhitespaceAll(doctype));
1029
1140
  },
1030
1141
  customAttrAssign: options.customAttrAssign,
@@ -1060,9 +1171,7 @@ function minify(value, options, partialMarkup) {
1060
1171
  conservativeCollapse: true
1061
1172
  }, /^\s/.test(chunk), /\s$/.test(chunk));
1062
1173
  }
1063
- else {
1064
- return chunk;
1065
- }
1174
+ return chunk;
1066
1175
  });
1067
1176
  }
1068
1177
  if (uidIgnore) {