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.
@@ -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 {Partial<MinifierOptions>} inputOptions - User-provided options
29
- * @param {Object} deps - Dependencies from htmlminifier.js
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) : arr;
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 arr;
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 = cssMinifyCache.get(cssKey);
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 getLightningCSS();
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
- cssMinifyCache.set(cssKey, inFlight);
182
+ cssCache.set(cssKey, inFlight);
179
183
  const resolved = await inFlight;
180
- cssMinifyCache.set(cssKey, resolved);
184
+ cssCache.set(cssKey, resolved);
181
185
  return resolved;
182
186
  } catch (err) {
183
- cssMinifyCache.delete(cssKey);
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 = jsMinifyCache.get(jsKey);
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 getTerser();
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 getSwc();
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
- jsMinifyCache.set(jsKey, inFlight);
295
+ jsCache.set(jsKey, inFlight);
287
296
  const resolved = await inFlight;
288
- jsMinifyCache.set(jsKey, resolved);
297
+ jsCache.set(jsKey, resolved);
289
298
  return resolved;
290
299
  } catch (err) {
291
- if (jsKey) jsMinifyCache.delete(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 = svgMinifyCache.get(svgKey);
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 getSvgo();
386
+ const optimize = await loadSvgo();
374
387
  const result = optimize(svgContent, svgoOptions);
375
388
  return result.data;
376
389
  })();
377
390
 
378
- svgMinifyCache.set(svgKey, inFlight);
391
+ svgCache.set(svgKey, inFlight);
379
392
  const resolved = await inFlight;
380
- svgMinifyCache.set(svgKey, resolved);
393
+ svgCache.set(svgKey, resolved);
381
394
  return resolved;
382
395
  } catch (err) {
383
- svgMinifyCache.delete(svgKey);
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
  }
@@ -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
- function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
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[leadEnd])) {
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[trailStart - 1])) {
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[end - 1])) {
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 (let i = 0, len = this.keys.length; i < len; i++) {
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
- if (tokens[j] === token) {
22
+ const t = /** @type {string} */ (tokens[j]);
23
+ if (t === token) {
12
24
  matchCount++;
13
25
  } else {
14
- others.push(tokens[j]);
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 (let j = 0; j < others.length; j++) {
25
- tokens[writeIdx++] = others[j];
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).sort(tokens, newFromIndex);
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
- // Use map instead of object properties for better performance
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
- if (!this.map.has(token)) {
45
- this.map.set(token, { arrays: [], processed: 0 });
57
+ let entry = this.map.get(token);
58
+ if (!entry) {
59
+ entry = { arrays: [], processed: 0 };
60
+ this.map.set(token, entry);
46
61
  }
47
- this.map.get(token).arrays.push(tokens);
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 (let i = 0; i < tokens.length; i++) {
76
- if (tokens[i] !== token) {
77
- filtered.push(tokens[i]);
88
+ for (const t of tokens) {
89
+ if (t !== token) {
90
+ filtered.push(t);
78
91
  }
79
92
  }
80
93