html-minifier-next 4.16.4 → 4.17.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.
- package/README.md +57 -70
- package/cli.js +27 -25
- package/dist/htmlminifier.cjs +264 -138
- package/dist/htmlminifier.esm.bundle.js +264 -138
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +1 -1
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +16 -15
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/content.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +2 -2
- package/dist/types/lib/whitespace.d.ts +1 -1
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -1
- package/package.json +8 -7
- package/src/htmlminifier.js +49 -47
- package/src/htmlparser.js +44 -13
- package/src/lib/attributes.js +72 -30
- package/src/lib/constants.js +46 -39
- package/src/lib/content.js +0 -1
- package/src/lib/elements.js +15 -15
- package/src/lib/options.js +9 -9
- package/src/lib/svg.js +14 -14
- package/src/lib/whitespace.js +53 -4
- package/src/presets.js +4 -4
- package/src/tokenchain.js +2 -2
- package/src/lib/index.js +0 -20
package/dist/htmlminifier.cjs
CHANGED
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var entities = require('entities');
|
|
6
6
|
var RelateURL = require('relateurl');
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/*
|
|
9
9
|
* HTML Parser By John Resig (ejohn.org)
|
|
10
10
|
* Modified by Juriy “kangax” Zaytsev
|
|
11
11
|
* Original code by Erik Arvidsson, Mozilla Public License
|
|
@@ -16,10 +16,10 @@ var RelateURL = require('relateurl');
|
|
|
16
16
|
* Use like so:
|
|
17
17
|
*
|
|
18
18
|
* HTMLParser(htmlString, {
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* start: function(tag, attrs, unary) {},
|
|
20
|
+
* end: function(tag) {},
|
|
21
|
+
* chars: function(text) {},
|
|
22
|
+
* comment: function(text) {}
|
|
23
23
|
* });
|
|
24
24
|
*/
|
|
25
25
|
|
|
@@ -42,7 +42,7 @@ const singleAttrValues = [
|
|
|
42
42
|
];
|
|
43
43
|
// https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
|
|
44
44
|
const qnameCapture = (function () {
|
|
45
|
-
//
|
|
45
|
+
// https://www.npmjs.com/package/ncname
|
|
46
46
|
const combiningChar = '\\u0300-\\u0345\\u0360\\u0361\\u0483-\\u0486\\u0591-\\u05A1\\u05A3-\\u05B9\\u05BB-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u064B-\\u0652\\u0670\\u06D6-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0901-\\u0903\\u093C\\u093E-\\u094D\\u0951-\\u0954\\u0962\\u0963\\u0981-\\u0983\\u09BC\\u09BE-\\u09C4\\u09C7\\u09C8\\u09CB-\\u09CD\\u09D7\\u09E2\\u09E3\\u0A02\\u0A3C\\u0A3E-\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A70\\u0A71\\u0A81-\\u0A83\\u0ABC\\u0ABE-\\u0AC5\\u0AC7-\\u0AC9\\u0ACB-\\u0ACD\\u0B01-\\u0B03\\u0B3C\\u0B3E-\\u0B43\\u0B47\\u0B48\\u0B4B-\\u0B4D\\u0B56\\u0B57\\u0B82\\u0B83\\u0BBE-\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCD\\u0BD7\\u0C01-\\u0C03\\u0C3E-\\u0C44\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C82\\u0C83\\u0CBE-\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5\\u0CD6\\u0D02\\u0D03\\u0D3E-\\u0D43\\u0D46-\\u0D48\\u0D4A-\\u0D4D\\u0D57\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F3E\\u0F3F\\u0F71-\\u0F84\\u0F86-\\u0F8B\\u0F90-\\u0F95\\u0F97\\u0F99-\\u0FAD\\u0FB1-\\u0FB7\\u0FB9\\u20D0-\\u20DC\\u20E1\\u302A-\\u302F\\u3099\\u309A';
|
|
47
47
|
const digit = '0-9\\u0660-\\u0669\\u06F0-\\u06F9\\u0966-\\u096F\\u09E6-\\u09EF\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0BE7-\\u0BEF\\u0C66-\\u0C6F\\u0CE6-\\u0CEF\\u0D66-\\u0D6F\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F29';
|
|
48
48
|
const extender = '\\xB7\\u02D0\\u02D1\\u0387\\u0640\\u0E46\\u0EC6\\u3005\\u3031-\\u3035\\u309D\\u309E\\u30FC-\\u30FE';
|
|
@@ -82,7 +82,7 @@ const nonPhrasing = new CaseInsensitiveSet(['address', 'article', 'aside', 'base
|
|
|
82
82
|
const reCache = {};
|
|
83
83
|
|
|
84
84
|
// Pre-compiled regexes for common special elements (`script`, `style`, `noscript`)
|
|
85
|
-
// These are used frequently and pre-compiling them avoids regex creation overhead
|
|
85
|
+
// These are used frequently, and pre-compiling them avoids regex creation overhead
|
|
86
86
|
const preCompiledStackedTags = {
|
|
87
87
|
'script': /([\s\S]*?)<\/script[^>]*>/i,
|
|
88
88
|
'style': /([\s\S]*?)<\/style[^>]*>/i,
|
|
@@ -145,6 +145,7 @@ class HTMLParser {
|
|
|
145
145
|
// Use cached attribute regex for this handler configuration
|
|
146
146
|
const attribute = getAttrRegexForHandler(handler);
|
|
147
147
|
let prevTag = undefined, nextTag = undefined;
|
|
148
|
+
let prevAttrs = [], nextAttrs = [];
|
|
148
149
|
|
|
149
150
|
// Index-based parsing
|
|
150
151
|
let pos = 0;
|
|
@@ -188,6 +189,7 @@ class HTMLParser {
|
|
|
188
189
|
}
|
|
189
190
|
advance(commentEnd + 3);
|
|
190
191
|
prevTag = '';
|
|
192
|
+
prevAttrs = [];
|
|
191
193
|
continue;
|
|
192
194
|
}
|
|
193
195
|
}
|
|
@@ -202,6 +204,7 @@ class HTMLParser {
|
|
|
202
204
|
}
|
|
203
205
|
advance(conditionalEnd + 2);
|
|
204
206
|
prevTag = '';
|
|
207
|
+
prevAttrs = [];
|
|
205
208
|
continue;
|
|
206
209
|
}
|
|
207
210
|
}
|
|
@@ -214,6 +217,7 @@ class HTMLParser {
|
|
|
214
217
|
}
|
|
215
218
|
advance(doctypeMatch[0].length);
|
|
216
219
|
prevTag = '';
|
|
220
|
+
prevAttrs = [];
|
|
217
221
|
continue;
|
|
218
222
|
}
|
|
219
223
|
|
|
@@ -223,6 +227,7 @@ class HTMLParser {
|
|
|
223
227
|
advance(endTagMatch[0].length);
|
|
224
228
|
await parseEndTag(endTagMatch[0], endTagMatch[1]);
|
|
225
229
|
prevTag = '/' + endTagMatch[1].toLowerCase();
|
|
230
|
+
prevAttrs = [];
|
|
226
231
|
continue;
|
|
227
232
|
}
|
|
228
233
|
|
|
@@ -255,19 +260,24 @@ class HTMLParser {
|
|
|
255
260
|
let nextTagMatch = parseStartTag(nextHtml);
|
|
256
261
|
if (nextTagMatch) {
|
|
257
262
|
nextTag = nextTagMatch.tagName;
|
|
263
|
+
// Extract minimal attribute info for whitespace logic (just name/value pairs)
|
|
264
|
+
nextAttrs = extractAttrInfo(nextTagMatch.attrs);
|
|
258
265
|
} else {
|
|
259
266
|
nextTagMatch = nextHtml.match(endTag);
|
|
260
267
|
if (nextTagMatch) {
|
|
261
268
|
nextTag = '/' + nextTagMatch[1];
|
|
269
|
+
nextAttrs = [];
|
|
262
270
|
} else {
|
|
263
271
|
nextTag = '';
|
|
272
|
+
nextAttrs = [];
|
|
264
273
|
}
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
if (handler.chars) {
|
|
268
|
-
await handler.chars(text, prevTag, nextTag);
|
|
277
|
+
await handler.chars(text, prevTag, nextTag, prevAttrs, nextAttrs);
|
|
269
278
|
}
|
|
270
279
|
prevTag = '';
|
|
280
|
+
prevAttrs = [];
|
|
271
281
|
} else {
|
|
272
282
|
const stackedTag = lastTag.toLowerCase();
|
|
273
283
|
// Use pre-compiled regex for common tags (`script`, `style`, `noscript`) to avoid regex creation overhead
|
|
@@ -290,7 +300,7 @@ class HTMLParser {
|
|
|
290
300
|
} else {
|
|
291
301
|
// No closing tag found; to avoid infinite loop, break similarly to previous behavior
|
|
292
302
|
if (handler.continueOnParseError && handler.chars && html) {
|
|
293
|
-
await handler.chars(html[0], prevTag, '');
|
|
303
|
+
await handler.chars(html[0], prevTag, '', prevAttrs, []);
|
|
294
304
|
advance(1);
|
|
295
305
|
} else {
|
|
296
306
|
break;
|
|
@@ -302,10 +312,11 @@ class HTMLParser {
|
|
|
302
312
|
if (handler.continueOnParseError) {
|
|
303
313
|
// Skip the problematic character and continue
|
|
304
314
|
if (handler.chars) {
|
|
305
|
-
await handler.chars(fullHtml[pos], prevTag, '');
|
|
315
|
+
await handler.chars(fullHtml[pos], prevTag, '', prevAttrs, []);
|
|
306
316
|
}
|
|
307
317
|
advance(1);
|
|
308
318
|
prevTag = '';
|
|
319
|
+
prevAttrs = [];
|
|
309
320
|
continue;
|
|
310
321
|
}
|
|
311
322
|
const loc = getLineColumn(pos);
|
|
@@ -324,6 +335,23 @@ class HTMLParser {
|
|
|
324
335
|
await parseEndTag();
|
|
325
336
|
}
|
|
326
337
|
|
|
338
|
+
// Helper to extract minimal attribute info (name/value pairs) from raw attribute matches
|
|
339
|
+
// Used for whitespace collapsing logic—doesn’t need full processing
|
|
340
|
+
function extractAttrInfo(rawAttrs) {
|
|
341
|
+
if (!rawAttrs || !rawAttrs.length) return [];
|
|
342
|
+
|
|
343
|
+
const numCustomParts = handler.customAttrSurround ? handler.customAttrSurround.length * NCP : 0;
|
|
344
|
+
const baseIndex = 1 + numCustomParts;
|
|
345
|
+
|
|
346
|
+
return rawAttrs.map(args => {
|
|
347
|
+
// Extract attribute name (always at `baseIndex`)
|
|
348
|
+
const name = args[baseIndex];
|
|
349
|
+
// Extract value from double-quoted (`baseIndex + 2`), single-quoted (`baseIndex + 3`), or unquoted (`baseIndex + 4`)
|
|
350
|
+
const value = args[baseIndex + 2] ?? args[baseIndex + 3] ?? args[baseIndex + 4];
|
|
351
|
+
return { name: name?.toLowerCase(), value };
|
|
352
|
+
}).filter(attr => attr.name); // Filter out invalid entries
|
|
353
|
+
}
|
|
354
|
+
|
|
327
355
|
function parseStartTag(input) {
|
|
328
356
|
const start = input.match(startTagOpen);
|
|
329
357
|
if (start) {
|
|
@@ -336,7 +364,7 @@ class HTMLParser {
|
|
|
336
364
|
input = input.slice(consumed);
|
|
337
365
|
let end, attr;
|
|
338
366
|
|
|
339
|
-
// Safety limit:
|
|
367
|
+
// Safety limit: Max length of input to check for attributes
|
|
340
368
|
// Protects against catastrophic backtracking on massive attribute values
|
|
341
369
|
const MAX_ATTR_PARSE_LENGTH = 20000; // 20 KB should be enough for any reasonable tag
|
|
342
370
|
|
|
@@ -436,7 +464,7 @@ class HTMLParser {
|
|
|
436
464
|
}
|
|
437
465
|
|
|
438
466
|
async function parseEndTagAt(pos) {
|
|
439
|
-
// Close all open elements up to pos (mirrors parseEndTag
|
|
467
|
+
// Close all open elements up to `pos` (mirrors `parseEndTag`’s core branch)
|
|
440
468
|
for (let i = stack.length - 1; i >= pos; i--) {
|
|
441
469
|
if (handler.end) {
|
|
442
470
|
await handler.end(stack[i].tag, stack[i].attrs, true);
|
|
@@ -504,7 +532,7 @@ class HTMLParser {
|
|
|
504
532
|
const attrs = match.attrs.map(function (args) {
|
|
505
533
|
let name, value, customOpen, customClose, customAssign, quote;
|
|
506
534
|
|
|
507
|
-
// Hackish workaround for
|
|
535
|
+
// Hackish workaround for Firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=369778
|
|
508
536
|
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
|
|
509
537
|
if (args[3] === '') { delete args[3]; }
|
|
510
538
|
if (args[4] === '') { delete args[4]; }
|
|
@@ -561,6 +589,9 @@ class HTMLParser {
|
|
|
561
589
|
unarySlash = '';
|
|
562
590
|
}
|
|
563
591
|
|
|
592
|
+
// Store attributes for `prevAttrs` tracking (used in whitespace collapsing)
|
|
593
|
+
prevAttrs = attrs;
|
|
594
|
+
|
|
564
595
|
if (handler.start) {
|
|
565
596
|
await handler.start(tagName, attrs, unary, unarySlash);
|
|
566
597
|
}
|
|
@@ -656,7 +687,7 @@ class Sorter {
|
|
|
656
687
|
|
|
657
688
|
class TokenChain {
|
|
658
689
|
constructor() {
|
|
659
|
-
// Use
|
|
690
|
+
// Use map instead of object properties for better performance
|
|
660
691
|
this.map = new Map();
|
|
661
692
|
}
|
|
662
693
|
|
|
@@ -673,7 +704,7 @@ class TokenChain {
|
|
|
673
704
|
const sorter = new Sorter();
|
|
674
705
|
sorter.sorterMap = new Map();
|
|
675
706
|
|
|
676
|
-
// Convert
|
|
707
|
+
// Convert map entries to array and sort by frequency (descending), then alphabetically
|
|
677
708
|
const entries = Array.from(this.map.entries()).sort((a, b) => {
|
|
678
709
|
const m = a[1].arrays.length;
|
|
679
710
|
const n = b[1].arrays.length;
|
|
@@ -722,11 +753,11 @@ class TokenChain {
|
|
|
722
753
|
}
|
|
723
754
|
|
|
724
755
|
/**
|
|
725
|
-
* Preset configurations
|
|
756
|
+
* Preset configurations
|
|
726
757
|
*
|
|
727
758
|
* Presets provide curated option sets for common use cases:
|
|
728
|
-
* - conservative
|
|
729
|
-
* - comprehensive
|
|
759
|
+
* - `conservative`: Safe minification suitable for most projects
|
|
760
|
+
* - `comprehensive`: Aggressive minification for maximum file size reduction
|
|
730
761
|
*/
|
|
731
762
|
|
|
732
763
|
const presets = {
|
|
@@ -772,7 +803,7 @@ const presets = {
|
|
|
772
803
|
|
|
773
804
|
/**
|
|
774
805
|
* Get preset configuration by name
|
|
775
|
-
* @param {string} name - Preset name (
|
|
806
|
+
* @param {string} name - Preset name (“conservative” or “comprehensive”)
|
|
776
807
|
* @returns {object|null} Preset options object or null if not found
|
|
777
808
|
*/
|
|
778
809
|
function getPreset(name) {
|
|
@@ -871,7 +902,7 @@ async function replaceAsync(str, regex, asyncFn) {
|
|
|
871
902
|
return str.replace(regex, () => data.shift());
|
|
872
903
|
}
|
|
873
904
|
|
|
874
|
-
//
|
|
905
|
+
// Regex patterns (to avoid repeated allocations in hot paths)
|
|
875
906
|
|
|
876
907
|
const RE_WS_START = /^[ \n\r\t\f]+/;
|
|
877
908
|
const RE_WS_END = /[ \n\r\t\f]+$/;
|
|
@@ -892,7 +923,7 @@ const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
|
|
|
892
923
|
const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
|
|
893
924
|
const RE_NUMERIC_VALUE = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
894
925
|
|
|
895
|
-
// Inline element
|
|
926
|
+
// Inline element sets for whitespace handling
|
|
896
927
|
|
|
897
928
|
// Non-empty elements that will maintain whitespace around them
|
|
898
929
|
const inlineElementsToKeepWhitespaceAround = new Set(['a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'mark', 'math', 'meter', 'nobr', 'object', 'output', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr']);
|
|
@@ -903,6 +934,9 @@ const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b
|
|
|
903
934
|
// Elements that will always maintain whitespace around them
|
|
904
935
|
const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
|
|
905
936
|
|
|
937
|
+
// Form control elements (for conditional whitespace collapsing)
|
|
938
|
+
const formControlElements = new Set(['input', 'button', 'select', 'textarea', 'output', 'meter', 'progress']);
|
|
939
|
+
|
|
906
940
|
// Default attribute values
|
|
907
941
|
|
|
908
942
|
// Default attribute values (could apply to any element)
|
|
@@ -942,14 +976,17 @@ const tagDefaults = {
|
|
|
942
976
|
// Script MIME types
|
|
943
977
|
|
|
944
978
|
// https://mathiasbynens.be/demo/javascript-mime-type
|
|
945
|
-
// https://developer.mozilla.org/en/docs/Web/HTML/
|
|
979
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script
|
|
946
980
|
const executableScriptsMimetypes = new Set([
|
|
947
981
|
'text/javascript',
|
|
982
|
+
'text/x-javascript',
|
|
948
983
|
'text/ecmascript',
|
|
984
|
+
'text/x-ecmascript',
|
|
949
985
|
'text/jscript',
|
|
950
986
|
'application/javascript',
|
|
951
987
|
'application/x-javascript',
|
|
952
988
|
'application/ecmascript',
|
|
989
|
+
'application/x-ecmascript',
|
|
953
990
|
'module'
|
|
954
991
|
]);
|
|
955
992
|
|
|
@@ -957,15 +994,15 @@ const keepScriptsMimetypes = new Set([
|
|
|
957
994
|
'module'
|
|
958
995
|
]);
|
|
959
996
|
|
|
960
|
-
// Boolean attribute
|
|
997
|
+
// Boolean attribute sets
|
|
961
998
|
|
|
962
999
|
const isSimpleBoolean = new Set(['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']);
|
|
963
1000
|
|
|
964
1001
|
const isBooleanValue = new Set(['true', 'false']);
|
|
965
1002
|
|
|
966
|
-
// `srcset`
|
|
1003
|
+
// `srcset` elements
|
|
967
1004
|
|
|
968
|
-
const
|
|
1005
|
+
const srcsetElements = new Set(['img', 'source']);
|
|
969
1006
|
|
|
970
1007
|
// JSON script types
|
|
971
1008
|
|
|
@@ -981,7 +1018,7 @@ const jsonScriptTypes = new Set([
|
|
|
981
1018
|
'speculationrules',
|
|
982
1019
|
]);
|
|
983
1020
|
|
|
984
|
-
// Tag omission rules and element
|
|
1021
|
+
// Tag omission rules and element sets
|
|
985
1022
|
|
|
986
1023
|
// Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags with the following extensions:
|
|
987
1024
|
// - retain `<body>` if followed by `<noscript>`
|
|
@@ -992,35 +1029,35 @@ const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody'])
|
|
|
992
1029
|
|
|
993
1030
|
const optionalEndTags = new Set(['html', 'head', 'body', 'li', 'dt', 'dd', 'p', 'rb', 'rt', 'rtc', 'rp', 'optgroup', 'option', 'colgroup', 'caption', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th']);
|
|
994
1031
|
|
|
995
|
-
const
|
|
1032
|
+
const headerElements = new Set(['meta', 'link', 'script', 'style', 'template', 'noscript']);
|
|
996
1033
|
|
|
997
|
-
const
|
|
1034
|
+
const descriptionElements = new Set(['dt', 'dd']);
|
|
998
1035
|
|
|
999
|
-
const
|
|
1036
|
+
const pBlockElements = new Set(['address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'search', 'section', 'table', 'ul']);
|
|
1000
1037
|
|
|
1001
|
-
const
|
|
1038
|
+
const pInlineElements = new Set(['a', 'audio', 'del', 'ins', 'map', 'noscript', 'video']);
|
|
1002
1039
|
|
|
1003
1040
|
const rubyEndTagOmission = new Set(['rb', 'rt', 'rtc', 'rp']); // `</rb>`, `</rt>`, `</rp>` can be omitted if followed by `<rb>`, `<rt>`, `<rtc>`, or `<rp>`
|
|
1004
1041
|
|
|
1005
1042
|
const rubyRtcEndTagOmission = new Set(['rb', 'rtc']); // `</rtc>` can be omitted if followed by `<rb>` or `<rtc>` (not `<rt>` or `<rp>`)
|
|
1006
1043
|
|
|
1007
|
-
const
|
|
1044
|
+
const optionElements = new Set(['option', 'optgroup']);
|
|
1008
1045
|
|
|
1009
|
-
const
|
|
1046
|
+
const tableContentElements = new Set(['tbody', 'tfoot']);
|
|
1010
1047
|
|
|
1011
|
-
const
|
|
1048
|
+
const tableSectionElements = new Set(['thead', 'tbody', 'tfoot']);
|
|
1012
1049
|
|
|
1013
|
-
const
|
|
1050
|
+
const cellElements = new Set(['td', 'th']);
|
|
1014
1051
|
|
|
1015
|
-
const
|
|
1052
|
+
const topLevelElements = new Set(['html', 'head', 'body']);
|
|
1016
1053
|
|
|
1017
|
-
const
|
|
1054
|
+
const compactElements = new Set(['html', 'body']);
|
|
1018
1055
|
|
|
1019
|
-
const
|
|
1056
|
+
const looseElements = new Set(['head', 'colgroup', 'caption']);
|
|
1020
1057
|
|
|
1021
|
-
const
|
|
1058
|
+
const trailingElements = new Set(['dt', 'thead']);
|
|
1022
1059
|
|
|
1023
|
-
const
|
|
1060
|
+
const htmlElements = new Set(['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', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'search', 'section', 'select', 'selectedcontent', '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']);
|
|
1024
1061
|
|
|
1025
1062
|
// Empty attribute regex
|
|
1026
1063
|
|
|
@@ -1030,7 +1067,7 @@ const reEmptyAttribute = new RegExp(
|
|
|
1030
1067
|
|
|
1031
1068
|
// Special content elements
|
|
1032
1069
|
|
|
1033
|
-
const
|
|
1070
|
+
const specialContentElements = new Set(['script', 'style']);
|
|
1034
1071
|
|
|
1035
1072
|
// Imports
|
|
1036
1073
|
|
|
@@ -1094,7 +1131,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
1094
1131
|
}
|
|
1095
1132
|
|
|
1096
1133
|
if (trimLeft) {
|
|
1097
|
-
//
|
|
1134
|
+
// No-break space is specifically handled inside the replacer function
|
|
1098
1135
|
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
1099
1136
|
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
1100
1137
|
if (conservative && spaces === '\t') {
|
|
@@ -1105,7 +1142,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
1105
1142
|
}
|
|
1106
1143
|
|
|
1107
1144
|
if (trimRight) {
|
|
1108
|
-
//
|
|
1145
|
+
// No-break space is specifically handled inside the replacer function
|
|
1109
1146
|
str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
|
|
1110
1147
|
const conservative = !lineBreakAfter && options.conservativeCollapse;
|
|
1111
1148
|
if (conservative && spaces === '\t') {
|
|
@@ -1129,11 +1166,42 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
1129
1166
|
|
|
1130
1167
|
// Collapse whitespace smartly based on surrounding tags
|
|
1131
1168
|
|
|
1132
|
-
function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
|
|
1169
|
+
function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
|
|
1170
|
+
const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
|
|
1171
|
+
const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
|
|
1172
|
+
|
|
1173
|
+
// Helper: Check if an input element has `type="hidden"`
|
|
1174
|
+
const isHiddenInput = (tagName, attrs) => {
|
|
1175
|
+
if (tagName !== 'input' || !attrs || !attrs.length) return false;
|
|
1176
|
+
const typeAttr = attrs.find(attr => attr.name === 'type');
|
|
1177
|
+
return typeAttr && typeAttr.value === 'hidden';
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
// Check if prev/next are non-rendering (hidden) elements
|
|
1181
|
+
const prevIsHidden = isHiddenInput(prevTagName, prevAttrs);
|
|
1182
|
+
const nextIsHidden = isHiddenInput(nextTagName, nextAttrs);
|
|
1183
|
+
|
|
1133
1184
|
let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
1185
|
+
|
|
1186
|
+
// Smart default behavior: Collapse space after non-rendering elements (`type="hidden"`)
|
|
1187
|
+
// This happens even in basic `collapseWhitespace` mode (safe optimization)
|
|
1188
|
+
if (!trimLeft && prevIsHidden && str && !/\S/.test(str)) {
|
|
1189
|
+
trimLeft = true;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Aggressive mode: Collapse between all form controls (pure whitespace only)
|
|
1193
|
+
const isPureWhitespace = str && !/\S/.test(str);
|
|
1194
|
+
if (!trimLeft && prevTagName && nextTagName &&
|
|
1195
|
+
options.collapseInlineTagWhitespace &&
|
|
1196
|
+
isPureWhitespace &&
|
|
1197
|
+
formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
|
|
1198
|
+
trimLeft = true;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1134
1201
|
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
1135
1202
|
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
1136
1203
|
}
|
|
1204
|
+
|
|
1137
1205
|
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
1138
1206
|
if (trimLeft && options.collapseInlineTagWhitespace) {
|
|
1139
1207
|
const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
|
|
@@ -1141,10 +1209,26 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
1141
1209
|
trimLeft = false;
|
|
1142
1210
|
}
|
|
1143
1211
|
}
|
|
1212
|
+
|
|
1144
1213
|
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
1214
|
+
|
|
1215
|
+
// Smart default behavior: Collapse space before non-rendering elements (`type="hidden"`)
|
|
1216
|
+
if (!trimRight && nextIsHidden && str && !/\S/.test(str)) {
|
|
1217
|
+
trimRight = true;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Aggressive mode: Same as `trimLeft`
|
|
1221
|
+
if (!trimRight && prevTagName && nextTagName &&
|
|
1222
|
+
options.collapseInlineTagWhitespace &&
|
|
1223
|
+
isPureWhitespace &&
|
|
1224
|
+
formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
|
|
1225
|
+
trimRight = true;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1145
1228
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
1146
1229
|
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
1147
1230
|
}
|
|
1231
|
+
|
|
1148
1232
|
// When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
|
|
1149
1233
|
if (trimRight && options.collapseInlineTagWhitespace) {
|
|
1150
1234
|
const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
|
|
@@ -1152,6 +1236,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
|
|
|
1152
1236
|
trimRight = false;
|
|
1153
1237
|
}
|
|
1154
1238
|
}
|
|
1239
|
+
|
|
1155
1240
|
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
1156
1241
|
}
|
|
1157
1242
|
|
|
@@ -1172,7 +1257,6 @@ function canTrimWhitespace(tag) {
|
|
|
1172
1257
|
|
|
1173
1258
|
// Wrap CSS declarations for inline styles and media queries
|
|
1174
1259
|
// This ensures proper context for CSS minification
|
|
1175
|
-
|
|
1176
1260
|
function wrapCSS(text, type) {
|
|
1177
1261
|
switch (type) {
|
|
1178
1262
|
case 'inline':
|
|
@@ -1254,7 +1338,6 @@ async function processScript(text, options, currentAttrs, minifyHTML) {
|
|
|
1254
1338
|
|
|
1255
1339
|
/**
|
|
1256
1340
|
* Lightweight SVG optimizations:
|
|
1257
|
-
*
|
|
1258
1341
|
* - Numeric precision reduction for coordinates and path data
|
|
1259
1342
|
* - Whitespace removal in attribute values (numeric sequences)
|
|
1260
1343
|
* - Default attribute removal (safe, well-documented defaults)
|
|
@@ -1348,7 +1431,7 @@ function minifyNumber(num, precision = 3) {
|
|
|
1348
1431
|
if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
|
|
1349
1432
|
|
|
1350
1433
|
// Check cache
|
|
1351
|
-
// (Note:
|
|
1434
|
+
// (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
|
|
1352
1435
|
// This is intentional to avoid parsing overhead.
|
|
1353
1436
|
// Real-world SVG files from export tools typically use consistent formats.)
|
|
1354
1437
|
const cacheKey = `${num}:${precision}`;
|
|
@@ -1387,15 +1470,15 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
1387
1470
|
|
|
1388
1471
|
// Remove unnecessary spaces around path commands
|
|
1389
1472
|
// Safe to remove space after a command letter when it’s followed by a number (which may be negative)
|
|
1390
|
-
// M 10 20 → M10 20
|
|
1473
|
+
// `M 10 20` → `M10 20`, `L -5 -3` → `L-5-3`
|
|
1391
1474
|
result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
|
|
1392
1475
|
|
|
1393
1476
|
// Safe to remove space before command letter when preceded by a number
|
|
1394
|
-
// 0 L → 0L
|
|
1477
|
+
// `0 L` → `0L`, `20 M` → `20M`
|
|
1395
1478
|
result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
|
|
1396
1479
|
|
|
1397
1480
|
// Safe to remove space before negative number when preceded by a number
|
|
1398
|
-
// 10 -20 → 10-20 (numbers are separated by the minus sign)
|
|
1481
|
+
// `10 -20` → `10-20` (numbers are separated by the minus sign)
|
|
1399
1482
|
result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
|
|
1400
1483
|
|
|
1401
1484
|
return result;
|
|
@@ -1404,9 +1487,9 @@ function minifyPathData(pathData, precision = 3) {
|
|
|
1404
1487
|
/**
|
|
1405
1488
|
* Minify whitespace in numeric attribute values
|
|
1406
1489
|
* Examples:
|
|
1407
|
-
*
|
|
1408
|
-
*
|
|
1409
|
-
*
|
|
1490
|
+
* - “10 , 20" → "10,20"
|
|
1491
|
+
* - "translate( 10 20 )" → "translate(10 20)"
|
|
1492
|
+
* - "100, 10 40, 198" → "100,10 40,198"
|
|
1410
1493
|
*
|
|
1411
1494
|
* @param {string} value - Attribute value to minify
|
|
1412
1495
|
* @returns {string} Minified value
|
|
@@ -1439,8 +1522,7 @@ function minifyColor(color) {
|
|
|
1439
1522
|
|
|
1440
1523
|
// Don’t process values that aren’t simple colors (preserve case-sensitive references)
|
|
1441
1524
|
// `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
|
|
1442
|
-
if (trimmed.includes('url(') || trimmed.includes('var(') ||
|
|
1443
|
-
trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
1525
|
+
if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
|
|
1444
1526
|
return trimmed;
|
|
1445
1527
|
}
|
|
1446
1528
|
|
|
@@ -1448,7 +1530,7 @@ function minifyColor(color) {
|
|
|
1448
1530
|
const lower = trimmed.toLowerCase();
|
|
1449
1531
|
|
|
1450
1532
|
// Shorten 6-digit hex to 3-digit when possible
|
|
1451
|
-
//
|
|
1533
|
+
// `#aabbcc` → `#abc`, `#000000` → `#000`
|
|
1452
1534
|
const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
|
|
1453
1535
|
if (hexMatch) {
|
|
1454
1536
|
const hex = hexMatch[1];
|
|
@@ -1468,7 +1550,7 @@ function minifyColor(color) {
|
|
|
1468
1550
|
return NAMED_COLORS[lower] || lower;
|
|
1469
1551
|
}
|
|
1470
1552
|
|
|
1471
|
-
// Convert rgb(
|
|
1553
|
+
// Convert rgb() to hex
|
|
1472
1554
|
const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
1473
1555
|
if (rgbMatch) {
|
|
1474
1556
|
const r = parseInt(rgbMatch[1], 10);
|
|
@@ -1499,14 +1581,14 @@ function minifyColor(color) {
|
|
|
1499
1581
|
const NUMERIC_ATTRS = new Set([
|
|
1500
1582
|
'd', // Path data
|
|
1501
1583
|
'points', // Polygon/polyline points
|
|
1502
|
-
'viewBox', // viewBox coordinates
|
|
1584
|
+
'viewBox', // `viewBox` coordinates
|
|
1503
1585
|
'transform', // Transform functions
|
|
1504
1586
|
'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
|
|
1505
1587
|
'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
|
|
1506
1588
|
'width', 'height', // Dimensions
|
|
1507
1589
|
'dx', 'dy', // Text offsets
|
|
1508
1590
|
'offset', // Gradient offset
|
|
1509
|
-
'startOffset', // textPath
|
|
1591
|
+
'startOffset', // `textPath`
|
|
1510
1592
|
'pathLength', // Path length
|
|
1511
1593
|
'stdDeviation', // Filter params
|
|
1512
1594
|
'baseFrequency', // Turbulence
|
|
@@ -1690,8 +1772,8 @@ function shouldMinifyInnerHTML(options) {
|
|
|
1690
1772
|
/**
|
|
1691
1773
|
* @param {Partial<MinifierOptions>} inputOptions - User-provided options
|
|
1692
1774
|
* @param {Object} deps - Dependencies from htmlminifier.js
|
|
1693
|
-
* @param {Function} deps.getLightningCSS - Function to lazily load
|
|
1694
|
-
* @param {Function} deps.getTerser - Function to lazily load
|
|
1775
|
+
* @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
|
|
1776
|
+
* @param {Function} deps.getTerser - Function to lazily load Terser
|
|
1695
1777
|
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
1696
1778
|
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
1697
1779
|
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
@@ -1728,7 +1810,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1728
1810
|
if (typeof value === 'string') {
|
|
1729
1811
|
return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
|
|
1730
1812
|
}
|
|
1731
|
-
return value; // Already a RegExp or
|
|
1813
|
+
return value; // Already a RegExp or another type
|
|
1732
1814
|
};
|
|
1733
1815
|
|
|
1734
1816
|
const parseRegExpArray = (arr) => {
|
|
@@ -1865,7 +1947,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1865
1947
|
// Validate engine
|
|
1866
1948
|
const supportedEngines = ['terser', 'swc'];
|
|
1867
1949
|
if (!supportedEngines.includes(engine)) {
|
|
1868
|
-
throw new Error(`Unsupported JS minifier engine:
|
|
1950
|
+
throw new Error(`Unsupported JS minifier engine: “${engine}”. Supported engines: ${supportedEngines.join(', ')}`);
|
|
1869
1951
|
}
|
|
1870
1952
|
|
|
1871
1953
|
// Extract engine-specific options (excluding `engine` field itself)
|
|
@@ -1972,14 +2054,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
1972
2054
|
relateUrlOptions = {};
|
|
1973
2055
|
}
|
|
1974
2056
|
|
|
1975
|
-
// Cache
|
|
2057
|
+
// Cache relateurl instance for reuse (expensive to create)
|
|
1976
2058
|
const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
|
|
1977
2059
|
|
|
1978
2060
|
// Create instance-specific cache (results depend on site configuration)
|
|
1979
2061
|
const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
|
|
1980
2062
|
|
|
1981
2063
|
options.minifyURLs = function (text) {
|
|
1982
|
-
// Fast-path: Skip if text doesn
|
|
2064
|
+
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
1983
2065
|
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
1984
2066
|
if (!/[/:?#\s]/.test(text)) {
|
|
1985
2067
|
return text;
|
|
@@ -2011,17 +2093,17 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
|
|
|
2011
2093
|
};
|
|
2012
2094
|
} else if (key === 'minifySVG') {
|
|
2013
2095
|
// Process SVG minification options
|
|
2014
|
-
// Unlike minifyCSS
|
|
2096
|
+
// Unlike `minifyCSS`/`minifyJS`, this is a simple options object, not a function
|
|
2015
2097
|
// The actual minification is applied inline during attribute processing
|
|
2016
2098
|
options.minifySVG = getSVGMinifierOptions(option);
|
|
2017
2099
|
} else if (key === 'customAttrCollapse') {
|
|
2018
|
-
// Single
|
|
2100
|
+
// Single regex pattern
|
|
2019
2101
|
options[key] = parseRegExp(option);
|
|
2020
2102
|
} else if (key === 'customAttrSurround') {
|
|
2021
2103
|
// Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
|
|
2022
2104
|
options[key] = parseNestedRegExpArray(option);
|
|
2023
2105
|
} else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
|
|
2024
|
-
// Array of
|
|
2106
|
+
// Array of regex patterns
|
|
2025
2107
|
options[key] = parseRegExpArray(option);
|
|
2026
2108
|
} else {
|
|
2027
2109
|
options[key] = option;
|
|
@@ -2086,8 +2168,7 @@ function isAttributeRedundant(tag, attrName, attrValue, attrs) {
|
|
|
2086
2168
|
const tagHasDefaults = tag in tagDefaults;
|
|
2087
2169
|
|
|
2088
2170
|
// Check for legacy attribute rules (element- and attribute-specific)
|
|
2089
|
-
const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) ||
|
|
2090
|
-
(tag === 'a' && attrName === 'name');
|
|
2171
|
+
const isLegacyAttr = (tag === 'script' && (attrName === 'language' || attrName === 'charset')) || (tag === 'a' && attrName === 'name');
|
|
2091
2172
|
|
|
2092
2173
|
// If none of these conditions apply, attribute cannot be redundant
|
|
2093
2174
|
if (!hasGeneralDefault && !tagHasDefaults && !isLegacyAttr) {
|
|
@@ -2145,7 +2226,7 @@ function isStyleLinkTypeAttribute(attrValue = '') {
|
|
|
2145
2226
|
return attrValue === '' || attrValue === 'text/css';
|
|
2146
2227
|
}
|
|
2147
2228
|
|
|
2148
|
-
function
|
|
2229
|
+
function isStyleElement(tag, attrs) {
|
|
2149
2230
|
if (tag !== 'style') {
|
|
2150
2231
|
return false;
|
|
2151
2232
|
}
|
|
@@ -2202,11 +2283,11 @@ function isLinkType(tag, attrs, value) {
|
|
|
2202
2283
|
}
|
|
2203
2284
|
|
|
2204
2285
|
function isMediaQuery(tag, attrs, attrName) {
|
|
2205
|
-
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') ||
|
|
2286
|
+
return attrName === 'media' && (isLinkType(tag, attrs, 'stylesheet') || isStyleElement(tag, attrs));
|
|
2206
2287
|
}
|
|
2207
2288
|
|
|
2208
2289
|
function isSrcset(attrName, tag) {
|
|
2209
|
-
return attrName === 'srcset' &&
|
|
2290
|
+
return attrName === 'srcset' && srcsetElements.has(tag);
|
|
2210
2291
|
}
|
|
2211
2292
|
|
|
2212
2293
|
function isMetaViewport(tag, attrs) {
|
|
@@ -2214,7 +2295,7 @@ function isMetaViewport(tag, attrs) {
|
|
|
2214
2295
|
return false;
|
|
2215
2296
|
}
|
|
2216
2297
|
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
2217
|
-
if (attrs[i].name === 'name' && attrs[i].value === 'viewport') {
|
|
2298
|
+
if (attrs[i].name.toLowerCase() === 'name' && attrs[i].value.toLowerCase() === 'viewport') {
|
|
2218
2299
|
return true;
|
|
2219
2300
|
}
|
|
2220
2301
|
}
|
|
@@ -2234,7 +2315,7 @@ function isContentSecurityPolicy(tag, attrs) {
|
|
|
2234
2315
|
}
|
|
2235
2316
|
|
|
2236
2317
|
function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
2237
|
-
const isValueEmpty = !attrValue ||
|
|
2318
|
+
const isValueEmpty = !attrValue || attrValue.trim() === '';
|
|
2238
2319
|
if (!isValueEmpty) {
|
|
2239
2320
|
return false;
|
|
2240
2321
|
}
|
|
@@ -2257,7 +2338,7 @@ function hasAttrName(name, attrs) {
|
|
|
2257
2338
|
|
|
2258
2339
|
async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTMLSelf) {
|
|
2259
2340
|
// Apply early whitespace normalization if enabled
|
|
2260
|
-
// Preserves special spaces (
|
|
2341
|
+
// Preserves special spaces (no-break space, hair space, etc.) for consistency with `collapseWhitespace`
|
|
2261
2342
|
if (options.collapseAttributeWhitespace) {
|
|
2262
2343
|
// Fast path: Only process if whitespace exists (avoids regex overhead on clean values)
|
|
2263
2344
|
if (RE_ATTR_WS_CHECK.test(attrValue)) {
|
|
@@ -2313,7 +2394,7 @@ async function cleanAttributeValue(tag, attrName, attrValue, options, attrs, min
|
|
|
2313
2394
|
try {
|
|
2314
2395
|
attrValue = await options.minifyCSS(attrValue, 'inline');
|
|
2315
2396
|
// After minification, check if CSS consists entirely of invalid properties (no values)
|
|
2316
|
-
//
|
|
2397
|
+
// I.e., `color:` or `margin:;padding:` should be treated as empty
|
|
2317
2398
|
if (attrValue && /^(?:[a-z-]+:\s*;?\s*)+$/i.test(attrValue)) {
|
|
2318
2399
|
attrValue = '';
|
|
2319
2400
|
}
|
|
@@ -2433,13 +2514,13 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
2433
2514
|
}
|
|
2434
2515
|
|
|
2435
2516
|
if ((options.removeRedundantAttributes &&
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2517
|
+
isAttributeRedundant(tag, attrName, attrValue, attrs)) ||
|
|
2518
|
+
(options.removeScriptTypeAttributes && tag === 'script' &&
|
|
2519
|
+
attrName === 'type' && isScriptTypeAttribute(attrValue) && !keepScriptTypeAttribute(attrValue)) ||
|
|
2520
|
+
(options.removeStyleLinkTypeAttributes && (tag === 'style' || tag === 'link') &&
|
|
2521
|
+
attrName === 'type' && isStyleLinkTypeAttribute(attrValue)) ||
|
|
2522
|
+
(options.insideSVG && options.minifySVG &&
|
|
2523
|
+
shouldRemoveSVGAttribute(tag, attrName, attrValue, options.minifySVG))) {
|
|
2443
2524
|
return;
|
|
2444
2525
|
}
|
|
2445
2526
|
|
|
@@ -2448,7 +2529,7 @@ async function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
|
2448
2529
|
}
|
|
2449
2530
|
|
|
2450
2531
|
if (options.removeEmptyAttributes &&
|
|
2451
|
-
|
|
2532
|
+
canDeleteEmptyAttribute(tag, attrName, attrValue, options)) {
|
|
2452
2533
|
return;
|
|
2453
2534
|
}
|
|
2454
2535
|
|
|
@@ -2471,19 +2552,35 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
2471
2552
|
let attrFragment;
|
|
2472
2553
|
let emittedAttrValue;
|
|
2473
2554
|
|
|
2474
|
-
|
|
2475
|
-
|
|
2555
|
+
// Determine if we need to add/keep quotes
|
|
2556
|
+
const shouldAddQuotes = typeof attrValue !== 'undefined' && (
|
|
2557
|
+
// If `removeAttributeQuotes` is enabled, add quotes only if they can’t be removed
|
|
2558
|
+
(options.removeAttributeQuotes && (attrValue.indexOf(uidAttr) !== -1 || !canRemoveAttributeQuotes(attrValue))) ||
|
|
2559
|
+
// If `removeAttributeQuotes` is not enabled, preserve original quote style or add quotes if value requires them
|
|
2560
|
+
(!options.removeAttributeQuotes && (attrQuote !== '' || !canRemoveAttributeQuotes(attrValue) ||
|
|
2561
|
+
// Special case: With `removeTagWhitespace`, unquoted values that aren’t last will have space added,
|
|
2562
|
+
// which can create ambiguous/invalid HTML—add quotes to be safe
|
|
2563
|
+
(options.removeTagWhitespace && attrQuote === '' && !isLast)))
|
|
2564
|
+
);
|
|
2565
|
+
|
|
2566
|
+
if (shouldAddQuotes) {
|
|
2476
2567
|
// Determine the appropriate quote character
|
|
2477
2568
|
if (!options.preventAttributesEscaping) {
|
|
2478
|
-
// Normal mode:
|
|
2479
|
-
|
|
2569
|
+
// Normal mode: Choose optimal quote type to minimize escaping
|
|
2570
|
+
// unless we’re preserving original quotes and they don’t need escaping
|
|
2571
|
+
const needsEscaping = (attrQuote === '"' && attrValue.indexOf('"') !== -1) || (attrQuote === "'" && attrValue.indexOf("'") !== -1);
|
|
2572
|
+
|
|
2573
|
+
if (options.removeAttributeQuotes || typeof options.quoteCharacter !== 'undefined' || needsEscaping || attrQuote === '') {
|
|
2574
|
+
attrQuote = chooseAttributeQuote(attrValue, options);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2480
2577
|
if (attrQuote === '"') {
|
|
2481
2578
|
attrValue = attrValue.replace(/"/g, '"');
|
|
2482
2579
|
} else {
|
|
2483
2580
|
attrValue = attrValue.replace(/'/g, ''');
|
|
2484
2581
|
}
|
|
2485
2582
|
} else {
|
|
2486
|
-
// `preventAttributesEscaping` mode:
|
|
2583
|
+
// `preventAttributesEscaping` mode: Choose safe quotes but don't escape
|
|
2487
2584
|
// except when both quote types are present—then escape to prevent invalid HTML
|
|
2488
2585
|
const hasDoubleQuote = attrValue.indexOf('"') !== -1;
|
|
2489
2586
|
const hasSingleQuote = attrValue.indexOf("'") !== -1;
|
|
@@ -2502,8 +2599,18 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
2502
2599
|
attrQuote = "'";
|
|
2503
2600
|
} else if (attrQuote === "'" && hasSingleQuote && !hasDoubleQuote) {
|
|
2504
2601
|
attrQuote = '"';
|
|
2505
|
-
//
|
|
2506
|
-
} else if (attrQuote
|
|
2602
|
+
// If no quote character yet (empty string), choose based on content
|
|
2603
|
+
} else if (attrQuote === '') {
|
|
2604
|
+
if (hasSingleQuote && !hasDoubleQuote) {
|
|
2605
|
+
attrQuote = '"';
|
|
2606
|
+
} else if (hasDoubleQuote && !hasSingleQuote) {
|
|
2607
|
+
attrQuote = "'";
|
|
2608
|
+
} else {
|
|
2609
|
+
attrQuote = '"';
|
|
2610
|
+
}
|
|
2611
|
+
// Fallback for invalid/unsupported attrQuote values (not `"`, `'`, or empty string):
|
|
2612
|
+
// Choose safe default based on value content
|
|
2613
|
+
} else if (attrQuote !== '"' && attrQuote !== "'") {
|
|
2507
2614
|
if (hasSingleQuote && !hasDoubleQuote) {
|
|
2508
2615
|
attrQuote = '"';
|
|
2509
2616
|
} else if (hasDoubleQuote && !hasSingleQuote) {
|
|
@@ -2513,7 +2620,22 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
2513
2620
|
}
|
|
2514
2621
|
}
|
|
2515
2622
|
} else {
|
|
2516
|
-
|
|
2623
|
+
// `quoteCharacter` is explicitly set
|
|
2624
|
+
const preferredQuote = options.quoteCharacter === '\'' ? '\'' : '"';
|
|
2625
|
+
// Safety check: If the preferred quote conflicts with value content, switch to the opposite quote
|
|
2626
|
+
if ((preferredQuote === '"' && hasDoubleQuote && !hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && !hasDoubleQuote)) {
|
|
2627
|
+
attrQuote = preferredQuote === '"' ? "'" : '"';
|
|
2628
|
+
} else if ((preferredQuote === '"' && hasDoubleQuote && hasSingleQuote) || (preferredQuote === "'" && hasSingleQuote && hasDoubleQuote)) {
|
|
2629
|
+
// Both quote types present: Fall back to escaping despite `preventAttributesEscaping`
|
|
2630
|
+
attrQuote = preferredQuote;
|
|
2631
|
+
if (attrQuote === '"') {
|
|
2632
|
+
attrValue = attrValue.replace(/"/g, '"');
|
|
2633
|
+
} else {
|
|
2634
|
+
attrValue = attrValue.replace(/'/g, ''');
|
|
2635
|
+
}
|
|
2636
|
+
} else {
|
|
2637
|
+
attrQuote = preferredQuote;
|
|
2638
|
+
}
|
|
2517
2639
|
}
|
|
2518
2640
|
}
|
|
2519
2641
|
emittedAttrValue = attrQuote + attrValue + attrQuote;
|
|
@@ -2521,15 +2643,17 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
2521
2643
|
emittedAttrValue += ' ';
|
|
2522
2644
|
}
|
|
2523
2645
|
} else if (isLast && !hasUnarySlash) {
|
|
2524
|
-
// Last attribute in a non-self-closing tag:
|
|
2646
|
+
// Last attribute in a non-self-closing tag:
|
|
2647
|
+
// No space needed
|
|
2525
2648
|
emittedAttrValue = attrValue;
|
|
2526
2649
|
} else {
|
|
2527
|
-
// Not last attribute, or is a self-closing tag:
|
|
2650
|
+
// Not last attribute, or is a self-closing tag:
|
|
2651
|
+
// Unquoted values must have space after them to delimit from next attribute
|
|
2528
2652
|
emittedAttrValue = attrValue + ' ';
|
|
2529
2653
|
}
|
|
2530
2654
|
|
|
2531
2655
|
if (typeof attrValue === 'undefined' || (options.collapseBooleanAttributes &&
|
|
2532
|
-
|
|
2656
|
+
isBooleanAttribute(attrName.toLowerCase(), (attrValue || '').toLowerCase()))) {
|
|
2533
2657
|
attrFragment = attrName;
|
|
2534
2658
|
if (!isLast) {
|
|
2535
2659
|
attrFragment += ' ';
|
|
@@ -2552,7 +2676,7 @@ function canRemoveParentTag(optionalStartTag, tag) {
|
|
|
2552
2676
|
case 'head':
|
|
2553
2677
|
return true;
|
|
2554
2678
|
case 'body':
|
|
2555
|
-
return !
|
|
2679
|
+
return !headerElements.has(tag);
|
|
2556
2680
|
case 'colgroup':
|
|
2557
2681
|
return tag === 'col';
|
|
2558
2682
|
case 'tbody':
|
|
@@ -2566,7 +2690,7 @@ function isStartTagMandatory(optionalEndTag, tag) {
|
|
|
2566
2690
|
case 'colgroup':
|
|
2567
2691
|
return optionalEndTag === 'colgroup';
|
|
2568
2692
|
case 'tbody':
|
|
2569
|
-
return
|
|
2693
|
+
return tableSectionElements.has(optionalEndTag);
|
|
2570
2694
|
}
|
|
2571
2695
|
return false;
|
|
2572
2696
|
}
|
|
@@ -2585,9 +2709,9 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
2585
2709
|
return tag === optionalEndTag;
|
|
2586
2710
|
case 'dt':
|
|
2587
2711
|
case 'dd':
|
|
2588
|
-
return
|
|
2712
|
+
return descriptionElements.has(tag);
|
|
2589
2713
|
case 'p':
|
|
2590
|
-
return
|
|
2714
|
+
return pBlockElements.has(tag);
|
|
2591
2715
|
case 'rb':
|
|
2592
2716
|
case 'rt':
|
|
2593
2717
|
case 'rp':
|
|
@@ -2595,15 +2719,15 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
|
|
|
2595
2719
|
case 'rtc':
|
|
2596
2720
|
return rubyRtcEndTagOmission.has(tag);
|
|
2597
2721
|
case 'option':
|
|
2598
|
-
return
|
|
2722
|
+
return optionElements.has(tag);
|
|
2599
2723
|
case 'thead':
|
|
2600
2724
|
case 'tbody':
|
|
2601
|
-
return
|
|
2725
|
+
return tableContentElements.has(tag);
|
|
2602
2726
|
case 'tfoot':
|
|
2603
2727
|
return tag === 'tbody';
|
|
2604
2728
|
case 'td':
|
|
2605
2729
|
case 'th':
|
|
2606
|
-
return
|
|
2730
|
+
return cellElements.has(tag);
|
|
2607
2731
|
}
|
|
2608
2732
|
return false;
|
|
2609
2733
|
}
|
|
@@ -2706,7 +2830,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
|
|
|
2706
2830
|
if (typeof item === 'string') {
|
|
2707
2831
|
const spec = parseElementSpec(item, options);
|
|
2708
2832
|
if (!spec && options.log) {
|
|
2709
|
-
options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification:
|
|
2833
|
+
options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: “' + item + '”');
|
|
2710
2834
|
}
|
|
2711
2835
|
return spec;
|
|
2712
2836
|
}
|
|
@@ -3219,7 +3343,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3219
3343
|
}
|
|
3220
3344
|
|
|
3221
3345
|
// Pre-compile regex patterns for reuse (performance optimization)
|
|
3222
|
-
// These must be declared before scan() since scan uses them
|
|
3346
|
+
// These must be declared before `scan()` since scan uses them
|
|
3223
3347
|
const whitespaceSplitPatternScan = /[ \t\n\f\r]+/;
|
|
3224
3348
|
const whitespaceSplitPatternSort = /[ \n\f\r]+/;
|
|
3225
3349
|
|
|
@@ -3251,9 +3375,9 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3251
3375
|
chars: async function (text) {
|
|
3252
3376
|
// Only recursively scan HTML content, not JSON-LD or other non-HTML script types
|
|
3253
3377
|
// `scan()` is for analyzing HTML attribute order, not for parsing JSON
|
|
3254
|
-
if (options.processScripts &&
|
|
3255
|
-
|
|
3256
|
-
|
|
3378
|
+
if (options.processScripts && specialContentElements.has(currentTag) &&
|
|
3379
|
+
options.processScripts.indexOf(currentType) > -1 &&
|
|
3380
|
+
currentType === 'text/html') {
|
|
3257
3381
|
await scan(text);
|
|
3258
3382
|
}
|
|
3259
3383
|
},
|
|
@@ -3276,7 +3400,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3276
3400
|
// For the first pass, create a copy of options and disable aggressive minification.
|
|
3277
3401
|
// Keep attribute transformations (like `removeStyleLinkTypeAttributes`) for accurate analysis.
|
|
3278
3402
|
// This is safe because `createSortFns` is called before custom fragment UID markers (`uidAttr`) are added.
|
|
3279
|
-
// Note: `htmlmin:ignore` UID markers (uidIgnore) already exist and are expanded for analysis.
|
|
3403
|
+
// Note: `htmlmin:ignore` UID markers (`uidIgnore`) already exist and are expanded for analysis.
|
|
3280
3404
|
const firstPassOptions = Object.assign({}, options, {
|
|
3281
3405
|
// Disable sorting for the analysis pass
|
|
3282
3406
|
sortAttributes: false,
|
|
@@ -3295,7 +3419,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3295
3419
|
});
|
|
3296
3420
|
|
|
3297
3421
|
// Temporarily enable `continueOnParseError` for the `scan()` function call below.
|
|
3298
|
-
// Note: `firstPassOptions` already has `continueOnParseError: true` for the minifyHTML call.
|
|
3422
|
+
// Note: `firstPassOptions` already has `continueOnParseError: true` for the `minifyHTML` call.
|
|
3299
3423
|
const originalContinueOnParseError = options.continueOnParseError;
|
|
3300
3424
|
options.continueOnParseError = true;
|
|
3301
3425
|
|
|
@@ -3308,7 +3432,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3308
3432
|
: null;
|
|
3309
3433
|
|
|
3310
3434
|
try {
|
|
3311
|
-
// Expand UID tokens back to original content for frequency analysis
|
|
3435
|
+
// Expand UID tokens back to the original content for frequency analysis
|
|
3312
3436
|
let expandedValue = value;
|
|
3313
3437
|
if (uidReplacePattern) {
|
|
3314
3438
|
expandedValue = value.replace(uidReplacePattern, function (match, index) {
|
|
@@ -3357,7 +3481,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
3357
3481
|
attrOrderCache.set(cacheKey, sortedNames);
|
|
3358
3482
|
}
|
|
3359
3483
|
|
|
3360
|
-
// Apply the sorted order to attrs
|
|
3484
|
+
// Apply the sorted order to `attrs`
|
|
3361
3485
|
const attrMap = Object.create(null);
|
|
3362
3486
|
names.forEach(function (name, index) {
|
|
3363
3487
|
(attrMap[name] || (attrMap[name] = [])).push(attrs[index]);
|
|
@@ -3444,7 +3568,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3444
3568
|
const customElementsInput = options.inlineCustomElements ?? [];
|
|
3445
3569
|
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
3446
3570
|
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
3447
|
-
// Fast path: Reuse base
|
|
3571
|
+
// Fast path: Reuse base sets if no custom elements
|
|
3448
3572
|
const inlineTextSet = normalizedCustomElements.length
|
|
3449
3573
|
? new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements])
|
|
3450
3574
|
: inlineElementsToKeepWhitespaceWithin;
|
|
@@ -3464,7 +3588,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3464
3588
|
}
|
|
3465
3589
|
|
|
3466
3590
|
// Temporarily replace ignored chunks with comments, so that we don’t have to worry what’s there.
|
|
3467
|
-
// For all we care there might be completely-horribly-broken-alien-non-html-
|
|
3591
|
+
// For all we care there might be completely-horribly-broken-alien-non-html-emoji-cthulhu-filled content
|
|
3468
3592
|
value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
|
|
3469
3593
|
if (!uidIgnore) {
|
|
3470
3594
|
uidIgnore = uniqueId(value);
|
|
@@ -3485,7 +3609,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3485
3609
|
// Create sort functions after `htmlmin:ignore` processing but before custom fragment UID markers
|
|
3486
3610
|
// This allows proper frequency analysis with access to ignored content via UID tokens
|
|
3487
3611
|
if ((options.sortAttributes && typeof options.sortAttributes !== 'function') ||
|
|
3488
|
-
|
|
3612
|
+
(options.sortClassName && typeof options.sortClassName !== 'function')) {
|
|
3489
3613
|
await createSortFns(value, options, uidIgnore, null, ignoredMarkupChunks);
|
|
3490
3614
|
}
|
|
3491
3615
|
|
|
@@ -3547,11 +3671,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3547
3671
|
});
|
|
3548
3672
|
}
|
|
3549
3673
|
|
|
3550
|
-
function
|
|
3674
|
+
function canCollapseWhitespace$1(tag, attrs) {
|
|
3551
3675
|
return options.canCollapseWhitespace(tag, attrs, canCollapseWhitespace);
|
|
3552
3676
|
}
|
|
3553
3677
|
|
|
3554
|
-
function
|
|
3678
|
+
function canTrimWhitespace$1(tag, attrs) {
|
|
3555
3679
|
return options.canTrimWhitespace(tag, attrs, canTrimWhitespace);
|
|
3556
3680
|
}
|
|
3557
3681
|
|
|
@@ -3573,12 +3697,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3573
3697
|
|
|
3574
3698
|
// Look for trailing whitespaces, bypass any inline tags
|
|
3575
3699
|
function trimTrailingWhitespace(index, nextTag) {
|
|
3576
|
-
for (let endTag = null; index >= 0 &&
|
|
3700
|
+
for (let endTag = null; index >= 0 && canTrimWhitespace$1(endTag); index--) {
|
|
3577
3701
|
const str = buffer[index];
|
|
3578
3702
|
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
3579
3703
|
if (match) {
|
|
3580
3704
|
endTag = match[1];
|
|
3581
|
-
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
|
|
3705
|
+
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, [], [], options, inlineElements, inlineTextSet))) {
|
|
3582
3706
|
break;
|
|
3583
3707
|
}
|
|
3584
3708
|
}
|
|
@@ -3627,10 +3751,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3627
3751
|
|
|
3628
3752
|
let optional = options.removeOptionalTags;
|
|
3629
3753
|
if (optional) {
|
|
3630
|
-
const htmlTag =
|
|
3754
|
+
const htmlTag = htmlElements.has(tag);
|
|
3631
3755
|
// `<html>` may be omitted if first thing inside is not a comment
|
|
3632
3756
|
// `<head>` may be omitted if first thing inside is an element
|
|
3633
|
-
// `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`,
|
|
3757
|
+
// `<body>` may be omitted if first thing inside is not space, comment, `<meta>`, `<link>`, `<script>`, `<style>`, or `<template>`
|
|
3634
3758
|
// `<colgroup>` may be omitted if first thing inside is `<col>`
|
|
3635
3759
|
// `<tbody>` may be omitted if first thing inside is `<tr>`
|
|
3636
3760
|
if (htmlTag && canRemoveParentTag(optionalStartTag, tag)) {
|
|
@@ -3647,16 +3771,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3647
3771
|
optionalEndTag = '';
|
|
3648
3772
|
}
|
|
3649
3773
|
|
|
3650
|
-
// Set whitespace flags for nested tags (e.g.,
|
|
3774
|
+
// Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`)
|
|
3651
3775
|
if (options.collapseWhitespace) {
|
|
3652
3776
|
if (!stackNoTrimWhitespace.length) {
|
|
3653
3777
|
squashTrailingWhitespace(tag);
|
|
3654
3778
|
}
|
|
3655
3779
|
if (!unary) {
|
|
3656
|
-
if (!
|
|
3780
|
+
if (!canTrimWhitespace$1(tag, attrs) || stackNoTrimWhitespace.length) {
|
|
3657
3781
|
stackNoTrimWhitespace.push(tag);
|
|
3658
3782
|
}
|
|
3659
|
-
if (!
|
|
3783
|
+
if (!canCollapseWhitespace$1(tag, attrs) || stackNoCollapseWhitespace.length) {
|
|
3660
3784
|
stackNoCollapseWhitespace.push(tag);
|
|
3661
3785
|
}
|
|
3662
3786
|
}
|
|
@@ -3712,7 +3836,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3712
3836
|
squashTrailingWhitespace('/' + tag);
|
|
3713
3837
|
}
|
|
3714
3838
|
if (stackNoCollapseWhitespace.length &&
|
|
3715
|
-
|
|
3839
|
+
tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
|
|
3716
3840
|
stackNoCollapseWhitespace.pop();
|
|
3717
3841
|
}
|
|
3718
3842
|
}
|
|
@@ -3725,7 +3849,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3725
3849
|
|
|
3726
3850
|
if (options.removeOptionalTags) {
|
|
3727
3851
|
// `<html>`, `<head>` or `<body>` may be omitted if the element is empty
|
|
3728
|
-
if (isElementEmpty &&
|
|
3852
|
+
if (isElementEmpty && topLevelElements.has(optionalStartTag)) {
|
|
3729
3853
|
removeStartTag();
|
|
3730
3854
|
}
|
|
3731
3855
|
optionalStartTag = '';
|
|
@@ -3733,7 +3857,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3733
3857
|
// `</head>` may be omitted if not followed by space or comment
|
|
3734
3858
|
// `</p>` may be omitted if no more content in non-`</a>` parent
|
|
3735
3859
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
3736
|
-
if (tag && optionalEndTag && !
|
|
3860
|
+
if (tag && optionalEndTag && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || !pInlineElements.has(tag))) {
|
|
3737
3861
|
removeEndTag();
|
|
3738
3862
|
}
|
|
3739
3863
|
optionalEndTag = optionalEndTags.has(tag) ? tag : '';
|
|
@@ -3780,10 +3904,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3780
3904
|
}
|
|
3781
3905
|
}
|
|
3782
3906
|
},
|
|
3783
|
-
chars: async function (text, prevTag, nextTag) {
|
|
3907
|
+
chars: async function (text, prevTag, nextTag, prevAttrs, nextAttrs) {
|
|
3784
3908
|
prevTag = prevTag === '' ? 'comment' : prevTag;
|
|
3785
3909
|
nextTag = nextTag === '' ? 'comment' : nextTag;
|
|
3786
|
-
|
|
3910
|
+
prevAttrs = prevAttrs || [];
|
|
3911
|
+
nextAttrs = nextAttrs || [];
|
|
3912
|
+
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
3787
3913
|
if (text.indexOf('&') !== -1) {
|
|
3788
3914
|
text = entities.decodeHTML(text);
|
|
3789
3915
|
}
|
|
@@ -3819,7 +3945,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3819
3945
|
}
|
|
3820
3946
|
}
|
|
3821
3947
|
if (prevTag || nextTag) {
|
|
3822
|
-
text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
|
|
3948
|
+
text = collapseWhitespaceSmart(text, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet);
|
|
3823
3949
|
} else {
|
|
3824
3950
|
text = collapseWhitespace(text, options, true, true);
|
|
3825
3951
|
}
|
|
@@ -3831,13 +3957,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3831
3957
|
text = collapseWhitespace(text, options, false, false, true);
|
|
3832
3958
|
}
|
|
3833
3959
|
}
|
|
3834
|
-
if (
|
|
3960
|
+
if (specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs))) {
|
|
3835
3961
|
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
3836
3962
|
}
|
|
3837
3963
|
if (isExecutableScript(currentTag, currentAttrs)) {
|
|
3838
3964
|
text = await options.minifyJS(text);
|
|
3839
3965
|
}
|
|
3840
|
-
if (
|
|
3966
|
+
if (isStyleElement(currentTag, currentAttrs)) {
|
|
3841
3967
|
text = await options.minifyCSS(text);
|
|
3842
3968
|
}
|
|
3843
3969
|
if (options.removeOptionalTags && text) {
|
|
@@ -3849,7 +3975,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3849
3975
|
optionalStartTag = '';
|
|
3850
3976
|
// `</html>` or `</body>` may be omitted if not followed by comment
|
|
3851
3977
|
// `</head>`, `</colgroup>`, or `</caption>` may be omitted if not followed by space or comment
|
|
3852
|
-
if (
|
|
3978
|
+
if (compactElements.has(optionalEndTag) || (looseElements.has(optionalEndTag) && !/^\s/.test(text))) {
|
|
3853
3979
|
removeEndTag();
|
|
3854
3980
|
}
|
|
3855
3981
|
// Don’t reset optionalEndTag if text is only whitespace and will be collapsed (not conservatively)
|
|
@@ -3858,11 +3984,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3858
3984
|
}
|
|
3859
3985
|
}
|
|
3860
3986
|
charsPrevTag = /^\s*$/.test(text) ? prevTag : 'comment';
|
|
3861
|
-
if (options.decodeEntities && text && !
|
|
3987
|
+
if (options.decodeEntities && text && !specialContentElements.has(currentTag)) {
|
|
3862
3988
|
// Escape any `&` symbols that start either:
|
|
3863
|
-
// 1) a legacy
|
|
3989
|
+
// 1) a legacy-named character reference (i.e., one that doesn’t end with `;`)
|
|
3864
3990
|
// 2) or any other character reference (i.e., one that does end with `;`)
|
|
3865
|
-
// Note that `&` can be escaped as `&`, without the
|
|
3991
|
+
// Note that `&` can be escaped as `&`, without the semicolon.
|
|
3866
3992
|
// https://mathiasbynens.be/notes/ambiguous-ampersands
|
|
3867
3993
|
if (text.indexOf('&') !== -1) {
|
|
3868
3994
|
text = text.replace(RE_LEGACY_ENTITIES, '&$1');
|
|
@@ -3927,7 +4053,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3927
4053
|
|
|
3928
4054
|
// Only collapse whitespace if both blocks contain HTML (start with `<`)
|
|
3929
4055
|
// Don’t collapse if either contains plain text, as that would change meaning
|
|
3930
|
-
// Note: This check will match HTML comments (`<!-- … -->`), but the tag
|
|
4056
|
+
// Note: This check will match HTML comments (`<!-- … -->`), but the tag name
|
|
3931
4057
|
// regex below requires starting with a letter, so comments are intentionally
|
|
3932
4058
|
// excluded by the `currentTagMatch && prevTagMatch` guard
|
|
3933
4059
|
if (currentContent && prevContent && /^\s*</.test(currentContent) && /^\s*</.test(prevContent)) {
|
|
@@ -3988,11 +4114,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3988
4114
|
if (options.removeOptionalTags) {
|
|
3989
4115
|
// `<html>` may be omitted if first thing inside is not a comment
|
|
3990
4116
|
// `<head>` or `<body>` may be omitted if empty
|
|
3991
|
-
if (
|
|
4117
|
+
if (topLevelElements.has(optionalStartTag)) {
|
|
3992
4118
|
removeStartTag();
|
|
3993
4119
|
}
|
|
3994
4120
|
// except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
|
|
3995
|
-
if (optionalEndTag && !
|
|
4121
|
+
if (optionalEndTag && !trailingElements.has(optionalEndTag)) {
|
|
3996
4122
|
removeEndTag();
|
|
3997
4123
|
}
|
|
3998
4124
|
}
|