You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ca80be9
Browse filesBrowse the repository at this point in the historyBrowse files
fix(html): drop a shell tag kept for whitespace that prints nothing, and respell escaped quotes (#21717)
* fix(html): drop a shell tag kept for whitespace that prints nothing
`_canOmitShellStart` and `_canOmitShellEnd` skipped only a droppable comment,
so a whitespace-only text node blocked `<head>` and `</head>` even though the
printer discards it in every `collapseWhitespace` tier — whitespace under
`<head>` or `<html>` renders nowhere. Route both through `_printsNothing`,
which the §13.1.2.4 end-tag tests already use for exactly this.
* fix(html): spell a value under the delimiter that escapes fewer of its quotes
A value whose quotes the source escaped is shorter under the other delimiter:
`alt="say "hi""` is `alt='say "hi"'`, five bytes less per escape
and the same value. Tried before unquoting, which would keep every `"`
it leaves bare. The round-trip case compared `attributes()`, which reports
source bytes rather than the value the parser builds, so it read the respelling
as a tree change — decode there, as its own contract already promised.
* test(html): let the minifier comparison see whitespace it loses
The DOM fingerprint normalized text with `replace(/\s+/g, " ").trim()` and
dropped what was left empty, so a removed whitespace-only node — which fuses
two inline boxes — read as no difference at all. Record each as a run instead,
folding adjacent ones: dropping a comment merges the nodes either side, and
counting those as lost whitespace flags every tool for a loss none of them
took. Not under `<head>` / `<html>`, where nothing renders it.
* fix(html): keep an empty attribute the spec does not define on that element
`removeEmptyAttributes` read a flat name table, so `blocking`, `for`,
`headers`, `ping`, `rel` and `sizes` were dropped from any element —
including a custom one, where each is an ordinary author attribute a script
reads back. The table now carries the element scope the token-list table
already did, and the browser oracle probes each name on an element that
reflects it rather than on `<a>`, where most of them read as unknown and
were skipped.
* test(html): mark a whitespace run with a non-string the text cannot be
A literal U+0001 survives parsing into a text node, so the string marker was
a value a document could carry — a minifier that replaced that text with
whitespace would have fingerprinted the same. Also trims two comments past
the two-line limit.
* refactor(html): escape an attribute value through one escaper
The quote respelling hand-rolled a second escape chain beside
`escapeAttribute`, agreeing with it by coincidence rather than by
construction. `escapeAttribute` now takes the delimiter to escape and the
respelling calls it, so the two cannot drift apart. The delimiter is compared
only when a quote is actually seen, leaving the ordinary character path as it
was.
* test(html): hold a declarative shadow root to the same standard as the page
The browser oracle parsed with `DOMParser`, which leaves `<template
shadowrootmode>` inert, so a shadow tree was compared as template content and
no fixture carried one at all. It now parses with `parseHTMLUnsafe` and walks
an open shadow root like the light tree, and a round-trip case covers open,
closed, nested, slotted and the shadow root flags.
* perf(html): write an attribute value in the fewest bytes the parser reads back
Three spellings the printer was leaving on the table, each checked against the
tokenizer's own rules: `<` and `>` carry no meaning inside a quoted value
(§13.2.5.36/38), an `&` no reference can start after it is data (§13.2.5.41),
and an apostrophe is free under a double quote just as a double quote is free
under an apostrophe. The value is now decoded once and written under whichever
delimiter costs least, so `title="<b>bold</b>"` is
`title="<b>bold</b>".
A bare value still keeps `>`, which would otherwise end it, and `?a=1&b=2`
keeps its escape because `&b` could continue into a reference. A reference
spelling whitespace is left alone: writing it out would let a later
`removeEmptyAttributes` pass read the attribute as empty.
* test(html): keep a closed shadow root in view of the browser oracle
Attaching declarative shadow roots hid what `shadowRoot` never hands back: a
closed root's content read as empty, so the minifier could have mangled it
unnoticed. Its content is now read off a second, inert parse where it is still
the `<template>` it was written as.
Copy file name to clipboardExpand all lines: .changeset/048-html-remove-redundant-attributes.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,4 +2,4 @@
2
2
"webpack": minor
3
3
---
4
4
5
-
Minify HTML further: leave out the implied `<html>` tag and any optional end tag nothing prints behind, print an empty attribute value bare, drop an empty attribute and a run of nested empty elements, fold an enumerated value to lower case, and add the `optimization.minimize.html` options `collapseWhitespace`, `mergeStyles`, `minifyConditionalComments`, `preserveComments`, `removeEmptyAttributes`, `removeEmptyElements`, `removeImpliedTags`, `removeRedundantAttributes`, `sortAttributes` and `sortTokenLists`.
5
+
Minify HTML further: leave out the implied `<html>` tag and any optional end tag nothing prints behind, print an empty attribute value bare, write an attribute value in the fewest bytes that parse back the same, drop an empty attribute and a run of nested empty elements, fold an enumerated value to lower case, and add the `optimization.minimize.html` options `collapseWhitespace`, `mergeStyles`, `minifyConditionalComments`, `preserveComments`, `removeEmptyAttributes`, `removeEmptyElements`, `removeImpliedTags`, `removeRedundantAttributes`, `sortAttributes` and `sortTokenLists`.
Copy file name to clipboardExpand all lines: .changeset/157-html-minify-text-fixes.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,4 +2,4 @@
2
2
"webpack": patch
3
3
---
4
4
5
-
Fix HTML minification losing a character after a CR line ending, dropping a body's leading whitespace when its tag is implied, omitting an end tag whose absence lets the parser's adoption agency restructure the tree, and the parser case-folding the non-ASCII characters of an element or attribute name.
5
+
Fix HTML minification losing a character after a CR line ending, dropping a body's leading whitespace when its tag is implied, keeping a shell tag over whitespace that prints nothing, omitting an end tag whose absence lets the parser's adoption agency restructure the tree, and the parser case-folding the non-ASCII characters of an element or attribute name.
* Drop an attribute whose empty or all-whitespace value leaves it in the state its absence gives: `class`, `id`, `style`, `dir`, `for`, `accesskey`, `itemprop`, `itemref`, `itemtype`, and every attribute reflecting a token list (`rel`, `ping`, `headers`, `part`, `sizes`, `blocking`) — an empty list is no tokens. Off by default: an attribute selector matches on presence, so `[class]` stops matching. Never dropped: `title` and `lang`, whose empty value means what absence does not; `sandbox`, whose empty list is the most restrictive state an `<iframe>` has; and an event handler, whose empty body still compiles to a function where absence reads null.
2412
+
* Drop an attribute whose empty or all-whitespace value leaves it in the state its absence gives: the globals `class`, `id`, `style`, `dir`, `accesskey`, `itemprop`, `itemref`, `itemtype` and `part`, and every attribute reflecting a token list on the elements the spec defines it for — `rel` on `<a>`, `<area>`, `<form>` and `<link>`, `ping` on `<a>` and `<area>`, `headers` on `<td>` and `<th>`, `blocking` on `<link>`, `<script>` and `<style>`, `sizes` on `<link>`, `for` on `<output>` — where an empty list is no tokens. Anywhere else that spelling is an author attribute whose meaning is a script's, so `<x-foo rel="">` and `<label for="">` keep it. Off by default: an attribute selector matches on presence, so `[class]` stops matching. Never dropped: `title` and `lang`, whose empty value means what absence does not; `sandbox`, whose empty list is the most restrictive state an `<iframe>` has; and an event handler, whose empty body still compiles to a function where absence reads null.
Copy file name to clipboardExpand all lines: lib/html/data.js
+18-18Lines changed: 18 additions & 18 deletions
Original file line number
Diff line number
Diff line change
@@ -1615,25 +1615,25 @@ const EMPTY_ELEMENT_KEPT = new Set([
1615
1615
]);
1616
1616
1617
1617
/**
1618
-
* Attributes `removeEmptyAttributes` may drop when their value is empty or all whitespace: the spec gives each the same state empty and absent. Four hand-reasoned globals, plus every token-list attribute (no tokens is what absence gives) and every §8.1.7.2.1 event handler (an empty handler body does nothing). `sandbox` is the token list held back — its empty list is the most restrictive state an `<iframe>` has. Only a presence selector (`[class]`) or a script reading the attribute back can tell, which is what keeps the option off by default.
1619
-
* @type {Set<string>}
1618
+
* Attributes `removeEmptyAttributes` may drop when their value is empty or all whitespace, mapped to the elements they mean that on (`null` = a global attribute): the spec gives each the same state empty and absent. Four hand-reasoned globals, plus every token-list attribute (no tokens is what absence gives). The element matters — on any element the spec does not define it for, `rel` or `headers` is an ordinary author attribute a script reads back, so `<x-foo rel="">` keeps it. `sandbox` is the token list held back: its empty list is the most restrictive state an `<iframe>` has. Only a presence selector (`[class]`) or a script reading the attribute back can tell, which is what keeps the option off by default.
Copy file name to clipboardExpand all lines: schemas/WebpackOptions.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -3781,7 +3781,7 @@
3781
3781
"added": "5.110.0"
3782
3782
},
3783
3783
"removeEmptyAttributes": {
3784
-
"description": "Drop an attribute whose empty or all-whitespace value leaves it in the state its absence gives: `class`, `id`, `style`, `dir`, `for`, `accesskey`, `itemprop`, `itemref`, `itemtype`, and every attribute reflecting a token list (`rel`, `ping`, `headers`, `part`, `sizes`, `blocking`) — an empty list is no tokens. Off by default: an attribute selector matches on presence, so `[class]` stops matching. Never dropped: `title` and `lang`, whose empty value means what absence does not; `sandbox`, whose empty list is the most restrictive state an `<iframe>` has; and an event handler, whose empty body still compiles to a function where absence reads null.",
3784
+
"description": "Drop an attribute whose empty or all-whitespace value leaves it in the state its absence gives: the globals `class`, `id`, `style`, `dir`, `accesskey`, `itemprop`, `itemref`, `itemtype` and `part`, and every attribute reflecting a token list on the elements the spec defines it for — `rel` on `<a>`, `<area>`, `<form>` and `<link>`, `ping` on `<a>` and `<area>`, `headers` on `<td>` and `<th>`, `blocking` on `<link>`, `<script>` and `<style>`, `sizes` on `<link>`, `for` on `<output>` — where an empty list is no tokens. Anywhere else that spelling is an author attribute whose meaning is a script's, so `<x-foo rel=\"\">` and `<label for=\"\">` keep it. Off by default: an attribute selector matches on presence, so `[class]` stops matching. Never dropped: `title` and `lang`, whose empty value means what absence does not; `sandbox`, whose empty list is the most restrictive state an `<iframe>` has; and an event handler, whose empty body still compiles to a function where absence reads null.",
0 commit comments