html-minifier-next 1.1.5 → 1.2.1
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 +25 -23
- package/cli.js +3 -2
- package/dist/htmlminifier.cjs +9 -10
- package/dist/htmlminifier.esm.bundle.js +559 -329
- package/dist/htmlminifier.umd.bundle.js +559 -329
- package/dist/htmlminifier.umd.bundle.min.js +3 -3
- package/package.json +14 -12
- package/src/htmlminifier.js +38 -32
- package/src/htmlparser.js +9 -9
package/package.json
CHANGED
|
@@ -15,22 +15,21 @@
|
|
|
15
15
|
"description": "Highly configurable, well-tested, JavaScript-based HTML minifier.",
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@commitlint/cli": "^19.8.1",
|
|
18
|
-
"@jest/globals": "^30.0.
|
|
18
|
+
"@jest/globals": "^30.0.5",
|
|
19
19
|
"@rollup/plugin-commonjs": "^28.0.6",
|
|
20
20
|
"@rollup/plugin-json": "^6.1.0",
|
|
21
21
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
22
22
|
"@rollup/plugin-terser": "^0.4.4",
|
|
23
23
|
"alpinejs": "^3.14.9",
|
|
24
24
|
"commitlint-config-non-conventional": "^1.0.1",
|
|
25
|
-
"eslint": "^9.
|
|
25
|
+
"eslint": "^9.32.0",
|
|
26
26
|
"husky": "^9.1.7",
|
|
27
27
|
"is-ci": "^4.1.0",
|
|
28
|
-
"jest": "^30.0.
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"rollup": "^4.44.1",
|
|
28
|
+
"jest": "^30.0.5",
|
|
29
|
+
"lint-staged": "^16.1.5",
|
|
30
|
+
"rollup": "^4.45.1",
|
|
32
31
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
33
|
-
"vite": "^7.0.
|
|
32
|
+
"vite": "^7.0.5"
|
|
34
33
|
},
|
|
35
34
|
"exports": {
|
|
36
35
|
".": {
|
|
@@ -72,6 +71,11 @@
|
|
|
72
71
|
"main": "./dist/htmlminifier.cjs",
|
|
73
72
|
"module": "./src/htmlminifier.js",
|
|
74
73
|
"name": "html-minifier-next",
|
|
74
|
+
"overrides": {
|
|
75
|
+
"glob": "^10.0.0",
|
|
76
|
+
"inflight": "npm:@nodelib/fs.stat@^3.0.0"
|
|
77
|
+
},
|
|
78
|
+
"overrides_comment": "@@ Remove when Jest fixes deprecated glob@7.2.3 and inflight dependencies",
|
|
75
79
|
"repository": "https://github.com/j9t/html-minifier-next.git",
|
|
76
80
|
"scripts": {
|
|
77
81
|
"build": "rollup -c",
|
|
@@ -80,11 +84,9 @@
|
|
|
80
84
|
"lint": "eslint .",
|
|
81
85
|
"prepare": "husky",
|
|
82
86
|
"serve": "npm run build && vite",
|
|
83
|
-
"test": "
|
|
84
|
-
"test:
|
|
85
|
-
"test:watch": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest verbose --watch",
|
|
86
|
-
"test:web": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --verbose --environment=jsdom"
|
|
87
|
+
"test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest",
|
|
88
|
+
"test:watch": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --watch"
|
|
87
89
|
},
|
|
88
90
|
"type": "module",
|
|
89
|
-
"version": "1.1
|
|
91
|
+
"version": "1.2.1"
|
|
90
92
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -52,28 +52,28 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (collapseAll) {
|
|
55
|
-
//
|
|
55
|
+
// Strip non-space whitespace then compress spaces to one
|
|
56
56
|
str = collapseWhitespaceAll(str);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
return lineBreakBefore + str + lineBreakAfter;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
//
|
|
67
|
-
const
|
|
62
|
+
// Non-empty elements that will maintain whitespace around them
|
|
63
|
+
const inlineElementsToKeepWhitespaceAround = ['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', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', 'wbr'];
|
|
64
|
+
// Non-empty elements that will maintain whitespace within them
|
|
65
|
+
const inlineElementsToKeepWhitespaceWithin = new Set(['a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var']);
|
|
66
|
+
// Elements that will always maintain whitespace around them
|
|
67
|
+
const inlineElementsToKeepWhitespace = new Set(['comment', 'img', 'input', 'wbr']);
|
|
68
68
|
|
|
69
|
-
function collapseWhitespaceSmart(str, prevTag, nextTag, options) {
|
|
70
|
-
let trimLeft = prevTag && !
|
|
69
|
+
function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
|
|
70
|
+
let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
|
|
71
71
|
if (trimLeft && !options.collapseInlineTagWhitespace) {
|
|
72
|
-
trimLeft = prevTag.charAt(0) === '/' ? !
|
|
72
|
+
trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
|
|
73
73
|
}
|
|
74
|
-
let trimRight = nextTag && !
|
|
74
|
+
let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
|
|
75
75
|
if (trimRight && !options.collapseInlineTagWhitespace) {
|
|
76
|
-
trimRight = nextTag.charAt(0) === '/' ? !
|
|
76
|
+
trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
|
|
77
77
|
}
|
|
78
78
|
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
79
79
|
}
|
|
@@ -395,7 +395,7 @@ async function processScript(text, options, currentAttrs) {
|
|
|
395
395
|
// Tag omission rules from https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
|
|
396
396
|
// with the following deviations:
|
|
397
397
|
// - retain <body> if followed by <noscript>
|
|
398
|
-
// - </rb>, </rt>, </rtc>, </rp
|
|
398
|
+
// - </rb>, </rt>, </rtc>, </rp>, and </tfoot> follow https://www.w3.org/TR/html5/syntax.html#optional-tags
|
|
399
399
|
// - retain all tags which are adjacent to non-standard HTML tags
|
|
400
400
|
const optionalStartTags = new Set(['html', 'head', 'body', 'colgroup', 'tbody']);
|
|
401
401
|
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']);
|
|
@@ -605,7 +605,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
605
605
|
emittedAttrValue += ' ';
|
|
606
606
|
}
|
|
607
607
|
} else if (isLast && !hasUnarySlash && !/\/$/.test(attrValue)) {
|
|
608
|
-
//
|
|
608
|
+
// Make sure trailing slash is not interpreted as HTML self-closing tag
|
|
609
609
|
emittedAttrValue = attrValue;
|
|
610
610
|
} else {
|
|
611
611
|
emittedAttrValue = attrValue + ' ';
|
|
@@ -866,10 +866,16 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
866
866
|
let uidIgnore;
|
|
867
867
|
let uidAttr;
|
|
868
868
|
let uidPattern;
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
869
|
+
// Create inline tags/text sets with custom elements
|
|
870
|
+
const customElementsInput = options.inlineCustomElements ?? [];
|
|
871
|
+
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
872
|
+
const normalizedCustomElements = customElementsArr.map(name => options.name(name));
|
|
873
|
+
const inlineTextSet = new Set([...inlineElementsToKeepWhitespaceWithin, ...normalizedCustomElements]);
|
|
874
|
+
const inlineElements = new Set([...inlineElementsToKeepWhitespaceAround, ...normalizedCustomElements]);
|
|
875
|
+
|
|
876
|
+
// Temporarily replace ignored chunks with comments,
|
|
877
|
+
// so that we don’t have to worry what’s there.
|
|
878
|
+
// For all we care there might be
|
|
873
879
|
// completely-horribly-broken-alien-non-html-emoj-cthulhu-filled content
|
|
874
880
|
value = value.replace(/<!-- htmlmin:ignore -->([\s\S]*?)<!-- htmlmin:ignore -->/g, function (match, group1) {
|
|
875
881
|
if (!uidIgnore) {
|
|
@@ -990,20 +996,20 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
990
996
|
buffer.length = Math.max(0, index);
|
|
991
997
|
}
|
|
992
998
|
|
|
993
|
-
//
|
|
999
|
+
// Look for trailing whitespaces, bypass any inline tags
|
|
994
1000
|
function trimTrailingWhitespace(index, nextTag) {
|
|
995
1001
|
for (let endTag = null; index >= 0 && _canTrimWhitespace(endTag); index--) {
|
|
996
1002
|
const str = buffer[index];
|
|
997
1003
|
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
998
1004
|
if (match) {
|
|
999
1005
|
endTag = match[1];
|
|
1000
|
-
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options))) {
|
|
1006
|
+
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, null, nextTag, options, inlineElements, inlineTextSet))) {
|
|
1001
1007
|
break;
|
|
1002
1008
|
}
|
|
1003
1009
|
}
|
|
1004
1010
|
}
|
|
1005
1011
|
|
|
1006
|
-
//
|
|
1012
|
+
// Look for trailing whitespaces from previously processed text
|
|
1007
1013
|
// which may not be trimmed due to a following comment or an empty
|
|
1008
1014
|
// element which has now been removed
|
|
1009
1015
|
function squashTrailingWhitespace(nextTag) {
|
|
@@ -1034,7 +1040,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1034
1040
|
tag = options.name(tag);
|
|
1035
1041
|
currentTag = tag;
|
|
1036
1042
|
charsPrevTag = tag;
|
|
1037
|
-
if (!
|
|
1043
|
+
if (!inlineTextSet.has(tag)) {
|
|
1038
1044
|
currentChars = '';
|
|
1039
1045
|
}
|
|
1040
1046
|
hasChars = false;
|
|
@@ -1052,7 +1058,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1052
1058
|
removeStartTag();
|
|
1053
1059
|
}
|
|
1054
1060
|
optionalStartTag = '';
|
|
1055
|
-
//
|
|
1061
|
+
// End-tag-followed-by-start-tag omission rules
|
|
1056
1062
|
if (htmlTag && canRemovePrecedingTag(optionalEndTag, tag)) {
|
|
1057
1063
|
removeEndTag();
|
|
1058
1064
|
// <colgroup> cannot be omitted if preceding </colgroup> is omitted
|
|
@@ -1062,7 +1068,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1062
1068
|
optionalEndTag = '';
|
|
1063
1069
|
}
|
|
1064
1070
|
|
|
1065
|
-
//
|
|
1071
|
+
// Set whitespace flags for nested tags (eg. <code> within a <pre>)
|
|
1066
1072
|
if (options.collapseWhitespace) {
|
|
1067
1073
|
if (!stackNoTrimWhitespace.length) {
|
|
1068
1074
|
squashTrailingWhitespace(tag);
|
|
@@ -1098,7 +1104,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1098
1104
|
buffer.push(' ');
|
|
1099
1105
|
buffer.push.apply(buffer, parts);
|
|
1100
1106
|
} else if (optional && optionalStartTags.has(tag)) {
|
|
1101
|
-
//
|
|
1107
|
+
// Start tag must never be omitted if it has any attributes
|
|
1102
1108
|
optionalStartTag = tag;
|
|
1103
1109
|
}
|
|
1104
1110
|
|
|
@@ -1115,7 +1121,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1115
1121
|
}
|
|
1116
1122
|
tag = options.name(tag);
|
|
1117
1123
|
|
|
1118
|
-
//
|
|
1124
|
+
// Check if current tag is in a whitespace stack
|
|
1119
1125
|
if (options.collapseWhitespace) {
|
|
1120
1126
|
if (stackNoTrimWhitespace.length) {
|
|
1121
1127
|
if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
|
|
@@ -1153,7 +1159,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1153
1159
|
}
|
|
1154
1160
|
|
|
1155
1161
|
if (options.removeEmptyElements && isElementEmpty && canRemoveElement(tag, attrs)) {
|
|
1156
|
-
//
|
|
1162
|
+
// Remove last “element” from buffer
|
|
1157
1163
|
removeStartTag();
|
|
1158
1164
|
optionalStartTag = '';
|
|
1159
1165
|
optionalEndTag = '';
|
|
@@ -1164,7 +1170,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1164
1170
|
buffer.push('</' + tag + '>');
|
|
1165
1171
|
}
|
|
1166
1172
|
charsPrevTag = '/' + tag;
|
|
1167
|
-
if (!
|
|
1173
|
+
if (!inlineElements.has(tag)) {
|
|
1168
1174
|
currentChars = '';
|
|
1169
1175
|
} else if (isElementEmpty) {
|
|
1170
1176
|
currentChars += '|';
|
|
@@ -1203,12 +1209,12 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1203
1209
|
}
|
|
1204
1210
|
trimTrailingWhitespace(tagIndex - 1, 'br');
|
|
1205
1211
|
}
|
|
1206
|
-
} else if (
|
|
1212
|
+
} else if (inlineTextSet.has(prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag)) {
|
|
1207
1213
|
text = collapseWhitespace(text, options, /(?:^|\s)$/.test(currentChars));
|
|
1208
1214
|
}
|
|
1209
1215
|
}
|
|
1210
1216
|
if (prevTag || nextTag) {
|
|
1211
|
-
text = collapseWhitespaceSmart(text, prevTag, nextTag, options);
|
|
1217
|
+
text = collapseWhitespaceSmart(text, prevTag, nextTag, options, inlineElements, inlineTextSet);
|
|
1212
1218
|
} else {
|
|
1213
1219
|
text = collapseWhitespace(text, options, true, true);
|
|
1214
1220
|
}
|
|
@@ -1278,7 +1284,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1278
1284
|
text = prefix + text + suffix;
|
|
1279
1285
|
}
|
|
1280
1286
|
if (options.removeOptionalTags && text) {
|
|
1281
|
-
//
|
|
1287
|
+
// Preceding comments suppress tag omissions
|
|
1282
1288
|
optionalStartTag = '';
|
|
1283
1289
|
optionalEndTag = '';
|
|
1284
1290
|
}
|
|
@@ -1382,4 +1388,4 @@ export const minify = async function (value, options) {
|
|
|
1382
1388
|
return result;
|
|
1383
1389
|
};
|
|
1384
1390
|
|
|
1385
|
-
export default { minify };
|
|
1391
|
+
export default { minify };
|
package/src/htmlparser.js
CHANGED
|
@@ -131,7 +131,7 @@ export class HTMLParser {
|
|
|
131
131
|
let last, prevTag, nextTag;
|
|
132
132
|
while (html) {
|
|
133
133
|
last = html;
|
|
134
|
-
// Make sure we
|
|
134
|
+
// Make sure we’re not in a `script` or `style` element
|
|
135
135
|
if (!lastTag || !special.has(lastTag)) {
|
|
136
136
|
let textEnd = html.indexOf('<');
|
|
137
137
|
if (textEnd === 0) {
|
|
@@ -207,7 +207,7 @@ export class HTMLParser {
|
|
|
207
207
|
html = '';
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
//
|
|
210
|
+
// Next tag
|
|
211
211
|
let nextTagMatch = parseStartTag(html);
|
|
212
212
|
if (nextTagMatch) {
|
|
213
213
|
nextTag = nextTagMatch.tagName;
|
|
@@ -320,9 +320,9 @@ export class HTMLParser {
|
|
|
320
320
|
|
|
321
321
|
const attrs = match.attrs.map(function (args) {
|
|
322
322
|
let name, value, customOpen, customClose, customAssign, quote;
|
|
323
|
-
const ncp = 7; //
|
|
323
|
+
const ncp = 7; // Number of captured parts, scalar
|
|
324
324
|
|
|
325
|
-
//
|
|
325
|
+
// Hackish workaround for FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
|
|
326
326
|
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
|
|
327
327
|
if (args[3] === '') { delete args[3]; }
|
|
328
328
|
if (args[4] === '') { delete args[4]; }
|
|
@@ -512,13 +512,13 @@ export const HTMLtoDOM = (html, doc) => {
|
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
-
// If we
|
|
516
|
-
// the body element
|
|
515
|
+
// If we’re working with a document, inject contents into
|
|
516
|
+
// the `body` element
|
|
517
517
|
let curParentNode = one.body;
|
|
518
518
|
|
|
519
519
|
const parser = new HTMLParser(html, {
|
|
520
520
|
start: function (tagName, attrs, unary) {
|
|
521
|
-
// If it
|
|
521
|
+
// If it’s a pre-built element, then we can ignore
|
|
522
522
|
// its construction
|
|
523
523
|
if (one[tagName]) {
|
|
524
524
|
curParentNode = one[tagName];
|
|
@@ -552,7 +552,7 @@ export const HTMLtoDOM = (html, doc) => {
|
|
|
552
552
|
curParentNode.appendChild(doc.createTextNode(text));
|
|
553
553
|
},
|
|
554
554
|
comment: function (/* text */) {
|
|
555
|
-
//
|
|
555
|
+
// Create comment node
|
|
556
556
|
},
|
|
557
557
|
ignore: function (/* text */) {
|
|
558
558
|
// What to do here?
|
|
@@ -562,4 +562,4 @@ export const HTMLtoDOM = (html, doc) => {
|
|
|
562
562
|
parser.parse();
|
|
563
563
|
|
|
564
564
|
return doc;
|
|
565
|
-
};
|
|
565
|
+
};
|