html-minifier-next 6.2.7 → 6.2.9
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 +67 -45
- package/dist/htmlminifier.cjs +916 -583
- package/dist/types/htmlminifier.d.ts +77 -60
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts +19 -3
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/dist/types/lib/attributes.d.ts +137 -23
- package/dist/types/lib/attributes.d.ts.map +1 -1
- package/dist/types/lib/content.d.ts +37 -5
- package/dist/types/lib/content.d.ts.map +1 -1
- package/dist/types/lib/elements.d.ts +29 -4
- package/dist/types/lib/elements.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts +17 -14
- package/dist/types/lib/options.d.ts.map +1 -1
- package/dist/types/lib/utils.d.ts +28 -11
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/lib/whitespace.d.ts +40 -6
- package/dist/types/lib/whitespace.d.ts.map +1 -1
- package/dist/types/tokenchain.d.ts +17 -3
- package/dist/types/tokenchain.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/htmlminifier.js +439 -372
- package/src/htmlparser.js +101 -48
- package/src/lib/attributes.js +166 -50
- package/src/lib/content.js +28 -12
- package/src/lib/elements.js +30 -7
- package/src/lib/options.js +55 -42
- package/src/lib/utils.js +26 -7
- package/src/lib/whitespace.js +30 -11
- package/src/presets.js +1 -1
- package/src/tokenchain.js +30 -17
package/src/lib/options.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Imports
|
|
2
|
-
|
|
3
1
|
import { createUrlMinifier } from './urls.js';
|
|
4
2
|
import { LRU, stableStringify, hashContent, identity, lowercase, replaceAsync, parseRegExp } from './utils.js';
|
|
5
3
|
import { RE_TRAILING_SEMICOLON } from './constants.js';
|
|
@@ -8,8 +6,17 @@ import { wrapCSS, unwrapCSS } from './content.js';
|
|
|
8
6
|
import { getPreset, getPresetNames } from '../presets.js';
|
|
9
7
|
import { optionDefaults } from './option-definitions.js';
|
|
10
8
|
|
|
9
|
+
// Type definitions
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Record<string, any>} MinifierOptions
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// @@ Extract a `ProcessedOptions` typedef (with `name`, `log`, `canCollapseWhitespace`, `canTrimWhitespace`,`minifyCSS`, `minifyJS`, `minifyURLs` typed as required, non-optional) to replace the current `Record<string, any>` escape hatch and eliminate the `/** @type {Function} */` casts in htmlminifier.js
|
|
16
|
+
|
|
11
17
|
// Helper functions
|
|
12
18
|
|
|
19
|
+
/** @param {MinifierOptions} options */
|
|
13
20
|
function shouldMinifyInnerHTML(options) {
|
|
14
21
|
return Boolean(
|
|
15
22
|
options.collapseWhitespace ||
|
|
@@ -25,18 +32,12 @@ function shouldMinifyInnerHTML(options) {
|
|
|
25
32
|
// Main options processor
|
|
26
33
|
|
|
27
34
|
/**
|
|
28
|
-
* @param {
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
|
|
31
|
-
* @param {Function} deps.getTerser - Function to lazily load Terser
|
|
32
|
-
* @param {Function} deps.getSwc - Function to lazily load @swc/core
|
|
33
|
-
* @param {Function} deps.getSvgo - Function to lazily load SVGO
|
|
34
|
-
* @param {LRU} deps.cssMinifyCache - CSS minification cache
|
|
35
|
-
* @param {LRU} deps.jsMinifyCache - JS minification cache
|
|
36
|
-
* @param {LRU} deps.svgMinifyCache - SVG minification cache
|
|
35
|
+
* @param {MinifierOptions} inputOptions - User-provided options
|
|
36
|
+
* @param {{getLightningCSS?: Function | undefined, getTerser?: Function | undefined, getSwc?: Function | undefined, getSvgo?: Function | undefined, cssMinifyCache?: LRU | undefined, jsMinifyCache?: LRU | undefined, svgMinifyCache?: LRU | undefined}} [deps] - Dependencies from htmlminifier.js
|
|
37
37
|
* @returns {MinifierOptions} Normalized options with defaults applied
|
|
38
38
|
*/
|
|
39
39
|
const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getSvgo, cssMinifyCache, jsMinifyCache, svgMinifyCache } = {}) => {
|
|
40
|
+
/** @type {MinifierOptions} */
|
|
40
41
|
const options = {
|
|
41
42
|
name: lowercase,
|
|
42
43
|
canCollapseWhitespace,
|
|
@@ -49,13 +50,13 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
49
50
|
minifySVG: null
|
|
50
51
|
};
|
|
51
52
|
|
|
52
|
-
const parseRegExpArray = (arr) => {
|
|
53
|
-
return Array.isArray(arr) ? arr.map(parseRegExp) :
|
|
53
|
+
const parseRegExpArray = (/** @type {unknown} */ arr) => {
|
|
54
|
+
return Array.isArray(arr) ? arr.map(parseRegExp) : [];
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
// Helper for nested arrays (e.g., `customAttrSurround: [[start, end], …]`)
|
|
57
|
-
const parseNestedRegExpArray = (arr) => {
|
|
58
|
-
if (!Array.isArray(arr)) return
|
|
58
|
+
const parseNestedRegExpArray = (/** @type {unknown} */ arr) => {
|
|
59
|
+
if (!Array.isArray(arr)) return [];
|
|
59
60
|
return arr.map(item => {
|
|
60
61
|
// If item is an array (a pair), recursively convert each element
|
|
61
62
|
if (Array.isArray(item)) {
|
|
@@ -96,13 +97,16 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
96
97
|
options.log = option;
|
|
97
98
|
}
|
|
98
99
|
} else if (key === 'minifyCSS' && typeof option !== 'function') {
|
|
99
|
-
if (!option) {
|
|
100
|
+
if (!option || !getLightningCSS || !cssMinifyCache) {
|
|
100
101
|
return;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
const lightningCssOptions = typeof option === 'object' ? option : {};
|
|
105
|
+
// Capture to preserve TypeScript narrowing across the async closure boundary below
|
|
106
|
+
const cssLoader = getLightningCSS;
|
|
107
|
+
const cssCache = cssMinifyCache;
|
|
104
108
|
|
|
105
|
-
options.minifyCSS = async function (text, type) {
|
|
109
|
+
options.minifyCSS = async function (/** @type {string} */ text, /** @type {string} */ type) {
|
|
106
110
|
// Fast path: Nothing to minify
|
|
107
111
|
if (!text || !text.trim()) {
|
|
108
112
|
return text;
|
|
@@ -114,7 +118,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
114
118
|
text = await replaceAsync(
|
|
115
119
|
text,
|
|
116
120
|
/(url\s*\(\s*)(?:"([^"]*)"|'([^']*)'|([^\s)]+))(\s*\))/ig,
|
|
117
|
-
async function (match, prefix, dq, sq, unq, suffix) {
|
|
121
|
+
async function (/** @type {string} */ match, /** @type {string} */ prefix, /** @type {string | undefined} */ dq, /** @type {string | undefined} */ sq, /** @type {string | undefined} */ unq, /** @type {string} */ suffix) {
|
|
118
122
|
const quote = dq != null ? '"' : (sq != null ? "'" : '');
|
|
119
123
|
const url = dq ?? sq ?? unq ?? '';
|
|
120
124
|
try {
|
|
@@ -139,8 +143,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
139
143
|
: (inputCSS + '|' + type + '|' + cssSig);
|
|
140
144
|
|
|
141
145
|
try {
|
|
142
|
-
const cached =
|
|
143
|
-
if (cached) {
|
|
146
|
+
const cached = cssCache.get(cssKey);
|
|
147
|
+
if (cached !== undefined) {
|
|
144
148
|
// Support both resolved values and in-flight promises
|
|
145
149
|
return await cached;
|
|
146
150
|
}
|
|
@@ -148,7 +152,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
148
152
|
// In-flight promise caching: Prevent duplicate concurrent minifications
|
|
149
153
|
// of the same CSS content (same pattern as JS minification)
|
|
150
154
|
const inFlight = (async () => {
|
|
151
|
-
const transformCSS = await
|
|
155
|
+
const transformCSS = await cssLoader();
|
|
152
156
|
// Note: `Buffer.from()` is required—Lightning CSS API expects Uint8Array
|
|
153
157
|
const result = transformCSS({
|
|
154
158
|
filename: 'input.css',
|
|
@@ -175,12 +179,12 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
175
179
|
return (text.trim() && !outputCSS.trim() && (looksLikeTemplate || hasUID)) ? text : outputCSS;
|
|
176
180
|
})();
|
|
177
181
|
|
|
178
|
-
|
|
182
|
+
cssCache.set(cssKey, inFlight);
|
|
179
183
|
const resolved = await inFlight;
|
|
180
|
-
|
|
184
|
+
cssCache.set(cssKey, resolved);
|
|
181
185
|
return resolved;
|
|
182
186
|
} catch (err) {
|
|
183
|
-
|
|
187
|
+
cssCache.delete(cssKey);
|
|
184
188
|
if (!options.continueOnMinifyError) {
|
|
185
189
|
throw err;
|
|
186
190
|
}
|
|
@@ -189,10 +193,15 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
189
193
|
}
|
|
190
194
|
};
|
|
191
195
|
} else if (key === 'minifyJS' && typeof option !== 'function') {
|
|
192
|
-
if (!option) {
|
|
196
|
+
if (!option || !getTerser || !getSwc || !jsMinifyCache) {
|
|
193
197
|
return;
|
|
194
198
|
}
|
|
195
199
|
|
|
200
|
+
// Capture to preserve TypeScript narrowing across the async closure boundary below
|
|
201
|
+
const loadTerser = getTerser;
|
|
202
|
+
const loadSwc = getSwc;
|
|
203
|
+
const jsCache = jsMinifyCache;
|
|
204
|
+
|
|
196
205
|
// Parse configuration
|
|
197
206
|
const config = typeof option === 'object' ? option : {};
|
|
198
207
|
const engine = (config.engine || 'terser').toLowerCase();
|
|
@@ -227,7 +236,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
227
236
|
cont: !!options.continueOnMinifyError
|
|
228
237
|
});
|
|
229
238
|
|
|
230
|
-
options.minifyJS = async function (text, inline, isModule) {
|
|
239
|
+
options.minifyJS = async function (/** @type {string} */ text, /** @type {boolean} */ inline, /** @type {boolean} */ isModule) {
|
|
231
240
|
const start = text.match(/^\s*<!--.*/);
|
|
232
241
|
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
233
242
|
|
|
@@ -249,8 +258,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
249
258
|
jsKey = (code.length > 2048 ? (hashContent(code) + '|') : (code + '|'))
|
|
250
259
|
+ (inline ? '1' : '0') + '|' + (isModule ? 'm' : '') + '|' + useEngine + '|' + optsSig;
|
|
251
260
|
|
|
252
|
-
const cached =
|
|
253
|
-
if (cached) {
|
|
261
|
+
const cached = jsCache.get(jsKey);
|
|
262
|
+
if (cached !== undefined) {
|
|
254
263
|
return await cached;
|
|
255
264
|
}
|
|
256
265
|
|
|
@@ -266,11 +275,11 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
266
275
|
},
|
|
267
276
|
...(isModule ? { module: true } : {}) // Overrides user options: module detection takes precedence for `<script type=module>`
|
|
268
277
|
};
|
|
269
|
-
const terser = await
|
|
278
|
+
const terser = await loadTerser();
|
|
270
279
|
const result = await terser(code, terserCallOptions);
|
|
271
280
|
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
272
281
|
} else if (useEngine === 'swc') {
|
|
273
|
-
const swc = await
|
|
282
|
+
const swc = await loadSwc();
|
|
274
283
|
// `swc.minify()` takes compress and mangle directly as options
|
|
275
284
|
const result = await swc.minify(code, {
|
|
276
285
|
compress: true,
|
|
@@ -283,12 +292,12 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
283
292
|
throw new Error(`Unknown JS minifier engine: ${useEngine}`);
|
|
284
293
|
})();
|
|
285
294
|
|
|
286
|
-
|
|
295
|
+
jsCache.set(jsKey, inFlight);
|
|
287
296
|
const resolved = await inFlight;
|
|
288
|
-
|
|
297
|
+
jsCache.set(jsKey, resolved);
|
|
289
298
|
return resolved;
|
|
290
299
|
} catch (err) {
|
|
291
|
-
if (jsKey)
|
|
300
|
+
if (jsKey) jsCache.delete(jsKey);
|
|
292
301
|
if (!options.continueOnMinifyError) {
|
|
293
302
|
throw err;
|
|
294
303
|
}
|
|
@@ -314,7 +323,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
314
323
|
// Create instance-specific cache (results depend on site configuration)
|
|
315
324
|
const instanceCache = new LRU(500);
|
|
316
325
|
|
|
317
|
-
options.minifyURLs = function (text) {
|
|
326
|
+
options.minifyURLs = function (/** @type {string} */ text) {
|
|
318
327
|
// Fast-path: Skip if text doesn’t look like a URL that needs processing
|
|
319
328
|
// Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
|
|
320
329
|
if (!/[/:?#\s]/.test(text)) {
|
|
@@ -341,10 +350,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
341
350
|
}
|
|
342
351
|
};
|
|
343
352
|
} else if (key === 'minifySVG' && typeof option !== 'function') {
|
|
344
|
-
if (!option) {
|
|
353
|
+
if (!option || !getSvgo || !svgMinifyCache) {
|
|
345
354
|
return;
|
|
346
355
|
}
|
|
347
356
|
|
|
357
|
+
// Capture to preserve TypeScript narrowing across the async closure boundary below
|
|
358
|
+
const loadSvgo = getSvgo;
|
|
359
|
+
const svgCache = svgMinifyCache;
|
|
360
|
+
|
|
348
361
|
const svgoOptions = typeof option === 'object' ? option : {};
|
|
349
362
|
|
|
350
363
|
// Pre-compute option signature for cache keys
|
|
@@ -353,7 +366,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
353
366
|
cont: !!options.continueOnMinifyError
|
|
354
367
|
});
|
|
355
368
|
|
|
356
|
-
options.minifySVG = async function (svgContent) {
|
|
369
|
+
options.minifySVG = async function (/** @type {string} */ svgContent) {
|
|
357
370
|
if (!svgContent || !svgContent.trim()) {
|
|
358
371
|
return svgContent;
|
|
359
372
|
}
|
|
@@ -364,23 +377,23 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
364
377
|
: (svgContent + '|' + svgSig);
|
|
365
378
|
|
|
366
379
|
try {
|
|
367
|
-
const cached =
|
|
368
|
-
if (cached) {
|
|
380
|
+
const cached = svgCache.get(svgKey);
|
|
381
|
+
if (cached !== undefined) {
|
|
369
382
|
return await cached;
|
|
370
383
|
}
|
|
371
384
|
|
|
372
385
|
const inFlight = (async () => {
|
|
373
|
-
const optimize = await
|
|
386
|
+
const optimize = await loadSvgo();
|
|
374
387
|
const result = optimize(svgContent, svgoOptions);
|
|
375
388
|
return result.data;
|
|
376
389
|
})();
|
|
377
390
|
|
|
378
|
-
|
|
391
|
+
svgCache.set(svgKey, inFlight);
|
|
379
392
|
const resolved = await inFlight;
|
|
380
|
-
|
|
393
|
+
svgCache.set(svgKey, resolved);
|
|
381
394
|
return resolved;
|
|
382
395
|
} catch (err) {
|
|
383
|
-
|
|
396
|
+
svgCache.delete(svgKey);
|
|
384
397
|
if (!options.continueOnMinifyError) {
|
|
385
398
|
throw err;
|
|
386
399
|
}
|
package/src/lib/utils.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// Stringify for options signatures (sorted keys, shallow, nested objects)
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @param {unknown} obj
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
3
7
|
function stableStringify(obj) {
|
|
4
8
|
if (obj == null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
5
9
|
if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
|
|
6
10
|
const keys = Object.keys(obj).sort();
|
|
7
11
|
let out = '{';
|
|
8
12
|
for (let i = 0; i < keys.length; i++) {
|
|
9
|
-
const k = keys[i];
|
|
10
|
-
out += JSON.stringify(k) + ':' + stableStringify(obj[k]) + (i < keys.length - 1 ? ',' : '');
|
|
13
|
+
const k = keys[i] ?? '';
|
|
14
|
+
out += JSON.stringify(k) + ':' + stableStringify(/** @type {Record<string, unknown>} */ (obj)[k]) + (i < keys.length - 1 ? ',' : '');
|
|
11
15
|
}
|
|
12
16
|
return out + '}';
|
|
13
17
|
}
|
|
@@ -17,8 +21,10 @@ function stableStringify(obj) {
|
|
|
17
21
|
class LRU {
|
|
18
22
|
constructor(limit = 200) {
|
|
19
23
|
this.limit = limit;
|
|
24
|
+
/** @type {Map<string, unknown>} */
|
|
20
25
|
this.map = new Map();
|
|
21
26
|
}
|
|
27
|
+
/** @param {string} key */
|
|
22
28
|
get(key) {
|
|
23
29
|
if (this.map.has(key)) {
|
|
24
30
|
const v = this.map.get(key);
|
|
@@ -28,19 +34,25 @@ class LRU {
|
|
|
28
34
|
}
|
|
29
35
|
return undefined;
|
|
30
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} key
|
|
39
|
+
* @param {unknown} value
|
|
40
|
+
*/
|
|
31
41
|
set(key, value) {
|
|
32
42
|
if (this.map.has(key)) this.map.delete(key);
|
|
33
43
|
this.map.set(key, value);
|
|
34
44
|
if (this.map.size > this.limit) {
|
|
35
45
|
const first = this.map.keys().next().value;
|
|
36
|
-
this.map.delete(first);
|
|
46
|
+
if (first !== undefined) this.map.delete(first);
|
|
37
47
|
}
|
|
38
48
|
}
|
|
49
|
+
/** @param {string} key */
|
|
39
50
|
delete(key) { this.map.delete(key); }
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
// FNV-1a 32-bit hash for large-input cache keys
|
|
43
54
|
|
|
55
|
+
/** @param {string} str */
|
|
44
56
|
function hashContent(str) {
|
|
45
57
|
let hash = 2166136261;
|
|
46
58
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -52,6 +64,7 @@ function hashContent(str) {
|
|
|
52
64
|
|
|
53
65
|
// Unique ID generator
|
|
54
66
|
|
|
67
|
+
/** @param {string} value */
|
|
55
68
|
function uniqueId(value) {
|
|
56
69
|
let id;
|
|
57
70
|
do {
|
|
@@ -62,14 +75,17 @@ function uniqueId(value) {
|
|
|
62
75
|
|
|
63
76
|
// Identity and transform functions
|
|
64
77
|
|
|
78
|
+
/** @param {string} value */
|
|
65
79
|
function identity(value) {
|
|
66
80
|
return value;
|
|
67
81
|
}
|
|
68
82
|
|
|
83
|
+
/** @param {unknown} value */
|
|
69
84
|
function isThenable(value) {
|
|
70
|
-
return value != null && typeof value === 'object' && typeof value.then === 'function';
|
|
85
|
+
return value != null && typeof value === 'object' && typeof /** @type {any} */ (value).then === 'function';
|
|
71
86
|
}
|
|
72
87
|
|
|
88
|
+
/** @param {string} value */
|
|
73
89
|
function lowercase(value) {
|
|
74
90
|
return value.toLowerCase();
|
|
75
91
|
}
|
|
@@ -84,25 +100,28 @@ function lowercase(value) {
|
|
|
84
100
|
* @returns {Promise<string>} Processed string
|
|
85
101
|
*/
|
|
86
102
|
async function replaceAsync(str, regex, asyncFn) {
|
|
103
|
+
/** @type {Promise<string>[]} */
|
|
87
104
|
const promises = [];
|
|
88
105
|
|
|
89
|
-
str.replace(regex, (match, ...args) => {
|
|
106
|
+
str.replace(regex, /** @returns {string} */ (match, ...args) => {
|
|
90
107
|
const promise = asyncFn(match, ...args);
|
|
91
108
|
promises.push(promise);
|
|
109
|
+
return match;
|
|
92
110
|
});
|
|
93
111
|
|
|
94
112
|
const data = await Promise.all(promises);
|
|
95
|
-
return str.replace(regex, () => data.shift());
|
|
113
|
+
return str.replace(regex, () => data.shift() ?? '');
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
// String patterns to RegExp conversion (for JSON config support)
|
|
99
117
|
|
|
118
|
+
/** @param {string | RegExp} value */
|
|
100
119
|
function parseRegExp(value) {
|
|
101
120
|
if (typeof value === 'string') {
|
|
102
121
|
if (!value) return undefined; // Empty string = not configured
|
|
103
122
|
const match = value.match(/^\/(.+)\/([dgimsuvy]*)$/);
|
|
104
123
|
if (match) {
|
|
105
|
-
return new RegExp(match[1], match[2]);
|
|
124
|
+
return new RegExp(match[1] ?? '', match[2] ?? '');
|
|
106
125
|
}
|
|
107
126
|
return new RegExp(value);
|
|
108
127
|
}
|
package/src/lib/whitespace.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Imports
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
RE_WS_START,
|
|
5
3
|
RE_WS_END,
|
|
@@ -15,6 +13,7 @@ import {
|
|
|
15
13
|
|
|
16
14
|
// Trim whitespace
|
|
17
15
|
|
|
16
|
+
/** @param {string} str */
|
|
18
17
|
const trimWhitespace = str => {
|
|
19
18
|
if (!str) return str;
|
|
20
19
|
// Fast path: If no whitespace at start or end, return early
|
|
@@ -26,6 +25,7 @@ const trimWhitespace = str => {
|
|
|
26
25
|
|
|
27
26
|
// Collapse all whitespace
|
|
28
27
|
|
|
28
|
+
/** @param {string} str */
|
|
29
29
|
function collapseWhitespaceAll(str) {
|
|
30
30
|
if (!str) return str;
|
|
31
31
|
// Fast path: If there are no common whitespace characters, return early
|
|
@@ -33,7 +33,7 @@ function collapseWhitespaceAll(str) {
|
|
|
33
33
|
return str;
|
|
34
34
|
}
|
|
35
35
|
// No-break space is specifically handled inside the replacer function here:
|
|
36
|
-
return str.replace(RE_ALL_WS_NBSP, function (spaces) {
|
|
36
|
+
return str.replace(RE_ALL_WS_NBSP, function (/** @param {string} spaces */ spaces) {
|
|
37
37
|
// Preserve standalone tabs
|
|
38
38
|
if (spaces === '\t') return '\t';
|
|
39
39
|
// Fast path: No no-break space, common case—just collapse to single space
|
|
@@ -46,7 +46,14 @@ function collapseWhitespaceAll(str) {
|
|
|
46
46
|
|
|
47
47
|
// Collapse whitespace with options
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} str
|
|
51
|
+
* @param {{preserveLineBreaks?: boolean | undefined, conservativeCollapse?: boolean | undefined}} options
|
|
52
|
+
* @param {boolean} trimLeft
|
|
53
|
+
* @param {boolean} trimRight
|
|
54
|
+
* @param {boolean} [collapseAll]
|
|
55
|
+
*/
|
|
56
|
+
function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll = false) {
|
|
50
57
|
let lineBreakBefore = ''; let lineBreakAfter = '';
|
|
51
58
|
|
|
52
59
|
if (!str) return str;
|
|
@@ -66,7 +73,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
66
73
|
// (avoids polynomial backtracking with end-anchored lazy quantifiers)
|
|
67
74
|
const WS_CHARS = ' \n\r\t\f';
|
|
68
75
|
let leadEnd = 0;
|
|
69
|
-
while (leadEnd < str.length && WS_CHARS.includes(str
|
|
76
|
+
while (leadEnd < str.length && WS_CHARS.includes(str.charAt(leadEnd))) {
|
|
70
77
|
leadEnd++;
|
|
71
78
|
}
|
|
72
79
|
if (leadEnd > 0) {
|
|
@@ -77,7 +84,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
let trailStart = str.length;
|
|
80
|
-
while (trailStart > 0 && WS_CHARS.includes(str
|
|
87
|
+
while (trailStart > 0 && WS_CHARS.includes(str.charAt(trailStart - 1))) {
|
|
81
88
|
trailStart--;
|
|
82
89
|
}
|
|
83
90
|
if (trailStart < str.length) {
|
|
@@ -91,7 +98,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
91
98
|
|
|
92
99
|
if (trimLeft) {
|
|
93
100
|
// No-break space is specifically handled inside the replacer function
|
|
94
|
-
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
|
|
101
|
+
str = str.replace(/^[ \n\r\t\f\xA0]+/, function (/** @type {string} */ spaces) {
|
|
95
102
|
const conservative = !lineBreakBefore && options.conservativeCollapse;
|
|
96
103
|
if (conservative && spaces === '\t') {
|
|
97
104
|
return '\t';
|
|
@@ -104,7 +111,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
104
111
|
// Find trailing whitespace boundary manually (avoids polynomial backtracking
|
|
105
112
|
// with `/[ \n\r\t\f\xA0]+$/` on strings with long internal whitespace runs)
|
|
106
113
|
let end = str.length;
|
|
107
|
-
while (end > 0 && ' \n\r\t\f\xA0'.includes(str
|
|
114
|
+
while (end > 0 && ' \n\r\t\f\xA0'.includes(str.charAt(end - 1))) {
|
|
108
115
|
end--;
|
|
109
116
|
}
|
|
110
117
|
if (end < str.length) {
|
|
@@ -135,14 +142,24 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
|
|
|
135
142
|
|
|
136
143
|
// Collapse whitespace smartly based on surrounding tags
|
|
137
144
|
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} str
|
|
147
|
+
* @param {string} prevTag
|
|
148
|
+
* @param {string} nextTag
|
|
149
|
+
* @param {Array<{name: string, value?: string}>} prevAttrs
|
|
150
|
+
* @param {Array<{name: string, value?: string}>} nextAttrs
|
|
151
|
+
* @param {{preserveLineBreaks?: boolean, conservativeCollapse?: boolean, collapseInlineTagWhitespace?: boolean}} options
|
|
152
|
+
* @param {Set<string>} inlineElements
|
|
153
|
+
* @param {Set<string>} inlineTextSet
|
|
154
|
+
*/
|
|
138
155
|
function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
|
|
139
156
|
const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
|
|
140
157
|
const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
|
|
141
158
|
|
|
142
159
|
// Helper: Check if an input element has `type="hidden"`
|
|
143
|
-
const isHiddenInput = (tagName, attrs) => {
|
|
160
|
+
const isHiddenInput = (/** @type {string} */ tagName, /** @type {Array<{name: string, value?: string}>} */ attrs) => {
|
|
144
161
|
if (tagName !== 'input' || !attrs || !attrs.length) return false;
|
|
145
|
-
const typeAttr = attrs.find(attr => attr.name === 'type');
|
|
162
|
+
const typeAttr = attrs.find((/** @type {{name: string, value?: string}} */ attr) => attr.name === 'type');
|
|
146
163
|
return typeAttr && typeAttr.value === 'hidden';
|
|
147
164
|
};
|
|
148
165
|
|
|
@@ -206,7 +223,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
|
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
225
|
|
|
209
|
-
return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
|
|
226
|
+
return collapseWhitespace(str, options, Boolean(trimLeft), Boolean(trimRight), Boolean(prevTag && nextTag));
|
|
210
227
|
}
|
|
211
228
|
|
|
212
229
|
// Collapse/trim whitespace for given tag
|
|
@@ -214,10 +231,12 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, op
|
|
|
214
231
|
const noCollapseWhitespaceTags = new Set(['script', 'style', 'pre', 'textarea']);
|
|
215
232
|
const noTrimWhitespaceTags = new Set(['pre', 'textarea']);
|
|
216
233
|
|
|
234
|
+
/** @param {string} tag */
|
|
217
235
|
function canCollapseWhitespace(tag) {
|
|
218
236
|
return !noCollapseWhitespaceTags.has(tag);
|
|
219
237
|
}
|
|
220
238
|
|
|
239
|
+
/** @param {string} tag */
|
|
221
240
|
function canTrimWhitespace(tag) {
|
|
222
241
|
return !noTrimWhitespaceTags.has(tag);
|
|
223
242
|
}
|
package/src/presets.js
CHANGED
|
@@ -47,7 +47,7 @@ export const presets = {
|
|
|
47
47
|
export function getPreset(name) {
|
|
48
48
|
if (!name) return null;
|
|
49
49
|
const normalizedName = name.toLowerCase();
|
|
50
|
-
return presets[normalizedName] || null;
|
|
50
|
+
return /** @type {Record<string, object>} */ (presets)[normalizedName] || null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
package/src/tokenchain.js
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
class Sorter {
|
|
2
|
+
constructor() {
|
|
3
|
+
/** @type {string[]} */
|
|
4
|
+
this.keys = [];
|
|
5
|
+
/** @type {Map<string, Sorter>} */
|
|
6
|
+
this.sorterMap = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string[]} tokens
|
|
11
|
+
* @param {number} fromIndex
|
|
12
|
+
* @returns {string[]}
|
|
13
|
+
*/
|
|
2
14
|
sort(tokens, fromIndex = 0) {
|
|
3
|
-
for (
|
|
4
|
-
const token = this.keys[i];
|
|
15
|
+
for (const token of this.keys) {
|
|
5
16
|
|
|
6
17
|
// Single pass: Count matches and collect non-matches
|
|
7
18
|
let matchCount = 0;
|
|
8
19
|
const others = [];
|
|
9
20
|
|
|
10
21
|
for (let j = fromIndex; j < tokens.length; j++) {
|
|
11
|
-
|
|
22
|
+
const t = /** @type {string} */ (tokens[j]);
|
|
23
|
+
if (t === token) {
|
|
12
24
|
matchCount++;
|
|
13
25
|
} else {
|
|
14
|
-
others.push(
|
|
26
|
+
others.push(t);
|
|
15
27
|
}
|
|
16
28
|
}
|
|
17
29
|
|
|
@@ -21,12 +33,12 @@ class Sorter {
|
|
|
21
33
|
for (let j = 0; j < matchCount; j++) {
|
|
22
34
|
tokens[writeIdx++] = token;
|
|
23
35
|
}
|
|
24
|
-
for (
|
|
25
|
-
tokens[writeIdx++] =
|
|
36
|
+
for (const other of others) {
|
|
37
|
+
tokens[writeIdx++] = other;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
const newFromIndex = fromIndex + matchCount;
|
|
29
|
-
return this.sorterMap.get(token)
|
|
41
|
+
return this.sorterMap.get(token)?.sort(tokens, newFromIndex) ?? tokens;
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
44
|
return tokens;
|
|
@@ -35,22 +47,24 @@ class Sorter {
|
|
|
35
47
|
|
|
36
48
|
class TokenChain {
|
|
37
49
|
constructor() {
|
|
38
|
-
|
|
50
|
+
/** @type {Map<string, {arrays: string[][], processed: number}>} */
|
|
39
51
|
this.map = new Map();
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
/** @param {string[]} tokens */
|
|
42
55
|
add(tokens) {
|
|
43
56
|
tokens.forEach((token) => {
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
let entry = this.map.get(token);
|
|
58
|
+
if (!entry) {
|
|
59
|
+
entry = { arrays: [], processed: 0 };
|
|
60
|
+
this.map.set(token, entry);
|
|
46
61
|
}
|
|
47
|
-
|
|
62
|
+
entry.arrays.push(tokens);
|
|
48
63
|
});
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
createSorter() {
|
|
52
67
|
const sorter = new Sorter();
|
|
53
|
-
sorter.sorterMap = new Map();
|
|
54
68
|
|
|
55
69
|
// Convert map entries to array and sort by frequency (descending), then alphabetically
|
|
56
70
|
const entries = Array.from(this.map.entries()).sort((a, b) => {
|
|
@@ -63,18 +77,17 @@ class TokenChain {
|
|
|
63
77
|
return a[0].localeCompare(b[0]);
|
|
64
78
|
});
|
|
65
79
|
|
|
66
|
-
sorter.keys = [];
|
|
67
|
-
|
|
68
80
|
entries.forEach(([token, data]) => {
|
|
69
81
|
if (data.processed < data.arrays.length) {
|
|
70
82
|
const chain = new TokenChain();
|
|
71
83
|
|
|
72
84
|
data.arrays.forEach((tokens) => {
|
|
73
85
|
// Build new array without the current token instead of splicing
|
|
86
|
+
/** @type {string[]} */
|
|
74
87
|
const filtered = [];
|
|
75
|
-
for (
|
|
76
|
-
if (
|
|
77
|
-
filtered.push(
|
|
88
|
+
for (const t of tokens) {
|
|
89
|
+
if (t !== token) {
|
|
90
|
+
filtered.push(t);
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
93
|
|