html-minifier-next 4.16.3 → 4.17.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.
@@ -1,15 +1,15 @@
1
1
  // Imports
2
2
 
3
3
  import {
4
- headerTags,
5
- descriptionTags,
6
- pBlockTags,
4
+ headerElements,
5
+ descriptionElements,
6
+ pBlockElements,
7
7
  rubyEndTagOmission,
8
8
  rubyRtcEndTagOmission,
9
- optionTag,
10
- tableContentTags,
11
- tableSectionTags,
12
- cellTags
9
+ optionElements,
10
+ tableContentElements,
11
+ tableSectionElements,
12
+ cellElements
13
13
  } from './constants.js';
14
14
  import { hasAttrName } from './attributes.js';
15
15
 
@@ -21,7 +21,7 @@ function canRemoveParentTag(optionalStartTag, tag) {
21
21
  case 'head':
22
22
  return true;
23
23
  case 'body':
24
- return !headerTags.has(tag);
24
+ return !headerElements.has(tag);
25
25
  case 'colgroup':
26
26
  return tag === 'col';
27
27
  case 'tbody':
@@ -35,7 +35,7 @@ function isStartTagMandatory(optionalEndTag, tag) {
35
35
  case 'colgroup':
36
36
  return optionalEndTag === 'colgroup';
37
37
  case 'tbody':
38
- return tableSectionTags.has(optionalEndTag);
38
+ return tableSectionElements.has(optionalEndTag);
39
39
  }
40
40
  return false;
41
41
  }
@@ -54,9 +54,9 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
54
54
  return tag === optionalEndTag;
55
55
  case 'dt':
56
56
  case 'dd':
57
- return descriptionTags.has(tag);
57
+ return descriptionElements.has(tag);
58
58
  case 'p':
59
- return pBlockTags.has(tag);
59
+ return pBlockElements.has(tag);
60
60
  case 'rb':
61
61
  case 'rt':
62
62
  case 'rp':
@@ -64,15 +64,15 @@ function canRemovePrecedingTag(optionalEndTag, tag) {
64
64
  case 'rtc':
65
65
  return rubyRtcEndTagOmission.has(tag);
66
66
  case 'option':
67
- return optionTag.has(tag);
67
+ return optionElements.has(tag);
68
68
  case 'thead':
69
69
  case 'tbody':
70
- return tableContentTags.has(tag);
70
+ return tableContentElements.has(tag);
71
71
  case 'tfoot':
72
72
  return tag === 'tbody';
73
73
  case 'td':
74
74
  case 'th':
75
- return cellTags.has(tag);
75
+ return cellElements.has(tag);
76
76
  }
77
77
  return false;
78
78
  }
@@ -175,7 +175,7 @@ function parseRemoveEmptyElementsExcept(input, options) {
175
175
  if (typeof item === 'string') {
176
176
  const spec = parseElementSpec(item, options);
177
177
  if (!spec && options.log) {
178
- options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: "' + item + '"');
178
+ options.log('Warning: Unable to parse “removeEmptyElementsExcept” specification: ' + item + '');
179
179
  }
180
180
  return spec;
181
181
  }
@@ -26,8 +26,8 @@ function shouldMinifyInnerHTML(options) {
26
26
  /**
27
27
  * @param {Partial<MinifierOptions>} inputOptions - User-provided options
28
28
  * @param {Object} deps - Dependencies from htmlminifier.js
29
- * @param {Function} deps.getLightningCSS - Function to lazily load lightningcss
30
- * @param {Function} deps.getTerser - Function to lazily load terser
29
+ * @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
30
+ * @param {Function} deps.getTerser - Function to lazily load Terser
31
31
  * @param {Function} deps.getSwc - Function to lazily load @swc/core
32
32
  * @param {LRU} deps.cssMinifyCache - CSS minification cache
33
33
  * @param {LRU} deps.jsMinifyCache - JS minification cache
@@ -64,7 +64,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
64
64
  if (typeof value === 'string') {
65
65
  return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
66
66
  }
67
- return value; // Already a RegExp or other type
67
+ return value; // Already a RegExp or another type
68
68
  };
69
69
 
70
70
  const parseRegExpArray = (arr) => {
@@ -201,7 +201,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
201
201
  // Validate engine
202
202
  const supportedEngines = ['terser', 'swc'];
203
203
  if (!supportedEngines.includes(engine)) {
204
- throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
204
+ throw new Error(`Unsupported JS minifier engine: “${engine}”. Supported engines: ${supportedEngines.join(', ')}`);
205
205
  }
206
206
 
207
207
  // Extract engine-specific options (excluding `engine` field itself)
@@ -308,14 +308,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
308
308
  relateUrlOptions = {};
309
309
  }
310
310
 
311
- // Cache RelateURL instance for reuse (expensive to create)
311
+ // Cache relateurl instance for reuse (expensive to create)
312
312
  const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
313
313
 
314
314
  // Create instance-specific cache (results depend on site configuration)
315
315
  const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
316
316
 
317
317
  options.minifyURLs = function (text) {
318
- // Fast-path: Skip if text doesn't look like a URL that needs processing
318
+ // Fast-path: Skip if text doesnt look like a URL that needs processing
319
319
  // Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
320
320
  if (!/[/:?#\s]/.test(text)) {
321
321
  return text;
@@ -347,17 +347,17 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
347
347
  };
348
348
  } else if (key === 'minifySVG') {
349
349
  // Process SVG minification options
350
- // Unlike minifyCSS/minifyJS, this is a simple options object, not a function
350
+ // Unlike `minifyCSS`/`minifyJS`, this is a simple options object, not a function
351
351
  // The actual minification is applied inline during attribute processing
352
352
  options.minifySVG = getSVGMinifierOptions(option);
353
353
  } else if (key === 'customAttrCollapse') {
354
- // Single RegExp pattern
354
+ // Single regex pattern
355
355
  options[key] = parseRegExp(option);
356
356
  } else if (key === 'customAttrSurround') {
357
357
  // Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
358
358
  options[key] = parseNestedRegExpArray(option);
359
359
  } else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
360
- // Array of RegExp patterns
360
+ // Array of regex patterns
361
361
  options[key] = parseRegExpArray(option);
362
362
  } else {
363
363
  options[key] = option;
package/src/lib/svg.js CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Lightweight SVG optimizations:
3
- *
4
3
  * - Numeric precision reduction for coordinates and path data
5
4
  * - Whitespace removal in attribute values (numeric sequences)
6
5
  * - Default attribute removal (safe, well-documented defaults)
@@ -9,6 +8,8 @@
9
8
  * - Path data space optimization
10
9
  */
11
10
 
11
+ // Imports
12
+
12
13
  import { LRU } from './utils.js';
13
14
  import { RE_NUMERIC_VALUE } from './constants.js';
14
15
 
@@ -96,7 +97,7 @@ function minifyNumber(num, precision = 3) {
96
97
  if (num === '1.0' || num === '1.00' || num === '1.000') return '1';
97
98
 
98
99
  // Check cache
99
- // (Note: uses input string as key, so “0.0000” and “0.00000” create separate entries.
100
+ // (Note: Uses input string as key, so “0.0000” and “0.00000” create separate entries.
100
101
  // This is intentional to avoid parsing overhead.
101
102
  // Real-world SVG files from export tools typically use consistent formats.)
102
103
  const cacheKey = `${num}:${precision}`;
@@ -135,15 +136,15 @@ function minifyPathData(pathData, precision = 3) {
135
136
 
136
137
  // Remove unnecessary spaces around path commands
137
138
  // Safe to remove space after a command letter when it’s followed by a number (which may be negative)
138
- // M 10 20 → M10 20, L -5 -3 → L-5-3
139
+ // `M 10 20``M10 20`, `L -5 -3``L-5-3`
139
140
  result = result.replace(/([MLHVCSQTAZmlhvcsqtaz])\s+(?=-?\d)/g, '$1');
140
141
 
141
142
  // Safe to remove space before command letter when preceded by a number
142
- // 0 L → 0L, 20 M → 20M
143
+ // `0 L``0L`, `20 M``20M`
143
144
  result = result.replace(/(\d)\s+([MLHVCSQTAZmlhvcsqtaz])/g, '$1$2');
144
145
 
145
146
  // Safe to remove space before negative number when preceded by a number
146
- // 10 -20 → 10-20 (numbers are separated by the minus sign)
147
+ // `10 -20``10-20` (numbers are separated by the minus sign)
147
148
  result = result.replace(/(\d)\s+(-\d)/g, '$1$2');
148
149
 
149
150
  return result;
@@ -152,9 +153,9 @@ function minifyPathData(pathData, precision = 3) {
152
153
  /**
153
154
  * Minify whitespace in numeric attribute values
154
155
  * Examples:
155
- * "10 , 20" → "10,20"
156
- * "translate( 10 20 )" → "translate(10 20)"
157
- * "100, 10 40, 198" → "100,10 40,198"
156
+ * - “10 , 20" → "10,20"
157
+ * - "translate( 10 20 )" → "translate(10 20)"
158
+ * - "100, 10 40, 198" → "100,10 40,198"
158
159
  *
159
160
  * @param {string} value - Attribute value to minify
160
161
  * @returns {string} Minified value
@@ -187,8 +188,7 @@ function minifyColor(color) {
187
188
 
188
189
  // Don’t process values that aren’t simple colors (preserve case-sensitive references)
189
190
  // `url(#id)`, `var(--name)`, `inherit`, `currentColor`, etc.
190
- if (trimmed.includes('url(') || trimmed.includes('var(') ||
191
- trimmed === 'inherit' || trimmed === 'currentColor') {
191
+ if (trimmed.includes('url(') || trimmed.includes('var(') || trimmed === 'inherit' || trimmed === 'currentColor') {
192
192
  return trimmed;
193
193
  }
194
194
 
@@ -196,7 +196,7 @@ function minifyColor(color) {
196
196
  const lower = trimmed.toLowerCase();
197
197
 
198
198
  // Shorten 6-digit hex to 3-digit when possible
199
- // #aabbcc → #abc, #000000 → #000
199
+ // `#aabbcc``#abc`, `#000000``#000`
200
200
  const hexMatch = lower.match(/^#([0-9a-f]{6})$/);
201
201
  if (hexMatch) {
202
202
  const hex = hexMatch[1];
@@ -216,7 +216,7 @@ function minifyColor(color) {
216
216
  return NAMED_COLORS[lower] || lower;
217
217
  }
218
218
 
219
- // Convert rgb(255,255,255) to hex
219
+ // Convert rgb() to hex
220
220
  const rgbMatch = lower.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
221
221
  if (rgbMatch) {
222
222
  const r = parseInt(rgbMatch[1], 10);
@@ -247,14 +247,14 @@ function minifyColor(color) {
247
247
  const NUMERIC_ATTRS = new Set([
248
248
  'd', // Path data
249
249
  'points', // Polygon/polyline points
250
- 'viewBox', // viewBox coordinates
250
+ 'viewBox', // `viewBox` coordinates
251
251
  'transform', // Transform functions
252
252
  'x', 'y', 'x1', 'y1', 'x2', 'y2', // Coordinates
253
253
  'cx', 'cy', 'r', 'rx', 'ry', // Circle/ellipse
254
254
  'width', 'height', // Dimensions
255
255
  'dx', 'dy', // Text offsets
256
256
  'offset', // Gradient offset
257
- 'startOffset', // textPath
257
+ 'startOffset', // `textPath`
258
258
  'pathLength', // Path length
259
259
  'stdDeviation', // Filter params
260
260
  'baseFrequency', // Turbulence
@@ -9,7 +9,8 @@ import {
9
9
  RE_NBSP_TRAILING_GROUP,
10
10
  RE_NBSP_TRAILING_STRIP,
11
11
  inlineElementsToKeepWhitespace,
12
- inlineElementsToKeepWhitespaceWithin
12
+ inlineElementsToKeepWhitespaceWithin,
13
+ formControlElements
13
14
  } from './constants.js';
14
15
 
15
16
  // Trim whitespace
@@ -71,7 +72,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
71
72
  }
72
73
 
73
74
  if (trimLeft) {
74
- // Non-breaking space is specifically handled inside the replacer function
75
+ // No-break space is specifically handled inside the replacer function
75
76
  str = str.replace(/^[ \n\r\t\f\xA0]+/, function (spaces) {
76
77
  const conservative = !lineBreakBefore && options.conservativeCollapse;
77
78
  if (conservative && spaces === '\t') {
@@ -82,7 +83,7 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
82
83
  }
83
84
 
84
85
  if (trimRight) {
85
- // Non-breaking space is specifically handled inside the replacer function
86
+ // No-break space is specifically handled inside the replacer function
86
87
  str = str.replace(/[ \n\r\t\f\xA0]+$/, function (spaces) {
87
88
  const conservative = !lineBreakAfter && options.conservativeCollapse;
88
89
  if (conservative && spaces === '\t') {
@@ -106,11 +107,42 @@ function collapseWhitespace(str, options, trimLeft, trimRight, collapseAll) {
106
107
 
107
108
  // Collapse whitespace smartly based on surrounding tags
108
109
 
109
- function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements, inlineTextSet) {
110
+ function collapseWhitespaceSmart(str, prevTag, nextTag, prevAttrs, nextAttrs, options, inlineElements, inlineTextSet) {
111
+ const prevTagName = prevTag && (prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag);
112
+ const nextTagName = nextTag && (nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag);
113
+
114
+ // Helper: Check if an input element has `type="hidden"`
115
+ const isHiddenInput = (tagName, attrs) => {
116
+ if (tagName !== 'input' || !attrs || !attrs.length) return false;
117
+ const typeAttr = attrs.find(attr => attr.name === 'type');
118
+ return typeAttr && typeAttr.value === 'hidden';
119
+ };
120
+
121
+ // Check if prev/next are non-rendering (hidden) elements
122
+ const prevIsHidden = isHiddenInput(prevTagName, prevAttrs);
123
+ const nextIsHidden = isHiddenInput(nextTagName, nextAttrs);
124
+
110
125
  let trimLeft = prevTag && !inlineElementsToKeepWhitespace.has(prevTag);
126
+
127
+ // Smart default behavior: Collapse space after non-rendering elements (`type="hidden"`)
128
+ // This happens even in basic `collapseWhitespace` mode (safe optimization)
129
+ if (!trimLeft && prevIsHidden && str && !/\S/.test(str)) {
130
+ trimLeft = true;
131
+ }
132
+
133
+ // Aggressive mode: Collapse between all form controls (pure whitespace only)
134
+ const isPureWhitespace = str && !/\S/.test(str);
135
+ if (!trimLeft && prevTagName && nextTagName &&
136
+ options.collapseInlineTagWhitespace &&
137
+ isPureWhitespace &&
138
+ formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
139
+ trimLeft = true;
140
+ }
141
+
111
142
  if (trimLeft && !options.collapseInlineTagWhitespace) {
112
143
  trimLeft = prevTag.charAt(0) === '/' ? !inlineElements.has(prevTag.slice(1)) : !inlineTextSet.has(prevTag);
113
144
  }
145
+
114
146
  // When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
115
147
  if (trimLeft && options.collapseInlineTagWhitespace) {
116
148
  const tagName = prevTag.charAt(0) === '/' ? prevTag.slice(1) : prevTag;
@@ -118,10 +150,26 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
118
150
  trimLeft = false;
119
151
  }
120
152
  }
153
+
121
154
  let trimRight = nextTag && !inlineElementsToKeepWhitespace.has(nextTag);
155
+
156
+ // Smart default behavior: Collapse space before non-rendering elements (`type="hidden"`)
157
+ if (!trimRight && nextIsHidden && str && !/\S/.test(str)) {
158
+ trimRight = true;
159
+ }
160
+
161
+ // Aggressive mode: Same as `trimLeft`
162
+ if (!trimRight && prevTagName && nextTagName &&
163
+ options.collapseInlineTagWhitespace &&
164
+ isPureWhitespace &&
165
+ formControlElements.has(prevTagName) && formControlElements.has(nextTagName)) {
166
+ trimRight = true;
167
+ }
168
+
122
169
  if (trimRight && !options.collapseInlineTagWhitespace) {
123
170
  trimRight = nextTag.charAt(0) === '/' ? !inlineTextSet.has(nextTag.slice(1)) : !inlineElements.has(nextTag);
124
171
  }
172
+
125
173
  // When `collapseInlineTagWhitespace` is enabled, still preserve whitespace around inline text elements
126
174
  if (trimRight && options.collapseInlineTagWhitespace) {
127
175
  const tagName = nextTag.charAt(0) === '/' ? nextTag.slice(1) : nextTag;
@@ -129,6 +177,7 @@ function collapseWhitespaceSmart(str, prevTag, nextTag, options, inlineElements,
129
177
  trimRight = false;
130
178
  }
131
179
  }
180
+
132
181
  return collapseWhitespace(str, options, trimLeft, trimRight, prevTag && nextTag);
133
182
  }
134
183
 
package/src/presets.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Preset configurations for HTML Minifier Next
2
+ * Preset configurations
3
3
  *
4
4
  * Presets provide curated option sets for common use cases:
5
- * - conservative: Safe minification suitable for most projects
6
- * - comprehensive: Aggressive minification for maximum file size reduction
5
+ * - `conservative`: Safe minification suitable for most projects
6
+ * - `comprehensive`: Aggressive minification for maximum file size reduction
7
7
  */
8
8
 
9
9
  export const presets = {
@@ -49,7 +49,7 @@ export const presets = {
49
49
 
50
50
  /**
51
51
  * Get preset configuration by name
52
- * @param {string} name - Preset name ('conservative' or 'comprehensive')
52
+ * @param {string} name - Preset name (conservative or comprehensive)
53
53
  * @returns {object|null} Preset options object or null if not found
54
54
  */
55
55
  export function getPreset(name) {
package/src/tokenchain.js CHANGED
@@ -35,7 +35,7 @@ class Sorter {
35
35
 
36
36
  class TokenChain {
37
37
  constructor() {
38
- // Use Map instead of object properties for better performance
38
+ // Use map instead of object properties for better performance
39
39
  this.map = new Map();
40
40
  }
41
41
 
@@ -52,7 +52,7 @@ class TokenChain {
52
52
  const sorter = new Sorter();
53
53
  sorter.sorterMap = new Map();
54
54
 
55
- // Convert Map entries to array and sort by frequency (descending) then alphabetically
55
+ // Convert map entries to array and sort by frequency (descending), then alphabetically
56
56
  const entries = Array.from(this.map.entries()).sort((a, b) => {
57
57
  const m = a[1].arrays.length;
58
58
  const n = b[1].arrays.length;
package/src/lib/index.js DELETED
@@ -1,20 +0,0 @@
1
- // Utils
2
- export * from './utils.js';
3
-
4
- // Constants
5
- export * from './constants.js';
6
-
7
- // Whitespace
8
- export * from './whitespace.js';
9
-
10
- // Attributes
11
- export * from './attributes.js';
12
-
13
- // Elements
14
- export * from './elements.js';
15
-
16
- // Content processors
17
- export * from './content.js';
18
-
19
- // Options
20
- export * from './options.js';
package/src/utils.js DELETED
@@ -1,11 +0,0 @@
1
- export async function replaceAsync(str, regex, asyncFn) {
2
- const promises = [];
3
-
4
- str.replace(regex, (match, ...args) => {
5
- const promise = asyncFn(match, ...args);
6
- promises.push(promise);
7
- });
8
-
9
- const data = await Promise.all(promises);
10
- return str.replace(regex, () => data.shift());
11
- }