@marko/runtime-tags 6.1.13 → 6.1.15

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.
@@ -1,8 +1,11 @@
1
+ export declare function assertValidAttrValue(name: string, value: unknown): void;
2
+ export declare function assertValidTextValue(value: unknown): void;
3
+ export declare function assertValidLoopKey(key: unknown, seenKeys?: Set<unknown>): void;
4
+ export declare function assertValidAttrName(name: string): void;
1
5
  export declare function _el_read_error(): void;
2
6
  export declare function _hoist_read_error(): void;
3
7
  export declare function _assert_hoist(value: unknown): void;
4
8
  export declare function assertExclusiveAttrs(attrs: Record<string, unknown> | undefined, onError?: typeof throwErr): void;
5
- export declare function assertValidEventHandlerAttr(name: string, value: unknown): void;
6
9
  export declare function assertHandlerIsFunction(name: string, value: unknown): void;
7
10
  export declare function assertValidTagName(tagName: string): void;
8
11
  declare function throwErr(msg: string): void;
@@ -1,5 +1,6 @@
1
1
  export declare const htmlAttrNameReg: RegExp;
2
2
  export declare const userAttrNameReg: RegExp;
3
+ export declare function getWrongAttrSuggestion(name: string): string | undefined;
3
4
  export declare function _call<T>(fn: (v: T) => unknown, v: T): T;
4
5
  export declare function stringifyClassObject(name: string, value: unknown): string;
5
6
  export declare function stringifyStyleObject(name: string, value: unknown): string;
package/dist/debug/dom.js CHANGED
@@ -21,8 +21,137 @@ function* attrTagIterator() {
21
21
  yield* this[rest];
22
22
  }
23
23
  //#endregion
24
+ //#region src/common/helpers.ts
25
+ const htmlAttrNameReg = /^[^a-z_]|[^a-z0-9._:-]/i;
26
+ const knownWrongAttrs = {
27
+ className: "class",
28
+ classList: "class",
29
+ htmlFor: "for",
30
+ acceptCharset: "accept-charset",
31
+ httpEquiv: "http-equiv",
32
+ defaultValue: "value",
33
+ defaultChecked: "checked",
34
+ dangerouslySetInnerHTML: "$!{html}",
35
+ key: "<for by>",
36
+ ref: "<tag/ref>",
37
+ "v-if": "<if>",
38
+ "v-else": "<else>",
39
+ "v-else-if": "<else if>",
40
+ "v-for": "<for>",
41
+ "v-show": "<if>",
42
+ "v-model": "value:=state",
43
+ "v-bind": "...attrs",
44
+ "v-html": "$!{html}",
45
+ "v-text": "${text}"
46
+ };
47
+ function getWrongAttrSuggestion(name) {
48
+ const exact = knownWrongAttrs[name];
49
+ if (exact) return exact;
50
+ const colon = name.indexOf(":");
51
+ if (colon > 0) {
52
+ const rest = name.slice(colon + 1);
53
+ switch (name.slice(0, colon)) {
54
+ case "class": return `class={ ${rest}: condition }`;
55
+ case "style": return `style={ ${rest}: value }`;
56
+ case "on":
57
+ case "v-on": return `on${rest.charAt(0).toUpperCase()}${rest.slice(1)}`;
58
+ case "bind":
59
+ case "v-model": return `${rest}:=state`;
60
+ case "v-bind": return rest;
61
+ }
62
+ }
63
+ }
64
+ function _call(fn, v) {
65
+ fn(v);
66
+ return v;
67
+ }
68
+ function stringifyClassObject(name, value) {
69
+ return value ? name : "";
70
+ }
71
+ function stringifyStyleObject(name, value) {
72
+ return value || value === 0 ? name + ":" + value : "";
73
+ }
74
+ const toDelimitedString = function toDelimitedString(val, delimiter, stringify) {
75
+ let str = "";
76
+ let sep = "";
77
+ let part;
78
+ if (val) if (typeof val !== "object") str += val;
79
+ else if (Array.isArray(val)) for (const v of val) {
80
+ part = toDelimitedString(v, delimiter, stringify);
81
+ if (part) {
82
+ str += sep + part;
83
+ sep = delimiter;
84
+ }
85
+ }
86
+ else for (const name in val) {
87
+ part = stringify(name, val[name]);
88
+ if (part) {
89
+ str += sep + part;
90
+ sep = delimiter;
91
+ }
92
+ }
93
+ return str;
94
+ };
95
+ function isEventHandler(name) {
96
+ return /^on[A-Z-]/.test(name);
97
+ }
98
+ function getEventHandlerName(name) {
99
+ return name[2] === "-" ? name.slice(3) : name.slice(2).toLowerCase();
100
+ }
101
+ function isNotVoid(value) {
102
+ return value != null && value !== false;
103
+ }
104
+ function normalizeDynamicRenderer(value) {
105
+ if (value) {
106
+ if (typeof value === "string") return value;
107
+ const normalized = value.content || value.default || value;
108
+ if (!((typeof normalized === "object" || typeof normalized === "function") && "id" in normalized)) {
109
+ if (value.content) throw new Error(`A dynamic tag must be a string tag name (like \`"div"\`) or a Marko template/component, but received an object whose \`content\` is not a template/component.`);
110
+ if (typeof value !== "object" && typeof value !== "function") throw new Error(`A dynamic tag must be a string tag name (like \`"div"\`) or a Marko template/component, but received a ${typeof value}.`);
111
+ }
112
+ if ("id" in normalized) return normalized;
113
+ }
114
+ }
115
+ //#endregion
24
116
  //#region src/common/errors.ts
25
117
  const lowercaseEventHandlerReg = /^on[a-z]/;
118
+ function assertValidAttrValue(name, value) {
119
+ if (value && typeof value !== "string" && lowercaseEventHandlerReg.test(name)) throw new Error(`The \`${name}\` attribute must be a string or a falsey value (\`null\`, \`undefined\`, \`false\`, \`0\`, …), but received type "${typeof value}". To attach an event listener, use the \`on${name[2].toUpperCase()}${name.slice(3)}\` event handler instead.`);
120
+ if (typeof value === "function") {
121
+ if (name === "content" || /^on/i.test(name) || /Change$/.test(name)) return;
122
+ throw new Error(`The \`${name}\` attribute cannot be a function.`);
123
+ }
124
+ const unrenderable = describeUnrenderable(value);
125
+ if (unrenderable) throw new Error(`The \`${name}\` attribute cannot be ${unrenderable}.`);
126
+ }
127
+ function assertValidTextValue(value) {
128
+ const unrenderable = describeUnrenderable(value);
129
+ if (unrenderable) throw new Error(`Text content cannot be ${unrenderable}.`);
130
+ }
131
+ function describeUnrenderable(value) {
132
+ if (typeof value === "symbol") return "a symbol";
133
+ if (typeof value === "object" && value !== null) {
134
+ let stringified;
135
+ try {
136
+ stringified = `${value}`;
137
+ } catch {
138
+ stringified = "[object Object]";
139
+ }
140
+ if (/^\[object \w+\]$/.test(stringified)) return stringified === "[object Promise]" ? "a promise (use the `<await>` tag to render its resolved value)" : stringified === "[object Object]" ? "a plain object (it would render as `[object Object]`)" : `a value that renders as \`${stringified}\``;
141
+ }
142
+ }
143
+ function assertValidLoopKey(key, seenKeys) {
144
+ if (typeof key !== "string" && typeof key !== "number") throw new Error(`A \`<for>\` tag's \`by\` attribute must return a string or number for each item, but received ${key === null ? "null" : `type "${typeof key}"`}.`);
145
+ if (seenKeys) {
146
+ if (seenKeys.has(key)) throw new Error(`A \`<for>\` tag's \`by\` attribute must return a unique value for each item, but \`${key}\` was used more than once.`);
147
+ seenKeys.add(key);
148
+ }
149
+ }
150
+ function assertValidAttrName(name) {
151
+ if (htmlAttrNameReg.test(name)) throw new Error(`Invalid attribute name: ${JSON.stringify(name)}`);
152
+ const suggestion = getWrongAttrSuggestion(name);
153
+ if (suggestion) throw new Error(`\`${name}\` is not a valid attribute, did you mean \`${suggestion}\`?`);
154
+ }
26
155
  function _el_read_error() {
27
156
  throw new Error("Element references can only be read in scripts and event handlers.");
28
157
  }
@@ -47,9 +176,6 @@ function assertExclusiveAttrs(attrs, onError = throwErr) {
47
176
  if (exclusiveAttrs && exclusiveAttrs.length > 1) onError(`The attributes ${joinWithAnd(exclusiveAttrs)} are mutually exclusive.`);
48
177
  }
49
178
  }
50
- function assertValidEventHandlerAttr(name, value) {
51
- if (value && typeof value !== "string" && lowercaseEventHandlerReg.test(name)) throw new Error(`The \`${name}\` attribute must be a string or a falsey value (\`null\`, \`undefined\`, \`false\`, \`0\`, …), but received type "${typeof value}". To attach an event listener, use the \`on${name[2].toUpperCase()}${name.slice(3)}\` event handler instead.`);
52
- }
53
179
  function assertHandlerIsFunction(name, value) {
54
180
  if (value && typeof value !== "function") throw new Error(`The \`${name}\` handler must be a function or a falsey value (\`null\`, \`undefined\`, \`false\`, \`0\`, …), but received type "${typeof value}".`);
55
181
  }
@@ -89,56 +215,6 @@ function forUntil(until, from, step, cb) {
89
215
  for (let steps = (until - start) / delta, i = 0; i < steps; i++) cb(start + i * delta);
90
216
  }
91
217
  //#endregion
92
- //#region src/common/helpers.ts
93
- const htmlAttrNameReg = /^[^a-z_]|[^a-z0-9._:-]/i;
94
- function _call(fn, v) {
95
- fn(v);
96
- return v;
97
- }
98
- function stringifyClassObject(name, value) {
99
- return value ? name : "";
100
- }
101
- function stringifyStyleObject(name, value) {
102
- return value || value === 0 ? name + ":" + value : "";
103
- }
104
- const toDelimitedString = function toDelimitedString(val, delimiter, stringify) {
105
- let str = "";
106
- let sep = "";
107
- let part;
108
- if (val) if (typeof val !== "object") str += val;
109
- else if (Array.isArray(val)) for (const v of val) {
110
- part = toDelimitedString(v, delimiter, stringify);
111
- if (part) {
112
- str += sep + part;
113
- sep = delimiter;
114
- }
115
- }
116
- else for (const name in val) {
117
- part = stringify(name, val[name]);
118
- if (part) {
119
- str += sep + part;
120
- sep = delimiter;
121
- }
122
- }
123
- return str;
124
- };
125
- function isEventHandler(name) {
126
- return /^on[A-Z-]/.test(name);
127
- }
128
- function getEventHandlerName(name) {
129
- return name[2] === "-" ? name.slice(3) : name.slice(2).toLowerCase();
130
- }
131
- function isNotVoid(value) {
132
- return value != null && value !== false;
133
- }
134
- function normalizeDynamicRenderer(value) {
135
- if (value) {
136
- if (typeof value === "string") return value;
137
- const normalized = value.content || value.default || value;
138
- if ("id" in normalized) return normalized;
139
- }
140
- }
141
- //#endregion
142
218
  //#region src/common/meta.ts
143
219
  const DYNAMIC_TAG_SCRIPT_REGISTER_ID = "_dynamicTagScript";
144
220
  //#endregion
@@ -919,6 +995,7 @@ function _attr_select_value(scope, nodeAccessor, value, valueChange) {
919
995
  assertHandlerIsFunction("valueChange", valueChange);
920
996
  scope["ControlledHandler:" + nodeAccessor] = valueChange;
921
997
  scope["ControlledType:" + nodeAccessor] = valueChange ? 3 : 5;
998
+ if (valueChange) pendingEffects.unshift(() => assertSelectValueMatchesOption(el, normalizedValue, value), scope);
922
999
  if (valueChange && existing) pendingEffects.unshift(() => setSelectValue(el, normalizedValue, multiple), scope);
923
1000
  else _attr_select_value_default(scope, nodeAccessor, normalizedValue);
924
1001
  }
@@ -961,6 +1038,13 @@ function setSelectValue(el, value, multiple) {
961
1038
  function getSelectValue(el, multiple) {
962
1039
  return multiple ? Array.from(el.selectedOptions, (opt) => opt.value) : el.value;
963
1040
  }
1041
+ function assertSelectValueMatchesOption(el, normalizedValue, value) {
1042
+ const multiple = Array.isArray(normalizedValue);
1043
+ if (multiple ? normalizedValue.some(Boolean) : normalizedValue) {
1044
+ for (const opt of el.options) if (multiple ? normalizedValue.includes(opt.value) : opt.value === normalizedValue) return;
1045
+ console.error("A controlled `<select>`'s `value` has no matching `<option>`:", value);
1046
+ }
1047
+ }
964
1048
  function _attr_details_or_dialog_open_default(scope, nodeAccessor, open) {
965
1049
  if (scope["#Gen"] === runId) scope[nodeAccessor].open = isNotVoid(open);
966
1050
  }
@@ -1025,10 +1109,11 @@ function updateList(arr, val, push) {
1025
1109
  //#endregion
1026
1110
  //#region src/dom/dom.ts
1027
1111
  function _to_text(value) {
1112
+ assertValidTextValue(value);
1028
1113
  return value || value === 0 ? value + "" : "";
1029
1114
  }
1030
1115
  function _attr(element, name, value) {
1031
- assertValidEventHandlerAttr(name, value);
1116
+ assertValidAttrValue(name, value);
1032
1117
  setAttribute(element, name, normalizeAttrValue(value));
1033
1118
  }
1034
1119
  function setAttribute(element, name, value) {
@@ -1106,6 +1191,8 @@ function attrsInternal(scope, nodeAccessor, nextAttrs) {
1106
1191
  let events = scope["EventAttributes:" + nodeAccessor];
1107
1192
  let skip;
1108
1193
  for (const name in events) events[name] = 0;
1194
+ scope["ControlledType:" + nodeAccessor] = 5;
1195
+ scope["ControlledHandler:" + nodeAccessor] = 0;
1109
1196
  switch (el.tagName) {
1110
1197
  case "INPUT":
1111
1198
  if ("checked" in nextAttrs || "checkedChange" in nextAttrs) _attr_input_checked(scope, nodeAccessor, nextAttrs.checked, nextAttrs.checkedChange);
@@ -1144,7 +1231,7 @@ function attrsInternal(scope, nodeAccessor, nextAttrs) {
1144
1231
  _attr_style(el, value);
1145
1232
  break;
1146
1233
  default:
1147
- if (htmlAttrNameReg.test(name)) throw new Error(`Invalid attribute name: ${JSON.stringify(name)}`);
1234
+ assertValidAttrName(name);
1148
1235
  if (isEventHandler(name)) (events ||= scope["EventAttributes:" + nodeAccessor] = {})[getEventHandlerName(name)] = value;
1149
1236
  else if (!(skip?.test(name) || name === "content" && el.tagName !== "META")) _attr(el, name, value);
1150
1237
  break;
@@ -1481,9 +1568,7 @@ function loop(forEach) {
1481
1568
  let hasPotentialMoves;
1482
1569
  var seenKeys = /* @__PURE__ */ new Set();
1483
1570
  forEach(value, (key, args) => {
1484
- if (typeof key !== "string" && typeof key !== "number") console.error(`A <for> tag's \`by\` attribute must return a string or number, but it returned:`, key);
1485
- else if (seenKeys.has(key)) console.error(`A <for> tag's \`by\` attribute must return a unique value for each item, but a duplicate was found matching:`, key);
1486
- else seenKeys.add(key);
1571
+ assertValidLoopKey(key, seenKeys);
1487
1572
  let branch = oldLen && (oldScopesByKey ||= oldScopes.reduce((map, scope, i) => map.set(scope["#LoopKey"] ?? i, scope), /* @__PURE__ */ new Map())).get(key);
1488
1573
  if (branch) hasPotentialMoves = oldScopesByKey.delete(key);
1489
1574
  else branch = createAndSetupBranch(scope["$global"], renderer, scope, parentNode);
@@ -19,8 +19,137 @@ function* attrTagIterator() {
19
19
  yield* this[rest];
20
20
  }
21
21
  //#endregion
22
+ //#region src/common/helpers.ts
23
+ const htmlAttrNameReg = /^[^a-z_]|[^a-z0-9._:-]/i;
24
+ const knownWrongAttrs = {
25
+ className: "class",
26
+ classList: "class",
27
+ htmlFor: "for",
28
+ acceptCharset: "accept-charset",
29
+ httpEquiv: "http-equiv",
30
+ defaultValue: "value",
31
+ defaultChecked: "checked",
32
+ dangerouslySetInnerHTML: "$!{html}",
33
+ key: "<for by>",
34
+ ref: "<tag/ref>",
35
+ "v-if": "<if>",
36
+ "v-else": "<else>",
37
+ "v-else-if": "<else if>",
38
+ "v-for": "<for>",
39
+ "v-show": "<if>",
40
+ "v-model": "value:=state",
41
+ "v-bind": "...attrs",
42
+ "v-html": "$!{html}",
43
+ "v-text": "${text}"
44
+ };
45
+ function getWrongAttrSuggestion(name) {
46
+ const exact = knownWrongAttrs[name];
47
+ if (exact) return exact;
48
+ const colon = name.indexOf(":");
49
+ if (colon > 0) {
50
+ const rest = name.slice(colon + 1);
51
+ switch (name.slice(0, colon)) {
52
+ case "class": return `class={ ${rest}: condition }`;
53
+ case "style": return `style={ ${rest}: value }`;
54
+ case "on":
55
+ case "v-on": return `on${rest.charAt(0).toUpperCase()}${rest.slice(1)}`;
56
+ case "bind":
57
+ case "v-model": return `${rest}:=state`;
58
+ case "v-bind": return rest;
59
+ }
60
+ }
61
+ }
62
+ function _call(fn, v) {
63
+ fn(v);
64
+ return v;
65
+ }
66
+ function stringifyClassObject(name, value) {
67
+ return value ? name : "";
68
+ }
69
+ function stringifyStyleObject(name, value) {
70
+ return value || value === 0 ? name + ":" + value : "";
71
+ }
72
+ const toDelimitedString = function toDelimitedString(val, delimiter, stringify) {
73
+ let str = "";
74
+ let sep = "";
75
+ let part;
76
+ if (val) if (typeof val !== "object") str += val;
77
+ else if (Array.isArray(val)) for (const v of val) {
78
+ part = toDelimitedString(v, delimiter, stringify);
79
+ if (part) {
80
+ str += sep + part;
81
+ sep = delimiter;
82
+ }
83
+ }
84
+ else for (const name in val) {
85
+ part = stringify(name, val[name]);
86
+ if (part) {
87
+ str += sep + part;
88
+ sep = delimiter;
89
+ }
90
+ }
91
+ return str;
92
+ };
93
+ function isEventHandler(name) {
94
+ return /^on[A-Z-]/.test(name);
95
+ }
96
+ function getEventHandlerName(name) {
97
+ return name[2] === "-" ? name.slice(3) : name.slice(2).toLowerCase();
98
+ }
99
+ function isNotVoid(value) {
100
+ return value != null && value !== false;
101
+ }
102
+ function normalizeDynamicRenderer(value) {
103
+ if (value) {
104
+ if (typeof value === "string") return value;
105
+ const normalized = value.content || value.default || value;
106
+ if (!((typeof normalized === "object" || typeof normalized === "function") && "id" in normalized)) {
107
+ if (value.content) throw new Error(`A dynamic tag must be a string tag name (like \`"div"\`) or a Marko template/component, but received an object whose \`content\` is not a template/component.`);
108
+ if (typeof value !== "object" && typeof value !== "function") throw new Error(`A dynamic tag must be a string tag name (like \`"div"\`) or a Marko template/component, but received a ${typeof value}.`);
109
+ }
110
+ if ("id" in normalized) return normalized;
111
+ }
112
+ }
113
+ //#endregion
22
114
  //#region src/common/errors.ts
23
115
  const lowercaseEventHandlerReg = /^on[a-z]/;
116
+ function assertValidAttrValue(name, value) {
117
+ if (value && typeof value !== "string" && lowercaseEventHandlerReg.test(name)) throw new Error(`The \`${name}\` attribute must be a string or a falsey value (\`null\`, \`undefined\`, \`false\`, \`0\`, …), but received type "${typeof value}". To attach an event listener, use the \`on${name[2].toUpperCase()}${name.slice(3)}\` event handler instead.`);
118
+ if (typeof value === "function") {
119
+ if (name === "content" || /^on/i.test(name) || /Change$/.test(name)) return;
120
+ throw new Error(`The \`${name}\` attribute cannot be a function.`);
121
+ }
122
+ const unrenderable = describeUnrenderable(value);
123
+ if (unrenderable) throw new Error(`The \`${name}\` attribute cannot be ${unrenderable}.`);
124
+ }
125
+ function assertValidTextValue(value) {
126
+ const unrenderable = describeUnrenderable(value);
127
+ if (unrenderable) throw new Error(`Text content cannot be ${unrenderable}.`);
128
+ }
129
+ function describeUnrenderable(value) {
130
+ if (typeof value === "symbol") return "a symbol";
131
+ if (typeof value === "object" && value !== null) {
132
+ let stringified;
133
+ try {
134
+ stringified = `${value}`;
135
+ } catch {
136
+ stringified = "[object Object]";
137
+ }
138
+ if (/^\[object \w+\]$/.test(stringified)) return stringified === "[object Promise]" ? "a promise (use the `<await>` tag to render its resolved value)" : stringified === "[object Object]" ? "a plain object (it would render as `[object Object]`)" : `a value that renders as \`${stringified}\``;
139
+ }
140
+ }
141
+ function assertValidLoopKey(key, seenKeys) {
142
+ if (typeof key !== "string" && typeof key !== "number") throw new Error(`A \`<for>\` tag's \`by\` attribute must return a string or number for each item, but received ${key === null ? "null" : `type "${typeof key}"`}.`);
143
+ if (seenKeys) {
144
+ if (seenKeys.has(key)) throw new Error(`A \`<for>\` tag's \`by\` attribute must return a unique value for each item, but \`${key}\` was used more than once.`);
145
+ seenKeys.add(key);
146
+ }
147
+ }
148
+ function assertValidAttrName(name) {
149
+ if (htmlAttrNameReg.test(name)) throw new Error(`Invalid attribute name: ${JSON.stringify(name)}`);
150
+ const suggestion = getWrongAttrSuggestion(name);
151
+ if (suggestion) throw new Error(`\`${name}\` is not a valid attribute, did you mean \`${suggestion}\`?`);
152
+ }
24
153
  function _el_read_error() {
25
154
  throw new Error("Element references can only be read in scripts and event handlers.");
26
155
  }
@@ -45,9 +174,6 @@ function assertExclusiveAttrs(attrs, onError = throwErr) {
45
174
  if (exclusiveAttrs && exclusiveAttrs.length > 1) onError(`The attributes ${joinWithAnd(exclusiveAttrs)} are mutually exclusive.`);
46
175
  }
47
176
  }
48
- function assertValidEventHandlerAttr(name, value) {
49
- if (value && typeof value !== "string" && lowercaseEventHandlerReg.test(name)) throw new Error(`The \`${name}\` attribute must be a string or a falsey value (\`null\`, \`undefined\`, \`false\`, \`0\`, …), but received type "${typeof value}". To attach an event listener, use the \`on${name[2].toUpperCase()}${name.slice(3)}\` event handler instead.`);
50
- }
51
177
  function assertHandlerIsFunction(name, value) {
52
178
  if (value && typeof value !== "function") throw new Error(`The \`${name}\` handler must be a function or a falsey value (\`null\`, \`undefined\`, \`false\`, \`0\`, …), but received type "${typeof value}".`);
53
179
  }
@@ -87,56 +213,6 @@ function forUntil(until, from, step, cb) {
87
213
  for (let steps = (until - start) / delta, i = 0; i < steps; i++) cb(start + i * delta);
88
214
  }
89
215
  //#endregion
90
- //#region src/common/helpers.ts
91
- const htmlAttrNameReg = /^[^a-z_]|[^a-z0-9._:-]/i;
92
- function _call(fn, v) {
93
- fn(v);
94
- return v;
95
- }
96
- function stringifyClassObject(name, value) {
97
- return value ? name : "";
98
- }
99
- function stringifyStyleObject(name, value) {
100
- return value || value === 0 ? name + ":" + value : "";
101
- }
102
- const toDelimitedString = function toDelimitedString(val, delimiter, stringify) {
103
- let str = "";
104
- let sep = "";
105
- let part;
106
- if (val) if (typeof val !== "object") str += val;
107
- else if (Array.isArray(val)) for (const v of val) {
108
- part = toDelimitedString(v, delimiter, stringify);
109
- if (part) {
110
- str += sep + part;
111
- sep = delimiter;
112
- }
113
- }
114
- else for (const name in val) {
115
- part = stringify(name, val[name]);
116
- if (part) {
117
- str += sep + part;
118
- sep = delimiter;
119
- }
120
- }
121
- return str;
122
- };
123
- function isEventHandler(name) {
124
- return /^on[A-Z-]/.test(name);
125
- }
126
- function getEventHandlerName(name) {
127
- return name[2] === "-" ? name.slice(3) : name.slice(2).toLowerCase();
128
- }
129
- function isNotVoid(value) {
130
- return value != null && value !== false;
131
- }
132
- function normalizeDynamicRenderer(value) {
133
- if (value) {
134
- if (typeof value === "string") return value;
135
- const normalized = value.content || value.default || value;
136
- if ("id" in normalized) return normalized;
137
- }
138
- }
139
- //#endregion
140
216
  //#region src/common/meta.ts
141
217
  const DYNAMIC_TAG_SCRIPT_REGISTER_ID = "_dynamicTagScript";
142
218
  //#endregion
@@ -917,6 +993,7 @@ function _attr_select_value(scope, nodeAccessor, value, valueChange) {
917
993
  assertHandlerIsFunction("valueChange", valueChange);
918
994
  scope["ControlledHandler:" + nodeAccessor] = valueChange;
919
995
  scope["ControlledType:" + nodeAccessor] = valueChange ? 3 : 5;
996
+ if (valueChange) pendingEffects.unshift(() => assertSelectValueMatchesOption(el, normalizedValue, value), scope);
920
997
  if (valueChange && existing) pendingEffects.unshift(() => setSelectValue(el, normalizedValue, multiple), scope);
921
998
  else _attr_select_value_default(scope, nodeAccessor, normalizedValue);
922
999
  }
@@ -959,6 +1036,13 @@ function setSelectValue(el, value, multiple) {
959
1036
  function getSelectValue(el, multiple) {
960
1037
  return multiple ? Array.from(el.selectedOptions, (opt) => opt.value) : el.value;
961
1038
  }
1039
+ function assertSelectValueMatchesOption(el, normalizedValue, value) {
1040
+ const multiple = Array.isArray(normalizedValue);
1041
+ if (multiple ? normalizedValue.some(Boolean) : normalizedValue) {
1042
+ for (const opt of el.options) if (multiple ? normalizedValue.includes(opt.value) : opt.value === normalizedValue) return;
1043
+ console.error("A controlled `<select>`'s `value` has no matching `<option>`:", value);
1044
+ }
1045
+ }
962
1046
  function _attr_details_or_dialog_open_default(scope, nodeAccessor, open) {
963
1047
  if (scope["#Gen"] === runId) scope[nodeAccessor].open = isNotVoid(open);
964
1048
  }
@@ -1023,10 +1107,11 @@ function updateList(arr, val, push) {
1023
1107
  //#endregion
1024
1108
  //#region src/dom/dom.ts
1025
1109
  function _to_text(value) {
1110
+ assertValidTextValue(value);
1026
1111
  return value || value === 0 ? value + "" : "";
1027
1112
  }
1028
1113
  function _attr(element, name, value) {
1029
- assertValidEventHandlerAttr(name, value);
1114
+ assertValidAttrValue(name, value);
1030
1115
  setAttribute(element, name, normalizeAttrValue(value));
1031
1116
  }
1032
1117
  function setAttribute(element, name, value) {
@@ -1104,6 +1189,8 @@ function attrsInternal(scope, nodeAccessor, nextAttrs) {
1104
1189
  let events = scope["EventAttributes:" + nodeAccessor];
1105
1190
  let skip;
1106
1191
  for (const name in events) events[name] = 0;
1192
+ scope["ControlledType:" + nodeAccessor] = 5;
1193
+ scope["ControlledHandler:" + nodeAccessor] = 0;
1107
1194
  switch (el.tagName) {
1108
1195
  case "INPUT":
1109
1196
  if ("checked" in nextAttrs || "checkedChange" in nextAttrs) _attr_input_checked(scope, nodeAccessor, nextAttrs.checked, nextAttrs.checkedChange);
@@ -1142,7 +1229,7 @@ function attrsInternal(scope, nodeAccessor, nextAttrs) {
1142
1229
  _attr_style(el, value);
1143
1230
  break;
1144
1231
  default:
1145
- if (htmlAttrNameReg.test(name)) throw new Error(`Invalid attribute name: ${JSON.stringify(name)}`);
1232
+ assertValidAttrName(name);
1146
1233
  if (isEventHandler(name)) (events ||= scope["EventAttributes:" + nodeAccessor] = {})[getEventHandlerName(name)] = value;
1147
1234
  else if (!(skip?.test(name) || name === "content" && el.tagName !== "META")) _attr(el, name, value);
1148
1235
  break;
@@ -1479,9 +1566,7 @@ function loop(forEach) {
1479
1566
  let hasPotentialMoves;
1480
1567
  var seenKeys = /* @__PURE__ */ new Set();
1481
1568
  forEach(value, (key, args) => {
1482
- if (typeof key !== "string" && typeof key !== "number") console.error(`A <for> tag's \`by\` attribute must return a string or number, but it returned:`, key);
1483
- else if (seenKeys.has(key)) console.error(`A <for> tag's \`by\` attribute must return a unique value for each item, but a duplicate was found matching:`, key);
1484
- else seenKeys.add(key);
1569
+ assertValidLoopKey(key, seenKeys);
1485
1570
  let branch = oldLen && (oldScopesByKey ||= oldScopes.reduce((map, scope, i) => map.set(scope["#LoopKey"] ?? i, scope), /* @__PURE__ */ new Map())).get(key);
1486
1571
  if (branch) hasPotentialMoves = oldScopesByKey.delete(key);
1487
1572
  else branch = createAndSetupBranch(scope["$global"], renderer, scope, parentNode);