@reckona/mreact-compiler 0.0.132 → 0.0.135

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.
@@ -291,6 +291,21 @@ export function htmlAttributeName(name: string): string {
291
291
  return HTML_ATTRIBUTE_ALIASES[name] ?? name;
292
292
  }
293
293
 
294
+ export function isBooleanishStringAttribute(name: string): boolean {
295
+ const attributeName = htmlAttributeName(name).toLowerCase();
296
+ return (
297
+ attributeName.startsWith("aria-") ||
298
+ attributeName.startsWith("data-") ||
299
+ BOOLEANISH_STRING_ATTRIBUTES.has(attributeName)
300
+ );
301
+ }
302
+
303
+ const BOOLEANISH_STRING_ATTRIBUTES = new Set<string>([
304
+ "contenteditable",
305
+ "draggable",
306
+ "spellcheck",
307
+ ]);
308
+
294
309
  function unwrapParenthesized(code: string): string {
295
310
  let current = code;
296
311
 
@@ -18,6 +18,7 @@ import {
18
18
  import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
19
19
  import {
20
20
  htmlAttributeName,
21
+ isBooleanishStringAttribute,
21
22
  isDangerousHtmlAttribute,
22
23
  isStaticUrlValueUnsafe,
23
24
  isUrlAttribute,
@@ -286,7 +287,12 @@ function emitClientBoundaryHelper(name: string): string {
286
287
  ` const _props = props ?? {};`,
287
288
  ` const _nonSerializable = ${propsHelperName}(_props);`,
288
289
  ` const _nonSerializableAttr = _nonSerializable ? ' data-mreact-client-boundary-nonserializable="true"' : "";`,
289
- ` const _json = (JSON.stringify(_props) ?? "{}").replaceAll("<", "\\\\u003c");`,
290
+ ` const _json = (JSON.stringify(_props) ?? "{}")`,
291
+ ` .replaceAll("&", "\\\\u0026")`,
292
+ ` .replaceAll("<", "\\\\u003c")`,
293
+ ` .replaceAll(">", "\\\\u003e")`,
294
+ ` .replaceAll("\\u2028", "\\\\u2028")`,
295
+ ` .replaceAll("\\u2029", "\\\\u2029");`,
290
296
  ` return \`<template data-mreact-client-boundary="\${_escapedName}"\${_nonSerializableAttr}></template>\${childrenHtml}<script type="application/json" data-mreact-client-boundary-props="\${_escapedName}">\${_json}</script>\`;`,
291
297
  `}`,
292
298
  ].join("\n");
@@ -357,11 +363,13 @@ function emitSpreadAttributesHelper(
357
363
  ` let _out = "";`,
358
364
  ` for (const _rawName of Object.keys(props)) {`,
359
365
  ` let _value = props[_rawName];`,
360
- ` if (_value == null || _value === false) continue;`,
366
+ ` if (_value == null) continue;`,
361
367
  ` if (_rawName === "key" || _rawName === "ref" || _rawName === "children") continue;`,
362
368
  ` if (/^on[A-Za-z]/.test(_rawName)) continue;`,
363
369
  ` let _name = tagName === "input" && _rawName === "defaultValue" ? "value" : tagName === "input" && _rawName === "defaultChecked" ? "checked" : (${name}$aliases[_rawName] ?? _rawName);`,
364
370
  ` if (!/^[A-Za-z_:][A-Za-z0-9:_.-]*$/.test(_name)) continue;`,
371
+ ` const _booleanish = _name.startsWith("aria-") || _name.startsWith("data-") || _name === "contenteditable" || _name === "draggable" || _name === "spellcheck";`,
372
+ ` if (_value === false && !_booleanish) continue;`,
365
373
  ` if (_name === "style") {`,
366
374
  ` const _style = ${name}$style(_value);`,
367
375
  ` if (_style !== "") _out += " style=\\"" + ${escapeHelperName}(_style) + "\\"";`,
@@ -377,7 +385,7 @@ function emitSpreadAttributesHelper(
377
385
  ` _value = ${urlSafeHelperName}(_name, _value === true ? "" : _value);`,
378
386
  ` if (_value === undefined) continue;`,
379
387
  ` }`,
380
- ` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value === true ? "" : _value) + "\\"";`,
388
+ ` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value === true && !_booleanish ? "" : _value) + "\\"";`,
381
389
  ` }`,
382
390
  ` return _out;`,
383
391
  `}`,
@@ -1737,6 +1745,8 @@ function emitDynamicAttributeExpression(
1737
1745
  code: string,
1738
1746
  escapeHelperName: string,
1739
1747
  ): string {
1748
+ const booleanishString = isBooleanishStringAttribute(name);
1749
+
1740
1750
  if (isUrlAttribute(name)) {
1741
1751
  return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; const _checked = ${currentUrlSafeHelperName}(${stringLiteral(name)}, _value === true ? "" : _value); return _checked === undefined ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_checked) + ${stringLiteral('"')}; })()`;
1742
1752
  }
@@ -1745,10 +1755,14 @@ function emitDynamicAttributeExpression(
1745
1755
 
1746
1756
  if (inlineExpr !== undefined) {
1747
1757
  // Inline 3 evaluations to avoid per-attribute IIFE closure allocation.
1748
- return `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral('"')})`;
1758
+ return booleanishString
1759
+ ? `(${inlineExpr} == null ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr}) + ${stringLiteral('"')})`
1760
+ : `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral('"')})`;
1749
1761
  }
1750
1762
 
1751
- return `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral('"')}; })()`;
1763
+ return booleanishString
1764
+ ? `(() => { const _value = (${code}); return _value == null ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value) + ${stringLiteral('"')}; })()`
1765
+ : `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral('"')}; })()`;
1752
1766
  }
1753
1767
 
1754
1768
  function emitDynamicStyleAttributeExpression(
@@ -5,6 +5,7 @@ import { createCodeBuilder } from "./emit-code-builder.js";
5
5
  import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
6
6
  import {
7
7
  htmlAttributeName,
8
+ isBooleanishStringAttribute,
8
9
  isDangerousHtmlAttribute,
9
10
  isStaticUrlValueUnsafe,
10
11
  isUrlAttribute,
@@ -1144,6 +1145,8 @@ function emitDynamicAttributeExpression(
1144
1145
  code: string,
1145
1146
  escapeHelperName: string,
1146
1147
  ): string {
1148
+ const booleanishString = isBooleanishStringAttribute(name);
1149
+
1147
1150
  if (isUrlAttribute(name)) {
1148
1151
  // Run the value through the inline URL safety helper. The helper
1149
1152
  // returns the value when safe and `undefined` when the attribute
@@ -1159,10 +1162,14 @@ function emitDynamicAttributeExpression(
1159
1162
  // Safe because `simpleSideEffectFreeExpression` only matches expressions
1160
1163
  // whose evaluation has no observable side effects (identifier read,
1161
1164
  // member chain, literal, this).
1162
- return `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral('"')})`;
1165
+ return booleanishString
1166
+ ? `(${inlineExpr} == null ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr}) + ${stringLiteral('"')})`
1167
+ : `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral('"')})`;
1163
1168
  }
1164
1169
 
1165
- return `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral('"')}; })()`;
1170
+ return booleanishString
1171
+ ? `(() => { const _value = (${code}); return _value == null ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value) + ${stringLiteral('"')}; })()`
1172
+ : `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral('"')}; })()`;
1166
1173
  }
1167
1174
 
1168
1175
  function emitDynamicStyleAttributeExpression(
@@ -1676,7 +1683,12 @@ function emitClientBoundaryHelper(name: string): string {
1676
1683
  ` const _props = props ?? {};`,
1677
1684
  ` const _nonSerializable = ${propsHelperName}(_props);`,
1678
1685
  ` const _nonSerializableAttr = _nonSerializable ? ' data-mreact-client-boundary-nonserializable="true"' : "";`,
1679
- ` const _json = (JSON.stringify(_props) ?? "{}").replaceAll("<", "\\\\u003c");`,
1686
+ ` const _json = (JSON.stringify(_props) ?? "{}")`,
1687
+ ` .replaceAll("&", "\\\\u0026")`,
1688
+ ` .replaceAll("<", "\\\\u003c")`,
1689
+ ` .replaceAll(">", "\\\\u003e")`,
1690
+ ` .replaceAll("\\u2028", "\\\\u2028")`,
1691
+ ` .replaceAll("\\u2029", "\\\\u2029");`,
1680
1692
  ` return \`<template data-mreact-client-boundary="\${_escapedName}"\${_nonSerializableAttr}></template>\${childrenHtml}<script type="application/json" data-mreact-client-boundary-props="\${_escapedName}">\${_json}</script>\`;`,
1681
1693
  `}`,
1682
1694
  ].join("\n");
@@ -1747,11 +1759,13 @@ function emitSpreadAttributesHelper(
1747
1759
  ` let _out = "";`,
1748
1760
  ` for (const _rawName of Object.keys(props)) {`,
1749
1761
  ` let _value = props[_rawName];`,
1750
- ` if (_value == null || _value === false) continue;`,
1762
+ ` if (_value == null) continue;`,
1751
1763
  ` if (_rawName === "key" || _rawName === "ref" || _rawName === "children") continue;`,
1752
1764
  ` if (/^on[A-Za-z]/.test(_rawName)) continue;`,
1753
1765
  ` let _name = tagName === "input" && _rawName === "defaultValue" ? "value" : tagName === "input" && _rawName === "defaultChecked" ? "checked" : (${name}$aliases[_rawName] ?? _rawName);`,
1754
1766
  ` if (!/^[A-Za-z_:][A-Za-z0-9:_.-]*$/.test(_name)) continue;`,
1767
+ ` const _booleanish = _name.startsWith("aria-") || _name.startsWith("data-") || _name === "contenteditable" || _name === "draggable" || _name === "spellcheck";`,
1768
+ ` if (_value === false && !_booleanish) continue;`,
1755
1769
  ` if (_name === "style") {`,
1756
1770
  ` const _style = ${name}$style(_value);`,
1757
1771
  ` if (_style !== "") _out += " style=\\"" + ${escapeHelperName}(_style) + "\\"";`,
@@ -1767,7 +1781,7 @@ function emitSpreadAttributesHelper(
1767
1781
  ` _value = ${urlSafeHelperName}(_name, _value === true ? "" : _value);`,
1768
1782
  ` if (_value === undefined) continue;`,
1769
1783
  ` }`,
1770
- ` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value === true ? "" : _value) + "\\"";`,
1784
+ ` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value === true && !_booleanish ? "" : _value) + "\\"";`,
1771
1785
  ` }`,
1772
1786
  ` return _out;`,
1773
1787
  `}`,