html-minifier-next 7.5.2 → 7.6.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 +47 -8
- package/cli.js +31 -11
- package/dist/types/htmlminifier.d.ts +18 -0
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/constants.d.ts +2 -1
- package/dist/types/lib/constants.d.ts.map +1 -1
- package/dist/types/lib/elements.d.ts +1 -1
- package/dist/types/lib/option-definitions.d.ts +4 -0
- package/dist/types/lib/option-definitions.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +32 -4
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/unused-css.d.ts +42 -0
- package/dist/types/lib/unused-css.d.ts.map +1 -0
- package/dist/types/lib/urls.d.ts +1 -1
- package/dist/types/lib/utils.d.ts +10 -2
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/presets.d.ts +1 -1
- package/html-minifier-next.schema.json +7 -3
- package/package.json +10 -9
- package/src/htmlminifier.js +52 -39
- package/src/htmlparser.js +28 -28
- package/src/lib/attributes.js +5 -5
- package/src/lib/constants.js +2 -0
- package/src/lib/elements.js +3 -3
- package/src/lib/option-definitions.js +7 -3
- package/src/lib/options.js +127 -12
- package/src/lib/unused-css.js +394 -0
- package/src/lib/urls.js +4 -4
- package/src/lib/utils.js +24 -1
- package/src/presets.js +1 -1
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,
|
|
@@ -437,6 +439,20 @@ import { processOptions } from './lib/options.js';
|
|
|
437
439
|
*
|
|
438
440
|
* Default: `false`
|
|
439
441
|
*
|
|
442
|
+
* @prop {boolean | {safelist?: Array<string | RegExp>, scripts?: boolean}} [removeUnusedCSS]
|
|
443
|
+
* **Note that this can change how a document renders!**
|
|
444
|
+
*
|
|
445
|
+
* Remove rules from `style` elements whose class or ID selectors the document
|
|
446
|
+
* never references. Requires `minifyCSS` (removal runs through Lightning CSS)
|
|
447
|
+
* and has no effect on `style` or `media` attributes.
|
|
448
|
+
*
|
|
449
|
+
* Class names and IDs are collected from the markup, from `data-*` attributes,
|
|
450
|
+
* and—unless `scripts` is set to `false`—from inline `script` elements. Names
|
|
451
|
+
* that only ever appear in external scripts cannot be detected; list those
|
|
452
|
+
* under `safelist` (strings or regular expressions) to keep them.
|
|
453
|
+
*
|
|
454
|
+
* Default: `false`
|
|
455
|
+
*
|
|
440
456
|
* @prop {boolean | ((tag: string, attrs: HTMLAttribute[]) => void)} [sortAttributes]
|
|
441
457
|
* When true, enables sorting of attributes. If a function is provided it
|
|
442
458
|
* will be used as a custom attribute sorter, which should mutate `attrs`
|
|
@@ -570,28 +586,6 @@ const RE_FOREIGN_ELEMENT_PROBE = /<(?:svg|math)[\s/>]/i;
|
|
|
570
586
|
|
|
571
587
|
// Script merging
|
|
572
588
|
|
|
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
589
|
/**
|
|
596
590
|
* Merge consecutive inline script tags into one (`mergeConsecutiveScripts`).
|
|
597
591
|
* Only merges scripts that are compatible:
|
|
@@ -910,23 +904,23 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
910
904
|
// Memoize `sortClassNames` results—class lists often repeat in templates
|
|
911
905
|
const classNameCache = new LRU(500);
|
|
912
906
|
|
|
913
|
-
options.sortClassNames = function (/** @type {string} */
|
|
907
|
+
options.sortClassNames = function (/** @type {string} */ classNames) {
|
|
914
908
|
// Fast path: Single class (no spaces) needs no sorting
|
|
915
|
-
if (
|
|
916
|
-
return
|
|
909
|
+
if (classNames.indexOf(' ') === -1) {
|
|
910
|
+
return classNames;
|
|
917
911
|
}
|
|
918
912
|
|
|
919
913
|
// Check cache first
|
|
920
|
-
const cached = /** @type {string | undefined} */ (classNameCache.get(
|
|
914
|
+
const cached = /** @type {string | undefined} */ (classNameCache.get(classNames));
|
|
921
915
|
if (cached !== undefined) {
|
|
922
916
|
return cached;
|
|
923
917
|
}
|
|
924
918
|
|
|
925
919
|
// Expand UID tokens back to original content before sorting
|
|
926
920
|
// Fast path: Skip if no HTML comments (UID markers) present
|
|
927
|
-
let expandedValue =
|
|
928
|
-
if (uidReplacePattern &&
|
|
929
|
-
expandedValue =
|
|
921
|
+
let expandedValue = classNames;
|
|
922
|
+
if (uidReplacePattern && classNames.indexOf('<!--') !== -1) {
|
|
923
|
+
expandedValue = classNames.replace(uidReplacePattern, function (/** @type {string} */ _match, /** @type {string} */ index) {
|
|
930
924
|
return ignoredMarkupChunks[+index] || '';
|
|
931
925
|
});
|
|
932
926
|
// Reset `lastIndex` for pattern reuse
|
|
@@ -939,7 +933,7 @@ async function createSortFns(value, options, uidIgnore, uidAttr, ignoredMarkupCh
|
|
|
939
933
|
const result = sorted.join(' ');
|
|
940
934
|
|
|
941
935
|
// Cache the result
|
|
942
|
-
classNameCache.set(
|
|
936
|
+
classNameCache.set(classNames, result);
|
|
943
937
|
return result;
|
|
944
938
|
};
|
|
945
939
|
}
|
|
@@ -1092,13 +1086,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1092
1086
|
|
|
1093
1087
|
if (options.minifyCSS !== identity) {
|
|
1094
1088
|
options.minifyCSS = (function (/** @type {ProcessedOptions['minifyCSS']} */ fn) {
|
|
1095
|
-
return function (/** @type {string} */ text, /** @type {string | undefined} */ type) {
|
|
1089
|
+
return function (/** @type {string} */ text, /** @type {string | undefined} */ type, /** @type {ProcessedOptions['cssContext']} */ context) {
|
|
1096
1090
|
text = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1097
1091
|
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1098
1092
|
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1099
1093
|
});
|
|
1100
1094
|
|
|
1101
|
-
return fn(text, type);
|
|
1095
|
+
return fn(text, type, context);
|
|
1102
1096
|
};
|
|
1103
1097
|
})(options.minifyCSS);
|
|
1104
1098
|
}
|
|
@@ -1148,11 +1142,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1148
1142
|
|
|
1149
1143
|
// Look for trailing whitespaces, bypass any inline tags
|
|
1150
1144
|
function trimTrailingWhitespace(/** @type {number} */ index, /** @type {string} */ nextTag) {
|
|
1151
|
-
for (let
|
|
1145
|
+
for (let prevTag = ''; index >= 0 && canTrimWhitespace(prevTag, emptyAttrs); index--) {
|
|
1152
1146
|
const str = buffer[index] ?? '';
|
|
1153
1147
|
const match = str.match(/^<\/([\w:-]+)>$/);
|
|
1154
1148
|
if (match) {
|
|
1155
|
-
|
|
1149
|
+
prevTag = match[1] ?? '';
|
|
1156
1150
|
} else if (/>$/.test(str) || (buffer[index] = collapseWhitespaceSmart(str, '', nextTag, emptyAttrs, emptyAttrs, options, inlineElements, inlineTextSet))) {
|
|
1157
1151
|
break;
|
|
1158
1152
|
}
|
|
@@ -1398,13 +1392,13 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1398
1392
|
// Collapse if both sides are element/closing tags or HTML comments, and neither is inline
|
|
1399
1393
|
if ((currentTagMatch || currentIsHtmlComment || currentClosingTagMatch) &&
|
|
1400
1394
|
(prevTagMatch || prevIsHtmlComment || prevClosingTagMatch)) {
|
|
1401
|
-
const
|
|
1395
|
+
const currentTagName = currentTagMatch ? resolveName(currentTagMatch[1] ?? '')
|
|
1402
1396
|
: currentClosingTagMatch ? resolveName(currentClosingTagMatch[1] ?? '') : null;
|
|
1403
|
-
const
|
|
1397
|
+
const prevTagName = prevTagMatch ? resolveName(prevTagMatch[1] ?? '')
|
|
1404
1398
|
: prevClosingTagMatch ? resolveName(prevClosingTagMatch[1] ?? '') : null;
|
|
1405
1399
|
|
|
1406
1400
|
// Don’t collapse between inline elements (HTML comments count as non-inline)
|
|
1407
|
-
if (!inlineElements.has(
|
|
1401
|
+
if (!inlineElements.has(currentTagName ?? '') && !inlineElements.has(prevTagName ?? '')) {
|
|
1408
1402
|
// Collapse whitespace respecting context rules
|
|
1409
1403
|
let collapsedText = prevText;
|
|
1410
1404
|
|
|
@@ -1758,7 +1752,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1758
1752
|
text = await options.minifyJS(text, false, isModuleScript);
|
|
1759
1753
|
}
|
|
1760
1754
|
if (needsMinifyCSS) {
|
|
1761
|
-
text = await options.minifyCSS(text);
|
|
1755
|
+
text = await options.minifyCSS(text, undefined, options.cssContext);
|
|
1762
1756
|
}
|
|
1763
1757
|
charsFinalize(text);
|
|
1764
1758
|
})();
|
|
@@ -2094,6 +2088,25 @@ export const minify = async function (value, options) {
|
|
|
2094
2088
|
}
|
|
2095
2089
|
// Work on a shallow copy so per-call reassignments don’t reach the cached base
|
|
2096
2090
|
const processedOptions = /** @type {ProcessedOptions} */ ({ ...processedBase });
|
|
2091
|
+
|
|
2092
|
+
// Warnings are deduplicated per document, so the state has to live on the per-call
|
|
2093
|
+
// copy; the `minifyCSS` closure hangs off the memoized base, shared across calls
|
|
2094
|
+
processedOptions.cssContext = { warned: new Set() };
|
|
2095
|
+
|
|
2096
|
+
// Unused-CSS removal needs the whole document’s symbols before the first `style`
|
|
2097
|
+
// element is minified, so collect them upfront from the raw input. A document
|
|
2098
|
+
// without a style sheet has nothing to remove from, and scanning it would be work
|
|
2099
|
+
// spent on an empty result.
|
|
2100
|
+
if (processedOptions.removeUnusedCSS && RE_STYLE_ELEMENT.test(value)) {
|
|
2101
|
+
processedOptions.cssContext.usedSymbols = collectUsedSymbols(
|
|
2102
|
+
value,
|
|
2103
|
+
processedOptions.removeUnusedCSS.scripts,
|
|
2104
|
+
value.indexOf('&') !== -1
|
|
2105
|
+
? /** @type {(text: string) => string} */ (await getDecodeHTML())
|
|
2106
|
+
: undefined
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2097
2110
|
let result = await minifyHTML(value, processedOptions);
|
|
2098
2111
|
|
|
2099
2112
|
// Post-processing: Merge consecutive inline scripts if enabled
|
package/src/htmlparser.js
CHANGED
|
@@ -154,7 +154,7 @@ function buildAttrRegex(handler) {
|
|
|
154
154
|
|
|
155
155
|
/** @param {HTMLParserHandler} handler */
|
|
156
156
|
function getAttrRegexForHandler(handler) {
|
|
157
|
-
|
|
157
|
+
const cached = attrRegexCache.get(handler);
|
|
158
158
|
if (cached) return cached;
|
|
159
159
|
const compiled = buildAttrRegex(handler);
|
|
160
160
|
attrRegexCache.set(handler, compiled);
|
|
@@ -166,7 +166,7 @@ const attrRegexStickyCache = new WeakMap();
|
|
|
166
166
|
|
|
167
167
|
/** @param {HTMLParserHandler} handler */
|
|
168
168
|
function getAttrRegexStickyForHandler(handler) {
|
|
169
|
-
|
|
169
|
+
const cached = attrRegexStickyCache.get(handler);
|
|
170
170
|
if (cached) return cached;
|
|
171
171
|
const nonSticky = getAttrRegexForHandler(handler);
|
|
172
172
|
// Derive sticky version: Remove `^` anchor, add `y` flag
|
|
@@ -657,12 +657,12 @@ export class HTMLParser {
|
|
|
657
657
|
|
|
658
658
|
// `needle` must already be lowercase
|
|
659
659
|
function findTagInCurrentTable(/** @type {string} */ needle) {
|
|
660
|
-
let
|
|
661
|
-
for (
|
|
662
|
-
const entry = stack[
|
|
660
|
+
let stackIndex;
|
|
661
|
+
for (stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {
|
|
662
|
+
const entry = stack[stackIndex];
|
|
663
663
|
const currentTag = entry?.lowerTag;
|
|
664
664
|
if (currentTag === needle) {
|
|
665
|
-
return
|
|
665
|
+
return stackIndex;
|
|
666
666
|
}
|
|
667
667
|
// Stop searching if hitting a table boundary
|
|
668
668
|
if (currentTag === 'table') {
|
|
@@ -672,25 +672,25 @@ export class HTMLParser {
|
|
|
672
672
|
return -1;
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
-
function parseEndTagAt(/** @type {number} */
|
|
676
|
-
// Close all open elements up to `
|
|
675
|
+
function parseEndTagAt(/** @type {number} */ stackIndex) {
|
|
676
|
+
// Close all open elements up to `stackIndex` (mirrors `parseEndTag`’s core branch);
|
|
677
677
|
// `end` handlers are synchronous—invoked without awaiting, as in `parseEndTag`
|
|
678
|
-
for (let i = stack.length - 1; i >=
|
|
678
|
+
for (let i = stack.length - 1; i >= stackIndex; i--) {
|
|
679
679
|
const entry = /** @type {NonNullable<(typeof stack)[number]>} */ (stack[i]);
|
|
680
680
|
if (handler.end) {
|
|
681
681
|
handler.end(entry.tag, entry.attrs, true);
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
|
-
stack.length =
|
|
685
|
-
lastTag =
|
|
686
|
-
lastTagLower =
|
|
684
|
+
stack.length = stackIndex;
|
|
685
|
+
lastTag = stackIndex ? (stack[stackIndex - 1]?.tag ?? '') : '';
|
|
686
|
+
lastTagLower = stackIndex ? (stack[stackIndex - 1]?.lowerTag ?? '') : '';
|
|
687
687
|
}
|
|
688
688
|
|
|
689
689
|
function closeIfFoundInCurrentTable(/** @type {string} */ tagName) {
|
|
690
|
-
const
|
|
691
|
-
if (
|
|
690
|
+
const stackIndex = findTagInCurrentTable(tagName);
|
|
691
|
+
if (stackIndex >= 0) {
|
|
692
692
|
// Close at the specific index to avoid re-searching
|
|
693
|
-
parseEndTagAt(
|
|
693
|
+
parseEndTagAt(stackIndex);
|
|
694
694
|
return true;
|
|
695
695
|
}
|
|
696
696
|
return false;
|
|
@@ -812,38 +812,38 @@ export class HTMLParser {
|
|
|
812
812
|
|
|
813
813
|
// `needle` must already be lowercase
|
|
814
814
|
function findTag(/** @type {string} */ needle) {
|
|
815
|
-
let
|
|
816
|
-
for (
|
|
817
|
-
if (stack[
|
|
815
|
+
let stackIndex;
|
|
816
|
+
for (stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {
|
|
817
|
+
if (stack[stackIndex]?.lowerTag === needle) {
|
|
818
818
|
break;
|
|
819
819
|
}
|
|
820
820
|
}
|
|
821
|
-
return
|
|
821
|
+
return stackIndex;
|
|
822
822
|
}
|
|
823
823
|
|
|
824
824
|
async function parseEndTag(/** @type {string} */ tag, /** @type {string} */ tagName) {
|
|
825
|
-
let
|
|
825
|
+
let stackIndex;
|
|
826
826
|
const lowerTagName = tagName ? tagName.toLowerCase() : '';
|
|
827
827
|
|
|
828
828
|
// Find the closest opened tag of the same type
|
|
829
829
|
if (tagName) {
|
|
830
|
-
|
|
830
|
+
stackIndex = findTag(lowerTagName);
|
|
831
831
|
} else { // If no tag name is provided, clean shop
|
|
832
|
-
|
|
832
|
+
stackIndex = 0;
|
|
833
833
|
}
|
|
834
834
|
|
|
835
|
-
if (
|
|
835
|
+
if (stackIndex >= 0) {
|
|
836
836
|
// Close all the open elements, up the stack
|
|
837
|
-
for (let i = stack.length - 1; i >=
|
|
837
|
+
for (let i = stack.length - 1; i >= stackIndex; i--) {
|
|
838
838
|
if (handler.end) {
|
|
839
|
-
handler.end(stack[i]?.tag, stack[i]?.attrs, i >
|
|
839
|
+
handler.end(stack[i]?.tag, stack[i]?.attrs, i > stackIndex || !tag);
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
842
|
|
|
843
843
|
// Remove the open elements from the stack
|
|
844
|
-
stack.length =
|
|
845
|
-
lastTag =
|
|
846
|
-
lastTagLower =
|
|
844
|
+
stack.length = stackIndex;
|
|
845
|
+
lastTag = stackIndex ? (stack[stackIndex - 1]?.tag ?? '') : '';
|
|
846
|
+
lastTagLower = stackIndex ? (stack[stackIndex - 1]?.lowerTag ?? '') : '';
|
|
847
847
|
} else if (handler.partialMarkup && tagName) {
|
|
848
848
|
// In partial markup mode, preserve stray end tags
|
|
849
849
|
if (handler.end) {
|
package/src/lib/attributes.js
CHANGED
|
@@ -470,7 +470,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
470
470
|
attrValue = attrValue.replace(/\s*;$/, ';');
|
|
471
471
|
}
|
|
472
472
|
const originalAttrValue = attrValue;
|
|
473
|
-
const cssResult = options.minifyCSS(attrValue, 'inline');
|
|
473
|
+
const cssResult = options.minifyCSS(attrValue, 'inline', options.cssContext);
|
|
474
474
|
if (isThenable(cssResult)) {
|
|
475
475
|
return cssResult
|
|
476
476
|
.then((/** @type {string} */ minified) => {
|
|
@@ -487,7 +487,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
487
487
|
}
|
|
488
488
|
// Sync path (`minifyCSS` disabled—identity function)
|
|
489
489
|
if (cssResult && /^(?:[a-z-]+:[;\s]*)+$/i.test(cssResult)) return '';
|
|
490
|
-
return cssResult
|
|
490
|
+
return cssResult ?? attrValue;
|
|
491
491
|
}
|
|
492
492
|
return attrValue;
|
|
493
493
|
}
|
|
@@ -581,7 +581,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
581
581
|
return attrValue;
|
|
582
582
|
}
|
|
583
583
|
const originalAttrValue = attrValue;
|
|
584
|
-
const cssResult = options.minifyCSS(attrValue, 'media');
|
|
584
|
+
const cssResult = options.minifyCSS(attrValue, 'media', options.cssContext);
|
|
585
585
|
if (isThenable(cssResult)) {
|
|
586
586
|
return cssResult.catch((/** @type {Error} */ err) => {
|
|
587
587
|
if (!options.continueOnMinifyError) throw err;
|
|
@@ -589,7 +589,7 @@ function cleanAttributeValue(tag, attrName, attrValue, options, attrs, minifyHTM
|
|
|
589
589
|
return originalAttrValue;
|
|
590
590
|
});
|
|
591
591
|
}
|
|
592
|
-
return cssResult
|
|
592
|
+
return cssResult ?? attrValue;
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
if (tag === 'iframe' && attrName === 'srcdoc') {
|
|
@@ -635,7 +635,7 @@ function chooseAttributeQuote(attrValue, options) {
|
|
|
635
635
|
*/
|
|
636
636
|
function normalizeAttr(attr, attrs, tag, options, minifyHTML) {
|
|
637
637
|
const attrName = options.name(attr.name);
|
|
638
|
-
|
|
638
|
+
const attrValue = attr.value;
|
|
639
639
|
|
|
640
640
|
// Entity decoding requires a lazy import—async only when `&` is present
|
|
641
641
|
if (options.decodeEntities && attrValue && attrValue.indexOf('&') !== -1) {
|
package/src/lib/constants.js
CHANGED
|
@@ -16,6 +16,7 @@ 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;
|
|
19
20
|
|
|
20
21
|
// Inline element sets for whitespace handling
|
|
21
22
|
|
|
@@ -192,6 +193,7 @@ export {
|
|
|
192
193
|
RE_ATTR_WS_CHECK,
|
|
193
194
|
RE_ATTR_WS_COLLAPSE,
|
|
194
195
|
RE_ATTR_WS_TRIM,
|
|
196
|
+
RE_STYLE_ELEMENT,
|
|
195
197
|
// Inline element sets
|
|
196
198
|
inlineElementsToKeepWhitespaceAround,
|
|
197
199
|
inlineElementsToKeepWhitespaceWithin,
|
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));
|
|
@@ -104,15 +104,15 @@ const optionDefinitions = {
|
|
|
104
104
|
},
|
|
105
105
|
minifyCSS: {
|
|
106
106
|
description: 'Minify CSS in `style` elements and attributes (uses Lightning CSS)',
|
|
107
|
-
type: '
|
|
107
|
+
type: 'jsonObject'
|
|
108
108
|
},
|
|
109
109
|
minifyJS: {
|
|
110
110
|
description: 'Minify JavaScript in `script` elements and event attributes (uses Terser or SWC; pass `{"engine": "swc"}` for SWC)',
|
|
111
|
-
type: '
|
|
111
|
+
type: 'jsonObject'
|
|
112
112
|
},
|
|
113
113
|
minifySVG: {
|
|
114
114
|
description: 'Minify SVG elements (uses SVGO)',
|
|
115
|
-
type: '
|
|
115
|
+
type: 'jsonObject'
|
|
116
116
|
},
|
|
117
117
|
minifyURLs: {
|
|
118
118
|
description: 'Minify URLs in various attributes',
|
|
@@ -178,6 +178,10 @@ const optionDefinitions = {
|
|
|
178
178
|
description: 'Remove space between attributes whenever possible; note that this will result in invalid HTML',
|
|
179
179
|
type: 'boolean'
|
|
180
180
|
},
|
|
181
|
+
removeUnusedCSS: {
|
|
182
|
+
description: 'Remove rules from `style` elements whose class or ID selectors the document never references (requires `--minify-css`); note that class names only applied by external scripts cannot be detected—use `{"safelist": […]}` for those',
|
|
183
|
+
type: 'jsonObject'
|
|
184
|
+
},
|
|
181
185
|
sortAttributes: {
|
|
182
186
|
description: 'Sort attributes by frequency',
|
|
183
187
|
type: 'boolean'
|