html-minifier-next 8.6.0 → 8.7.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.
package/README.md CHANGED
@@ -249,6 +249,8 @@ you get the following output (condensed, `\n` represents an actual line break):
249
249
 
250
250
  Where the modifiers disagree, the preserving one wins—`conservativeCollapse` and `preserveLineBreaks` do not let `collapseInlineTagWhitespace` remove a space or line break entirely.
251
251
 
252
+ Regardless of these options, whitespace at the start and end of the output goes, and so do lines that a removed comment or tag leaves with nothing but whitespace (blank lines of the source stay). Whitespace stays, however, where it’s kept verbatim (in `pre` or `textarea`, or in `pre`, `textarea`, or `script` content left open at the end), next to custom fragments where their handling keeps it (see `trimCustomFragments`), at either end of partial markup (cf. `partialMarkup`), and where `conservativeCollapse` or `preserveLineBreaks` keep it.
253
+
252
254
  ### Sorting attributes and style classes
253
255
 
254
256
  `sortAttributes` and `sortClassNames` reorder attributes and class names by frequency, so that repeated markup looks more alike. This doesn’t change the plain-text size of the output, only how well it compresses: `sortAttributes` makes Gzip and Brotli output slightly smaller on average, though not on every page (see [“Optimizing for compression”](#optimizing-for-compression)); `sortClassNames` doesn’t help, and alongside `sortAttributes` cancels most of its gain with Brotli.
@@ -677,7 +679,7 @@ By default, HMN parses markup into a complete tree structure, then modifies it (
677
679
 
678
680
  _Input markup (e.g., `<p id="">foo`) → Internal representation of markup in a form of tree (e.g., `{ tag: "p", attr: "id", children: ["foo"] }`) → Transformation of internal representation (e.g., removal of `id` attribute) → Output of resulting markup (e.g., `<p>foo</p>`)_
679
681
 
680
- For partial HTML fragments (such as template includes, SSI fragments, or closing tags without opening tags), use the `partialMarkup: true` option. This preserves stray end tags (closing tags without corresponding opening tags) and prevents auto-closing of unclosed tags at the end of input. Note that normal HTML auto-closing rules still apply during parsing—for example, a closing parent tag will still auto-close its unclosed child elements.
682
+ For partial HTML fragments (such as template includes, SSI fragments, or closing tags without opening tags), use the `partialMarkup: true` option. This preserves stray end tags (closing tags without corresponding opening tags) and prevents auto-closing of unclosed tags at the end of input. Note that normal HTML auto-closing rules still apply during parsing—for example, a closing parent tag will still auto-close its unclosed child elements. Whitespace at the start and end of a fragment stays, too, as it may separate the fragment from what surrounds it (unless `collapseWhitespace` removes it).
681
683
 
682
684
  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/).
683
685
 
@@ -842,6 +844,8 @@ Parameters:
842
844
 
843
845
  To compare branches (A/B run), execute `npm run benchmark -- --save` on `main`, then `npm run benchmark` on the branch to see the deltas. Add `--core` on both ends when measuring changes to HMN rather than bundled minifiers, or `--cold` when measuring changes to the CSS, JS, or SVG minification paths.
844
846
 
847
+ For changes of around 1% or less, use `npm run benchmark:ab -- main` instead: It times the working tree against any Git ref in the same session, runs both in turn per file, averages both loading orders, and reports `collapseWhitespace` on and off separately. Add `--aa` to compare the ref with a copy of itself—the noise floor any finding must clear. The script’s header lists further parameters.
848
+
845
849
  Reported times are the *fastest* iteration, not the median: Interference can only make a run slower, so the minimum is the most stable estimate. Each run also reports its noise—how far the reported figure moves between the first and second half of the iterations—and any delta smaller than that is marked `within noise` rather than shown as a win or a regression. Raise `--iterations` until the noise sits below the change you are trying to measure.
846
850
 
847
851
  #### Profiling
@@ -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":"AAEA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AA4E/D,YAAkB,aAAa,GAI/B;IAAe,IAAI,EAAZ,MAAM,CACb;IAAgB,KAAK,AAArB,CACA,EADO,MAAM,CACb;IAAgB,KAAK,AAArB,CACA,EADO,MAAM,CACb;IAAgB,YAAY,AAA5B,CACA,EADO,MAAM,CACb;IAAgB,UAAU,AAA1B,CACA,EADO,MAAM,CACb;IAAgB,WAAW,AAA3B,CAA4B,EAArB,MAAM,CAAe;CAAA,CAAA;AAI5B,YAAkB,eAAe,GAIjC;;;;;;;IAA2G,qBAAqB,AAAhI,CAMA,EANO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,qBAAqB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO,CAMxG;;;;;;;;;;IAAuE,YAAY,AAAnF,CASA,EATO,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,KAAK,OAAO,CAAC,GAAG,IAAI,CASpE;;;;;;;;;IAA8D,WAAW,AAAzE,CAQA,EARO,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,IAAI,CAQ3D;;;;;;;;;IAA4C,YAAY,AAAxD,CAQA,EARO,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,IAAI,CAQzC;;;;;;;IAA0H,iBAAiB,AAA3I,CAMA,EANO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,SAAS,EAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO,CAMvH;;;;;;;;;;IAAgB,QAAQ,AAAxB,CASA,EATO,MAAM,CASb;;;;;;;;;;IAAgB,OAAO,AAAvB,CASA,EATO,MAAM,CASb;;;;;;;;;;IAAgB,QAAQ,AAAxB,CASA,EATO,MAAM,CASb;;;;;;;;IAAiB,aAAa,AAA9B,CAOC,EAPM,OAAO,CAOb;;;;;;;;;IAAiB,2BAA2B,AAA5C,CAQD,EARQ,OAAO,CAQf;;;;;;;;IAAiB,yBAAyB,AAA1C,CAOA,EAPO,OAAO,CAOd;;;;;;;;IAAiB,2BAA2B,AAA5C,CAOA,EAPO,OAAO,CAOd;;;;;;;;;;IAAiB,qBAAqB,AAAtC,CASA,EATO,OAAO,CASd;;;;;;;;IAAiB,kBAAkB,AAAnC,CAOA,EAPO,OAAO,CAOd;;;;;;;;IAAiB,oBAAoB,AAArC,CAOA,EAPO,OAAO,CAOd;;;;;;;;IAAiB,qBAAqB,AAAtC,CAOA,EAPO,OAAO,CAOd;;;;;;;IAAiB,oBAAoB,AAArC,CAMA,EANO,OAAO,CAMd;;;;;;;;IAAkB,gBAAgB,AAAlC,CAOA,EAPO,MAAM,EAAE,CAOf;;;;;;IAAgB,kBAAkB,AAAlC,CAKA,EALO,MAAM,CAKb;;;;;;IAA4B,kBAAkB,AAA9C,CAKA,EALO,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAKzB;;;;;;;IAAkB,qBAAqB,AAAvC,CAMA,EANO,MAAM,EAAE,CAMf;;;;;;;;;IAAiB,qBAAqB,AAAtC,CAQA,EARO,OAAO,CAQd;;;;;;;IAAiB,cAAc,AAA/B,CAOA,EAPO,OAAO,CAOd;;;;;;;;IAAkB,oBAAoB,AAAtC,CAOA,EAPO,MAAM,EAAE,CAOf;;;;;;;;;IAAkB,qBAAqB,AAAvC,CAQA,EARO,MAAM,EAAE,CAQf;;;;;;;IAAiB,wBAAwB,AAAzC,CAMA,EANO,OAAO,CAMd;;;;;;;IAA2B,oBAAoB,AAA/C,CAMA,EANO,SAAS,CAAC,MAAM,CAAC,CAMxB;;;;;;;;IAAiB,gBAAgB,AAAjC,CAOA,EAPO,OAAO,CAOd;;;;;;;;IAAoC,GAAG,AAAvC,CAOA,EAPO,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAOjC;;;;;;;;IAAgB,cAAc,AAA9B,CAOA,EAPO,MAAM,CAOb;;;;;;;;IAAgB,aAAa,AAA7B,CAOA,EAPO,MAAM,CAOb;;;;;;;;IAAiB,YAAY,AAA7B,CAOA,EAPO,OAAO,CAOd;;;;;;;;;;IAAwK,SAAS,AAAjL,CASA,EATO,OAAO,GAAG,OAAO,CAAC,OAAO,cAAc,EAAE,gBAAgB,CAAC,OAAO,cAAc,EAAE,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CASrK;;;;;;;;;;;;;;IAAqK,QAAQ,AAA7K,CAaA,EAbO,OAAO,GAAG,OAAO,QAAQ,EAAE,aAAa,GAAG;QAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAalK;;;;;;;;;;IAA2F,UAAU,AAArG,CASA,EATO,OAAO,GAAG,MAAM,GAAG;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CASxF;;;;;;;;;;;IAA0B,SAAS,AAAnC,CAUA,EAVO,OAAO,GAAG,MAAM,CAUvB;;;;;;;IAAiB,wBAAwB,AAAzC,CAMA,EANO,OAAO,CAMd;;;;;;;;;;IAAiB,aAAa,AAA9B,CASA,EATO,OAAO,CASd;;;;;;;;IAAiB,kBAAkB,AAAnC,CAOA,EAPO,OAAO,CAOd;;;;;;;;IAAgB,MAAM,AAAtB,CAOA,EAPO,MAAM,CAOb;;;;;;;IAAiB,yBAAyB,AAA1C,CAMA,EANO,OAAO,CAMd;;;;;;;;;;IAAkB,cAAc,AAAhC,CASA,EATO,MAAM,EAAE,CASf;;;;;;;IAAoB,cAAc,AAAlC,CAMA,EANO,IAAI,GAAG,GAAG,CAMjB;;;;;;;IAAiB,qBAAqB,AAAtC,CAMA,EANO,OAAO,CAMd;;;;;;;;IAAiB,cAAc,AAA/B,CAOC,EAPM,OAAO,CAOb;;;;;;;IAAiB,2BAA2B,AAA5C,CAMD,EANQ,OAAO,CAMf;;;;;;;;;IAAgE,qBAAqB,AAArF,CAQA,EARO,OAAO,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAQ7D;;;;;;;;IAAiB,mBAAmB,AAApC,CAOA,EAPO,OAAO,CAOd;;;;;;;;;;;;;;;;;;;;;;;;;;IAAkB,yBAAyB,AAA3C,CAyBA,EAzBO,MAAM,EAAE,CAyBf;;;;;;;;IAAiB,kBAAkB,AAAnC,CAOA,EAPO,OAAO,CAOd;;;;;;;;IAAiB,yBAAyB,AAA1C,CAOA,EAPO,OAAO,CAOd;;;;;;;;;;IAAiB,mBAAmB,AAApC,CASA,EATO,OAAO,CASd;;;;;;;;;;;;;;;IAA0E,eAAe,AAAzF,CAcA,EAdO,OAAO,GAAG;QAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAC,CAcvE;;;;;;;;;IAAmE,cAAc,AAAjF,CAQA,EARO,OAAO,GAAG,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC,CAQhE;;;;;;;;;IAA+C,cAAc,AAA7D,CAQA,EARO,OAAO,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,CAQ5C;;;;;;;;;IAAiB,mBAAmB,AAApC,CAQA,EARO,OAAO,CAQd;;;;;;;IAAiB,eAAe,AAAhC,CAKF,EALS,OAAO,CAKhB;CAAA,CAAA;AA0EE,YAAkB,UAAU,GAG5B;;;;IAAe,IAAI,EAAZ,MAAM,CACb;;;;IAAe,IAAI,EAAZ,MAAM,CACb;;;;IAAe,IAAI,EAAZ,MAAM,CACb;;;;IAAe,KAAK,EAAb,MAAM,CACf;CAAA,CAAA;AAsyDD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,IAFhB;IAAE,GAAG,EAAE,UAAU,CAAC;IAAC,EAAE,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,UAAU,CAAA;CAAE,CAQhE;AA2FD;;;;GAIG;AACH,eAAO,MAAM,MAAM,UAJR,MAAM,YACN,eAAe,KACb,OAAO,CAAC,MAAM,CAsE1B,CAAC;AAIF,OAAO,EACL,OAAO,EACP,SAAS,EACT,cAAc,EACf,CAAC"}
@@ -4,6 +4,7 @@ export type HTMLParserHandler = {
4
4
  chars?: Function;
5
5
  comment?: Function;
6
6
  doctype?: Function;
7
+ strayEnd?: Function;
7
8
  continueOnParseError?: boolean | undefined;
8
9
  partialMarkup?: boolean | undefined;
9
10
  wantsNextTag?: boolean | undefined;
@@ -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;AA6NvB,qBAAa,UAAU;IAMd,IAAI;IACJ,OAAO;IANd;;;OAGG;IACH,YAAY,IAAI,EAHL,MAGK,EAAE,OAAO,EAFd,iBAEc,EAGxB;IAEK,KAAK,kBA2yBV;CACF"}
1
+ {"version":3,"file":"htmlparser.d.ts","sourceRoot":"","sources":["../../src/htmlparser.js"],"names":[],"mappings":"AAeG,YAaG,iBAAiB,GAbV;IACR,KAAK,CAAC,WAAW;IACjB,GAAG,CAAC,WAAW;IACf,KAAK,CAAC,WAAW;IACjB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,WAAW;IACnB,QAAQ,CAAC,WAAW;IACpB,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,kBA8yBV;CACF"}
package/package.json CHANGED
@@ -95,5 +95,5 @@
95
95
  },
96
96
  "type": "module",
97
97
  "types": "dist/types/htmlminifier.d.ts",
98
- "version": "8.6.0"
98
+ "version": "8.7.0"
99
99
  }
@@ -640,6 +640,10 @@ const RE_END_TAG = /^<\//;
640
640
  const RE_WHITESPACE_ONLY = /^\s+$/;
641
641
  const RE_TABS = /\t/g;
642
642
  const RE_TRAILING_WHITESPACE = /\s$/;
643
+ const RE_LINE_BREAK_BLANKS_END = /(?:\r\n?|\n)[ \t\f]*$/;
644
+ const RE_BLANKS_LINE_BREAK_START = /^[ \t\f]*(?:\r\n?|\n)/;
645
+ const RE_BLANKS_START = /^[ \t\f]+/;
646
+ const RE_BLANKS_ONLY = /^[ \t\f]+$/;
643
647
 
644
648
  // Pre-compiled patterns for `htmlmin:ignore` block content analysis
645
649
  const RE_HTML_COMMENT_START = /^\s*<!--/;
@@ -1079,6 +1083,10 @@ async function minifyHTML(value, options, partialMarkup) {
1079
1083
  let optionalStartTag = '';
1080
1084
  let optionalEndTag = '';
1081
1085
  let optionalEndTagEmitted = false;
1086
+ // Whether a node was removed since the last output
1087
+ let removed = false;
1088
+ // Whether the latest text was kept verbatim, as in `pre` or `script`
1089
+ let textVerbatim = false;
1082
1090
  /** @type {string[]} */
1083
1091
  const ignoredMarkupChunks = [];
1084
1092
  /** @type {Array<RegExpExecArray | null>} */
@@ -1300,11 +1308,88 @@ async function minifyHTML(value, options, partialMarkup) {
1300
1308
  function bufferPush(/** @type {string} */ entry, /** @type {boolean} */ isTag) {
1301
1309
  buffer.push(entry);
1302
1310
  bufferTags?.push(isTag);
1311
+ if (entry) removed = false;
1303
1312
  }
1304
1313
 
1305
1314
  function bufferTruncate(/** @type {number} */ length) {
1306
1315
  buffer.length = Math.max(0, length);
1307
1316
  if (bufferTags) bufferTags.length = buffer.length;
1317
+ removed = true;
1318
+ }
1319
+
1320
+ // The nearest entries before and from `index` that aren’t empty, as removed nodes leave them
1321
+ /** @returns {[number, number]} */
1322
+ function findNeighbors(/** @type {number} */ index) {
1323
+ let before = index - 1;
1324
+ while (before >= 0 && buffer[before] === '') {
1325
+ before--;
1326
+ }
1327
+ let after = index;
1328
+ while (after < buffer.length && buffer[after] === '') {
1329
+ after++;
1330
+ }
1331
+ return [before, after];
1332
+ }
1333
+
1334
+ // With whitespace kept as is, a removed node must not leave a line of nothing but
1335
+ // whitespace behind: The text around the removal (at `index`) drops that line. The start
1336
+ // of the output counts as a line break, and blank lines of the source stay.
1337
+ function removeWhitespaceLine(/** @type {number} */ index) {
1338
+ const [first, after] = findNeighbors(index);
1339
+ // The end of the output is left to `trimOutputWhitespace`
1340
+ if (after === buffer.length) {
1341
+ return;
1342
+ }
1343
+ const next = buffer[after] ?? '';
1344
+ if (!RE_BLANKS_LINE_BREAK_START.test(next)) {
1345
+ return;
1346
+ }
1347
+ // Blanks between nodes removed from the same line belong to that line
1348
+ let before = first;
1349
+ while (before >= 0 && (buffer[before] === '' || RE_BLANKS_ONLY.test(buffer[before] ?? ''))) {
1350
+ before--;
1351
+ }
1352
+ const prev = buffer[before] ?? '';
1353
+ if (before >= 0 && !RE_LINE_BREAK_BLANKS_END.test(prev)) {
1354
+ return;
1355
+ }
1356
+ for (let i = before + 1; i <= first; i++) {
1357
+ buffer[i] = '';
1358
+ }
1359
+ // The line break goes on the side of the text before the removal, so that the text after
1360
+ // it keeps its own for a removal that may follow
1361
+ if (before < 0) {
1362
+ buffer[after] = next.replace(RE_BLANKS_LINE_BREAK_START, '');
1363
+ } else {
1364
+ buffer[before] = prev.replace(RE_LINE_BREAK_BLANKS_END, '');
1365
+ buffer[after] = next.replace(RE_BLANKS_START, '');
1366
+ }
1367
+ }
1368
+
1369
+ // Whitespace at either end of the output goes, but not verbatim whitespace, nor whitespace
1370
+ // that `conservativeCollapse` or `preserveLineBreaks` promise to keep, nor that of partial
1371
+ // markup, whose ends join other markup
1372
+ function trimOutputWhitespace() {
1373
+ if (options.partialMarkup || (options.collapseWhitespace && (options.conservativeCollapse || options.preserveLineBreaks))) {
1374
+ return;
1375
+ }
1376
+ // Tags start with `<` and end with `>`, so whitespace at either end belongs to text
1377
+ if (!textVerbatim) {
1378
+ let index = buffer.length - 1;
1379
+ while (index >= 0 && (buffer[index] === '' || RE_WS_ONLY.test(buffer[index] ?? ''))) {
1380
+ buffer[index--] = '';
1381
+ }
1382
+ if (index >= 0) {
1383
+ buffer[index] = (buffer[index] ?? '').replace(RE_WS_END, '');
1384
+ }
1385
+ }
1386
+ let index = 0;
1387
+ while (index < buffer.length && (buffer[index] === '' || RE_WS_ONLY.test(buffer[index] ?? ''))) {
1388
+ buffer[index++] = '';
1389
+ }
1390
+ if (index < buffer.length) {
1391
+ buffer[index] = (buffer[index] ?? '').replace(RE_WS_START, '');
1392
+ }
1308
1393
  }
1309
1394
 
1310
1395
  function removeStartTag() {
@@ -1332,14 +1417,7 @@ async function minifyHTML(value, options, partialMarkup) {
1332
1417
  // single character it is due: a line break under `preserveLineBreaks`, a no-break space
1333
1418
  // where the run holds one, a space under `conservativeCollapse`
1334
1419
  function mergeWhitespaceRuns(/** @type {number} */ index) {
1335
- let before = index - 1;
1336
- while (before >= 0 && buffer[before] === '') {
1337
- before--;
1338
- }
1339
- let after = index;
1340
- while (after < buffer.length && buffer[after] === '') {
1341
- after++;
1342
- }
1420
+ const [before, after] = findNeighbors(index);
1343
1421
  if (before < 0 || after >= buffer.length) {
1344
1422
  return;
1345
1423
  }
@@ -1357,7 +1435,8 @@ async function minifyHTML(value, options, partialMarkup) {
1357
1435
  }
1358
1436
  }
1359
1437
 
1360
- function removeEndTag() {
1438
+ // `insideNoTrim` stands in for the stack once the end tag that omits this one has left it
1439
+ function removeEndTag(/** @type {boolean} */ insideNoTrim = false) {
1361
1440
  let index = buffer.length - 1;
1362
1441
  while (index > 0 && !RE_END_TAG.test(buffer[index] ?? '')) {
1363
1442
  index--;
@@ -1365,13 +1444,17 @@ async function minifyHTML(value, options, partialMarkup) {
1365
1444
  // Text that follows the end tag is kept as is, so drop the tag alone. Whitespace kept
1366
1445
  // verbatim—in `pre`, in `textarea`, or wherever `canTrimWhitespace` says so—counts as
1367
1446
  // kept text too, no matter how whitespace is collapsed elsewhere.
1368
- const noTrim = stackNoTrimWhitespace.length > 0;
1447
+ const noTrim = insideNoTrim || stackNoTrimWhitespace.length > 0;
1369
1448
  if (index < buffer.length - 1 && (keepsWhitespace() || noTrim) && RE_END_TAG.test(buffer[index] ?? '')) {
1370
1449
  buffer.splice(index, 1);
1371
1450
  bufferTags?.splice(index, 1);
1372
- // Only collapsed whitespace can merge
1373
- if (options.collapseWhitespace && !noTrim) {
1374
- mergeWhitespaceRuns(index);
1451
+ // Collapsed whitespace can merge, kept whitespace can leave a line of only whitespace
1452
+ if (!noTrim) {
1453
+ if (options.collapseWhitespace) {
1454
+ mergeWhitespaceRuns(index);
1455
+ } else {
1456
+ removeWhitespaceLine(index);
1457
+ }
1375
1458
  }
1376
1459
  return;
1377
1460
  }
@@ -1452,7 +1535,7 @@ async function minifyHTML(value, options, partialMarkup) {
1452
1535
  // Multiple newlines are likely intentional formatting, single newline is often a template artifact
1453
1536
  // Treat CRLF (`\r\n`), CR (`\r`), and LF (`\n`) as single line-ending units
1454
1537
  if (textNextTag && textNextTag === '/' + topTag && /[^\r\n](?:\r\n|\r|\n)[ \t]*$/.test(text)) {
1455
- text = text.replace(/(?:\r\n|\r|\n)[ \t]*$/, '');
1538
+ text = text.replace(RE_LINE_BREAK_BLANKS_END, '');
1456
1539
  }
1457
1540
  }
1458
1541
  }
@@ -1569,7 +1652,11 @@ async function minifyHTML(value, options, partialMarkup) {
1569
1652
  }
1570
1653
  }
1571
1654
  charsPrevTag = /^\s*$/.test(text) ? textPrevTag : 'comment';
1572
- if (options.decodeEntities && text && !holdsRawText()) {
1655
+ const rawText = Boolean(text) && holdsRawText();
1656
+ if (text) {
1657
+ textVerbatim = rawText || stackNoTrimWhitespace.length > 0;
1658
+ }
1659
+ if (options.decodeEntities && text && !rawText) {
1573
1660
  // Escape any `&` symbols that start either:
1574
1661
  // 1. a legacy-named character reference (i.e., one that doesn’t end with `;`)
1575
1662
  // 2. or any other character reference (i.e., one that does end with `;`)
@@ -1594,7 +1681,11 @@ async function minifyHTML(value, options, partialMarkup) {
1594
1681
  if (text) {
1595
1682
  hasChars = true;
1596
1683
  }
1684
+ const followsRemoval = removed && Boolean(text) && !options.collapseWhitespace && !stackNoTrimWhitespace.length;
1597
1685
  bufferPush(text, false);
1686
+ if (followsRemoval) {
1687
+ removeWhitespaceLine(buffer.length - 1);
1688
+ }
1598
1689
  }
1599
1690
 
1600
1691
  // Comment finalization (sync): Optional tag handling, `htmlmin:ignore` whitespace collapsing, buffer push
@@ -1858,19 +1949,18 @@ async function minifyHTML(value, options, partialMarkup) {
1858
1949
  optionalEndTagEmitted = false;
1859
1950
  }
1860
1951
 
1861
- // Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`)
1862
- if (options.collapseWhitespace) {
1863
- if (!stackNoTrimWhitespace.length) {
1864
- squashTrailingWhitespace(tag);
1952
+ // Set whitespace flags for nested tags (e.g., `<code>` within a `<pre>`); verbatim
1953
+ // whitespace is tracked without `collapseWhitespace` too, as `removeWhitespaceLine` spares it
1954
+ if (options.collapseWhitespace && !stackNoTrimWhitespace.length) {
1955
+ squashTrailingWhitespace(tag);
1956
+ }
1957
+ if (!unary) {
1958
+ if (!canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
1959
+ stackNoTrimWhitespace.push(tag);
1960
+ if (tag === 'pre' || tag === 'textarea') preTextareaDepth++;
1865
1961
  }
1866
- if (!unary) {
1867
- if (!canTrimWhitespace(tag, attrs) || stackNoTrimWhitespace.length) {
1868
- stackNoTrimWhitespace.push(tag);
1869
- if (tag === 'pre' || tag === 'textarea') preTextareaDepth++;
1870
- }
1871
- if (!canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length) {
1872
- stackNoCollapseWhitespace.push(tag);
1873
- }
1962
+ if (options.collapseWhitespace && (!canCollapseWhitespace(tag, attrs) || stackNoCollapseWhitespace.length)) {
1963
+ stackNoCollapseWhitespace.push(tag);
1874
1964
  }
1875
1965
  }
1876
1966
 
@@ -2004,19 +2094,18 @@ async function minifyHTML(value, options, partialMarkup) {
2004
2094
  }
2005
2095
 
2006
2096
  // Check if current tag is in a whitespace stack
2007
- if (options.collapseWhitespace) {
2008
- if (stackNoTrimWhitespace.length) {
2009
- if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
2010
- if (tag === 'pre' || tag === 'textarea') preTextareaDepth--;
2011
- stackNoTrimWhitespace.pop();
2012
- }
2013
- } else {
2014
- squashTrailingWhitespace('/' + tag);
2015
- }
2016
- if (stackNoCollapseWhitespace.length &&
2017
- tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
2018
- stackNoCollapseWhitespace.pop();
2097
+ const insideNoTrim = stackNoTrimWhitespace.length > 0;
2098
+ if (stackNoTrimWhitespace.length) {
2099
+ if (tag === stackNoTrimWhitespace[stackNoTrimWhitespace.length - 1]) {
2100
+ if (tag === 'pre' || tag === 'textarea') preTextareaDepth--;
2101
+ stackNoTrimWhitespace.pop();
2019
2102
  }
2103
+ } else if (options.collapseWhitespace) {
2104
+ squashTrailingWhitespace('/' + tag);
2105
+ }
2106
+ if (stackNoCollapseWhitespace.length &&
2107
+ tag === stackNoCollapseWhitespace[stackNoCollapseWhitespace.length - 1]) {
2108
+ stackNoCollapseWhitespace.pop();
2020
2109
  }
2021
2110
 
2022
2111
  let isElementEmpty = false;
@@ -2038,7 +2127,7 @@ async function minifyHTML(value, options, partialMarkup) {
2038
2127
  // (https://html.spec.whatwg.org/multipage/syntax.html#optional-tags);
2039
2128
  // except for `</dt>` or `</thead>`, end tags may be omitted if no more content in parent element
2040
2129
  if (tag && optionalEndTag && optionalEndTagEmitted && !trailingElements.has(optionalEndTag) && (optionalEndTag !== 'p' || (!pInlineElements.has(tag) && !tag.includes('-')))) {
2041
- removeEndTag();
2130
+ removeEndTag(insideNoTrim);
2042
2131
  }
2043
2132
  optionalEndTag = optionalEndTags.has(tag) ? tag : '';
2044
2133
  optionalEndTagEmitted = true;
@@ -2145,12 +2234,16 @@ async function minifyHTML(value, options, partialMarkup) {
2145
2234
  text = prefix + text + suffix;
2146
2235
  } else {
2147
2236
  text = '';
2237
+ removed = true;
2148
2238
  }
2149
2239
  } else {
2150
2240
  text = prefix + text + suffix;
2151
2241
  }
2152
2242
  commentFinalize(text);
2153
2243
  },
2244
+ strayEnd: function () {
2245
+ removed = true;
2246
+ },
2154
2247
  doctype: function (/** @type {string} */ doctype) {
2155
2248
  bufferPush(options.useShortDoctype
2156
2249
  ? '<!doctype' +
@@ -2190,9 +2283,12 @@ async function minifyHTML(value, options, partialMarkup) {
2190
2283
  removeEndTag();
2191
2284
  }
2192
2285
  }
2286
+ // Collapsing also trims whitespace before trailing inline end tags; trimming the output
2287
+ // catches what it leaves at either end, like a space after a removed leading comment
2193
2288
  if (options.collapseWhitespace) {
2194
2289
  squashTrailingWhitespace('br');
2195
2290
  }
2291
+ trimOutputWhitespace();
2196
2292
 
2197
2293
  return joinResultSegments(buffer, bufferTags ?? [], [uidAttr, uidIgnore].filter(marker => marker !== undefined), options, uidPattern
2198
2294
  ? function (/** @type {string} */ str) {
package/src/htmlparser.js CHANGED
@@ -19,6 +19,7 @@ import { endlessRawTextElements, escapableRawTextElements, genericRawTextElement
19
19
  * chars?: Function,
20
20
  * comment?: Function,
21
21
  * doctype?: Function,
22
+ * strayEnd?: Function,
22
23
  * continueOnParseError?: boolean | undefined,
23
24
  * partialMarkup?: boolean | undefined,
24
25
  * wantsNextTag?: boolean | undefined,
@@ -1063,6 +1064,9 @@ export class HTMLParser {
1063
1064
  if (handler.end) {
1064
1065
  handler.end(tagName, []);
1065
1066
  }
1067
+ } else if (handler.strayEnd) {
1068
+ // An end tag with nothing to close is dropped
1069
+ handler.strayEnd();
1066
1070
  }
1067
1071
  // Returned so the parse loop can skip lowercasing the name again
1068
1072
  return lowerTagName;