html-minifier-next 8.5.0 → 8.5.2
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 +12 -4
- package/dist/types/htmlminifier.d.ts +8 -6
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/htmlparser.d.ts +0 -1
- package/dist/types/htmlparser.d.ts.map +1 -1
- package/html-minifier-next.schema.json +5 -5
- package/package.json +2 -2
- package/src/htmlminifier.js +236 -66
- package/src/htmlparser.js +5 -3
- package/src/lib/option-definitions.js +5 -5
package/README.md
CHANGED
|
@@ -151,18 +151,18 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
151
151
|
| `customEventAttributes`<br>`--custom-event-attributes` | Array of regexes that allow to support custom event attributes (e.g., `ng-click`)—use with `minifyJS` | `[ /^on[a-z]{3,}$/ ]` |
|
|
152
152
|
| `decodeEntities`<br>`--decode-entities` | Use direct Unicode characters whenever possible | `false` |
|
|
153
153
|
| `ignoreCustomComments`<br>`--ignore-custom-comments` | Array of regexes that allow to ignore matching comments | `[ /^!/, /^\s*#/ ]` |
|
|
154
|
-
| `ignoreCustomFragments`<br>`--ignore-custom-fragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`, etc
|
|
154
|
+
| `ignoreCustomFragments`<br>`--ignore-custom-fragments` | Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`, etc.; see [“Minifying server-side templates”](#minifying-server-side-templates)) | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
|
|
155
155
|
| `includeAutoGeneratedTags`<br>`--include-auto-generated-tags` | Insert elements generated by HTML parser | `false` |
|
|
156
156
|
| `inlineCustomElements`<br>`--inline-custom-elements` | Array of names of custom elements which are inline, for whitespace handling—use with `collapseWhitespace` | `[]` |
|
|
157
157
|
| `keepClosingSlash`<br>`--keep-closing-slash` | Keep the trailing slash on void elements and other start tags that carry one, and read it as closing the element | `false` |
|
|
158
158
|
| `maxInputLength`<br>`--max-input-length` | Maximum input length to prevent ReDoS attacks (disabled by default) | `undefined` |
|
|
159
|
-
| `maxLineLength`<br>`--max-line-length` | Specify a maximum line length;
|
|
159
|
+
| `maxLineLength`<br>`--max-line-length` | Specify a maximum line length; output will be split by newlines in tags only (between attributes or before a tag’s `>`), where they can’t change the page, so lines may run longer | `undefined` |
|
|
160
160
|
| `mergeScripts`<br>`--merge-scripts` | Merge consecutive inline `script` elements into one (only merges compatible scripts with same `type`, matching `async`/`defer`/`nomodule`/`nonce`) | `false` |
|
|
161
161
|
| `minifyCSS`<br>`--minify-css` | Minify CSS in `style` elements and attributes (uses [Lightning CSS](https://lightningcss.dev/)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
|
|
162
162
|
| `minifyJS`<br>`--minify-js` | Minify JavaScript in `script` elements and event attributes (uses [Terser](https://terser.org/) or [SWC](https://swc.rs/)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
|
|
163
163
|
| `minifySVG`<br>`--minify-svg` | Minify SVG elements (uses [SVGO](https://svgo.dev/)) | `false` (could be `true`, `Object`) |
|
|
164
164
|
| `minifyURLs`<br>`--minify-urls` | Minify URLs in various attributes | `false` (could be `true`, `String`, `Object`, `Function(text)`) |
|
|
165
|
-
| `noNewlinesBeforeTagClose`<br>`--no-newlines-before-tag-close` | Never
|
|
165
|
+
| `noNewlinesBeforeTagClose`<br>`--no-newlines-before-tag-close` | Never split a line in a tag that closes an element—use with `maxLineLength` | `false` |
|
|
166
166
|
| `partialMarkup`<br>`--partial-markup` | Treat input as a partial HTML fragment, preserving stray end tags (closing tags without opening tags) and preventing auto-closing of unclosed tags at end of input | `false` |
|
|
167
167
|
| `preserveLineBreaks`<br>`--preserve-line-breaks` | Always collapse to one line break (never remove it entirely) when whitespace between tags includes a line break—use with [`collapseWhitespace`](#combining-whitespace-options) | `false` |
|
|
168
168
|
| `preventAttributesEscaping`<br>`--prevent-attributes-escaping` | Prevents the escaping of the values of attributes | `false` |
|
|
@@ -181,7 +181,7 @@ Options can be used in config files (camelCase) or via CLI flags (kebab-case wit
|
|
|
181
181
|
| `sortAttributes`<br>`--sort-attributes` | [Sort attributes by frequency](#sorting-attributes-and-style-classes) | `false` |
|
|
182
182
|
| `sortClassNames`<br>`--sort-class-names` | [Sort style classes by frequency](#sorting-attributes-and-style-classes) | `false` |
|
|
183
183
|
| `strictCustomFragments`<br>`--strict-custom-fragments` | [Reject `ignoreCustomFragments` patterns that risk catastrophic backtracking](#redos-protection) (rather than warning about them) | `false` |
|
|
184
|
-
| `trimCustomFragments`<br>`--trim-custom-fragments` | Trim whitespace around custom fragments (`ignoreCustomFragments`)—use with `collapseWhitespace` | `false` |
|
|
184
|
+
| `trimCustomFragments`<br>`--trim-custom-fragments` | Trim whitespace around custom fragments (`ignoreCustomFragments`) where it does not affect the output (see [“Minifying server-side templates”](#minifying-server-side-templates))—use with `collapseWhitespace` | `false` |
|
|
185
185
|
| `useShortDoctype`<br>`--use-short-doctype` | [Replaces the doctype with the short HTML doctype](https://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype) | `false` |
|
|
186
186
|
|
|
187
187
|
### API-only options
|
|
@@ -680,6 +680,14 @@ For partial HTML fragments (such as template includes, SSI fragments, or closing
|
|
|
680
680
|
|
|
681
681
|
To validate complete HTML markup, use [the W3C validator](https://validator.w3.org/) or one of [several validator packages](https://meiert.com/blog/html-validator-packages/).
|
|
682
682
|
|
|
683
|
+
### Minifying server-side templates
|
|
684
|
+
|
|
685
|
+
By default, `ignoreCustomFragments` preserves `<? … ?>` and `<% … %>` blocks, so PHP, ERB, JSP, and ASP templates can be minified as part of a build, with the markup, CSS, and JavaScript around those blocks being minified as usual. For includes and other partials, add `partialMarkup`.
|
|
686
|
+
|
|
687
|
+
Whitespace next to template blocks is collapsed but kept, as it may matter once the template renders. `trimCustomFragments` removes it where HTML Minifier Next would remove whitespace anyway, as between block-level elements: `<ul> <?php foreach ($items as $item): ?> <li>` loses it, `Hello <?= $name ?>!` keeps it. Tags keep the spacing of their source, and whitespace next to template blocks in attribute values, CSS, and JavaScript is always kept.
|
|
688
|
+
|
|
689
|
+
Where pages are rendered ahead of serving, as with static exports or full-page caches, you can also minify the rendered HTML instead, which makes this a non-issue.
|
|
690
|
+
|
|
683
691
|
### Regex options and flags
|
|
684
692
|
|
|
685
693
|
`customAttrAssign` and `customAttrSurround` patterns are merged into one attribute pattern which carries no flags of its own. `i` and `s` are written into each pattern’s source instead, so they survive the merge. `u`, `v`, and `m` cannot be, and none of them fails loudly when dropped: `u` and `v` only narrow what syntax is legal, so a source valid under either stays valid without it and quietly matches something else—a dropped `u` leaves `\p{L}` matching the literal text `p{L}`—while a dropped `m` leaves `^` and `$` matching at the ends of the input rather than of each line.
|
|
@@ -244,7 +244,8 @@ export type MinifierOptions = {
|
|
|
244
244
|
maxInputLength?: number;
|
|
245
245
|
/**
|
|
246
246
|
* Maximum line length for the output. When set the minifier will wrap
|
|
247
|
-
* output to the given number of characters where possible
|
|
247
|
+
* output to the given number of characters where possible, splitting
|
|
248
|
+
* lines only in tags—between attributes or before a tag’s `>`.
|
|
248
249
|
*
|
|
249
250
|
* Default: No limit
|
|
250
251
|
*/
|
|
@@ -308,8 +309,8 @@ export type MinifierOptions = {
|
|
|
308
309
|
*/
|
|
309
310
|
minifySVG?: boolean | Object;
|
|
310
311
|
/**
|
|
311
|
-
* When wrapping lines,
|
|
312
|
-
*
|
|
312
|
+
* When wrapping lines, never split a line in an end tag (e.g., keeps
|
|
313
|
+
* `</a>` from becoming `</a` and `>` on two lines).
|
|
313
314
|
*
|
|
314
315
|
* Default: `false`
|
|
315
316
|
*/
|
|
@@ -492,9 +493,10 @@ export type MinifierOptions = {
|
|
|
492
493
|
*/
|
|
493
494
|
sortClassNames?: boolean | ((value: string) => string);
|
|
494
495
|
/**
|
|
495
|
-
* When true, whitespace around ignored custom fragments
|
|
496
|
-
*
|
|
497
|
-
*
|
|
496
|
+
* When true, whitespace around ignored custom fragments is removed rather
|
|
497
|
+
* than collapsed where the minifier removes whitespace anyway (e.g., between
|
|
498
|
+
* block-level elements). Whitespace in tags, attribute values, CSS, and
|
|
499
|
+
* JavaScript is kept.
|
|
498
500
|
*
|
|
499
501
|
* Default: `false`
|
|
500
502
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAeG,YAYG,iBAAiB,GAZV;IACR,KAAK,CAAC,WAAW;IACjB,GAAG,CAAC,WAAW;IACf,KAAK,CAAC,WAAW;IACjB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IACnB,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CACxC,CAAmB;
|
|
1
|
+
{"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAeG,YAYG,iBAAiB,GAZV;IACR,KAAK,CAAC,WAAW;IACjB,GAAG,CAAC,WAAW;IACf,KAAK,CAAC,WAAW;IACjB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IACnB,oBAAoB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;CACxC,CAAmB;AA6NvB,qBAAa,UAAU;IAMd,IAAI;IACJ,OAAO;IANd;;;OAGG;IACH,YAAY,IAAI,EAHL,MAGK,EAAE,OAAO,EAFd,iBAEc,EAGxB;IAEK,KAAK,kBA2yBV;CACF"}
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
134
|
"ignoreCustomFragments": {
|
|
135
|
-
"description": "Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}
|
|
135
|
+
"description": "Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php … ?>`, `{{ … }}`, etc.)",
|
|
136
136
|
"type": [
|
|
137
137
|
"string",
|
|
138
138
|
"array"
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"minimum": 0
|
|
166
166
|
},
|
|
167
167
|
"maxLineLength": {
|
|
168
|
-
"description": "Specify a maximum line length;
|
|
168
|
+
"description": "Specify a maximum line length; output will be split by newlines in tags only (between attributes or before a tag’s `>`), where they can’t change the page, so lines may run longer",
|
|
169
169
|
"type": "integer",
|
|
170
170
|
"minimum": 0
|
|
171
171
|
},
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
]
|
|
204
204
|
},
|
|
205
205
|
"noNewlinesBeforeTagClose": {
|
|
206
|
-
"description": "Never
|
|
206
|
+
"description": "Never split a line in a tag that closes an element—use with `--max-line-length`",
|
|
207
207
|
"type": "boolean"
|
|
208
208
|
},
|
|
209
209
|
"partialMarkup": {
|
|
@@ -253,7 +253,7 @@
|
|
|
253
253
|
"type": "boolean"
|
|
254
254
|
},
|
|
255
255
|
"removeEmptyElementsExcept": {
|
|
256
|
-
"description": "Array of elements to preserve (e.g., `td`, `<span aria-hidden=\"true\"
|
|
256
|
+
"description": "Array of elements to preserve (e.g., `td`, `<span aria-hidden=\"true\">`, etc.)—use with `--remove-empty-elements`",
|
|
257
257
|
"type": [
|
|
258
258
|
"string",
|
|
259
259
|
"array"
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
"type": "boolean"
|
|
291
291
|
},
|
|
292
292
|
"trimCustomFragments": {
|
|
293
|
-
"description": "Trim whitespace around custom fragments (`--ignore-custom-fragments`)—use with `--collapse-whitespace`",
|
|
293
|
+
"description": "Trim whitespace around custom fragments (`--ignore-custom-fragments`) where it does not affect the output—use with `--collapse-whitespace`",
|
|
294
294
|
"type": "boolean"
|
|
295
295
|
},
|
|
296
296
|
"strictCustomFragments": {
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@commitlint/cli": "^21.2.2",
|
|
18
18
|
"@eslint/js": "^10.0.1",
|
|
19
19
|
"@swc/core": "^1.16.2",
|
|
20
|
-
"@types/node": "^26.
|
|
20
|
+
"@types/node": "^26.6.1",
|
|
21
21
|
"eslint": "^10.10.0",
|
|
22
22
|
"globals": "^17.12.0",
|
|
23
23
|
"typescript": "^7.0.2",
|
|
@@ -95,5 +95,5 @@
|
|
|
95
95
|
},
|
|
96
96
|
"type": "module",
|
|
97
97
|
"types": "dist/types/htmlminifier.d.ts",
|
|
98
|
-
"version": "8.5.
|
|
98
|
+
"version": "8.5.2"
|
|
99
99
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HTMLParser
|
|
1
|
+
import { HTMLParser } from './htmlparser.js';
|
|
2
2
|
import TokenChain from './tokenchain.js';
|
|
3
3
|
import { presets, getPreset, getPresetNames } from './presets.js';
|
|
4
4
|
|
|
@@ -297,7 +297,8 @@ import { toFragment, replaceCustomFragments } from './lib/fragments.js';
|
|
|
297
297
|
*
|
|
298
298
|
* @prop {number} [maxLineLength]
|
|
299
299
|
* Maximum line length for the output. When set the minifier will wrap
|
|
300
|
-
* output to the given number of characters where possible
|
|
300
|
+
* output to the given number of characters where possible, splitting
|
|
301
|
+
* lines only in tags—between attributes or before a tag’s `>`.
|
|
301
302
|
*
|
|
302
303
|
* Default: No limit
|
|
303
304
|
*
|
|
@@ -350,8 +351,8 @@ import { toFragment, replaceCustomFragments } from './lib/fragments.js';
|
|
|
350
351
|
* Default: `false`
|
|
351
352
|
*
|
|
352
353
|
* @prop {boolean} [noNewlinesBeforeTagClose]
|
|
353
|
-
* When wrapping lines,
|
|
354
|
-
*
|
|
354
|
+
* When wrapping lines, never split a line in an end tag (e.g., keeps
|
|
355
|
+
* `</a>` from becoming `</a` and `>` on two lines).
|
|
355
356
|
*
|
|
356
357
|
* Default: `false`
|
|
357
358
|
*
|
|
@@ -512,9 +513,10 @@ import { toFragment, replaceCustomFragments } from './lib/fragments.js';
|
|
|
512
513
|
* Default: `false`
|
|
513
514
|
*
|
|
514
515
|
* @prop {boolean} [trimCustomFragments]
|
|
515
|
-
* When true, whitespace around ignored custom fragments
|
|
516
|
-
*
|
|
517
|
-
*
|
|
516
|
+
* When true, whitespace around ignored custom fragments is removed rather
|
|
517
|
+
* than collapsed where the minifier removes whitespace anyway (e.g., between
|
|
518
|
+
* block-level elements). Whitespace in tags, attribute values, CSS, and
|
|
519
|
+
* JavaScript is kept.
|
|
518
520
|
*
|
|
519
521
|
* Default: `false`
|
|
520
522
|
*
|
|
@@ -623,6 +625,7 @@ const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript
|
|
|
623
625
|
const RE_START_TAG = /^<[^/!]/;
|
|
624
626
|
const RE_END_TAG = /^<\//;
|
|
625
627
|
const RE_WHITESPACE_ONLY = /^\s+$/;
|
|
628
|
+
const RE_TABS = /\t/g;
|
|
626
629
|
const RE_TRAILING_WHITESPACE = /\s$/;
|
|
627
630
|
|
|
628
631
|
// Pre-compiled patterns for `htmlmin:ignore` block content analysis
|
|
@@ -1045,6 +1048,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1045
1048
|
|
|
1046
1049
|
/** @type {string[]} */
|
|
1047
1050
|
const buffer = [];
|
|
1051
|
+
// Parallel to `buffer`: whether an entry belongs to a start or end tag, the only place lines may break
|
|
1052
|
+
const bufferTags = options.maxLineLength ? /** @type {boolean[]} */ ([]) : null;
|
|
1048
1053
|
/** @type {string} */
|
|
1049
1054
|
let charsPrevTag = '';
|
|
1050
1055
|
let currentChars = '';
|
|
@@ -1065,6 +1070,15 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1065
1070
|
const ignoredMarkupChunks = [];
|
|
1066
1071
|
/** @type {Array<RegExpExecArray | null>} */
|
|
1067
1072
|
const ignoredCustomMarkupChunks = [];
|
|
1073
|
+
// Custom fragments whose surrounding whitespace is never trimmed: in tags, where it
|
|
1074
|
+
// separates attributes, and in CSS and JavaScript, where minifiers can’t judge it
|
|
1075
|
+
/** @type {Set<number>} */
|
|
1076
|
+
const customMarkupInTags = new Set();
|
|
1077
|
+
/** @type {Set<number>} */
|
|
1078
|
+
const customMarkupInCode = new Set();
|
|
1079
|
+
// Attributes joined with the custom fragments they run into, to be written as they are
|
|
1080
|
+
/** @type {WeakSet<HTMLAttribute>} */
|
|
1081
|
+
const opaqueAttrs = new WeakSet();
|
|
1068
1082
|
/** @type {string | undefined} */
|
|
1069
1083
|
let uidIgnore;
|
|
1070
1084
|
/** @type {RegExp | undefined} */
|
|
@@ -1075,6 +1089,8 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1075
1089
|
let uidPattern;
|
|
1076
1090
|
/** @type {RegExp | undefined} */
|
|
1077
1091
|
let uidAttrLeadingPattern;
|
|
1092
|
+
/** @type {RegExp | undefined} */
|
|
1093
|
+
let uidAttrExactPattern;
|
|
1078
1094
|
// Create inline tags/text sets with custom elements
|
|
1079
1095
|
const customElementsInput = options.inlineCustomElements ?? [];
|
|
1080
1096
|
const customElementsArr = Array.isArray(customElementsInput) ? customElementsInput : Array.from(customElementsInput);
|
|
@@ -1149,12 +1165,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1149
1165
|
uidAttr = uniqueId(value);
|
|
1150
1166
|
uidPattern = new RegExp('(\\s*)' + uidAttr + '([0-9]+)' + uidAttr + '(\\s*)', 'g');
|
|
1151
1167
|
uidAttrLeadingPattern = new RegExp('^\\s*' + uidAttr + '(\\d+)' + uidAttr);
|
|
1168
|
+
uidAttrExactPattern = new RegExp('^' + uidAttr + '(\\d+)' + uidAttr + '$');
|
|
1152
1169
|
|
|
1153
1170
|
if (options.minifyCSS !== identity) {
|
|
1154
1171
|
options.minifyCSS = (function (/** @type {ProcessedOptions['minifyCSS']} */ fn) {
|
|
1155
1172
|
return function (/** @type {string} */ text, /** @type {string | undefined} */ type, /** @type {ProcessedOptions['cssContext']} */ context) {
|
|
1156
1173
|
text = text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1157
1174
|
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1175
|
+
customMarkupInCode.add(+index);
|
|
1158
1176
|
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1159
1177
|
});
|
|
1160
1178
|
|
|
@@ -1168,6 +1186,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1168
1186
|
return function (/** @type {string} */ text, /** @type {boolean | undefined} */ inline, /** @type {boolean | undefined} */ isModule) {
|
|
1169
1187
|
return fn(text.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ _match, /** @type {string} */ _prefix, /** @type {string} */ index) {
|
|
1170
1188
|
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1189
|
+
customMarkupInCode.add(+index);
|
|
1171
1190
|
return (chunks?.[1] ?? '') + uidAttr + index + uidAttr + (chunks?.[2] ?? '');
|
|
1172
1191
|
}), inline, isModule);
|
|
1173
1192
|
};
|
|
@@ -1183,6 +1202,77 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1183
1202
|
value = replaceCustomFragments(value, customFragments, replaceFragment);
|
|
1184
1203
|
}
|
|
1185
1204
|
|
|
1205
|
+
// Record the custom fragments in (part of) a tag
|
|
1206
|
+
function markTagFragments(/** @type {string} */ str) {
|
|
1207
|
+
for (const uidMatch of str.matchAll(/** @type {RegExp} */ (uidPattern))) {
|
|
1208
|
+
customMarkupInTags.add(+(uidMatch[2] ?? 0));
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Source text of an attribute, as far as it matters
|
|
1213
|
+
function rawAttr(/** @type {HTMLAttribute} */ attr) {
|
|
1214
|
+
if (opaqueAttrs.has(attr)) {
|
|
1215
|
+
return attr.name;
|
|
1216
|
+
}
|
|
1217
|
+
let raw = attr.name;
|
|
1218
|
+
// Skip values the parser filled in for bare boolean attributes
|
|
1219
|
+
if (attr.value !== undefined && !(attr.quote === '' && attr.value === attr.name)) {
|
|
1220
|
+
raw += (attr.customAssign ?? '=') + (attr.quote ?? '') + attr.value + (attr.quote ?? '');
|
|
1221
|
+
}
|
|
1222
|
+
return (attr.customOpen ?? '') + raw + (attr.customClose ?? '');
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Join each attribute a custom fragment runs into without whitespace with that fragment,
|
|
1227
|
+
* so that no later step separates, drops, changes, or reorders them
|
|
1228
|
+
* @param {HTMLAttribute[]} attrs - Joined in place
|
|
1229
|
+
* @returns {boolean} Whether the first attribute runs into the tag name
|
|
1230
|
+
*/
|
|
1231
|
+
function joinFragmentAttrs(attrs) {
|
|
1232
|
+
const pattern = /** @type {RegExp} */ (uidAttrExactPattern);
|
|
1233
|
+
let joinsTag = false;
|
|
1234
|
+
let writeIndex = 0;
|
|
1235
|
+
// Padding of the previous attribute if that is a fragment without value
|
|
1236
|
+
/** @type {RegExpExecArray | null | undefined} */
|
|
1237
|
+
let prevChunks = null;
|
|
1238
|
+
for (const attr of attrs) {
|
|
1239
|
+
const uidMatch = pattern.exec(attr.name);
|
|
1240
|
+
const chunks = uidMatch ? ignoredCustomMarkupChunks[+(uidMatch[1] ?? 0)] : null;
|
|
1241
|
+
const joins = prevChunks ? prevChunks[2] === '' : chunks?.[1] === '';
|
|
1242
|
+
prevChunks = attr.value === undefined ? chunks : null;
|
|
1243
|
+
if (!joins) {
|
|
1244
|
+
attrs[writeIndex++] = attr;
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
const prev = writeIndex ? attrs[writeIndex - 1] : undefined;
|
|
1248
|
+
if (!prev) {
|
|
1249
|
+
joinsTag = true;
|
|
1250
|
+
}
|
|
1251
|
+
/** @type {HTMLAttribute} */
|
|
1252
|
+
const joined = { name: (prev ? rawAttr(prev) : '') + rawAttr(attr), customAssign: '=', customOpen: '', customClose: '', quote: '' };
|
|
1253
|
+
opaqueAttrs.add(joined);
|
|
1254
|
+
attrs[prev ? writeIndex - 1 : writeIndex++] = joined;
|
|
1255
|
+
}
|
|
1256
|
+
attrs.length = writeIndex;
|
|
1257
|
+
return joinsTag;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Restore the whitespace on one side of a custom fragment
|
|
1262
|
+
* @param {string} str - Whitespace the minifier left there and the fragment’s own padding
|
|
1263
|
+
* @param {boolean} trim
|
|
1264
|
+
* @param {boolean} leading
|
|
1265
|
+
*/
|
|
1266
|
+
function restoreFragmentSide(str, trim, leading) {
|
|
1267
|
+
if (!str) {
|
|
1268
|
+
return str;
|
|
1269
|
+
}
|
|
1270
|
+
return collapseWhitespace(str, {
|
|
1271
|
+
preserveLineBreaks: options.preserveLineBreaks,
|
|
1272
|
+
conservativeCollapse: !trim
|
|
1273
|
+
}, leading, !leading);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1186
1276
|
function canCollapseWhitespace(/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs) {
|
|
1187
1277
|
return options.canCollapseWhitespace(tag, attrs, defaultCanCollapseWhitespace);
|
|
1188
1278
|
}
|
|
@@ -1191,12 +1281,22 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1191
1281
|
return options.canTrimWhitespace(tag, attrs, defaultCanTrimWhitespace);
|
|
1192
1282
|
}
|
|
1193
1283
|
|
|
1284
|
+
function bufferPush(/** @type {string} */ entry, /** @type {boolean} */ isTag) {
|
|
1285
|
+
buffer.push(entry);
|
|
1286
|
+
bufferTags?.push(isTag);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function bufferTruncate(/** @type {number} */ length) {
|
|
1290
|
+
buffer.length = Math.max(0, length);
|
|
1291
|
+
if (bufferTags) bufferTags.length = buffer.length;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1194
1294
|
function removeStartTag() {
|
|
1195
1295
|
let index = buffer.length - 1;
|
|
1196
1296
|
while (index > 0 && !RE_START_TAG.test(buffer[index] ?? '')) {
|
|
1197
1297
|
index--;
|
|
1198
1298
|
}
|
|
1199
|
-
|
|
1299
|
+
bufferTruncate(index);
|
|
1200
1300
|
}
|
|
1201
1301
|
|
|
1202
1302
|
// Whether the element being parsed holds text rather than markup. The namespace decides
|
|
@@ -1252,13 +1352,14 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1252
1352
|
const noTrim = stackNoTrimWhitespace.length > 0;
|
|
1253
1353
|
if (index < buffer.length - 1 && (keepsWhitespace() || noTrim) && RE_END_TAG.test(buffer[index] ?? '')) {
|
|
1254
1354
|
buffer.splice(index, 1);
|
|
1355
|
+
bufferTags?.splice(index, 1);
|
|
1255
1356
|
// Only collapsed whitespace can merge
|
|
1256
1357
|
if (options.collapseWhitespace && !noTrim) {
|
|
1257
1358
|
mergeWhitespaceRuns(index);
|
|
1258
1359
|
}
|
|
1259
1360
|
return;
|
|
1260
1361
|
}
|
|
1261
|
-
|
|
1362
|
+
bufferTruncate(index);
|
|
1262
1363
|
}
|
|
1263
1364
|
|
|
1264
1365
|
// Look for trailing whitespaces, bypass any inline tags
|
|
@@ -1458,7 +1559,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1458
1559
|
if (text) {
|
|
1459
1560
|
hasChars = true;
|
|
1460
1561
|
}
|
|
1461
|
-
|
|
1562
|
+
bufferPush(text, false);
|
|
1462
1563
|
}
|
|
1463
1564
|
|
|
1464
1565
|
// Comment finalization (sync): Optional tag handling, `htmlmin:ignore` whitespace collapsing, buffer push
|
|
@@ -1569,7 +1670,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1569
1670
|
}
|
|
1570
1671
|
}
|
|
1571
1672
|
|
|
1572
|
-
|
|
1673
|
+
bufferPush(comment, false);
|
|
1573
1674
|
}
|
|
1574
1675
|
|
|
1575
1676
|
// Inline scripts are minified one at a time as the parse reaches them, which serializes
|
|
@@ -1752,7 +1853,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1752
1853
|
const needsXMLSlash = unary && Boolean(options.insideSVG) && Boolean(options.minifySVG);
|
|
1753
1854
|
const hasUnarySlash = needsXMLSlash || (unarySlash && (useNameParentForTag ? optionsParent : options).keepClosingSlash);
|
|
1754
1855
|
|
|
1755
|
-
|
|
1856
|
+
bufferPush(openTag, true);
|
|
1857
|
+
|
|
1858
|
+
const fragmentJoinsTag = uidAttrExactPattern ? joinFragmentAttrs(attrs) : false;
|
|
1859
|
+
// Whatever runs into the tag name has to stay first
|
|
1860
|
+
const tagFragment = fragmentJoinsTag ? attrs.shift() : undefined;
|
|
1756
1861
|
|
|
1757
1862
|
// Remove duplicate attributes (per HTML spec, first occurrence wins)
|
|
1758
1863
|
// Duplicate attributes result in invalid HTML
|
|
@@ -1763,11 +1868,17 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1763
1868
|
// By the time tags are processed, `createSortFns` has replaced any truthy non-function value
|
|
1764
1869
|
/** @type {(tag: string, attrs: HTMLAttribute[]) => void} */ (options.sortAttributes)(tag, attrs);
|
|
1765
1870
|
}
|
|
1871
|
+
if (tagFragment) {
|
|
1872
|
+
attrs.unshift(tagFragment);
|
|
1873
|
+
}
|
|
1766
1874
|
|
|
1767
1875
|
const attrResults = new Array(attrs.length);
|
|
1768
1876
|
let anyThenable = false;
|
|
1769
1877
|
for (let i = 0; i < attrs.length; i++) {
|
|
1770
|
-
const
|
|
1878
|
+
const attr = /** @type {HTMLAttribute} */ (attrs[i]);
|
|
1879
|
+
const result = opaqueAttrs.has(attr)
|
|
1880
|
+
? { name: attr.name, value: undefined, attr }
|
|
1881
|
+
: normalizeAttr(attr, attrs, tag, options, minifyHTML);
|
|
1771
1882
|
if (!anyThenable && isThenable(result)) {
|
|
1772
1883
|
anyThenable = true;
|
|
1773
1884
|
}
|
|
@@ -1785,11 +1896,22 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1785
1896
|
}
|
|
1786
1897
|
}
|
|
1787
1898
|
if (lastKeptIndex !== -1) {
|
|
1788
|
-
|
|
1899
|
+
if (!fragmentJoinsTag) {
|
|
1900
|
+
bufferPush(' ', true);
|
|
1901
|
+
}
|
|
1789
1902
|
for (let i = 0; i <= lastKeptIndex; i++) {
|
|
1790
1903
|
const normalizedAttr = normalizedAttrs[i];
|
|
1791
1904
|
if (normalizedAttr) {
|
|
1792
|
-
|
|
1905
|
+
if (uidAttr && normalizedAttr.name.includes(uidAttr)) {
|
|
1906
|
+
markTagFragments(normalizedAttr.name);
|
|
1907
|
+
}
|
|
1908
|
+
const builtAttr = buildAttr(normalizedAttr, hasUnarySlash, options, i === lastKeptIndex, uidAttr);
|
|
1909
|
+
// One entry with the tag name, so that no line break can come between them
|
|
1910
|
+
if (i === 0 && fragmentJoinsTag) {
|
|
1911
|
+
buffer[buffer.length - 1] += builtAttr;
|
|
1912
|
+
} else {
|
|
1913
|
+
bufferPush(builtAttr, true);
|
|
1914
|
+
}
|
|
1793
1915
|
}
|
|
1794
1916
|
}
|
|
1795
1917
|
} else if (optional && optionalStartTags.has(tag)) {
|
|
@@ -1797,7 +1919,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1797
1919
|
optionalStartTag = tag;
|
|
1798
1920
|
}
|
|
1799
1921
|
|
|
1800
|
-
buffer
|
|
1922
|
+
buffer[buffer.length - 1] += (hasUnarySlash ? '/' : '') + '>';
|
|
1801
1923
|
|
|
1802
1924
|
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
1803
1925
|
removeStartTag();
|
|
@@ -1815,7 +1937,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1815
1937
|
currentTag = '';
|
|
1816
1938
|
}
|
|
1817
1939
|
},
|
|
1818
|
-
end: function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs, /** @type {boolean} */ autoGenerated) {
|
|
1940
|
+
end: function (/** @type {string} */ tag, /** @type {HTMLAttribute[]} */ attrs, /** @type {boolean} */ autoGenerated, /** @type {string | undefined} */ rest) {
|
|
1819
1941
|
// As in `start`: `lowerTag` stays '' when no foreign content is around
|
|
1820
1942
|
let lowerTag = '';
|
|
1821
1943
|
// Named as the start tag was: an `svg` or `math` element in its own context,
|
|
@@ -1833,6 +1955,18 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1833
1955
|
}
|
|
1834
1956
|
}
|
|
1835
1957
|
tag = name(tag);
|
|
1958
|
+
let endTagText = '</' + tag + '>';
|
|
1959
|
+
// Custom fragments after the name are kept, next to it if they were in the source
|
|
1960
|
+
if (rest && uidAttr && rest.includes(uidAttr)) {
|
|
1961
|
+
markTagFragments(rest);
|
|
1962
|
+
const uidMatch = /** @type {RegExp} */ (uidAttrLeadingPattern).exec(rest);
|
|
1963
|
+
const joinsTag = uidMatch !== null && rest.startsWith('\t' + uidAttr) &&
|
|
1964
|
+
ignoredCustomMarkupChunks[+(uidMatch[1] ?? 0)]?.[1] === '';
|
|
1965
|
+
// Anything else an end tag holds is dropped, as without fragments
|
|
1966
|
+
const marker = uidAttr;
|
|
1967
|
+
const fragments = Array.from(rest.matchAll(/** @type {RegExp} */ (uidPattern)), uid => marker + (uid[2] ?? '') + marker);
|
|
1968
|
+
endTagText = '</' + tag + (joinsTag ? '' : ' ') + fragments.join(' ') + '>';
|
|
1969
|
+
}
|
|
1836
1970
|
|
|
1837
1971
|
// Check if current tag is in a whitespace stack
|
|
1838
1972
|
if (options.collapseWhitespace) {
|
|
@@ -1894,7 +2028,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1894
2028
|
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
1895
2029
|
optionalEndTagEmitted = false;
|
|
1896
2030
|
} else {
|
|
1897
|
-
|
|
2031
|
+
bufferPush(endTagText, true);
|
|
1898
2032
|
}
|
|
1899
2033
|
charsPrevTag = '/' + tag;
|
|
1900
2034
|
if (!inlineElements.has(tag)) {
|
|
@@ -1907,7 +2041,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1907
2041
|
if (autoGenerated && !options.includeAutoGeneratedTags) {
|
|
1908
2042
|
optionalEndTagEmitted = false;
|
|
1909
2043
|
} else {
|
|
1910
|
-
|
|
2044
|
+
bufferPush(endTagText, true);
|
|
1911
2045
|
}
|
|
1912
2046
|
charsPrevTag = '/' + tag;
|
|
1913
2047
|
if (!inlineElements.has(tag)) {
|
|
@@ -1983,10 +2117,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1983
2117
|
commentFinalize(text);
|
|
1984
2118
|
},
|
|
1985
2119
|
doctype: function (/** @type {string} */ doctype) {
|
|
1986
|
-
|
|
2120
|
+
bufferPush(options.useShortDoctype
|
|
1987
2121
|
? '<!doctype' +
|
|
1988
2122
|
(options.removeTagWhitespace ? '' : ' ') + 'html>'
|
|
1989
|
-
: collapseWhitespaceAll(doctype));
|
|
2123
|
+
: collapseWhitespaceAll(doctype), false);
|
|
1990
2124
|
}
|
|
1991
2125
|
});
|
|
1992
2126
|
|
|
@@ -2003,7 +2137,10 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2003
2137
|
);
|
|
2004
2138
|
for (let i = svgBlocks.length - 1; i >= 0; i--) {
|
|
2005
2139
|
const block = svgBlocks[i];
|
|
2006
|
-
if (block)
|
|
2140
|
+
if (block) {
|
|
2141
|
+
buffer.splice(block.start, block.end - block.start, optimized[i] ?? '');
|
|
2142
|
+
bufferTags?.splice(block.start, block.end - block.start, false);
|
|
2143
|
+
}
|
|
2007
2144
|
}
|
|
2008
2145
|
}
|
|
2009
2146
|
|
|
@@ -2022,23 +2159,28 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2022
2159
|
squashTrailingWhitespace('br');
|
|
2023
2160
|
}
|
|
2024
2161
|
|
|
2025
|
-
return joinResultSegments(buffer, options, uidPattern
|
|
2162
|
+
return joinResultSegments(buffer, bufferTags ?? [], [uidAttr, uidIgnore].filter(marker => marker !== undefined), options, uidPattern
|
|
2026
2163
|
? function (/** @type {string} */ str) {
|
|
2027
|
-
return str.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ match, /** @type {string} */ prefix, /** @type {string} */ index, /** @type {string} */ suffix) {
|
|
2028
|
-
|
|
2029
|
-
if (
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
chunk += suffix;
|
|
2035
|
-
}
|
|
2036
|
-
return collapseWhitespace(chunk, {
|
|
2037
|
-
preserveLineBreaks: options.preserveLineBreaks,
|
|
2038
|
-
conservativeCollapse: !options.trimCustomFragments
|
|
2039
|
-
}, /^[ \n\r\t\f]/.test(chunk), /[ \n\r\t\f]$/.test(chunk));
|
|
2164
|
+
return str.replace(/** @type {RegExp} */ (uidPattern), function (/** @type {string} */ match, /** @type {string} */ prefix, /** @type {string} */ index, /** @type {string} */ suffix, /** @type {number} */ offset, /** @type {string} */ string) {
|
|
2165
|
+
const chunks = ignoredCustomMarkupChunks[+index];
|
|
2166
|
+
if (!chunks) {
|
|
2167
|
+
return match;
|
|
2168
|
+
}
|
|
2169
|
+
if (!options.collapseWhitespace) {
|
|
2170
|
+
return chunks[0];
|
|
2040
2171
|
}
|
|
2041
|
-
|
|
2172
|
+
const leading = chunks[1] ?? '';
|
|
2173
|
+
const trailing = chunks[2] ?? '';
|
|
2174
|
+
const fragment = chunks[0].slice(leading.length, chunks[0].length - trailing.length);
|
|
2175
|
+
const inTag = customMarkupInTags.has(+index);
|
|
2176
|
+
const keep = inTag || customMarkupInCode.has(+index) || !options.trimCustomFragments;
|
|
2177
|
+
// The padding tabs are whitespace to the minifier, so where one is gone, the
|
|
2178
|
+
// minifier removed the whitespace, which makes it safe to trim
|
|
2179
|
+
const trimLeading = !keep && prefix === '';
|
|
2180
|
+
const trimTrailing = inTag ? string.charCodeAt(offset + match.length) === 62 /* > */ : !keep && suffix === '';
|
|
2181
|
+
return restoreFragmentSide(prefix.replace(RE_TABS, '') + leading, trimLeading, true) +
|
|
2182
|
+
fragment +
|
|
2183
|
+
restoreFragmentSide(suffix.replace(RE_TABS, '') + trailing, trimTrailing, false);
|
|
2042
2184
|
});
|
|
2043
2185
|
}
|
|
2044
2186
|
: identity, uidIgnore
|
|
@@ -2052,46 +2194,74 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
2052
2194
|
|
|
2053
2195
|
/**
|
|
2054
2196
|
* @param {string[]} results
|
|
2197
|
+
* @param {boolean[]} resultTags Whether each result belongs to a start or end tag
|
|
2198
|
+
* @param {string[]} markers Custom fragment and `htmlmin:ignore` placeholder markers
|
|
2055
2199
|
* @param {ProcessedOptions} options
|
|
2056
2200
|
* @param {Function} restoreCustom
|
|
2057
2201
|
* @param {Function} restoreIgnore
|
|
2058
2202
|
*/
|
|
2059
|
-
function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
2203
|
+
function joinResultSegments(results, resultTags, markers, options, restoreCustom, restoreIgnore) {
|
|
2060
2204
|
let str;
|
|
2061
2205
|
const maxLineLength = options.maxLineLength;
|
|
2062
|
-
const noNewlinesBeforeTagClose = options.noNewlinesBeforeTagClose;
|
|
2063
2206
|
|
|
2064
2207
|
if (maxLineLength) {
|
|
2065
|
-
|
|
2066
|
-
//
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2208
|
+
// Whitespace in tags is the only whitespace that can’t change the output, so lines break
|
|
2209
|
+
// there alone—in place of the space between attributes, or right before a tag’s `>`
|
|
2210
|
+
const noNewlinesBeforeTagClose = options.noNewlinesBeforeTagClose;
|
|
2211
|
+
/** @type {string[]} */
|
|
2212
|
+
const lines = [];
|
|
2213
|
+
let lineLength = 0;
|
|
2214
|
+
// Output since the last break opportunity, and what that opportunity is when not taken;
|
|
2215
|
+
// fragments are restored by chunk, as whitespace around them decides how they come back
|
|
2216
|
+
let chunk = '';
|
|
2217
|
+
/** @type {string | null} */
|
|
2218
|
+
let separator = null;
|
|
2219
|
+
const flush = (/** @type {string | null} */ nextSeparator) => {
|
|
2220
|
+
chunk = restoreIgnore(restoreCustom(chunk));
|
|
2221
|
+
const newline = chunk.indexOf('\n');
|
|
2222
|
+
const head = newline < 0 ? chunk.length : newline;
|
|
2223
|
+
// A line already too long gains nothing from moving a `>` that ends a line of its own
|
|
2224
|
+
const isUseless = separator === '' && head === 1 && lineLength > maxLineLength;
|
|
2225
|
+
if (separator !== null) {
|
|
2226
|
+
if (lineLength > 0 && lineLength + separator.length + head > maxLineLength && !isUseless) {
|
|
2227
|
+
lines.push('\n');
|
|
2228
|
+
lineLength = 0;
|
|
2229
|
+
} else if (separator) {
|
|
2230
|
+
lines.push(separator);
|
|
2231
|
+
lineLength += separator.length;
|
|
2232
|
+
}
|
|
2082
2233
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2234
|
+
lines.push(chunk);
|
|
2235
|
+
lineLength = newline < 0 ? lineLength + chunk.length : chunk.length - chunk.lastIndexOf('\n') - 1;
|
|
2236
|
+
chunk = '';
|
|
2237
|
+
separator = nextSeparator;
|
|
2238
|
+
};
|
|
2239
|
+
for (let index = 0; index < results.length; index++) {
|
|
2240
|
+
const result = results[index] ?? '';
|
|
2241
|
+
if (!resultTags[index]) {
|
|
2242
|
+
chunk += result;
|
|
2243
|
+
} else if (markers.length && markers.some(marker => result.includes(marker))) {
|
|
2244
|
+
// Whitespace next to a fragment stays as is, as it may belong to it
|
|
2245
|
+
if (!chunk && separator !== null) {
|
|
2246
|
+
chunk = separator;
|
|
2247
|
+
separator = null;
|
|
2248
|
+
}
|
|
2249
|
+
chunk += result;
|
|
2250
|
+
} else if (result.charCodeAt(result.length - 1) === 32 /* space */) {
|
|
2251
|
+
chunk += result.slice(0, -1);
|
|
2252
|
+
flush(' ');
|
|
2253
|
+
} else if (result.charCodeAt(result.length - 1) === 62 /* > */ &&
|
|
2254
|
+
result.charCodeAt(result.length - 2) !== 47 /* / */ &&
|
|
2255
|
+
!(noNewlinesBeforeTagClose && result.startsWith('</'))) {
|
|
2256
|
+
chunk += result.slice(0, -1);
|
|
2257
|
+
flush('');
|
|
2258
|
+
chunk = '>';
|
|
2259
|
+
} else {
|
|
2260
|
+
chunk += result;
|
|
2089
2261
|
}
|
|
2090
2262
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
}
|
|
2094
|
-
str = lines.join('\n');
|
|
2263
|
+
flush(null);
|
|
2264
|
+
str = lines.join('');
|
|
2095
2265
|
} else {
|
|
2096
2266
|
str = restoreIgnore(restoreCustom(results.join('')));
|
|
2097
2267
|
}
|
package/src/htmlparser.js
CHANGED
|
@@ -67,7 +67,6 @@ const qnameCapture = (function () {
|
|
|
67
67
|
})();
|
|
68
68
|
const startTagOpen = new RegExp('^<' + qnameCapture);
|
|
69
69
|
const endTagOpen = new RegExp('^</' + qnameCapture);
|
|
70
|
-
export const endTag = new RegExp('^</' + qnameCapture + '[^>]*>');
|
|
71
70
|
|
|
72
71
|
let IS_REGEX_CAPTURING_BROKEN = false;
|
|
73
72
|
'x'.replace(/x(.)?/g, function (/** @type {string} */ m, /** @type {string | undefined} */ g) {
|
|
@@ -1033,11 +1032,14 @@ export class HTMLParser {
|
|
|
1033
1032
|
stackIndex = 0;
|
|
1034
1033
|
}
|
|
1035
1034
|
|
|
1035
|
+
// Whatever follows the name, which browsers drop but a custom fragment may stand in for
|
|
1036
|
+
const rest = tag.length > tagName.length + 3 ? tag.slice(tagName.length + 2, -1) : '';
|
|
1037
|
+
|
|
1036
1038
|
if (stackIndex >= 0) {
|
|
1037
1039
|
// Close all the open elements, up the stack
|
|
1038
1040
|
for (let i = stack.length - 1; i >= stackIndex; i--) {
|
|
1039
1041
|
if (handler.end) {
|
|
1040
|
-
handler.end(stack[i]?.tag, stack[i]?.attrs, i > stackIndex || !tag);
|
|
1042
|
+
handler.end(stack[i]?.tag, stack[i]?.attrs, i > stackIndex || !tag, i === stackIndex ? rest : '');
|
|
1041
1043
|
}
|
|
1042
1044
|
}
|
|
1043
1045
|
|
|
@@ -1048,7 +1050,7 @@ export class HTMLParser {
|
|
|
1048
1050
|
} else if (handler.partialMarkup && tagName) {
|
|
1049
1051
|
// In partial markup mode, preserve stray end tags
|
|
1050
1052
|
if (handler.end) {
|
|
1051
|
-
handler.end(tagName, [], false);
|
|
1053
|
+
handler.end(tagName, [], false, rest);
|
|
1052
1054
|
}
|
|
1053
1055
|
} else if (lowerTagName === 'br') {
|
|
1054
1056
|
if (handler.start) {
|
|
@@ -81,7 +81,7 @@ const optionDefinitions = {
|
|
|
81
81
|
type: 'regexpArray'
|
|
82
82
|
},
|
|
83
83
|
ignoreCustomFragments: {
|
|
84
|
-
description: 'Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php \u2026 ?>`, `{{ \u2026 }}
|
|
84
|
+
description: 'Array of regexes that allow to ignore certain fragments, when matched (e.g., `<?php \u2026 ?>`, `{{ \u2026 }}`, etc.)',
|
|
85
85
|
type: 'regexpArray'
|
|
86
86
|
},
|
|
87
87
|
includeAutoGeneratedTags: {
|
|
@@ -101,7 +101,7 @@ const optionDefinitions = {
|
|
|
101
101
|
type: 'int'
|
|
102
102
|
},
|
|
103
103
|
maxLineLength: {
|
|
104
|
-
description: 'Specify a maximum line length;
|
|
104
|
+
description: 'Specify a maximum line length; output will be split by newlines in tags only (between attributes or before a tag’s `>`), where they can’t change the page, so lines may run longer',
|
|
105
105
|
type: 'int'
|
|
106
106
|
},
|
|
107
107
|
mergeScripts: {
|
|
@@ -125,7 +125,7 @@ const optionDefinitions = {
|
|
|
125
125
|
type: 'json'
|
|
126
126
|
},
|
|
127
127
|
noNewlinesBeforeTagClose: {
|
|
128
|
-
description: 'Never
|
|
128
|
+
description: 'Never split a line in a tag that closes an element—use with `--max-line-length`',
|
|
129
129
|
type: 'boolean'
|
|
130
130
|
},
|
|
131
131
|
partialMarkup: {
|
|
@@ -169,7 +169,7 @@ const optionDefinitions = {
|
|
|
169
169
|
type: 'boolean'
|
|
170
170
|
},
|
|
171
171
|
removeEmptyElementsExcept: {
|
|
172
|
-
description: 'Array of elements to preserve (e.g., `td`, `<span aria-hidden="true"
|
|
172
|
+
description: 'Array of elements to preserve (e.g., `td`, `<span aria-hidden="true">`, etc.)—use with `--remove-empty-elements`',
|
|
173
173
|
type: 'jsonArray'
|
|
174
174
|
},
|
|
175
175
|
removeOptionalTags: {
|
|
@@ -197,7 +197,7 @@ const optionDefinitions = {
|
|
|
197
197
|
type: 'boolean'
|
|
198
198
|
},
|
|
199
199
|
trimCustomFragments: {
|
|
200
|
-
description: 'Trim whitespace around custom fragments (`--ignore-custom-fragments`)—use with `--collapse-whitespace`',
|
|
200
|
+
description: 'Trim whitespace around custom fragments (`--ignore-custom-fragments`) where it does not affect the output—use with `--collapse-whitespace`',
|
|
201
201
|
type: 'boolean'
|
|
202
202
|
},
|
|
203
203
|
strictCustomFragments: {
|