html-minifier-next 4.16.4 → 4.17.1

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
  }
@@ -6,6 +6,7 @@ import { RE_TRAILING_SEMICOLON } from './constants.js';
6
6
  import { canCollapseWhitespace, canTrimWhitespace } from './whitespace.js';
7
7
  import { wrapCSS, unwrapCSS } from './content.js';
8
8
  import { getSVGMinifierOptions } from './svg.js';
9
+ import { getPreset, getPresetNames } from '../presets.js';
9
10
 
10
11
  // Helper functions
11
12
 
@@ -26,8 +27,8 @@ function shouldMinifyInnerHTML(options) {
26
27
  /**
27
28
  * @param {Partial<MinifierOptions>} inputOptions - User-provided options
28
29
  * @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
30
+ * @param {Function} deps.getLightningCSS - Function to lazily load Lightning CSS
31
+ * @param {Function} deps.getTerser - Function to lazily load Terser
31
32
  * @param {Function} deps.getSwc - Function to lazily load @swc/core
32
33
  * @param {LRU} deps.cssMinifyCache - CSS minification cache
33
34
  * @param {LRU} deps.jsMinifyCache - JS minification cache
@@ -64,7 +65,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
64
65
  if (typeof value === 'string') {
65
66
  return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
66
67
  }
67
- return value; // Already a RegExp or other type
68
+ return value; // Already a RegExp or another type
68
69
  };
69
70
 
70
71
  const parseRegExpArray = (arr) => {
@@ -84,9 +85,25 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
84
85
  });
85
86
  };
86
87
 
88
+ // Apply preset first if specified (so user options can override preset values)
89
+ if (inputOptions.preset) {
90
+ const preset = getPreset(inputOptions.preset);
91
+ if (preset) {
92
+ Object.assign(options, preset);
93
+ } else {
94
+ const available = getPresetNames().join(', ');
95
+ console.warn(`HTML Minifier Next: Unknown preset “${inputOptions.preset}”. Available presets: ${available}`);
96
+ }
97
+ }
98
+
87
99
  Object.keys(inputOptions).forEach(function (key) {
88
100
  const option = inputOptions[key];
89
101
 
102
+ // Skip preset key—it’s already been processed
103
+ if (key === 'preset') {
104
+ return;
105
+ }
106
+
90
107
  if (key === 'caseSensitive') {
91
108
  if (option) {
92
109
  options.name = identity;
@@ -201,7 +218,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
201
218
  // Validate engine
202
219
  const supportedEngines = ['terser', 'swc'];
203
220
  if (!supportedEngines.includes(engine)) {
204
- throw new Error(`Unsupported JS minifier engine: "${engine}". Supported engines: ${supportedEngines.join(', ')}`);
221
+ throw new Error(`Unsupported JS minifier engine: “${engine}”. Supported engines: ${supportedEngines.join(', ')}`);
205
222
  }
206
223
 
207
224
  // Extract engine-specific options (excluding `engine` field itself)
@@ -308,14 +325,14 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
308
325
  relateUrlOptions = {};
309
326
  }
310
327
 
311
- // Cache RelateURL instance for reuse (expensive to create)
328
+ // Cache relateurl instance for reuse (expensive to create)
312
329
  const relateUrlInstance = new RelateURL(relateUrlOptions.site || '', relateUrlOptions);
313
330
 
314
331
  // Create instance-specific cache (results depend on site configuration)
315
332
  const instanceCache = urlMinifyCache ? new (urlMinifyCache.constructor)(500) : null;
316
333
 
317
334
  options.minifyURLs = function (text) {
318
- // Fast-path: Skip if text doesn't look like a URL that needs processing
335
+ // Fast-path: Skip if text doesnt look like a URL that needs processing
319
336
  // Only process if contains URL-like characters (`/`, `:`, `#`, `?`) or spaces that need encoding
320
337
  if (!/[/:?#\s]/.test(text)) {
321
338
  return text;
@@ -347,17 +364,17 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, cssM
347
364
  };
348
365
  } else if (key === 'minifySVG') {
349
366
  // Process SVG minification options
350
- // Unlike minifyCSS/minifyJS, this is a simple options object, not a function
367
+ // Unlike `minifyCSS`/`minifyJS`, this is a simple options object, not a function
351
368
  // The actual minification is applied inline during attribute processing
352
369
  options.minifySVG = getSVGMinifierOptions(option);
353
370
  } else if (key === 'customAttrCollapse') {
354
- // Single RegExp pattern
371
+ // Single regex pattern
355
372
  options[key] = parseRegExp(option);
356
373
  } else if (key === 'customAttrSurround') {
357
374
  // Nested array of RegExp pairs: `[[openRegExp, closeRegExp], …]`
358
375
  options[key] = parseNestedRegExpArray(option);
359
376
  } else if (['customAttrAssign', 'customEventAttributes', 'ignoreCustomComments', 'ignoreCustomFragments'].includes(key)) {
360
- // Array of RegExp patterns
377
+ // Array of regex patterns
361
378
  options[key] = parseRegExpArray(option);
362
379
  } else {
363
380
  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 = {
@@ -24,7 +24,6 @@ export const presets = {
24
24
  comprehensive: {
25
25
  caseSensitive: true,
26
26
  collapseBooleanAttributes: true,
27
- collapseInlineTagWhitespace: true,
28
27
  collapseWhitespace: true,
29
28
  continueOnParseError: true,
30
29
  decodeEntities: true,
@@ -49,7 +48,7 @@ export const presets = {
49
48
 
50
49
  /**
51
50
  * Get preset configuration by name
52
- * @param {string} name - Preset name ('conservative' or 'comprehensive')
51
+ * @param {string} name - Preset name (conservative or comprehensive)
53
52
  * @returns {object|null} Preset options object or null if not found
54
53
  */
55
54
  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';