html-minifier 4.0.0 → 5.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/LICENSE.txt +23 -0
- package/README.md +851 -110
- package/cli.js +991 -207
- package/dist/types/htmlminifier.d.ts +629 -0
- package/dist/types/htmlminifier.d.ts.map +1 -0
- package/dist/types/htmlparser.d.ts +28 -0
- package/dist/types/htmlparser.d.ts.map +1 -0
- package/dist/types/lib/attributes.d.ts +87 -0
- package/dist/types/lib/attributes.d.ts.map +1 -0
- package/dist/types/lib/constants.d.ts +421 -0
- package/dist/types/lib/constants.d.ts.map +1 -0
- package/dist/types/lib/content.d.ts +44 -0
- package/dist/types/lib/content.d.ts.map +1 -0
- package/dist/types/lib/elements.d.ts +48 -0
- package/dist/types/lib/elements.d.ts.map +1 -0
- package/dist/types/lib/fragments.d.ts +47 -0
- package/dist/types/lib/fragments.d.ts.map +1 -0
- package/dist/types/lib/option-definitions.d.ts +33 -0
- package/dist/types/lib/option-definitions.d.ts.map +1 -0
- package/dist/types/lib/options.d.ts +93 -0
- package/dist/types/lib/options.d.ts.map +1 -0
- package/dist/types/lib/unused-css.d.ts +76 -0
- package/dist/types/lib/unused-css.d.ts.map +1 -0
- package/dist/types/lib/urls.d.ts +15 -0
- package/dist/types/lib/urls.d.ts.map +1 -0
- package/dist/types/lib/utils.d.ts +132 -0
- package/dist/types/lib/utils.d.ts.map +1 -0
- package/dist/types/lib/whitespace.d.ts +97 -0
- package/dist/types/lib/whitespace.d.ts.map +1 -0
- package/dist/types/presets.d.ts +50 -0
- package/dist/types/presets.d.ts.map +1 -0
- package/dist/types/tokenchain.d.ts +36 -0
- package/dist/types/tokenchain.d.ts.map +1 -0
- package/html-minifier.schema.json +322 -0
- package/package.json +70 -58
- package/src/htmlminifier.js +2845 -1107
- package/src/htmlparser.js +1163 -452
- package/src/lib/attributes.js +1196 -0
- package/src/lib/constants.js +521 -0
- package/src/lib/content.js +237 -0
- package/src/lib/elements.js +268 -0
- package/src/lib/file-pool.js +193 -0
- package/src/lib/file-write.js +87 -0
- package/src/lib/fragments.js +340 -0
- package/src/lib/option-definitions.js +246 -0
- package/src/lib/options.js +841 -0
- package/src/lib/unused-css.js +499 -0
- package/src/lib/urls.js +146 -0
- package/src/lib/utils.js +1026 -0
- package/src/lib/whitespace.js +431 -0
- package/src/presets.js +58 -0
- package/src/tokenchain.js +142 -56
- package/LICENSE +0 -22
- package/sample-cli-config-file.conf +0 -40
- package/src/utils.js +0 -18
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options processing and per-document minification state
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createUrlMinifier } from './urls.js';
|
|
6
|
+
import { LRU, MAX_CACHE_ENTRY_SIZE, stableStringify, hashContent, identity, isThenable, lowercase, paramCase, replaceAsync, parseRegExp, describeQuantifierRisk, lostFlag, findTagEnd } from './utils.js';
|
|
7
|
+
import { RE_TRAILING_SEMICOLON, MISSING_DEPENDENCY, svgoPluginsInline, namespaceSVG, namespaceMathML, namespaceXLink } from './constants.js';
|
|
8
|
+
import { canCollapseWhitespace, canTrimWhitespace } from './whitespace.js';
|
|
9
|
+
import { wrapCSS, unwrapCSS } from './content.js';
|
|
10
|
+
import { createSelectorPruner, findUnusedSymbols, normalizeUnusedCSSOptions } from './unused-css.js';
|
|
11
|
+
import { getPreset, getPresetNames } from '../presets.js';
|
|
12
|
+
import { optionDefinitions, optionDefaults } from './option-definitions.js';
|
|
13
|
+
|
|
14
|
+
/** @import { MinifierOptions, HTMLAttribute } from '../htmlminifier.js' */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Per-document state handed to `minifyCSS`. Its closure hangs off the memoized
|
|
18
|
+
* options object that every `minify()` call with those options shares, so state
|
|
19
|
+
* belonging to one document has to be passed in rather than captured.
|
|
20
|
+
*
|
|
21
|
+
* @typedef {{usedSymbols?: Set<string>, usedElements?: Set<string>, usedElementsKey?: string, warned: Set<string>}} CSSContext
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minified style sheet plus the warnings its transform produced, cached together
|
|
26
|
+
* so that a cache hit can report what the transform reported
|
|
27
|
+
*
|
|
28
|
+
* @typedef {{css: string, warnings: string[]}} CSSResult
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Options object produced by `processOptions` and consumed by `minifyHTML` and
|
|
33
|
+
* the lib/ helpers; normalization guarantees that the function-valued options
|
|
34
|
+
* below are always present (defaulting to identity/built-in functions), and
|
|
35
|
+
* minification adds writable internal state on top of the public options
|
|
36
|
+
* (set on prototype-chain forks during SVG/MathML namespace transitions)
|
|
37
|
+
*
|
|
38
|
+
* @typedef {Omit<MinifierOptions, 'preset' | 'canCollapseWhitespace' | 'canTrimWhitespace' | 'ignoreCustomComments' | 'log' | 'minifyCSS' | 'minifyJS' | 'minifyURLs' | 'minifySVG' | 'removeUnusedCSS'> & {
|
|
39
|
+
* name: (name: string) => string,
|
|
40
|
+
* log: (message: any) => unknown,
|
|
41
|
+
* ignoreCustomComments: RegExp[],
|
|
42
|
+
* canCollapseWhitespace: (tag: string, attrs: HTMLAttribute[], defaultFn: (tag: string) => boolean) => boolean,
|
|
43
|
+
* canTrimWhitespace: (tag: string, attrs: HTMLAttribute[], defaultFn: (tag: string) => boolean) => boolean,
|
|
44
|
+
* minifyCSS: (text: string, type?: string, context?: CSSContext) => string | Promise<string>,
|
|
45
|
+
* minifyJS: (text: string, inline?: boolean, isModule?: boolean) => string | Promise<string>,
|
|
46
|
+
* minifyURLs: (text: string) => string | Promise<string>,
|
|
47
|
+
* minifySVG: ((svgContent: string) => string | Promise<string>) | null,
|
|
48
|
+
* canMinifyCSS: ((text: string, type?: 'inline' | 'media') => boolean) | null,
|
|
49
|
+
* canMinifyJS: ((text: string, inline?: boolean) => boolean) | null,
|
|
50
|
+
* canMinifySVG: ((text: string) => boolean) | null,
|
|
51
|
+
* removeUnusedCSS: {safelist: Array<string | RegExp>, scripts: boolean} | null,
|
|
52
|
+
* cssContext?: CSSContext,
|
|
53
|
+
* documentState?: {hasMathML: boolean},
|
|
54
|
+
* baseState?: {hasBase: boolean | undefined, source: string},
|
|
55
|
+
* parallelJS?: boolean,
|
|
56
|
+
* nameParent?: (name: string) => string,
|
|
57
|
+
* namesAsWritten?: boolean,
|
|
58
|
+
* keepClosingSlashHTML?: boolean,
|
|
59
|
+
* insideSVG?: boolean,
|
|
60
|
+
* insideForeignContent?: boolean,
|
|
61
|
+
* namespaceForeign?: 'svg' | 'math'
|
|
62
|
+
* }} ProcessedOptions
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
const RE_SCOPE_AT_RULE = /@scope\b/i;
|
|
66
|
+
const RE_SVGO_EMPTY_ATTRIBUTE = / ([^\s"'<>/=]+)=""/g;
|
|
67
|
+
const RE_SVG_START = /^<svg(?=[\s/>])/;
|
|
68
|
+
const RE_QUOTED_VALUE = /"[^"]*"|'[^']*'/g;
|
|
69
|
+
const RE_XLINK_DECLARATION = /\sxmlns:xlink(?=[\s=/>]|$)/;
|
|
70
|
+
// The declaration as SVGO writes it, and where it may be, outside comments and
|
|
71
|
+
// CDATA sections, which SVGO writes as is (values and text escape `"`)
|
|
72
|
+
const XLINK_DECLARATION_SVGO = ' xmlns:xlink="' + namespaceXLink + '"';
|
|
73
|
+
const RE_XLINK_DECLARATION_SVGO = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>| xmlns:xlink="[^"]*"/g;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* SVGO plugin dropping the `xmlns` declarations HTML reads anyway on `svg` and `math` elements
|
|
77
|
+
* (`xmlns:xlink` goes after SVGO, as each pass of `multipass` reads XML again, which needs it)
|
|
78
|
+
*/
|
|
79
|
+
const pluginNamespacesHTML = {
|
|
80
|
+
name: 'removeNamespacesHTML',
|
|
81
|
+
fn: () => ({
|
|
82
|
+
element: {
|
|
83
|
+
enter: (/** @type {{name: string, attributes: Record<string, string>}} */ node) => {
|
|
84
|
+
const attrs = node.attributes;
|
|
85
|
+
if (attrs.xmlns === (node.name === 'math' ? namespaceMathML : node.name === 'svg' ? namespaceSVG : undefined)) {
|
|
86
|
+
delete attrs.xmlns;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** @param {ProcessedOptions} options */
|
|
94
|
+
function shouldMinifyInnerHTML(options) {
|
|
95
|
+
return Boolean(
|
|
96
|
+
options.collapseWhitespace ||
|
|
97
|
+
options.removeComments ||
|
|
98
|
+
options.removeOptionalTags ||
|
|
99
|
+
options.minifyJS !== identity ||
|
|
100
|
+
options.minifyCSS !== identity ||
|
|
101
|
+
options.minifyJSON ||
|
|
102
|
+
options.minifyURLs !== identity ||
|
|
103
|
+
options.minifySVG
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Persistent per-site URL minification caches—results depend only on the `site`
|
|
108
|
+
// configuration, so entries can be shared across `minify()` calls; bounded so
|
|
109
|
+
// batch runs with many distinct sites can’t grow memory without limit
|
|
110
|
+
const MAX_URL_CACHE_SITES = 20;
|
|
111
|
+
/** @type {Map<string, LRU>} */
|
|
112
|
+
const urlMinifyCaches = new Map();
|
|
113
|
+
|
|
114
|
+
/** @param {string} site */
|
|
115
|
+
function getUrlMinifyCache(site) {
|
|
116
|
+
let cache = urlMinifyCaches.get(site);
|
|
117
|
+
if (!cache) {
|
|
118
|
+
if (urlMinifyCaches.size >= MAX_URL_CACHE_SITES) {
|
|
119
|
+
const oldestSite = urlMinifyCaches.keys().next().value;
|
|
120
|
+
if (oldestSite !== undefined) {
|
|
121
|
+
urlMinifyCaches.delete(oldestSite);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
cache = new LRU(500);
|
|
125
|
+
urlMinifyCaches.set(site, cache);
|
|
126
|
+
}
|
|
127
|
+
return cache;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// User-facing option keys that are valid but not listed in `optionDefinitions`
|
|
131
|
+
const optionKeysExtra = new Set(['preset', 'log', 'canCollapseWhitespace', 'canTrimWhitespace', 'canMinifyCSS', 'canMinifyJS', 'canMinifySVG']);
|
|
132
|
+
|
|
133
|
+
// Unknown option keys and preset names already warned about—warn once per
|
|
134
|
+
// key per process, so repeated `minify` calls (e.g., batch runs) don’t flood STDERR
|
|
135
|
+
const optionKeysWarned = new Set();
|
|
136
|
+
const presetNamesWarned = new Set();
|
|
137
|
+
// Custom fragments whose shape risks ReDoS, warned about once per pattern per process
|
|
138
|
+
const customFragmentsWarned = new Set();
|
|
139
|
+
// Options set without the option they need, warned about once per message per process
|
|
140
|
+
const dependenciesWarned = new Set();
|
|
141
|
+
// Unused-CSS configuration that does not hold up, warned about once per message per process
|
|
142
|
+
const unusedCSSWarned = new Set();
|
|
143
|
+
// Object-valued options handed a string, warned about once per distinct value
|
|
144
|
+
const stringValuesWarned = new Set();
|
|
145
|
+
|
|
146
|
+
// Clause for options that only the built-in minifier behind `key` reads
|
|
147
|
+
const replacedByOwnFunction = (/** @type {string} */ key) =>
|
|
148
|
+
(/** @type {Record<string, any>} */ input) => typeof input[key] === 'function' && 'which a function of your own replaces';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Options that do nothing on their own, and the option each one needs to take effect
|
|
152
|
+
*
|
|
153
|
+
* Cache sizes are deliberately absent: They never change output, so they are
|
|
154
|
+
* configuration for a subsystem rather than a transform that silently fails.
|
|
155
|
+
*
|
|
156
|
+
* @typedef {object} OptionDependency
|
|
157
|
+
* @property {string} option
|
|
158
|
+
* @property {string} requires
|
|
159
|
+
* @property {(input: Record<string, any>) => string | false} [unusable] Clause saying why the prerequisite does not count even though it is set
|
|
160
|
+
* @property {boolean} [clear] Whether to null the option out, where leaving it set would cost work downstream
|
|
161
|
+
*
|
|
162
|
+
* @type {OptionDependency[]}
|
|
163
|
+
*/
|
|
164
|
+
const optionDependencies = [
|
|
165
|
+
{ option: 'canMinifyCSS', requires: 'minifyCSS', unusable: replacedByOwnFunction('minifyCSS'), clear: true },
|
|
166
|
+
{ option: 'canMinifyJS', requires: 'minifyJS', unusable: replacedByOwnFunction('minifyJS'), clear: true },
|
|
167
|
+
{ option: 'canMinifySVG', requires: 'minifySVG', unusable: replacedByOwnFunction('minifySVG'), clear: true },
|
|
168
|
+
{ option: 'collapseInlineTagWhitespace', requires: 'collapseWhitespace' },
|
|
169
|
+
{ option: 'collapseNoBreakSpaces', requires: 'collapseWhitespace' },
|
|
170
|
+
{ option: 'conservativeCollapse', requires: 'collapseWhitespace' },
|
|
171
|
+
{ option: 'customEventAttributes', requires: 'minifyJS' },
|
|
172
|
+
{ option: 'inlineCustomElements', requires: 'collapseWhitespace' },
|
|
173
|
+
{ option: 'noNewlinesBeforeTagClose', requires: 'maxLineLength' },
|
|
174
|
+
{ option: 'preserveLineBreaks', requires: 'collapseWhitespace' },
|
|
175
|
+
{ option: 'removeEmptyElementsExcept', requires: 'removeEmptyElements' },
|
|
176
|
+
{
|
|
177
|
+
option: 'removeUnusedCSS',
|
|
178
|
+
requires: 'minifyCSS',
|
|
179
|
+
// Removal rides along with Lightning CSS, which a function of one’s own replaces
|
|
180
|
+
unusable: replacedByOwnFunction('minifyCSS'),
|
|
181
|
+
clear: true
|
|
182
|
+
},
|
|
183
|
+
{ option: 'trimCustomFragments', requires: 'collapseWhitespace' }
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
// An empty array is a value the user supplied but did not populate, and so asks for
|
|
187
|
+
// nothing; `identity` is what a minifier left off resolves to
|
|
188
|
+
/** @param {unknown} value */
|
|
189
|
+
function isRequested(value) {
|
|
190
|
+
if (value === identity) return false;
|
|
191
|
+
return Array.isArray(value) ? value.length > 0 : Boolean(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// A `canMinify…` hook that returns false leaves the content as is, as does one that
|
|
195
|
+
// throws under `continueOnMinifyError`; hooks are synchronous, so a promise counts as an error
|
|
196
|
+
/**
|
|
197
|
+
* @param {ProcessedOptions} options - Processed options
|
|
198
|
+
* @param {'canMinifyCSS' | 'canMinifyJS' | 'canMinifySVG'} name - Hook to consult
|
|
199
|
+
* @param {...any} args - Arguments for the hook
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
202
|
+
function declinesMinify(options, name, ...args) {
|
|
203
|
+
const hook = /** @type {((...args: any[]) => unknown) | null} */ (options[name]);
|
|
204
|
+
if (!hook) return false;
|
|
205
|
+
try {
|
|
206
|
+
const result = hook(...args);
|
|
207
|
+
if (isThenable(result)) {
|
|
208
|
+
// Keep a later rejection from surfacing as unhandled
|
|
209
|
+
/** @type {Promise<unknown>} */ (result).catch(() => {});
|
|
210
|
+
throw new TypeError(`\`${name}\` must return a boolean, not a promise`);
|
|
211
|
+
}
|
|
212
|
+
return !result;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (!options.continueOnMinifyError) {
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
options.log(err);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Cache key for minifier output: the content, hashed where too large for a `Map` key, then
|
|
223
|
+
// what else the output depends on; `undefined` where the content is too large to cache
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} content
|
|
226
|
+
* @param {string} suffix
|
|
227
|
+
*/
|
|
228
|
+
function minifyCacheKey(content, suffix) {
|
|
229
|
+
if (content.length > MAX_CACHE_ENTRY_SIZE) return undefined;
|
|
230
|
+
return (content.length > 2048 ? hashContent(content) : content) + '|' + suffix;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Runs `compute` once per key: Concurrent callers share the in-flight promise, which the
|
|
234
|
+
// result replaces once settled; a failure leaves no entry behind
|
|
235
|
+
/**
|
|
236
|
+
* @template T
|
|
237
|
+
* @param {LRU} cache
|
|
238
|
+
* @param {string | undefined} key
|
|
239
|
+
* @param {() => Promise<T>} compute
|
|
240
|
+
* @returns {Promise<T>}
|
|
241
|
+
*/
|
|
242
|
+
async function cachedMinify(cache, key, compute) {
|
|
243
|
+
if (key === undefined) return compute();
|
|
244
|
+
const cached = /** @type {T | Promise<T> | undefined} */ (cache.get(key));
|
|
245
|
+
if (cached !== undefined) return cached;
|
|
246
|
+
const inFlight = compute();
|
|
247
|
+
cache.set(key, inFlight);
|
|
248
|
+
try {
|
|
249
|
+
const resolved = await inFlight;
|
|
250
|
+
cache.set(key, resolved);
|
|
251
|
+
return resolved;
|
|
252
|
+
} catch (err) {
|
|
253
|
+
cache.delete(key);
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** @param {unknown} err - Error raised while minifying */
|
|
259
|
+
function isMissingDependency(err) {
|
|
260
|
+
return Boolean(err) && /** @type {any} */ (err).code === MISSING_DEPENDENCY;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// An unset (falsy) engine takes the fallback and names match lowercase; anything else
|
|
264
|
+
// passes through untouched so that validation reports it instead of a type error
|
|
265
|
+
/**
|
|
266
|
+
* @param {unknown} engine - Configured engine name
|
|
267
|
+
* @param {string} fallback - Engine to use when none is configured
|
|
268
|
+
*/
|
|
269
|
+
function normalizeEngine(engine, fallback) {
|
|
270
|
+
if (!engine) return fallback;
|
|
271
|
+
return typeof engine === 'string' ? engine.toLowerCase() : engine;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** @param {unknown} engine - Engine name as it should read in an error message */
|
|
275
|
+
function describeEngine(engine) {
|
|
276
|
+
if (typeof engine === 'string') return engine;
|
|
277
|
+
try {
|
|
278
|
+
return JSON.stringify(engine) ?? String(engine);
|
|
279
|
+
} catch {
|
|
280
|
+
// BigInt and cyclic values do not serialize
|
|
281
|
+
return String(engine);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @param {MinifierOptions} inputOptions - User-provided options
|
|
287
|
+
* @param {{getLightningCSS: Function, getTerser: Function, getSwc: Function, getSvgo: Function, cssMinifyCache: LRU, jsMinifyCache: LRU, svgMinifyCache: LRU}} deps - Dependencies from htmlminifier.js
|
|
288
|
+
* @returns {ProcessedOptions} Normalized options with defaults applied
|
|
289
|
+
*/
|
|
290
|
+
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getSvgo, cssMinifyCache, jsMinifyCache, svgMinifyCache }) => {
|
|
291
|
+
/** @type {ProcessedOptions} */
|
|
292
|
+
const options = {
|
|
293
|
+
name: lowercase,
|
|
294
|
+
namesAsWritten: false,
|
|
295
|
+
canCollapseWhitespace,
|
|
296
|
+
canTrimWhitespace,
|
|
297
|
+
...optionDefaults,
|
|
298
|
+
log: identity,
|
|
299
|
+
minifyCSS: identity,
|
|
300
|
+
minifyJS: identity,
|
|
301
|
+
minifyURLs: identity,
|
|
302
|
+
minifySVG: null,
|
|
303
|
+
canMinifyCSS: null,
|
|
304
|
+
canMinifyJS: null,
|
|
305
|
+
canMinifySVG: null,
|
|
306
|
+
removeUnusedCSS: null
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const parseRegExpArray = (/** @type {unknown} */ arr) => {
|
|
310
|
+
return Array.isArray(arr) ? arr.map(parseRegExp) : [];
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Helper for nested arrays (e.g., `customAttrSurround: [[start, end], …]`)
|
|
314
|
+
const parseNestedRegExpArray = (/** @type {unknown} */ arr) => {
|
|
315
|
+
if (!Array.isArray(arr)) return [];
|
|
316
|
+
return arr.map(item => {
|
|
317
|
+
if (Array.isArray(item)) {
|
|
318
|
+
return item.map(parseRegExp);
|
|
319
|
+
}
|
|
320
|
+
return parseRegExp(item);
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// Route warnings through the user-provided `log` hook so API consumers can
|
|
325
|
+
// capture or suppress them consistently; fall back to `console.warn`.
|
|
326
|
+
//
|
|
327
|
+
// Message convention: Strings that may reach the console unsolicited (via
|
|
328
|
+
// this fallback) carry the “HTML Minifier: ” prefix for attribution and
|
|
329
|
+
// no “Warning:” label—the channel already conveys severity. Strings sent
|
|
330
|
+
// only to an explicitly provided `log` hook carry “Warning: ” instead, since
|
|
331
|
+
// the hook mixes severities (info strings, warnings, `Error` objects) and
|
|
332
|
+
// its consumer already knows the source.
|
|
333
|
+
const warn = typeof inputOptions.log === 'function' ? inputOptions.log : console.warn;
|
|
334
|
+
|
|
335
|
+
// Warn about unrecognized options—catches typos and options removed in earlier versions
|
|
336
|
+
Object.keys(inputOptions).forEach(function (key) {
|
|
337
|
+
if (!Object.hasOwn(optionDefinitions, key) && !optionKeysExtra.has(key) && !optionKeysWarned.has(key)) {
|
|
338
|
+
optionKeysWarned.add(key);
|
|
339
|
+
warn(`HTML Minifier: Ignoring unknown or deprecated option \`${key}\` (see README for available options)`);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Merge preset with user options (which take precedence), so all values go through normalization
|
|
344
|
+
let effectiveInput = inputOptions;
|
|
345
|
+
if (inputOptions.preset) {
|
|
346
|
+
const preset = getPreset(inputOptions.preset);
|
|
347
|
+
if (preset) {
|
|
348
|
+
effectiveInput = { ...preset, ...inputOptions };
|
|
349
|
+
} else if (!presetNamesWarned.has(inputOptions.preset)) {
|
|
350
|
+
presetNamesWarned.add(inputOptions.preset);
|
|
351
|
+
const available = getPresetNames().join(', ');
|
|
352
|
+
warn(`HTML Minifier: Unknown preset “${inputOptions.preset}”; available presets: ${available}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Escape hatch for the loop below, which reads and assigns user-provided values by dynamic key
|
|
357
|
+
const optionsDynamic = /** @type {Record<string, any>} */ (options);
|
|
358
|
+
|
|
359
|
+
Object.keys(effectiveInput).forEach(function (key) {
|
|
360
|
+
const option = /** @type {Record<string, any>} */ (effectiveInput)[key];
|
|
361
|
+
|
|
362
|
+
// Skip `preset` (already processed) and unrecognized keys (warned about above)—
|
|
363
|
+
// the latter also keeps internal keys from being overridden
|
|
364
|
+
if (key === 'preset' || (!Object.hasOwn(optionDefinitions, key) && !optionKeysExtra.has(key))) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// A string carries no configuration for these options. The CLI parses config
|
|
369
|
+
// values as JSON first, so only a value that is not JSON reaches this from
|
|
370
|
+
// there. (`minifyURLs` is deliberately excluded—there, a string names the site.)
|
|
371
|
+
const definition = optionDefinitions[key];
|
|
372
|
+
if (typeof option === 'string' && definition?.type === 'jsonObject') {
|
|
373
|
+
const message = `HTML Minifier: Ignoring \`${key}\`—it takes a boolean or an object, not a string (“${option}”)`;
|
|
374
|
+
if (!stringValuesWarned.has(message)) {
|
|
375
|
+
stringValuesWarned.add(message);
|
|
376
|
+
warn(message);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (key === 'caseSensitive') {
|
|
382
|
+
// HTML names are read in lowercase, like browsers do, and only written as in the source
|
|
383
|
+
if (option) {
|
|
384
|
+
options.namesAsWritten = true;
|
|
385
|
+
}
|
|
386
|
+
} else if (key === 'removeUnusedCSS') {
|
|
387
|
+
optionsDynamic.removeUnusedCSS = normalizeUnusedCSSOptions(option, message => {
|
|
388
|
+
if (!unusedCSSWarned.has(message)) {
|
|
389
|
+
unusedCSSWarned.add(message);
|
|
390
|
+
warn(`HTML Minifier: ${message}`);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
} else if (key === 'log') {
|
|
394
|
+
if (typeof option === 'function') {
|
|
395
|
+
options.log = option;
|
|
396
|
+
}
|
|
397
|
+
} else if (key === 'canMinifyCSS' || key === 'canMinifyJS' || key === 'canMinifySVG') {
|
|
398
|
+
if (typeof option === 'function') {
|
|
399
|
+
optionsDynamic[key] = option;
|
|
400
|
+
}
|
|
401
|
+
} else if (key === 'minifyCSS' && typeof option !== 'function') {
|
|
402
|
+
if (!option) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
407
|
+
|
|
408
|
+
options.minifyCSS = async function (/** @type {string} */ text, /** @type {string | undefined} */ type, /** @type {CSSContext | undefined} */ context) {
|
|
409
|
+
// Fast path: Nothing to minify
|
|
410
|
+
if (!text || !text.trim()) {
|
|
411
|
+
return text;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (declinesMinify(options, 'canMinifyCSS', text, type)) {
|
|
415
|
+
return text;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Warnings are cached with the result and replayed on every hit, so a second document with
|
|
419
|
+
// the same defect hears about it, too (`context.warned` keeps one document from repeating
|
|
420
|
+
// itself), and the output doesn’t depend on cache size or eviction. They are cached even
|
|
421
|
+
// with the no-op `log`, so a later document with a `log` hook still gets them.
|
|
422
|
+
const report = (/** @type {string[]} */ messages) => {
|
|
423
|
+
if (!messages.length || options.log === identity) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const warned = context?.warned;
|
|
427
|
+
for (const message of messages) {
|
|
428
|
+
if (warned) {
|
|
429
|
+
if (warned.has(message)) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
warned.add(message);
|
|
433
|
+
}
|
|
434
|
+
options.log(message);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Skip the costly `replaceAsync` when URL minification is off
|
|
439
|
+
if (options.minifyURLs !== identity) {
|
|
440
|
+
text = await replaceAsync(
|
|
441
|
+
text,
|
|
442
|
+
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
443
|
+
async function (/** @type {string} */ match, /** @type {string} */ prefix, /** @type {string | undefined} */ dq, /** @type {string | undefined} */ sq, /** @type {string | undefined} */ unq, /** @type {string} */ suffix) {
|
|
444
|
+
const quote = dq != null ? '"' : (sq != null ? "'" : '');
|
|
445
|
+
const url = dq ?? sq ?? unq ?? '';
|
|
446
|
+
try {
|
|
447
|
+
const out = await options.minifyURLs(url);
|
|
448
|
+
return prefix + quote + (typeof out === 'string' ? out : url) + quote + suffix;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (!options.continueOnMinifyError) {
|
|
451
|
+
throw err;
|
|
452
|
+
}
|
|
453
|
+
options.log(err);
|
|
454
|
+
return match;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Unused-symbol removal applies to style sheets only
|
|
461
|
+
const unusedCSSConfig = type === undefined ? options.removeUnusedCSS : undefined;
|
|
462
|
+
const unusedSymbols = (unusedCSSConfig && context?.usedSymbols)
|
|
463
|
+
? findUnusedSymbols(text, context.usedSymbols, unusedCSSConfig.safelist)
|
|
464
|
+
: undefined;
|
|
465
|
+
// Selectors are pruned one by one, too—unless a `visitor` of the caller’s own is
|
|
466
|
+
// set, or the sheet uses `@scope`, whose preludes the visitor reaches as well and
|
|
467
|
+
// where a dropped limit would widen the scope
|
|
468
|
+
const pruneSelectors = Boolean(unusedSymbols && context?.usedElements) &&
|
|
469
|
+
!('visitor' in lightningCssOptions) && !RE_SCOPE_AT_RULE.test(text);
|
|
470
|
+
|
|
471
|
+
// Cache key: Content + type + options signature; large inputs are hashed to avoid
|
|
472
|
+
// huge Map keys. The symbol list belongs in the signature: The cache outlives a
|
|
473
|
+
// single `minify()` call, so identical style sheets in differently marked-up
|
|
474
|
+
// documents must not share an entry; so do the element set and the safelist
|
|
475
|
+
// where the selector visitor reads them.
|
|
476
|
+
const inputCSS = wrapCSS(text, type);
|
|
477
|
+
const cssSig = stableStringify({
|
|
478
|
+
type,
|
|
479
|
+
opts: lightningCssOptions,
|
|
480
|
+
cont: !!options.continueOnMinifyError,
|
|
481
|
+
unused: unusedSymbols && unusedSymbols.length ? unusedSymbols.slice().sort() : undefined,
|
|
482
|
+
elements: pruneSelectors ? context?.usedElementsKey : undefined,
|
|
483
|
+
safelist: pruneSelectors ? unusedCSSConfig?.safelist : undefined
|
|
484
|
+
});
|
|
485
|
+
const cssKey = minifyCacheKey(inputCSS, type + '|' + cssSig);
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
const resolved = await cachedMinify(cssMinifyCache, cssKey, async () => {
|
|
489
|
+
const transformCSS = await getLightningCSS();
|
|
490
|
+
// `Buffer.from()` is required—the Lightning CSS API expects a `Uint8Array`
|
|
491
|
+
const result = transformCSS({
|
|
492
|
+
filename: 'input.css',
|
|
493
|
+
code: Buffer.from(inputCSS),
|
|
494
|
+
minify: true,
|
|
495
|
+
errorRecovery: !!options.continueOnMinifyError,
|
|
496
|
+
...lightningCssOptions,
|
|
497
|
+
// Union, so that a manually supplied `unusedSymbols` list survives
|
|
498
|
+
...(unusedSymbols && unusedSymbols.length
|
|
499
|
+
? { unusedSymbols: lightningCssOptions.unusedSymbols ? [...new Set([...lightningCssOptions.unusedSymbols, ...unusedSymbols])] : unusedSymbols }
|
|
500
|
+
: {}),
|
|
501
|
+
...(pruneSelectors && unusedCSSConfig && context?.usedElements
|
|
502
|
+
? { visitor: { Selector: createSelectorPruner(unusedSymbols ?? [], context.usedElements, unusedCSSConfig.safelist) } }
|
|
503
|
+
: {})
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// With `errorRecovery` enabled, Lightning CSS reports what it takes issue
|
|
507
|
+
// with instead of throwing—dropping the rule in some cases (`@property`
|
|
508
|
+
// with a bad `syntax`) and passing it through in others (an unknown
|
|
509
|
+
// at-rule), which is why the wording stops at “reported”
|
|
510
|
+
/** @type {string[]} */
|
|
511
|
+
const warnings = [];
|
|
512
|
+
if (result.warnings) {
|
|
513
|
+
for (const warning of result.warnings) {
|
|
514
|
+
const at = warning.loc ? ` (line ${warning.loc.line}, column ${warning.loc.column})` : '';
|
|
515
|
+
warnings.push(`Warning: Lightning CSS reported invalid CSS${at}: ${warning.message}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const outputCSS = unwrapCSS(result.code.toString(), type);
|
|
520
|
+
|
|
521
|
+
// Where Lightning CSS emptied template syntax (`<?php ?>`, `{{ }}`: `<` or `>` without
|
|
522
|
+
// `CDATA`) or custom-fragment UIDs (long lowercase alphanumeric runs), keep the
|
|
523
|
+
// original; CDATA sections, entities, and other invalid CSS may go
|
|
524
|
+
const isCDATA = text.includes('<![CDATA[');
|
|
525
|
+
const uidPattern = /[a-z0-9]{10,}/; // UIDs are long alphanumeric strings
|
|
526
|
+
const hasUID = uidPattern.test(text) && !isCDATA; // Exclude CDATA from UID detection
|
|
527
|
+
const looksLikeTemplate = (text.includes('<') || text.includes('>')) && !isCDATA;
|
|
528
|
+
|
|
529
|
+
const css = (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
530
|
+
return /** @type {CSSResult} */ ({ css, warnings });
|
|
531
|
+
});
|
|
532
|
+
report(resolved.warnings);
|
|
533
|
+
return resolved.css;
|
|
534
|
+
} catch (err) {
|
|
535
|
+
if (!options.continueOnMinifyError) {
|
|
536
|
+
throw err;
|
|
537
|
+
}
|
|
538
|
+
options.log(err);
|
|
539
|
+
return text;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
} else if (key === 'minifyJS' && typeof option !== 'function') {
|
|
543
|
+
if (!option) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const config = typeof option === 'object' ? option : {};
|
|
548
|
+
const engine = normalizeEngine(config.engine, 'terser');
|
|
549
|
+
|
|
550
|
+
const supportedEngines = ['terser', 'swc'];
|
|
551
|
+
if (!supportedEngines.includes(/** @type {string} */ (engine))) {
|
|
552
|
+
throw new Error(`Unsupported JS minifier engine: \`${describeEngine(engine)}\`. Supported engines: ${supportedEngines.join(', ')}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const engineOptions = { ...config };
|
|
556
|
+
delete engineOptions.engine;
|
|
557
|
+
|
|
558
|
+
// Terser options (when engine is `terser`)
|
|
559
|
+
const terserOptions = engine === 'terser' ? engineOptions : {};
|
|
560
|
+
terserOptions.parse = {
|
|
561
|
+
...terserOptions.parse,
|
|
562
|
+
bare_returns: false
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// SWC options (when engine is `swc`)
|
|
566
|
+
const swcOptions = engine === 'swc' ? engineOptions : {};
|
|
567
|
+
|
|
568
|
+
// Option signatures, computed once rather than per call
|
|
569
|
+
const terserSig = stableStringify({
|
|
570
|
+
...terserOptions,
|
|
571
|
+
cont: !!options.continueOnMinifyError
|
|
572
|
+
});
|
|
573
|
+
const swcSig = stableStringify({
|
|
574
|
+
...swcOptions,
|
|
575
|
+
cont: !!options.continueOnMinifyError
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
options.minifyJS = async function (/** @type {string} */ text, /** @type {boolean | undefined} */ inline, /** @type {boolean | undefined} */ isModule) {
|
|
579
|
+
const start = text.match(/^\s*<!--.*/);
|
|
580
|
+
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
581
|
+
|
|
582
|
+
// Fast path: Avoid invoking minifier for empty/whitespace-only content
|
|
583
|
+
if (!code || !code.trim()) {
|
|
584
|
+
return '';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (declinesMinify(options, 'canMinifyJS', text, inline)) {
|
|
588
|
+
return text;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const optsSig = engine === 'terser' ? terserSig : swcSig;
|
|
592
|
+
const jsKey = minifyCacheKey(code, (inline ? '1' : '0') + '|' + (isModule ? 'm' : '') + '|' + engine + '|' + optsSig);
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
return await cachedMinify(jsMinifyCache, jsKey, async () => {
|
|
596
|
+
if (engine === 'terser') {
|
|
597
|
+
// Create a copy to avoid mutating shared `terserOptions` (race condition)
|
|
598
|
+
const terserCallOptions = {
|
|
599
|
+
...terserOptions,
|
|
600
|
+
parse: {
|
|
601
|
+
...terserOptions.parse,
|
|
602
|
+
bare_returns: inline
|
|
603
|
+
},
|
|
604
|
+
...(isModule ? { module: true } : {}) // Overrides user options: module detection takes precedence for `<script type=module>`
|
|
605
|
+
};
|
|
606
|
+
const terser = await getTerser();
|
|
607
|
+
const result = await terser(code, terserCallOptions);
|
|
608
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
609
|
+
} else {
|
|
610
|
+
const swc = await getSwc();
|
|
611
|
+
if (inline) {
|
|
612
|
+
// `swc.minify()` rejects bare returns, which `transform()` can parse;
|
|
613
|
+
// no .swcrc may apply, and `esnext` keeps the syntax as written
|
|
614
|
+
const result = await swc.transform(code, {
|
|
615
|
+
swcrc: false,
|
|
616
|
+
configFile: false,
|
|
617
|
+
isModule: false,
|
|
618
|
+
minify: true,
|
|
619
|
+
jsc: {
|
|
620
|
+
parser: { syntax: 'ecmascript', allowReturnOutsideFunction: true },
|
|
621
|
+
target: 'esnext',
|
|
622
|
+
minify: { compress: true, mangle: true, ...swcOptions }
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
626
|
+
}
|
|
627
|
+
// `swc.minify()` takes compress and mangle directly as options
|
|
628
|
+
const result = await swc.minify(code, {
|
|
629
|
+
compress: true,
|
|
630
|
+
mangle: true,
|
|
631
|
+
...swcOptions,
|
|
632
|
+
...(isModule ? { module: true } : {}) // Overrides user options: module detection takes precedence for `<script type=module>`
|
|
633
|
+
});
|
|
634
|
+
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
} catch (err) {
|
|
638
|
+
if (!options.continueOnMinifyError || isMissingDependency(err)) {
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
options.log(err);
|
|
642
|
+
return text;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
// Whether dispatching script bodies ahead of the parse pays off. It needs a minifier
|
|
647
|
+
// that leaves the main thread—SWC hands work to its own threadpool, where overlapping
|
|
648
|
+
// calls genuinely run at once, while Terser would only compete with the parse for the
|
|
649
|
+
// one thread both share. A user-supplied `minifyJS` is excluded either way—only this
|
|
650
|
+
// wrapper is content-keyed and free of side effects.
|
|
651
|
+
options.parallelJS = engine === 'swc';
|
|
652
|
+
} else if (key === 'minifyURLs' && typeof option !== 'function') {
|
|
653
|
+
if (!option) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
let urlOptions = option;
|
|
658
|
+
|
|
659
|
+
if (typeof option === 'string') {
|
|
660
|
+
urlOptions = { site: option };
|
|
661
|
+
} else if (typeof option !== 'object') {
|
|
662
|
+
urlOptions = {};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const relate = createUrlMinifier(urlOptions.site || '');
|
|
666
|
+
|
|
667
|
+
// Reuse the persistent cache for this site configuration
|
|
668
|
+
const instanceCache = getUrlMinifyCache(urlOptions.site || '');
|
|
669
|
+
|
|
670
|
+
options.minifyURLs = function (/** @type {string} */ text) {
|
|
671
|
+
// Fast path: Skip text without URL-like characters (`/`, `:`, `#`, `?`) or spaces
|
|
672
|
+
if (!/[/:?#\s]/.test(text)) {
|
|
673
|
+
return text;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const cached = /** @type {string | undefined} */ (instanceCache.get(text));
|
|
677
|
+
if (cached !== undefined) {
|
|
678
|
+
return cached;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
const result = relate(text);
|
|
683
|
+
instanceCache.set(text, result);
|
|
684
|
+
return result;
|
|
685
|
+
} catch (err) {
|
|
686
|
+
// Don’t cache errors
|
|
687
|
+
if (!options.continueOnMinifyError) {
|
|
688
|
+
throw err;
|
|
689
|
+
}
|
|
690
|
+
options.log(err);
|
|
691
|
+
return text;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
} else if (key === 'minifySVG' && typeof option !== 'function') {
|
|
695
|
+
if (!option) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// A config without `plugins` of its own keeps the plugins fit for inline SVG
|
|
700
|
+
const svgoConfig = typeof option === 'object' ? /** @type {{plugins?: unknown[]}} */ (option) : {};
|
|
701
|
+
const svgoOptions = { ...svgoConfig, plugins: svgoConfig.plugins ?? svgoPluginsInline };
|
|
702
|
+
|
|
703
|
+
// Option signature for cache keys
|
|
704
|
+
const svgSig = stableStringify({
|
|
705
|
+
...svgoOptions,
|
|
706
|
+
cont: !!options.continueOnMinifyError
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
options.minifySVG = async function (/** @type {string} */ svgContent) {
|
|
710
|
+
if (!svgContent || !svgContent.trim()) {
|
|
711
|
+
return svgContent;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (declinesMinify(options, 'canMinifySVG', svgContent)) {
|
|
715
|
+
return svgContent;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// SVGO needs every attribute to carry a value, but its output is HTML again,
|
|
719
|
+
// where an empty value can go; it escapes quotes in values and text, so `=""`
|
|
720
|
+
// is always one. Read here, as options are processed in the order they are given.
|
|
721
|
+
const collapseEmpty = Boolean(options.collapseEmptyAttributes);
|
|
722
|
+
const removeNamespaces = Boolean(options.removeRedundantAttributes);
|
|
723
|
+
|
|
724
|
+
// XML reads `xlink:` names only where their prefix is declared, which HTML needs not be;
|
|
725
|
+
// a declaration added to the root for SVGO goes after it (one on a nested `svg` covers
|
|
726
|
+
// that one only)
|
|
727
|
+
const rootEnd = svgContent.indexOf('xlink:') !== -1 && RE_SVG_START.test(svgContent) ? findTagEnd(svgContent, 4) : -1;
|
|
728
|
+
// Values are quoted where SVGO reads, so what is left is names
|
|
729
|
+
const addsXLink = rootEnd !== -1 &&
|
|
730
|
+
!RE_XLINK_DECLARATION.test(svgContent.slice(4, rootEnd).replace(RE_QUOTED_VALUE, ''));
|
|
731
|
+
const svgInput = addsXLink
|
|
732
|
+
? '<svg xmlns:xlink="' + namespaceXLink + '"' + svgContent.slice(4)
|
|
733
|
+
: svgContent;
|
|
734
|
+
|
|
735
|
+
const svgKey = minifyCacheKey(svgContent, svgSig + (collapseEmpty ? '|c' : '') + (removeNamespaces ? '|n' : ''));
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
return await cachedMinify(svgMinifyCache, svgKey, async () => {
|
|
739
|
+
const optimize = await getSvgo();
|
|
740
|
+
const result = optimize(svgInput, removeNamespaces
|
|
741
|
+
? { ...svgoOptions, plugins: [...svgoOptions.plugins, pluginNamespacesHTML] }
|
|
742
|
+
: svgoOptions);
|
|
743
|
+
let data = result.data;
|
|
744
|
+
// The root’s declaration was added for SVGO, unless all of them go
|
|
745
|
+
if (removeNamespaces) {
|
|
746
|
+
data = data.replace(RE_XLINK_DECLARATION_SVGO, (/** @type {string} */ match) => match === XLINK_DECLARATION_SVGO ? '' : match);
|
|
747
|
+
} else if (addsXLink) {
|
|
748
|
+
// SVGO escapes `>` in values, so the root’s start tag ends at the first one
|
|
749
|
+
const at = data.indexOf(XLINK_DECLARATION_SVGO);
|
|
750
|
+
if (at !== -1 && at < data.indexOf('>')) {
|
|
751
|
+
data = data.slice(0, at) + data.slice(at + XLINK_DECLARATION_SVGO.length);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return /** @type {string} */ (collapseEmpty ? data.replace(RE_SVGO_EMPTY_ATTRIBUTE, ' $1') : data);
|
|
755
|
+
});
|
|
756
|
+
} catch (err) {
|
|
757
|
+
if (!options.continueOnMinifyError) {
|
|
758
|
+
throw err;
|
|
759
|
+
}
|
|
760
|
+
options.log(err);
|
|
761
|
+
return svgContent;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
} else if (key === 'customAttrCollapse') {
|
|
765
|
+
// Single regex pattern
|
|
766
|
+
optionsDynamic[key] = parseRegExp(option);
|
|
767
|
+
} else if (key === 'customAttrSurround') {
|
|
768
|
+
// Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
|
|
769
|
+
optionsDynamic[key] = parseNestedRegExpArray(option);
|
|
770
|
+
} else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
|
|
771
|
+
// Array of regex patterns
|
|
772
|
+
optionsDynamic[key] = parseRegExpArray(option);
|
|
773
|
+
} else {
|
|
774
|
+
optionsDynamic[key] = option;
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// The parser merges these into one attribute pattern, and a merged pattern
|
|
779
|
+
// carries no flags of its own. `i` and `s` are written into the source instead,
|
|
780
|
+
// but `u` and `v` change how a source reads and cannot be: dropped, `\p{L}`
|
|
781
|
+
// stops being a property escape and matches the literal text `p{L}`, silently
|
|
782
|
+
// and without failing to compile. Refusing them beats matching the wrong thing.
|
|
783
|
+
/** @type {[string, RegExp[]][]} */
|
|
784
|
+
const merged = [
|
|
785
|
+
['customAttrAssign', options.customAttrAssign || []],
|
|
786
|
+
['customAttrSurround', (options.customAttrSurround || []).flat()]
|
|
787
|
+
];
|
|
788
|
+
for (const [key, patterns] of merged) {
|
|
789
|
+
for (const re of patterns) {
|
|
790
|
+
if (!(re instanceof RegExp)) continue;
|
|
791
|
+
const flag = lostFlag(re);
|
|
792
|
+
if (!flag) continue;
|
|
793
|
+
throw new Error(`HTML Minifier: \`${key}\` pattern \`/${re.source}/${re.flags}\` carries \`${flag}\`, which the merged attribute pattern cannot carry—rewrite it without \`${flag}\``);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Quantifiers or alternation compounded under unbounded repetition backtrack catastrophically,
|
|
798
|
+
// and a linear scan cannot stand in for them; patterns too long or too deeply nested to read are
|
|
799
|
+
// refused for that alone. A lone `[\s\S]*?` up to a literal terminator is linear and passes, as
|
|
800
|
+
// HM’s default fragments have exactly that shape.
|
|
801
|
+
for (const re of options.ignoreCustomFragments || []) {
|
|
802
|
+
const risk = describeQuantifierRisk(re);
|
|
803
|
+
if (!risk) continue;
|
|
804
|
+
const problem = `Custom fragment \`/${re.source}/\` ${risk}`;
|
|
805
|
+
if (options.strictCustomFragments) {
|
|
806
|
+
throw new Error(`HTML Minifier: ${problem}`);
|
|
807
|
+
}
|
|
808
|
+
if (!customFragmentsWarned.has(re.source)) {
|
|
809
|
+
customFragmentsWarned.add(re.source);
|
|
810
|
+
warn(`HTML Minifier: ${problem}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Options that do nothing without the option they build on say so. The option is read from
|
|
815
|
+
// `effectiveInput` (user options over preset), so a default filled in above doesn’t count as
|
|
816
|
+
// asked for; the one it needs comes from the settled options, so an unloadable minifier is absent
|
|
817
|
+
const requestedOptions = /** @type {Record<string, any>} */ (effectiveInput);
|
|
818
|
+
const settledOptions = /** @type {Record<string, any>} */ (options);
|
|
819
|
+
for (const { option, requires, unusable, clear } of optionDependencies) {
|
|
820
|
+
if (!isRequested(requestedOptions[option])) continue;
|
|
821
|
+
const aside = unusable && unusable(requestedOptions);
|
|
822
|
+
if (!aside && isRequested(settledOptions[requires])) continue;
|
|
823
|
+
const message = `HTML Minifier: Ignoring \`${option}\`—use with \`${requires}\` (\`--${paramCase(requires)}\`)${aside ? `, ${aside}` : ''}`;
|
|
824
|
+
if (!dependenciesWarned.has(message)) {
|
|
825
|
+
dependenciesWarned.add(message);
|
|
826
|
+
warn(message);
|
|
827
|
+
}
|
|
828
|
+
if (clear) {
|
|
829
|
+
settledOptions[option] = null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return options;
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
export {
|
|
837
|
+
optionDependencies,
|
|
838
|
+
optionKeysExtra,
|
|
839
|
+
shouldMinifyInnerHTML,
|
|
840
|
+
processOptions
|
|
841
|
+
};
|