html-minifier-next 7.5.3 → 8.0.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 +60 -24
- package/cli.js +178 -55
- package/dist/types/htmlminifier.d.ts +24 -4
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +2 -2
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +3 -2
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/elements.d.ts +1 -1
- package/dist/types/lib/fragments.d.ts +46 -0
- package/dist/types/lib/fragments.d.ts.map +1 -0
- package/dist/types/lib/option-definitions.d.ts +21 -194
- package/dist/types/lib/option-definitions.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +33 -5
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/unused-css.d.ts +43 -0
- package/dist/types/lib/unused-css.d.ts.map +1 -0
- package/dist/types/lib/utils.d.ts +26 -1
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/tokenchain.d.ts +2 -2
- package/dist/types/tokenchain.d.ts.map +1 -1
- package/html-minifier-next.schema.json +11 -8
- package/package.json +2 -2
- package/src/htmlminifier.js +86 -88
- package/src/htmlparser.js +4 -4
- package/src/lib/attributes.js +65 -8
- package/src/lib/constants.js +8 -8
- package/src/lib/elements.js +3 -3
- package/src/lib/fragments.js +274 -0
- package/src/lib/option-definitions.js +19 -7
- package/src/lib/options.js +151 -28
- package/src/lib/unused-css.js +377 -0
- package/src/lib/utils.js +330 -2
- package/src/tokenchain.js +37 -30
package/src/htmlminifier.js
CHANGED
|
@@ -2,11 +2,13 @@ import { HTMLParser, endTag } from './htmlparser.js';
|
|
|
2
2
|
import TokenChain from './tokenchain.js';
|
|
3
3
|
import { presets, getPreset, getPresetNames } from './presets.js';
|
|
4
4
|
|
|
5
|
-
import { LRU, identity, isThenable, lowercase, uniqueId } from './lib/utils.js';
|
|
5
|
+
import { LRU, findTagEnd, identity, isThenable, lowercase, uniqueId } from './lib/utils.js';
|
|
6
|
+
import { collectUsedSymbols } from './lib/unused-css.js';
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
RE_LEGACY_ENTITIES,
|
|
9
10
|
RE_ESCAPE_LT,
|
|
11
|
+
RE_STYLE_ELEMENT,
|
|
10
12
|
inlineElementsToKeepWhitespaceAround,
|
|
11
13
|
inlineElementsToKeepWhitespaceWithin,
|
|
12
14
|
specialContentElements,
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
} from './lib/content.js';
|
|
54
56
|
|
|
55
57
|
import { processOptions } from './lib/options.js';
|
|
58
|
+
import { toFragment, replaceCustomFragments } from './lib/fragments.js';
|
|
56
59
|
|
|
57
60
|
/** @import { ProcessedOptions } from './lib/options.js' */
|
|
58
61
|
|
|
@@ -61,7 +64,7 @@ import { processOptions } from './lib/options.js';
|
|
|
61
64
|
/**
|
|
62
65
|
* @typedef {Object} HTMLAttribute
|
|
63
66
|
* Representation of an attribute from the HTML parser.
|
|
64
|
-
* Internal counterpart: `HTMLAttribute` in
|
|
67
|
+
* Internal counterpart: `HTMLAttribute` in lib/attributes.js—keep in sync.
|
|
65
68
|
*
|
|
66
69
|
* @prop {string} name
|
|
67
70
|
* @prop {string} [value]
|
|
@@ -194,11 +197,13 @@ import { processOptions } from './lib/options.js';
|
|
|
194
197
|
*
|
|
195
198
|
* Default: `[/^on[a-z]{3,}$/]`
|
|
196
199
|
*
|
|
197
|
-
* @prop {
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
+
* @prop {boolean} [strictCustomFragments]
|
|
201
|
+
* Rejects `ignoreCustomFragments` patterns that risk catastrophic backtracking,
|
|
202
|
+
* rather than warning about them: those that compound quantifiers or alternation
|
|
203
|
+
* (`(a+)+`, `(a|b)*`), those that repeat the same unbounded atom twice in a row
|
|
204
|
+
* (`.*.*`), and those too long or too deeply nested to analyze at all.
|
|
200
205
|
*
|
|
201
|
-
* Default: `
|
|
206
|
+
* Default: `false`
|
|
202
207
|
*
|
|
203
208
|
* @prop {boolean} [decodeEntities]
|
|
204
209
|
* When true, decodes HTML entities in text and attributes before
|
|
@@ -437,6 +442,20 @@ import { processOptions } from './lib/options.js';
|
|
|
437
442
|
*
|
|
438
443
|
* Default: `false`
|
|
439
444
|
*
|
|
445
|
+
* @prop {boolean | {safelist?: Array<string | RegExp>, scripts?: boolean}} [removeUnusedCSS]
|
|
446
|
+
* **Note that this can change how a document renders!**
|
|
447
|
+
*
|
|
448
|
+
* Remove rules from `style` elements whose class or ID selectors the document
|
|
449
|
+
* never references. Requires `minifyCSS` (removal runs through Lightning CSS)
|
|
450
|
+
* and has no effect on `style` or `media` attributes.
|
|
451
|
+
*
|
|
452
|
+
* Class names and IDs are collected from the markup, from `data-*` attributes,
|
|
453
|
+
* and—unless `scripts` is set to `false`—from inline `script` elements. Names
|
|
454
|
+
* that only ever appear in external scripts cannot be detected; list those
|
|
455
|
+
* under `safelist` (strings or regular expressions) to keep them.
|
|
456
|
+
*
|
|
457
|
+
* Default: `false`
|
|
458
|
+
*
|
|
440
459
|
* @prop {boolean | ((tag: string, attrs: HTMLAttribute[]) => void)} [sortAttributes]
|
|
441
460
|
* When true, enables sorting of attributes. If a function is provided it
|
|
442
461
|
* will be used as a custom attribute sorter, which should mutate `attrs`
|
|
@@ -570,28 +589,6 @@ const RE_FOREIGN_ELEMENT_PROBE = /<(?:svg|math)[\s/>]/i;
|
|
|
570
589
|
|
|
571
590
|
// Script merging
|
|
572
591
|
|
|
573
|
-
/**
|
|
574
|
-
* Find the index of the `>` that closes an opening tag, correctly skipping
|
|
575
|
-
* over quoted attribute values (which may contain `>`).
|
|
576
|
-
* @param {string} html
|
|
577
|
-
* @param {number} pos - Start position (just after the tag name)
|
|
578
|
-
* @returns {number} Index of the closing `>`, or -1 if not found
|
|
579
|
-
*/
|
|
580
|
-
function findTagEnd(html, pos) {
|
|
581
|
-
let i = pos;
|
|
582
|
-
while (i < html.length) {
|
|
583
|
-
const ch = html[i];
|
|
584
|
-
if (ch === '>') return i;
|
|
585
|
-
if (ch === '"' || ch === "'") {
|
|
586
|
-
const q = ch;
|
|
587
|
-
i++;
|
|
588
|
-
while (i < html.length && html[i] !== q) i++;
|
|
589
|
-
}
|
|
590
|
-
i++;
|
|
591
|
-
}
|
|
592
|
-
return -1;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
592
|
/**
|
|
596
593
|
* Merge consecutive inline script tags into one (`mergeConsecutiveScripts`).
|
|
597
594
|
* Only merges scripts that are compatible:
|
|
@@ -832,13 +829,12 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
832
829
|
const originalContinueOnParseError = options.continueOnParseError;
|
|
833
830
|
options.continueOnParseError = true;
|
|
834
831
|
|
|
835
|
-
// Pre-compile
|
|
832
|
+
// Pre-compile the UID replacement pattern, and read the fragments the way the
|
|
833
|
+
// main path does, so each keeps its own flags
|
|
836
834
|
const uidReplacePattern = uidIgnore && ignoredMarkupChunks
|
|
837
835
|
? new RegExp('<!--' + uidIgnore + '(\\d+)-->', 'g')
|
|
838
836
|
: null;
|
|
839
|
-
const
|
|
840
|
-
? new RegExp('(' + options.ignoreCustomFragments.map(re => re.source).join('|') + ')', 'g')
|
|
841
|
-
: null;
|
|
837
|
+
const customFragments = (options.ignoreCustomFragments || []).map(toFragment);
|
|
842
838
|
|
|
843
839
|
try {
|
|
844
840
|
// Expand UID tokens back to the original content for frequency analysis
|
|
@@ -858,8 +854,8 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
858
854
|
// because HTML comments in opening tags prevent proper attribute parsing;
|
|
859
855
|
// removed with a space to preserve attribute boundaries
|
|
860
856
|
let scanValue = firstPassOutput;
|
|
861
|
-
if (
|
|
862
|
-
scanValue = firstPassOutput
|
|
857
|
+
if (customFragments.length) {
|
|
858
|
+
scanValue = replaceCustomFragments(firstPassOutput, customFragments, () => ' ');
|
|
863
859
|
}
|
|
864
860
|
|
|
865
861
|
await scan(scanValue);
|
|
@@ -1063,63 +1059,46 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1063
1059
|
await createSortFns(value, options, uidIgnore ?? '', uidAttr ?? '', ignoredMarkupChunks);
|
|
1064
1060
|
}
|
|
1065
1061
|
|
|
1066
|
-
const customFragments = (options.ignoreCustomFragments || []).map(
|
|
1067
|
-
return re.source;
|
|
1068
|
-
});
|
|
1062
|
+
const customFragments = (options.ignoreCustomFragments || []).map(toFragment);
|
|
1069
1063
|
if (customFragments.length) {
|
|
1070
|
-
//
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
'g'
|
|
1085
|
-
);
|
|
1086
|
-
// Temporarily replace custom ignored fragments with unique attributes
|
|
1087
|
-
value = value.replace(reCustomIgnore, function (match) {
|
|
1088
|
-
if (!uidAttr) {
|
|
1089
|
-
uidAttr = uniqueId(value);
|
|
1090
|
-
uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
|
|
1091
|
-
uidAttrLeadingPattern = new RegExp('^\\s*' + uidAttr + '(\\d+)' + uidAttr);
|
|
1092
|
-
|
|
1093
|
-
if (options.minifyCSS !== identity) {
|
|
1094
|
-
options.minifyCSS = (function (/** @type {ProcessedOptions['minifyCSS']} */ fn) {
|
|
1095
|
-
return function (/** @type {string} */ text, /** @type {string | undefined} */ type) {
|
|
1096
|
-
text = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1097
|
-
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1098
|
-
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
return fn(text, type);
|
|
1102
|
-
};
|
|
1103
|
-
})(options.minifyCSS);
|
|
1104
|
-
}
|
|
1064
|
+
// Temporarily replace custom ignored fragments with unique attributes
|
|
1065
|
+
const replaceFragment = function (/** @type {string} */ match) {
|
|
1066
|
+
if (!uidAttr) {
|
|
1067
|
+
uidAttr = uniqueId(value);
|
|
1068
|
+
uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
|
|
1069
|
+
uidAttrLeadingPattern = new RegExp('^\\s*' + uidAttr + '(\\d+)' + uidAttr);
|
|
1070
|
+
|
|
1071
|
+
if (options.minifyCSS !== identity) {
|
|
1072
|
+
options.minifyCSS = (function (/** @type {ProcessedOptions['minifyCSS']} */ fn) {
|
|
1073
|
+
return function (/** @type {string} */ text, /** @type {string | undefined} */ type, /** @type {ProcessedOptions['cssContext']} */ context) {
|
|
1074
|
+
text = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1075
|
+
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1076
|
+
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1077
|
+
});
|
|
1105
1078
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
return fn(text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1110
|
-
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1111
|
-
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1112
|
-
}), inline, isModule);
|
|
1113
|
-
};
|
|
1114
|
-
})(options.minifyJS);
|
|
1115
|
-
}
|
|
1079
|
+
return fn(text, type, context);
|
|
1080
|
+
};
|
|
1081
|
+
})(options.minifyCSS);
|
|
1116
1082
|
}
|
|
1117
1083
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1084
|
+
if (options.minifyJS !== identity) {
|
|
1085
|
+
options.minifyJS = (function (/** @type {ProcessedOptions['minifyJS']} */ fn) {
|
|
1086
|
+
return function (/** @type {string} */ text, /** @type {boolean | undefined} */ inline, /** @type {boolean | undefined} */ isModule) {
|
|
1087
|
+
return fn(text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1088
|
+
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1089
|
+
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1090
|
+
}), inline, isModule);
|
|
1091
|
+
};
|
|
1092
|
+
})(options.minifyJS);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const token = uidAttr + ignoredCustomMarkupChunks.length + uidAttr;
|
|
1097
|
+
ignoredCustomMarkupChunks.push(/^(\s*)[\s\S]*?(\s*)$/.exec(match));
|
|
1098
|
+
return '\t' + token + '\t';
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
value = replaceCustomFragments(value, customFragments, replaceFragment);
|
|
1123
1102
|
}
|
|
1124
1103
|
|
|
1125
1104
|
function canCollapseWhitespace(/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
@@ -1758,7 +1737,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1758
1737
|
text = await options.minifyJS(text, false, isModuleScript);
|
|
1759
1738
|
}
|
|
1760
1739
|
if (needsMinifyCSS) {
|
|
1761
|
-
text = await options.minifyCSS(text);
|
|
1740
|
+
text = await options.minifyCSS(text, undefined, options.cssContext);
|
|
1762
1741
|
}
|
|
1763
1742
|
charsFinalize(text);
|
|
1764
1743
|
})();
|
|
@@ -2094,6 +2073,25 @@ export const minify = async function (value, options) {
|
|
|
2094
2073
|
}
|
|
2095
2074
|
// Work on a shallow copy so per-call reassignments don’t reach the cached base
|
|
2096
2075
|
const processedOptions = /** @type {ProcessedOptions} */ ({ ...processedBase });
|
|
2076
|
+
|
|
2077
|
+
// Warnings are deduplicated per document, so the state has to live on the per-call
|
|
2078
|
+
// copy; the `minifyCSS` closure hangs off the memoized base, shared across calls
|
|
2079
|
+
processedOptions.cssContext = { warned: new Set() };
|
|
2080
|
+
|
|
2081
|
+
// Unused-CSS removal needs the whole document’s symbols before the first `style`
|
|
2082
|
+
// element is minified, so collect them upfront from the raw input. A document
|
|
2083
|
+
// without a style sheet has nothing to remove from, and scanning it would be work
|
|
2084
|
+
// spent on an empty result.
|
|
2085
|
+
if (processedOptions.removeUnusedCSS && RE_STYLE_ELEMENT.test(value)) {
|
|
2086
|
+
processedOptions.cssContext.usedSymbols = collectUsedSymbols(
|
|
2087
|
+
value,
|
|
2088
|
+
processedOptions.removeUnusedCSS.scripts,
|
|
2089
|
+
value.indexOf('&') !== -1
|
|
2090
|
+
? /** @type {(text: string) => string} */ (await getDecodeHTML())
|
|
2091
|
+
: undefined
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2097
2095
|
let result = await minifyHTML(value, processedOptions);
|
|
2098
2096
|
|
|
2099
2097
|
// Post-processing: Merge consecutive inline scripts if enabled
|
package/src/htmlparser.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { isThenable } from './lib/utils.js';
|
|
8
|
+
import { isThenable, embedSource } from './lib/utils.js';
|
|
9
9
|
|
|
10
10
|
/** @import { HTMLAttribute } from './lib/attributes.js' */
|
|
11
11
|
|
|
@@ -141,9 +141,9 @@ function buildAttrRegex(handler) {
|
|
|
141
141
|
throw new Error('`customAttrSurround` entries must be `[RegExp, RegExp]` pairs');
|
|
142
142
|
}
|
|
143
143
|
attrClauses[i] = '(?:' +
|
|
144
|
-
'(' + pair[0]
|
|
144
|
+
'(' + embedSource(pair[0]) + ')\\s*' +
|
|
145
145
|
pattern +
|
|
146
|
-
'\\s*(' + pair[1]
|
|
146
|
+
'\\s*(' + embedSource(pair[1]) + ')' +
|
|
147
147
|
')';
|
|
148
148
|
}
|
|
149
149
|
attrClauses.push('(?:' + pattern + ')');
|
|
@@ -180,7 +180,7 @@ function joinSingleAttrAssigns(handler) {
|
|
|
180
180
|
return singleAttrAssigns.concat(
|
|
181
181
|
handler.customAttrAssign || []
|
|
182
182
|
).map(function (assign) {
|
|
183
|
-
return '(?:' + assign
|
|
183
|
+
return '(?:' + embedSource(assign) + ')';
|
|
184
184
|
}).join('|');
|
|
185
185
|
}
|
|
186
186
|
|
package/src/lib/attributes.js
CHANGED
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
isBooleanValue,
|
|
14
14
|
collapsibleValues,
|
|
15
15
|
srcsetElements,
|
|
16
|
-
|
|
16
|
+
RE_EMPTY_ATTRIBUTE,
|
|
17
|
+
RE_STYLE_ELEMENT
|
|
17
18
|
} from './constants.js';
|
|
18
19
|
import { trimWhitespace, collapseWhitespaceAll } from './whitespace.js';
|
|
19
20
|
import { shouldMinifyInnerHTML } from './options.js';
|
|
21
|
+
import { collectUsedSymbols } from './unused-css.js';
|
|
20
22
|
import { identity, isThenable } from './utils.js';
|
|
21
23
|
|
|
22
24
|
/** @import { ProcessedOptions } from './options.js' */
|
|
@@ -25,10 +27,11 @@ import { identity, isThenable } from './utils.js';
|
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* @typedef {{ name: string, value?: string | undefined, quote?: string, customAssign?: string, customOpen?: string, customClose?: string }} HTMLAttribute
|
|
28
|
-
* Internal counterpart of the public typedef in
|
|
30
|
+
* Internal counterpart of the public typedef in htmlminifier.js—keep in sync.
|
|
29
31
|
*/
|
|
30
32
|
|
|
31
|
-
// Lazy-load entities (used for `decodeEntities
|
|
33
|
+
// Lazy-load entities (used for `decodeEntities`, event-handler attribute
|
|
34
|
+
// decoding before `minifyJS`, and `srcdoc` decoding/re-encoding)
|
|
32
35
|
|
|
33
36
|
/** @type {Promise<Function> | undefined} */
|
|
34
37
|
let decodeHTMLStrictPromise;
|
|
@@ -39,6 +42,15 @@ async function getDecodeHTMLStrict() {
|
|
|
39
42
|
return decodeHTMLStrictPromise;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/** @type {Promise<Function> | undefined} */
|
|
46
|
+
let escapeAttributePromise;
|
|
47
|
+
async function getEscapeAttribute() {
|
|
48
|
+
if (!escapeAttributePromise) {
|
|
49
|
+
escapeAttributePromise = import('entities').then(m => m.escapeAttribute);
|
|
50
|
+
}
|
|
51
|
+
return escapeAttributePromise;
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
// Validators
|
|
43
55
|
|
|
44
56
|
/**
|
|
@@ -353,7 +365,7 @@ function canDeleteEmptyAttribute(tag, attrName, attrValue, options) {
|
|
|
353
365
|
if (typeof options.removeEmptyAttributes === 'function') {
|
|
354
366
|
return options.removeEmptyAttributes(attrName, tag);
|
|
355
367
|
}
|
|
356
|
-
return (tag === 'input' && attrName === 'value') ||
|
|
368
|
+
return (tag === 'input' && attrName === 'value') || RE_EMPTY_ATTRIBUTE.test(attrName);
|
|
357
369
|
}
|
|
358
370
|
|
|
359
371
|
/**
|
|
@@ -470,7 +482,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
470
482
|
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
471
483
|
}
|
|
472
484
|
const originalAttrValue = attrValue;
|
|
473
|
-
const cssResult = options.minifyCSS(attrValue, 'inline');
|
|
485
|
+
const cssResult = options.minifyCSS(attrValue, 'inline', options.cssContext);
|
|
474
486
|
if (isThenable(cssResult)) {
|
|
475
487
|
return cssResult
|
|
476
488
|
.then((/** @type {string} */ minified) => {
|
|
@@ -581,7 +593,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
581
593
|
return attrValue;
|
|
582
594
|
}
|
|
583
595
|
const originalAttrValue = attrValue;
|
|
584
|
-
const cssResult = options.minifyCSS(attrValue, 'media');
|
|
596
|
+
const cssResult = options.minifyCSS(attrValue, 'media', options.cssContext);
|
|
585
597
|
if (isThenable(cssResult)) {
|
|
586
598
|
return cssResult.catch((/** @type {Error} */ err) => {
|
|
587
599
|
if (!options.continueOnMinifyError) throw err;
|
|
@@ -593,17 +605,62 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
593
605
|
}
|
|
594
606
|
|
|
595
607
|
if (tag === 'iframe' && attrName === 'srcdoc') {
|
|
596
|
-
// Recursively minify HTML content within `srcdoc` attribute
|
|
597
608
|
// Fast-path: Skip if nothing would change
|
|
598
609
|
if (!shouldMinifyInnerHTML(options)) {
|
|
599
610
|
return attrValue;
|
|
600
611
|
}
|
|
601
|
-
return
|
|
612
|
+
return minifySrcdoc(attrValue, options, minifyHTMLSelf);
|
|
602
613
|
}
|
|
603
614
|
|
|
604
615
|
return attrValue;
|
|
605
616
|
}
|
|
606
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Recursively minify the document an `iframe srcdoc` attribute holds.
|
|
620
|
+
*
|
|
621
|
+
* Browsers resolve character references before parsing `srcdoc`, so an
|
|
622
|
+
* entity-encoded document is decoded first (unless `decodeEntities` already
|
|
623
|
+
* did) and re-encoded afterwards—but only then, so a literal value passes
|
|
624
|
+
* through byte-identical.
|
|
625
|
+
*
|
|
626
|
+
* @param {string} attrValue
|
|
627
|
+
* @param {ProcessedOptions} options
|
|
628
|
+
* @param {Function} minifyHTMLSelf
|
|
629
|
+
* @returns {Promise<string>}
|
|
630
|
+
*/
|
|
631
|
+
async function minifySrcdoc(attrValue, options, minifyHTMLSelf) {
|
|
632
|
+
let markup = attrValue;
|
|
633
|
+
let wasEncoded = false;
|
|
634
|
+
if (!options.decodeEntities && attrValue.indexOf('&') !== -1) {
|
|
635
|
+
const decode = await getDecodeHTMLStrict();
|
|
636
|
+
markup = decode(attrValue);
|
|
637
|
+
wasEncoded = markup !== attrValue;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
let srcdocOptions = options;
|
|
641
|
+
// The inner document sees none of the parent’s markup, so its style sheets
|
|
642
|
+
// minify against the symbols the `srcdoc` content references itself
|
|
643
|
+
if (options.removeUnusedCSS && RE_STYLE_ELEMENT.test(markup)) {
|
|
644
|
+
const decode = markup.indexOf('&') !== -1 ? await getDecodeHTMLStrict() : undefined;
|
|
645
|
+
srcdocOptions = {
|
|
646
|
+
...options,
|
|
647
|
+
cssContext: {
|
|
648
|
+
warned: options.cssContext ? options.cssContext.warned : new Set(),
|
|
649
|
+
usedSymbols: collectUsedSymbols(markup, options.removeUnusedCSS.scripts, /** @type {((text: string) => string) | undefined} */ (decode))
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
const minified = await minifyHTMLSelf(markup, srcdocOptions, true);
|
|
656
|
+
return wasEncoded ? (await getEscapeAttribute())(minified) : minified;
|
|
657
|
+
} catch (err) {
|
|
658
|
+
if (!options.continueOnMinifyError) throw err;
|
|
659
|
+
options.log && options.log(/** @type {Error} */ (err));
|
|
660
|
+
return attrValue;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
607
664
|
/**
|
|
608
665
|
* Choose appropriate quote character for an attribute value
|
|
609
666
|
* @param {string} attrValue - The attribute value
|
package/src/lib/constants.js
CHANGED
|
@@ -16,6 +16,10 @@ const RE_ESCAPE_LT = /</g;
|
|
|
16
16
|
const RE_ATTR_WS_CHECK = /[ \n\r\t\f]/;
|
|
17
17
|
const RE_ATTR_WS_COLLAPSE = /[ \n\r\t\f]+/g;
|
|
18
18
|
const RE_ATTR_WS_TRIM = /^[ \n\r\t\f]+|[ \n\r\t\f]+$/g;
|
|
19
|
+
const RE_STYLE_ELEMENT = /<style[\s/>]/i;
|
|
20
|
+
const RE_EMPTY_ATTRIBUTE = new RegExp(
|
|
21
|
+
'^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
|
|
22
|
+
'?:down|up|over|move|out)|key(?:press|down|up)))$');
|
|
19
23
|
|
|
20
24
|
// Inline element sets for whitespace handling
|
|
21
25
|
|
|
@@ -162,12 +166,6 @@ const trailingElements = new Set(['dt', 'thead']);
|
|
|
162
166
|
|
|
163
167
|
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']);
|
|
164
168
|
|
|
165
|
-
// Empty attribute regex
|
|
166
|
-
|
|
167
|
-
const reEmptyAttribute = new RegExp(
|
|
168
|
-
'^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(' +
|
|
169
|
-
'?:down|up|over|move|out)|key(?:press|down|up)))$');
|
|
170
|
-
|
|
171
169
|
// Special content elements
|
|
172
170
|
|
|
173
171
|
const specialContentElements = new Set(['script', 'style']);
|
|
@@ -192,6 +190,9 @@ export {
|
|
|
192
190
|
RE_ATTR_WS_CHECK,
|
|
193
191
|
RE_ATTR_WS_COLLAPSE,
|
|
194
192
|
RE_ATTR_WS_TRIM,
|
|
193
|
+
RE_STYLE_ELEMENT,
|
|
194
|
+
RE_EMPTY_ATTRIBUTE,
|
|
195
|
+
|
|
195
196
|
// Inline element sets
|
|
196
197
|
inlineElementsToKeepWhitespaceAround,
|
|
197
198
|
inlineElementsToKeepWhitespaceWithin,
|
|
@@ -234,7 +235,6 @@ export {
|
|
|
234
235
|
trailingElements,
|
|
235
236
|
htmlElements,
|
|
236
237
|
|
|
237
|
-
//
|
|
238
|
-
reEmptyAttribute,
|
|
238
|
+
// Special content elements
|
|
239
239
|
specialContentElements
|
|
240
240
|
};
|
package/src/lib/elements.js
CHANGED
|
@@ -137,7 +137,7 @@ function canRemoveElement(tag, attrs) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
* @param {string} str - Tag name or HTML-like element spec (e.g.,
|
|
140
|
+
* @param {string} str - Tag name or HTML-like element spec (e.g., `td` or `<span aria-hidden='true'>`)
|
|
141
141
|
* @param {ProcessedOptions} options - Options object for name normalization
|
|
142
142
|
* @returns {{tag: string, attrs: Object.<string, string|undefined>|null}|null} Parsed spec or null if invalid
|
|
143
143
|
*/
|
|
@@ -203,12 +203,12 @@ function parseRemoveEmptyElementsExcept(input, options) {
|
|
|
203
203
|
if (typeof item === 'string') {
|
|
204
204
|
const spec = parseElementSpec(item, options);
|
|
205
205
|
if (!spec && options.log) {
|
|
206
|
-
options.log('Warning: Unable to parse
|
|
206
|
+
options.log('Warning: Unable to parse `removeEmptyElementsExcept` specification: “' + item + '”');
|
|
207
207
|
}
|
|
208
208
|
return spec;
|
|
209
209
|
}
|
|
210
210
|
if (options.log) {
|
|
211
|
-
options.log('Warning:
|
|
211
|
+
options.log('Warning: `removeEmptyElementsExcept` specification must be a string, received: ' + typeof item);
|
|
212
212
|
}
|
|
213
213
|
return null;
|
|
214
214
|
}).filter(Boolean));
|