@@ -9150,16 +9150,36 @@ const _escapeTextContent = (s, minify) => {
91509150 * @returns {string} serialized opening tag
91519151 */
91529152const _synthesizeOpenTag = (path) => {
9153- let out = `<${path.tagName()}`;
9153+ const tagName = path.tagName();
9154+ let out = `<${tagName}`;
9155+ // Looked up once for the tag, as the attribute loop does for a written one.
9156+ const enumeratedOn =
9157+ _minifying && path.namespace(path.node) === NS_HTML
9158+ ? ENUMERATED_KEYWORDS[tagName]
9159+ : undefined;
91549160 for (const attribute of path.attributes()) {
91559161 const name =
91569162 attribute.serializedName !== undefined
91579163 ? attribute.serializedName
91589164 : attribute.name;
9159- out +=
9160- attribute.value === ""
9161- ? ` ${name}`
9162- : ` ${name}="${attribute.value.replace(/"/g, """)}"`;
9165+ if (attribute.value === "") {
9166+ out += ` ${name}`;
9167+ continue;
9168+ }
9169+ // Nothing was written here for this tag, so there is no source spelling to
9170+ // beat: the shortest one wins outright.
9171+ if (!_minifying || !_transforms.normalizeAttributeQuotes) {
9172+ out += ` ${name}="${attribute.value.replace(/"/g, """)}"`;
9173+ continue;
9174+ }
9175+ const value = _rewriteAttributeValue(
9176+ tagName,
9177+ name,
9178+ attribute.value,
9179+ false,
9180+ enumeratedOn
9181+ );
9182+ out += ` ${name}=${_minimalAttributeValue(decodeEntities(value, true))}`;
91639183 }
91649184 return `${out}>`;
91659185};
@@ -9893,9 +9913,10 @@ const _minifyStyleBody = (css) =>
98939913 * @param {string} value its rewritten value
98949914 * @param {string} rawValue its value as the source spelled it
98959915 * @param {string | null} decoded the value decoded, for one carrying character references, or null
9916+ * @param {boolean} quoted whether the source spelled the value with a delimiter, which is two characters this has to beat
98969917 * @returns {{ text: string, unquotedTail: boolean } | null} what to emit, or null to leave the source alone
98979918 */
9898- const _writeAttribute = (attributeName, value, rawValue, decoded) => {
9919+ const _writeAttribute = (attributeName, value, rawValue, decoded, quoted ) => {
98999920 const minimal =
99009921 _transforms.normalizeAttributeQuotes && decoded !== null
99019922 ? _minimalAttributeValue(decoded)
@@ -9905,7 +9926,8 @@ const _writeAttribute = (attributeName, value, rawValue, decoded) => {
99059926
99069927 if (
99079928 minimal !== null &&
9908- minimal.length < (unquotable ? value.length : rawValue.length + 2)
9929+ minimal.length <
9930+ (unquotable ? value.length : rawValue.length + (quoted ? 2 : 0))
99099931 ) {
99109932 const opened = minimal.charCodeAt(0);
99119933
@@ -11178,7 +11200,9 @@ const _minifyOpenTag = (path, open) => {
1117811200 continue;
1117911201 }
1118011202 }
11181- if (quoted) {
11203+ // Where nothing below can change the value: foreign content rewrites only
11204+ // `style`, and a bare value is already the shortest spelling of itself.
11205+ if (quoted || html || attributeName === "style" || rawValue.includes("&")) {
1118211206 const hasReference = rawValue.includes("&");
1118311207 // A character reference would decode to something these grammars read
1118411208 // differently, so only a reference-free value is rewritten.
@@ -11209,7 +11233,7 @@ const _minifyOpenTag = (path, open) => {
1120911233 // the switch that names the rest of them.
1121011234 if (
1121111235 _transforms.normalizeAttributeQuotes &&
11212- (rawValue === "" ||
11236+ (value === "" ||
1121311237 (html &&
1121411238 _transforms.collapseBooleanAttributes &&
1121511239 (_transforms.collapseBooleanAttributes === "all" ||
@@ -11244,11 +11268,17 @@ const _minifyOpenTag = (path, open) => {
1124411268 */
1124511269 const writeStyle = (minified) =>
1124611270 declarations !== null
11247- ? _writeAttribute(attributeName, rawValue, rawValue, minified)
11248- : _writeAttribute(attributeName, minified, rawValue, spellable);
11271+ ? _writeAttribute(attributeName, rawValue, rawValue, minified, quoted)
11272+ : _writeAttribute(
11273+ attributeName,
11274+ minified,
11275+ rawValue,
11276+ spellable,
11277+ quoted
11278+ );
1124911279 const written = isStyle
1125011280 ? writeStyle(declarations !== null ? declarations : value)
11251- : _writeAttribute(attributeName, value, rawValue, spellable);
11281+ : _writeAttribute(attributeName, value, rawValue, spellable, quoted );
1125211282 // The JavaScript an event handler holds, offered decoded — `&&`
1125311283 // is an operator to JavaScript rather than an entity. No built-in.
1125411284 const handler =
@@ -11262,7 +11292,7 @@ const _minifyOpenTag = (path, open) => {
1126211292 : null;
1126311293
1126411294 if (handler !== null && _asciiTrim(handler) !== "") {
11265- const delimiter = open[valueStart - tagStart - 1];
11295+ const delimiter = quoted ? open[valueStart - tagStart - 1] : "" ;
1126611296 // A foreign `/>` reads back whether this value ends unquoted, which a
1126711297 // deferred answer is not there to decide: the source's quoting stands.
1126811298 const holdQuoted = tailIsRead && _deferEmbeddedSource !== undefined;
@@ -11295,7 +11325,8 @@ const _minifyOpenTag = (path, open) => {
1129511325 attributeName,
1129611326 escapeAttribute(rendered, CC_QUOTATION_MARK, true),
1129711327 rawValue,
11298- rendered
11328+ rendered,
11329+ quoted
1129911330 );
1130011331
1130111332 return answered !== null && answered.text.length < asWritten.length
@@ -11333,7 +11364,7 @@ const _minifyOpenTag = (path, open) => {
1133311364 if (isStyle && _deferEmbeddedSource !== undefined) {
1133411365 // Declining leaves the attribute as written, delimiter and all — an
1133511366 // answer nothing beats is the renderer keeping what it was given.
11336- const delimiter = open[valueStart - tagStart - 1];
11367+ const delimiter = quoted ? open[valueStart - tagStart - 1] : "" ;
1133711368 const asWritten = ` ${attributeName}=${delimiter}${rawValue}${delimiter}`;
1133811369 // What stands until an answer arrives, and the tag ending it commits
1133911370 // to: the built-in's text where it beat the source, else the source.
0 commit comments