@webjsdev/core 0.7.49 → 0.7.50
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/dist/webjs-core-browser.js +4 -4
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +18 -18
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +5 -5
- package/dist/webjs-core.js.map +3 -3
- package/package.json +1 -1
- package/src/attribute-reader.js +178 -0
- package/src/component.d.ts +10 -0
- package/src/component.js +24 -59
- package/src/html-entities.js +2179 -0
- package/src/render-client.js +4 -5
- package/src/render-server.js +183 -112
- package/src/webjs-config.d.ts +13 -2
package/src/render-client.js
CHANGED
|
@@ -750,11 +750,10 @@ function buildFormActionRecord(el, onEl, parts) {
|
|
|
750
750
|
* record.
|
|
751
751
|
*
|
|
752
752
|
* FORMS FIRST, submitters second, regardless of the order the records were
|
|
753
|
-
* collected in.
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* template, so the ordering is made explicit rather than relied upon.
|
|
753
|
+
* collected in. Nothing reads an enclosing form's boundness any more (#1307
|
|
754
|
+
* made a bound submitter self-sufficient), but the ordering still makes a
|
|
755
|
+
* form's RELEASE run before its submitters reconcile, so it is kept rather
|
|
756
|
+
* than churned. See the note in the body.
|
|
758
757
|
*
|
|
759
758
|
* @param {FormActionRecord[] | null} formActions
|
|
760
759
|
* @param {BoundPart[]} bound
|
package/src/render-server.js
CHANGED
|
@@ -16,6 +16,8 @@ import { isSuspense } from './suspense.js';
|
|
|
16
16
|
import { unsafeHTML, isUnsafeHTML, isLive, isKeyed, isGuard, isTemplateContent, isRef, isCache, isUntil, isAsyncAppend, isAsyncReplace, isWatch } from './directives.js';
|
|
17
17
|
import { stringify, parse } from './serialize.js';
|
|
18
18
|
import { cspNonce } from './csp-nonce.js';
|
|
19
|
+
import { readAttributeValue, resolveAttributeProperty } from './attribute-reader.js';
|
|
20
|
+
import NAMED_ENTITIES, { LEGACY_NAMES } from './html-entities.js';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Render a TemplateResult (or any renderable value) to an HTML string.
|
|
@@ -180,23 +182,6 @@ async function renderTemplate(tr, ctx) {
|
|
|
180
182
|
let pendingPropAttrs = [];
|
|
181
183
|
/** @type {string[]} */
|
|
182
184
|
let pendingSubmitterProps = [];
|
|
183
|
-
// Whether the tag stream is currently inside a form that BOUND an action
|
|
184
|
-
// (#1207), as THIS scan can see it. Three states, and the third is the point:
|
|
185
|
-
// 'bound' an enclosing <form> opened here and bound an action
|
|
186
|
-
// 'unbound' an enclosing <form> opened here and bound nothing
|
|
187
|
-
// 'none' there is conclusively no enclosing <form>
|
|
188
|
-
// 'unknown' there may be one, but this scan cannot see it
|
|
189
|
-
//
|
|
190
|
-
// The last two look alike and must not be merged, which is what a boolean did.
|
|
191
|
-
// A component's template is rendered by a SEPARATE pass (`injectDSD` calls
|
|
192
|
-
// `render` on it), so a `<button formaction=${fn}>` inside a component inside
|
|
193
|
-
// a bound form read as "no form", was refused, and in production vanished
|
|
194
|
-
// from a page that still returned 200. That pass now says 'unknown' and the
|
|
195
|
-
// boundness question is skipped, exactly as the client skips it when it
|
|
196
|
-
// cannot reach the form, so both renderers are best effort in the same place
|
|
197
|
-
// and for the same reason. A top-level scan that simply contains no form
|
|
198
|
-
// stays 'none' and is still refused, because there the answer IS known.
|
|
199
|
-
//
|
|
200
185
|
let isCloseTag = false;
|
|
201
186
|
|
|
202
187
|
// A bound `action=${fn}` is committed at its hole, but the edits it implies
|
|
@@ -483,14 +468,13 @@ async function renderTemplate(tr, ctx) {
|
|
|
483
468
|
// #1207: the submitter binding. The identity replaces the
|
|
484
469
|
// `formaction=` hole IN PLACE with the button's own name/value
|
|
485
470
|
// pair, the one channel a browser submits for the pressed button
|
|
486
|
-
// alone. No `formaction` url is emitted, so the submission
|
|
487
|
-
//
|
|
488
|
-
// by this later entry.
|
|
471
|
+
// alone. No `formaction` url is emitted, so the submission targets
|
|
472
|
+
// whatever the FORM targets, and a form-level identity is simply
|
|
473
|
+
// overridden by this later entry.
|
|
489
474
|
//
|
|
490
475
|
// Refused here rather than at the `>` only where the answer cannot
|
|
491
|
-
// change later:
|
|
492
|
-
//
|
|
493
|
-
// hole is already in `out`. Everything else waits for the close,
|
|
476
|
+
// change later: an attribute written BEFORE the hole is already in
|
|
477
|
+
// `out`. Everything else waits for the close,
|
|
494
478
|
// where `assertSubmitterStartTag` sees the whole tag.
|
|
495
479
|
// A second binding hole on this same tag, refused here so the
|
|
496
480
|
// author gets the duplicate message rather than a confusing
|
|
@@ -1035,14 +1019,12 @@ async function injectDSD(html, ctx, ancestors = [], dev) {
|
|
|
1035
1019
|
// Render the template to HTML. injectDSD recurses on the result so
|
|
1036
1020
|
// nested custom elements (e.g. <theme-toggle> inside <blog-shell>)
|
|
1037
1021
|
// get their own DSD pass.
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1044
|
-
// isolated, made the button vanish from a page that still returned 200.
|
|
1045
|
-
const rawInner = await render(tpl, ctx, 'unknown');
|
|
1022
|
+
// This is a SEPARATE render pass over one component's own template,
|
|
1023
|
+
// driven by walking the already-emitted HTML, so it has no idea whether
|
|
1024
|
+
// the host tag sits inside a `<form>`. It does not need to: a bound
|
|
1025
|
+
// submitter carries its whole submission (#1307), so nothing here asks
|
|
1026
|
+
// about an enclosing form.
|
|
1027
|
+
const rawInner = await render(tpl, ctx);
|
|
1046
1028
|
|
|
1047
1029
|
if (isShadow) {
|
|
1048
1030
|
// Shadow DOM: native <slot> stays as-is in the DSD template. The
|
|
@@ -1279,7 +1261,7 @@ async function processSuspenseElements(html, ctx, ancestors = [], dev) {
|
|
|
1279
1261
|
result += rest.slice(0, openStart);
|
|
1280
1262
|
const attrs = m[1] || '';
|
|
1281
1263
|
const fbMatch = /data-webjs-fallback="([^"]*)"/i.exec(attrs);
|
|
1282
|
-
const fallbackHtml = fbMatch ?
|
|
1264
|
+
const fallbackHtml = fbMatch ? decodeAttrEntities(fbMatch[1]) : '';
|
|
1283
1265
|
|
|
1284
1266
|
// Pass the FULL-input ranges shifted into `rest` coordinates rather than
|
|
1285
1267
|
// letting the helper re-tokenize the suffix. `rest` can begin mid-comment
|
|
@@ -1650,7 +1632,7 @@ function parseAttrs(attrStr) {
|
|
|
1650
1632
|
function seedServerAttrs(instance, attrs) {
|
|
1651
1633
|
if (!instance || typeof instance.setAttribute !== 'function') return;
|
|
1652
1634
|
for (const [name, raw] of Object.entries(attrs)) {
|
|
1653
|
-
instance.setAttribute(name,
|
|
1635
|
+
instance.setAttribute(name, decodeAttrEntities(raw));
|
|
1654
1636
|
}
|
|
1655
1637
|
}
|
|
1656
1638
|
|
|
@@ -1701,67 +1683,41 @@ function appendReflectedAttrs(opening, instance, presentAttrNames) {
|
|
|
1701
1683
|
* based on its static `properties` declaration.
|
|
1702
1684
|
*/
|
|
1703
1685
|
function applyAttrsToInstance(instance, attrs, Cls) {
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1706
|
-
//
|
|
1707
|
-
//
|
|
1708
|
-
//
|
|
1709
|
-
//
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
//
|
|
1724
|
-
//
|
|
1725
|
-
//
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
// `<my-el cfg="oops">` SSRs holding a string and re-renders holding
|
|
1740
|
-
// something else the moment the element upgrades. `null` is the right
|
|
1741
|
-
// value to agree on: a string is never a valid value for a property the
|
|
1742
|
-
// author declared `Object` or `Array`. This function iterates the
|
|
1743
|
-
// attributes PRESENT on the source tag, so an absent one is not read here
|
|
1744
|
-
// at all and its property keeps whatever the constructor gave it; the
|
|
1745
|
-
// agreement being asserted is about a present, unparseable attribute.
|
|
1746
|
-
//
|
|
1747
|
-
// That agreement is about this FALLBACK, not a guarantee that the two
|
|
1748
|
-
// readers see the same attributes in the first place. This function
|
|
1749
|
-
// walks the parsed source tag while the client goes through
|
|
1750
|
-
// `observedAttributes` and the browser's own name-lowercasing, and
|
|
1751
|
-
// hand-written markup can land in the gaps between those routes (the
|
|
1752
|
-
// entity decoding just below is one, since it reverses far less than a
|
|
1753
|
-
// browser does). Those gaps predate #1253 and it neither causes nor
|
|
1754
|
-
// closes any of them; no attempt is made here to enumerate them,
|
|
1755
|
-
// because every attempt so far has been incomplete.
|
|
1756
|
-
//
|
|
1757
|
-
// Scoped to the DEFAULT converter, which is all this branch is. The
|
|
1758
|
-
// client reader tries `converter.fromAttribute` FIRST and only falls
|
|
1759
|
-
// through to here when there is none, while this function has no
|
|
1760
|
-
// converter arm at all, so a prop declaring one is read differently by
|
|
1761
|
-
// the two sides regardless of what this line does. That gap predates
|
|
1762
|
-
// #1253 and is left alone rather than widened.
|
|
1763
|
-
try { instance[propName] = JSON.parse(unescapeAttr(raw)); } catch { instance[propName] = null; }
|
|
1764
|
-
} else instance[propName] = raw;
|
|
1686
|
+
for (const [sourceName, sourceValue] of Object.entries(attrs)) {
|
|
1687
|
+
// The browser LOWERCASES every attribute name while parsing an HTML
|
|
1688
|
+
// document, so `cfgData="x"` reaches the client reader as `cfgdata` and a
|
|
1689
|
+
// camelCase name can never match anything in `observedAttributes`.
|
|
1690
|
+
// Resolving the source case here made SSR read a name the platform cannot
|
|
1691
|
+
// deliver, which is the divergence, not the fix (#1341).
|
|
1692
|
+
const resolved = resolveAttributeProperty(Cls, sourceName.toLowerCase());
|
|
1693
|
+
// An attribute mapping to no attribute-backed property is IGNORED, exactly
|
|
1694
|
+
// as `attributeChangedCallback` ignores it and as lit's reader does. This
|
|
1695
|
+
// used to assign it as an instance property (`instance[propName] = raw`),
|
|
1696
|
+
// which no browser upgrade ever reproduces, and which on a real
|
|
1697
|
+
// HTMLElement could mutate DOM state through `id` / `hidden` / `slot`.
|
|
1698
|
+
// `seedServerAttrs` has already applied every source attribute properly,
|
|
1699
|
+
// so nothing needs the copy (#1341).
|
|
1700
|
+
if (resolved === undefined) continue;
|
|
1701
|
+
const { propName, def } = resolved;
|
|
1702
|
+
// A browser decodes every character reference BEFORE any reader sees the
|
|
1703
|
+
// value, so decode once here, for every branch, and hand the shared reader
|
|
1704
|
+
// an already-decoded string exactly as the DOM hands the client one. It
|
|
1705
|
+
// used to be a `decode` argument the reader applied on the JSON and
|
|
1706
|
+
// converter branches alone, which left a `String`-typed prop holding the
|
|
1707
|
+
// raw source text while `getAttribute()` on the client returned the decoded
|
|
1708
|
+
// one (#1341).
|
|
1709
|
+
//
|
|
1710
|
+
// One reader for both sides (#1340): `readAttributeValue` in
|
|
1711
|
+
// `attribute-reader.js` is the same function `attributeChangedCallback` in
|
|
1712
|
+
// `component.js` calls, so a custom `converter.fromAttribute` runs here
|
|
1713
|
+
// too, ahead of type coercion, and the #1253 unparseable-JSON fallback is
|
|
1714
|
+
// shared rather than mirrored.
|
|
1715
|
+
//
|
|
1716
|
+
// A converter that THROWS is not caught here. It lands in the
|
|
1717
|
+
// per-component error isolation below, which is deliberate: an author who
|
|
1718
|
+
// supplies a converter owns the read, the same rule `_reflectAttribute`
|
|
1719
|
+
// states for `toAttribute`. See the comment on `readAttributeValue`.
|
|
1720
|
+
instance[propName] = readAttributeValue(def, decodeAttrEntities(sourceValue));
|
|
1765
1721
|
}
|
|
1766
1722
|
}
|
|
1767
1723
|
|
|
@@ -1770,11 +1726,6 @@ function camelCase(s) {
|
|
|
1770
1726
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1771
1727
|
}
|
|
1772
1728
|
|
|
1773
|
-
/** Kebab-case a property name for its default HTML attribute (matches component.js). @param {string} s */
|
|
1774
|
-
function hyphenate(s) {
|
|
1775
|
-
return s.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
1729
|
/**
|
|
1779
1730
|
* Inverse of camelCase. `userName` -> `user-name`, `userID` -> `user-i-d`.
|
|
1780
1731
|
* Used to serialize property-binding names into HTML attribute names,
|
|
@@ -1787,20 +1738,142 @@ function kebabCase(s) {
|
|
|
1787
1738
|
return s.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
1788
1739
|
}
|
|
1789
1740
|
|
|
1741
|
+
/** windows-1252 mappings the HTML tokenizer applies to the C1 range. */
|
|
1742
|
+
const C1_REPLACEMENTS = {
|
|
1743
|
+
0x80: 0x20ac, 0x82: 0x201a, 0x83: 0x0192, 0x84: 0x201e, 0x85: 0x2026,
|
|
1744
|
+
0x86: 0x2020, 0x87: 0x2021, 0x88: 0x02c6, 0x89: 0x2030, 0x8a: 0x0160,
|
|
1745
|
+
0x8b: 0x2039, 0x8c: 0x0152, 0x8e: 0x017d, 0x91: 0x2018, 0x92: 0x2019,
|
|
1746
|
+
0x93: 0x201c, 0x94: 0x201d, 0x95: 0x2022, 0x96: 0x2013, 0x97: 0x2014,
|
|
1747
|
+
0x98: 0x02dc, 0x99: 0x2122, 0x9a: 0x0161, 0x9b: 0x203a, 0x9c: 0x0153,
|
|
1748
|
+
0x9e: 0x017e, 0x9f: 0x0178,
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
// The trailing `;` is OPTIONAL on the named arm, because a browser decodes a
|
|
1752
|
+
// legacy semicolon-less reference too. The callback decides which of the two
|
|
1753
|
+
// forms it has; see `decodeNamed`.
|
|
1754
|
+
const CHAR_REF = /&(?:#(\d+);?|#[xX]([0-9a-fA-F]+);?|([a-zA-Z][a-zA-Z0-9]*)(;?))/g;
|
|
1755
|
+
|
|
1756
|
+
// A Map, not the imported object, so a name that collides with something on
|
|
1757
|
+
// `Object.prototype` misses instead of returning a function. See `decodeNamed`.
|
|
1758
|
+
const NAMED = new Map(Object.entries(NAMED_ENTITIES));
|
|
1759
|
+
const LEGACY = new Set(LEGACY_NAMES);
|
|
1760
|
+
|
|
1790
1761
|
/**
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
*
|
|
1762
|
+
* Decode HTML character references in an attribute value.
|
|
1763
|
+
*
|
|
1764
|
+
* `parseAttrs` hands back the literal characters between the quote marks, so
|
|
1765
|
+
* nothing has decoded them yet, while a browser decodes EVERY reference before
|
|
1766
|
+
* any reader sees the value. This closes that gap (#1341): the full WHATWG
|
|
1767
|
+
* named table plus decimal and hexadecimal numeric references, with the
|
|
1768
|
+
* tokenizer's own numeric fix-ups (null, a surrogate, and anything past
|
|
1769
|
+
* U+10FFFF become U+FFFD; the C1 range maps through windows-1252). It replaced
|
|
1770
|
+
* a three-entity `unescapeAttr`, which left a `String`-typed prop undecoded
|
|
1771
|
+
* entirely and turned `<script>` into the half-decoded `<script>`.
|
|
1772
|
+
*
|
|
1773
|
+
* SINGLE PASS on purpose. A replacement is never rescanned, so `&lt;`
|
|
1774
|
+
* decodes to the literal `<` and never to `<`. The old function got that
|
|
1775
|
+
* from replacing `&` last, which does not generalise past three entities.
|
|
1776
|
+
*
|
|
1777
|
+
* The 106 legacy semicolon-LESS names are covered too, because a browser really
|
|
1778
|
+
* does decode them and measurably: Chromium, Firefox, and WebKit all hand a
|
|
1779
|
+
* reader U+00A0 for `s=" "`. Leaving them literal would have been a value
|
|
1780
|
+
* divergence of exactly the kind this function exists to remove, not a
|
|
1781
|
+
* harmless non-goal. The rule that governs them, in an ATTRIBUTE value, is a
|
|
1782
|
+
* one-character LOOKAHEAD rather than deep tokenizer state, which is why it is
|
|
1783
|
+
* implementable here. `decodeNamed` carries the rule and the reason it takes
|
|
1784
|
+
* the shape it does; the short version is that ` ` at the end of a value
|
|
1785
|
+
* decodes while ` x`, ` =x`, and `¬in` stay literal, all three
|
|
1786
|
+
* verified against the three engines.
|
|
1787
|
+
*
|
|
1788
|
+
* The same function also decodes `data-webjs-fallback`, which is MARKUP, where
|
|
1789
|
+
* the tokenizer applies no such carve-out. Using the attribute rule there is
|
|
1790
|
+
* deliberate and strictly conservative: that payload is written by
|
|
1791
|
+
* `escapeAttr`, which emits only `&` / `"` / `<`, so every
|
|
1792
|
+
* reference in it is semicolon-terminated and never reaches this path.
|
|
1796
1793
|
*
|
|
1797
1794
|
* @param {string} s
|
|
1795
|
+
* @returns {string}
|
|
1796
|
+
*/
|
|
1797
|
+
function decodeAttrEntities(s) {
|
|
1798
|
+
// Load bearing, not a micro-optimisation: skipping the scan for the common
|
|
1799
|
+
// no-`&` value is what makes this cheaper per attribute than the three
|
|
1800
|
+
// chained `replace` calls it replaced, which paid for three passes always.
|
|
1801
|
+
if (s.indexOf('&') === -1) return s;
|
|
1802
|
+
return s.replace(CHAR_REF, (match, dec, hex, name, semi, offset) => {
|
|
1803
|
+
if (dec !== undefined) return fromCodePoint(parseInt(dec, 10));
|
|
1804
|
+
if (hex !== undefined) return fromCodePoint(parseInt(hex, 16));
|
|
1805
|
+
return decodeNamed(match, name, semi === ';', s[offset + match.length]);
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Resolve one named reference, semicolon-terminated or legacy.
|
|
1811
|
+
*
|
|
1812
|
+
* The table is read through a `Map` rather than by indexing the imported
|
|
1813
|
+
* object. An object-literal lookup resolves through `Object.prototype`, so
|
|
1814
|
+
* `&constructor;` / `&toString;` / `&hasOwnProperty;` and four more returned a
|
|
1815
|
+
* FUNCTION instead of `undefined` and threw on the spread in
|
|
1816
|
+
* `codePointsToString`, a path `seedServerAttrs` reaches for every attribute of
|
|
1817
|
+
* every custom element, which rendered the whole component as an SSR error box.
|
|
1818
|
+
* A browser leaves those literal, since they are not named references, so the
|
|
1819
|
+
* throw was a divergence of exactly the kind this file exists to remove. A
|
|
1820
|
+
* `Map` has no prototype chain to fall through, which closes the shape rather
|
|
1821
|
+
* than guarding one call site.
|
|
1822
|
+
*
|
|
1823
|
+
* WHY THERE IS NO LONGEST-PREFIX LOOP. The tokenizer consumes the longest name
|
|
1824
|
+
* in the table, so `¬in` is `¬` followed by `in`. Here that always
|
|
1825
|
+
* collapses to the whole name: `CHAR_REF` captures `[a-zA-Z][a-zA-Z0-9]*`, so a
|
|
1826
|
+
* prefix SHORTER than the name is by construction followed by an ASCII
|
|
1827
|
+
* alphanumeric, which is precisely when the attribute carve-out declines to
|
|
1828
|
+
* decode. A loop over shorter prefixes could therefore only ever return
|
|
1829
|
+
* `match`, which is what falling through to the end already does. `¬in`
|
|
1830
|
+
* stays literal either way, verified against the three engines, though note it
|
|
1831
|
+
* gets there by not being a legacy name rather than by the carve-out.
|
|
1832
|
+
*
|
|
1833
|
+
* That same greediness means an ASCII alphanumeric character can never follow
|
|
1834
|
+
* the match: whatever follows is by construction not `[a-zA-Z0-9]`, or the
|
|
1835
|
+
* capture would have eaten it. So only the `=` check is needed for the
|
|
1836
|
+
* attribute carve-out. ` x` is literal because `nbspx` is not a legacy
|
|
1837
|
+
* name, NOT because of the lookahead; ` =x` is the shape the lookahead
|
|
1838
|
+
* actually decides.
|
|
1839
|
+
*
|
|
1840
|
+
* @param {string} match the whole matched reference, returned unchanged when nothing decodes
|
|
1841
|
+
* @param {string} name the name, with no `&` and no `;`
|
|
1842
|
+
* @param {boolean} hadSemi
|
|
1843
|
+
* @param {string|undefined} nextChar the character after the match, if any
|
|
1844
|
+
* @returns {string}
|
|
1845
|
+
*/
|
|
1846
|
+
function decodeNamed(match, name, hadSemi, nextChar) {
|
|
1847
|
+
if (hadSemi) {
|
|
1848
|
+
const cp = NAMED.get(name);
|
|
1849
|
+
return cp === undefined ? match : codePointsToString(cp);
|
|
1850
|
+
}
|
|
1851
|
+
if (!LEGACY.has(name)) return match;
|
|
1852
|
+
// The attribute carve-out: a legacy name decodes only when what follows is
|
|
1853
|
+
// neither `=` nor an ASCII alphanumeric. Nothing following at all (the end of
|
|
1854
|
+
// the value) decodes, which is the common `s=" "` shape. Because CHAR_REF
|
|
1855
|
+
// captures greedily, what follows is never an ASCII alphanumeric, so only `=`
|
|
1856
|
+
// needs to be checked here.
|
|
1857
|
+
if (nextChar === '=') return match;
|
|
1858
|
+
// Every legacy name is in the table under the same name, asserted by a test
|
|
1859
|
+
// rather than left to trust, so this cannot miss.
|
|
1860
|
+
return codePointsToString(NAMED.get(name));
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/** @param {number|number[]} cp */
|
|
1864
|
+
function codePointsToString(cp) {
|
|
1865
|
+
return typeof cp === 'number' ? String.fromCodePoint(cp) : String.fromCodePoint(...cp);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/**
|
|
1869
|
+
* The tokenizer's numeric character reference fix-ups.
|
|
1870
|
+
* @param {number} n
|
|
1871
|
+
* @returns {string}
|
|
1798
1872
|
*/
|
|
1799
|
-
function
|
|
1800
|
-
return
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
.replace(/&/g, '&');
|
|
1873
|
+
function fromCodePoint(n) {
|
|
1874
|
+
if (n === 0 || n > 0x10ffff || (n >= 0xd800 && n <= 0xdfff)) return '\uFFFD';
|
|
1875
|
+
if (C1_REPLACEMENTS[n] !== undefined) return String.fromCodePoint(C1_REPLACEMENTS[n]);
|
|
1876
|
+
return String.fromCodePoint(n);
|
|
1804
1877
|
}
|
|
1805
1878
|
|
|
1806
1879
|
/**
|
|
@@ -1819,7 +1892,7 @@ function consumePropAttrs(attrs) {
|
|
|
1819
1892
|
if (!key.startsWith('data-webjs-prop-')) continue;
|
|
1820
1893
|
const propName = camelCase(key.slice('data-webjs-prop-'.length));
|
|
1821
1894
|
try {
|
|
1822
|
-
props[propName] = parse(
|
|
1895
|
+
props[propName] = parse(decodeAttrEntities(attrs[key]));
|
|
1823
1896
|
} catch {
|
|
1824
1897
|
// Malformed payload. Skip silently so the rest of the component
|
|
1825
1898
|
// can still render. The client-side hydration will also try and
|
|
@@ -2009,8 +2082,6 @@ async function streamTemplate(tr, ctx, controller) {
|
|
|
2009
2082
|
let pendingSubmitterProps = [];
|
|
2010
2083
|
/** @type {string | null} */
|
|
2011
2084
|
let pendingSubmitterTag = null; // tag of a bound submitter, until it closes
|
|
2012
|
-
// See the buffered machine: seeded from the caller so a nested template
|
|
2013
|
-
// rendered into a bound form's children knows it is inside one.
|
|
2014
2085
|
let isCloseTag = false;
|
|
2015
2086
|
|
|
2016
2087
|
// See the buffered machine for why this runs at the `>` rather than at the
|
package/src/webjs-config.d.ts
CHANGED
|
@@ -10,8 +10,19 @@
|
|
|
10
10
|
* This object is read key by key, mostly by the server and for a few keys
|
|
11
11
|
* by the CLI. Without a type or schema a typo'd key (e.g. `redirect` for
|
|
12
12
|
* `redirects`) was silently dropped and the feature stayed at its default
|
|
13
|
-
* with no diagnostic.
|
|
14
|
-
*
|
|
13
|
+
* with no diagnostic. Three things close that gap now, at three different
|
|
14
|
+
* moments: this type while an agent or a human authors the block, the
|
|
15
|
+
* published JSON Schema in an editor via the scaffold's `.vscode` `$ref`,
|
|
16
|
+
* and `webjs-config-validate.js` at BOOT (#1300), which runs that same
|
|
17
|
+
* schema from `createRequestHandler` and warns once, naming every unknown
|
|
18
|
+
* top-level key, every bad `enum` value, and every wrong-typed `boolean` /
|
|
19
|
+
* `integer` (9 of the 17 keys). It does NOT type-check the other 8
|
|
20
|
+
* (`headers`, `redirects`, `basePath`, `allowedOrigins`, `csp`, `dev`,
|
|
21
|
+
* `start`, `doctor`), and it does not descend into a nested object, so a
|
|
22
|
+
* misspelling inside `dev` or `start` is not reported either. Whether
|
|
23
|
+
* anything else notices what this check passes over is up to the
|
|
24
|
+
* individual reader. The boot check warns and never throws, so a typo
|
|
25
|
+
* costs one feature its setting rather than the whole app its boot.
|
|
15
26
|
*
|
|
16
27
|
* LOCKSTEP: this file, the JSON Schema at
|
|
17
28
|
* packages/server/webjs-config.schema.json, and the reader functions MUST
|