@marko/runtime-tags 6.1.12 → 6.1.14

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,7 +1,12 @@
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;
9
+ export declare function assertHandlerIsFunction(name: string, value: unknown): void;
5
10
  export declare function assertValidTagName(tagName: string): void;
6
11
  declare function throwErr(msg: string): void;
7
12
  export {};
@@ -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,7 +21,133 @@ 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 ("id" in normalized) return normalized;
109
+ }
110
+ }
111
+ //#endregion
24
112
  //#region src/common/errors.ts
113
+ const lowercaseEventHandlerReg = /^on[a-z]/;
114
+ function assertValidAttrValue(name, value) {
115
+ 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.`);
116
+ if (typeof value === "function") {
117
+ if (name === "content" || /^on/i.test(name) || /Change$/.test(name)) return;
118
+ throw new Error(`The \`${name}\` attribute cannot be a function.`);
119
+ }
120
+ const unrenderable = describeUnrenderable(value);
121
+ if (unrenderable) throw new Error(`The \`${name}\` attribute cannot be ${unrenderable}.`);
122
+ }
123
+ function assertValidTextValue(value) {
124
+ const unrenderable = describeUnrenderable(value);
125
+ if (unrenderable) throw new Error(`Text content cannot be ${unrenderable}.`);
126
+ }
127
+ function describeUnrenderable(value) {
128
+ if (typeof value === "symbol") return "a symbol";
129
+ if (typeof value === "object" && value !== null) {
130
+ let stringified;
131
+ try {
132
+ stringified = `${value}`;
133
+ } catch {
134
+ stringified = "[object Object]";
135
+ }
136
+ 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}\``;
137
+ }
138
+ }
139
+ function assertValidLoopKey(key, seenKeys) {
140
+ 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}"`}.`);
141
+ if (seenKeys) {
142
+ 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.`);
143
+ seenKeys.add(key);
144
+ }
145
+ }
146
+ function assertValidAttrName(name) {
147
+ if (htmlAttrNameReg.test(name)) throw new Error(`Invalid attribute name: ${JSON.stringify(name)}`);
148
+ const suggestion = getWrongAttrSuggestion(name);
149
+ if (suggestion) throw new Error(`\`${name}\` is not a valid attribute, did you mean \`${suggestion}\`?`);
150
+ }
25
151
  function _el_read_error() {
26
152
  throw new Error("Element references can only be read in scripts and event handlers.");
27
153
  }
@@ -46,6 +172,9 @@ function assertExclusiveAttrs(attrs, onError = throwErr) {
46
172
  if (exclusiveAttrs && exclusiveAttrs.length > 1) onError(`The attributes ${joinWithAnd(exclusiveAttrs)} are mutually exclusive.`);
47
173
  }
48
174
  }
175
+ function assertHandlerIsFunction(name, value) {
176
+ 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}".`);
177
+ }
49
178
  function assertValidTagName(tagName) {
50
179
  if (!/^[a-z][a-z0-9._-]*$/i.test(tagName)) throw new Error(`Invalid tag name: "${tagName}". Tag names must start with a letter and contain only letters, numbers, periods, hyphens, and underscores.`);
51
180
  }
@@ -82,56 +211,6 @@ function forUntil(until, from, step, cb) {
82
211
  for (let steps = (until - start) / delta, i = 0; i < steps; i++) cb(start + i * delta);
83
212
  }
84
213
  //#endregion
85
- //#region src/common/helpers.ts
86
- const htmlAttrNameReg = /^[^a-z_]|[^a-z0-9._:-]/i;
87
- function _call(fn, v) {
88
- fn(v);
89
- return v;
90
- }
91
- function stringifyClassObject(name, value) {
92
- return value ? name : "";
93
- }
94
- function stringifyStyleObject(name, value) {
95
- return value || value === 0 ? name + ":" + value : "";
96
- }
97
- const toDelimitedString = function toDelimitedString(val, delimiter, stringify) {
98
- let str = "";
99
- let sep = "";
100
- let part;
101
- if (val) if (typeof val !== "object") str += val;
102
- else if (Array.isArray(val)) for (const v of val) {
103
- part = toDelimitedString(v, delimiter, stringify);
104
- if (part) {
105
- str += sep + part;
106
- sep = delimiter;
107
- }
108
- }
109
- else for (const name in val) {
110
- part = stringify(name, val[name]);
111
- if (part) {
112
- str += sep + part;
113
- sep = delimiter;
114
- }
115
- }
116
- return str;
117
- };
118
- function isEventHandler(name) {
119
- return /^on[A-Z-]/.test(name);
120
- }
121
- function getEventHandlerName(name) {
122
- return name[2] === "-" ? name.slice(3) : name.slice(2).toLowerCase();
123
- }
124
- function isNotVoid(value) {
125
- return value != null && value !== false;
126
- }
127
- function normalizeDynamicRenderer(value) {
128
- if (value) {
129
- if (typeof value === "string") return value;
130
- const normalized = value.content || value.default || value;
131
- if ("id" in normalized) return normalized;
132
- }
133
- }
134
- //#endregion
135
214
  //#region src/common/meta.ts
136
215
  const DYNAMIC_TAG_SCRIPT_REGISTER_ID = "_dynamicTagScript";
137
216
  //#endregion
@@ -146,6 +225,7 @@ function push(opt, item) {
146
225
  //#region src/dom/event.ts
147
226
  const defaultDelegator = /* @__PURE__ */ createDelegator();
148
227
  function _on(element, type, handler) {
228
+ assertHandlerIsFunction("on" + type[0].toUpperCase() + type.slice(1), handler);
149
229
  if (element["$" + type] === void 0) defaultDelegator(element, type, handleDelegated);
150
230
  element["$" + type] = handler || null;
151
231
  }
@@ -793,6 +873,7 @@ function _attr_input_checked_default(scope, nodeAccessor, checked) {
793
873
  function _attr_input_checked(scope, nodeAccessor, checked, checkedChange) {
794
874
  const el = scope[nodeAccessor];
795
875
  const normalizedChecked = isNotVoid(checked);
876
+ assertHandlerIsFunction("checkedChange", checkedChange);
796
877
  scope["ControlledHandler:" + nodeAccessor] = checkedChange;
797
878
  scope["ControlledType:" + nodeAccessor] = checkedChange ? 0 : 5;
798
879
  if (checkedChange && scope["#Gen"] < runId) el.checked = normalizedChecked;
@@ -821,6 +902,7 @@ function _attr_input_checkedValue(scope, nodeAccessor, checkedValue, checkedValu
821
902
  const el = scope[nodeAccessor];
822
903
  const multiple = Array.isArray(checkedValue);
823
904
  const normalizedCheckedValue = scope["ControlledValue:" + nodeAccessor] = multiple ? checkedValue.map(normalizeStrProp) : normalizeStrProp(checkedValue);
905
+ assertHandlerIsFunction("checkedValueChange", checkedValueChange);
824
906
  scope["ControlledHandler:" + nodeAccessor] = checkedValueChange;
825
907
  scope["ControlledType:" + nodeAccessor] = checkedValueChange ? 1 : 5;
826
908
  if (checkedValueChange && scope["#Gen"] < runId) {
@@ -857,6 +939,7 @@ function _attr_input_value_default(scope, nodeAccessor, value) {
857
939
  function _attr_input_value(scope, nodeAccessor, value, valueChange) {
858
940
  const el = scope[nodeAccessor];
859
941
  const normalizedValue = normalizeAttrValue(value) || "";
942
+ assertHandlerIsFunction("valueChange", valueChange);
860
943
  scope["ControlledHandler:" + nodeAccessor] = valueChange;
861
944
  scope["ControlledValue:" + nodeAccessor] = normalizedValue;
862
945
  scope["ControlledType:" + nodeAccessor] = valueChange ? 2 : 5;
@@ -905,8 +988,10 @@ function _attr_select_value(scope, nodeAccessor, value, valueChange) {
905
988
  const existing = scope["#Gen"] < runId;
906
989
  const multiple = Array.isArray(value);
907
990
  const normalizedValue = scope["ControlledValue:" + nodeAccessor] = multiple ? value.map(normalizeStrProp) : normalizeStrProp(value);
991
+ assertHandlerIsFunction("valueChange", valueChange);
908
992
  scope["ControlledHandler:" + nodeAccessor] = valueChange;
909
993
  scope["ControlledType:" + nodeAccessor] = valueChange ? 3 : 5;
994
+ if (valueChange) pendingEffects.unshift(() => assertSelectValueMatchesOption(el, normalizedValue, value), scope);
910
995
  if (valueChange && existing) pendingEffects.unshift(() => setSelectValue(el, normalizedValue, multiple), scope);
911
996
  else _attr_select_value_default(scope, nodeAccessor, normalizedValue);
912
997
  }
@@ -949,11 +1034,19 @@ function setSelectValue(el, value, multiple) {
949
1034
  function getSelectValue(el, multiple) {
950
1035
  return multiple ? Array.from(el.selectedOptions, (opt) => opt.value) : el.value;
951
1036
  }
1037
+ function assertSelectValueMatchesOption(el, normalizedValue, value) {
1038
+ const multiple = Array.isArray(normalizedValue);
1039
+ if (multiple ? normalizedValue.some(Boolean) : normalizedValue) {
1040
+ for (const opt of el.options) if (multiple ? normalizedValue.includes(opt.value) : opt.value === normalizedValue) return;
1041
+ console.error("A controlled `<select>`'s `value` has no matching `<option>`:", value);
1042
+ }
1043
+ }
952
1044
  function _attr_details_or_dialog_open_default(scope, nodeAccessor, open) {
953
1045
  if (scope["#Gen"] === runId) scope[nodeAccessor].open = isNotVoid(open);
954
1046
  }
955
1047
  function _attr_details_or_dialog_open(scope, nodeAccessor, open, openChange) {
956
1048
  const normalizedOpen = scope["ControlledValue:" + nodeAccessor] = isNotVoid(open);
1049
+ assertHandlerIsFunction("openChange", openChange);
957
1050
  scope["ControlledHandler:" + nodeAccessor] = openChange;
958
1051
  scope["ControlledType:" + nodeAccessor] = openChange ? 4 : 5;
959
1052
  if (openChange && scope["#Gen"] < runId) scope[nodeAccessor].open = normalizedOpen;
@@ -1012,9 +1105,11 @@ function updateList(arr, val, push) {
1012
1105
  //#endregion
1013
1106
  //#region src/dom/dom.ts
1014
1107
  function _to_text(value) {
1108
+ assertValidTextValue(value);
1015
1109
  return value || value === 0 ? value + "" : "";
1016
1110
  }
1017
1111
  function _attr(element, name, value) {
1112
+ assertValidAttrValue(name, value);
1018
1113
  setAttribute(element, name, normalizeAttrValue(value));
1019
1114
  }
1020
1115
  function setAttribute(element, name, value) {
@@ -1089,8 +1184,11 @@ function _attrs_partial_content(scope, nodeAccessor, nextAttrs, skip) {
1089
1184
  }
1090
1185
  function attrsInternal(scope, nodeAccessor, nextAttrs) {
1091
1186
  const el = scope[nodeAccessor];
1092
- let events;
1187
+ let events = scope["EventAttributes:" + nodeAccessor];
1093
1188
  let skip;
1189
+ for (const name in events) events[name] = 0;
1190
+ scope["ControlledType:" + nodeAccessor] = 5;
1191
+ scope["ControlledHandler:" + nodeAccessor] = 0;
1094
1192
  switch (el.tagName) {
1095
1193
  case "INPUT":
1096
1194
  if ("checked" in nextAttrs || "checkedChange" in nextAttrs) _attr_input_checked(scope, nodeAccessor, nextAttrs.checked, nextAttrs.checkedChange);
@@ -1129,7 +1227,7 @@ function attrsInternal(scope, nodeAccessor, nextAttrs) {
1129
1227
  _attr_style(el, value);
1130
1228
  break;
1131
1229
  default:
1132
- if (htmlAttrNameReg.test(name)) throw new Error(`Invalid attribute name: ${JSON.stringify(name)}`);
1230
+ assertValidAttrName(name);
1133
1231
  if (isEventHandler(name)) (events ||= scope["EventAttributes:" + nodeAccessor] = {})[getEventHandlerName(name)] = value;
1134
1232
  else if (!(skip?.test(name) || name === "content" && el.tagName !== "META")) _attr(el, name, value);
1135
1233
  break;
@@ -1374,7 +1472,7 @@ function _if(nodeAccessor, ...branchesArgs) {
1374
1472
  while (i < branchesArgs.length) branches.push(_content("", branchesArgs[i++], branchesArgs[i++], branchesArgs[i++])());
1375
1473
  enableBranches();
1376
1474
  return (scope, newBranch) => {
1377
- if (newBranch !== scope[branchAccessor]) setConditionalRenderer(scope, nodeAccessor, branches[scope[branchAccessor] = newBranch], createAndSetupBranch);
1475
+ if (newBranch !== (scope[branchAccessor] ?? (scope["BranchScopes:" + nodeAccessor] && 0))) setConditionalRenderer(scope, nodeAccessor, branches[scope[branchAccessor] = newBranch], createAndSetupBranch);
1378
1476
  };
1379
1477
  }
1380
1478
  function patchDynamicTag(fn) {
@@ -1466,8 +1564,7 @@ function loop(forEach) {
1466
1564
  let hasPotentialMoves;
1467
1565
  var seenKeys = /* @__PURE__ */ new Set();
1468
1566
  forEach(value, (key, args) => {
1469
- 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);
1470
- else seenKeys.add(key);
1567
+ assertValidLoopKey(key, seenKeys);
1471
1568
  let branch = oldLen && (oldScopesByKey ||= oldScopes.reduce((map, scope, i) => map.set(scope["#LoopKey"] ?? i, scope), /* @__PURE__ */ new Map())).get(key);
1472
1569
  if (branch) hasPotentialMoves = oldScopesByKey.delete(key);
1473
1570
  else branch = createAndSetupBranch(scope["$global"], renderer, scope, parentNode);
@@ -19,7 +19,133 @@ 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 ("id" in normalized) return normalized;
107
+ }
108
+ }
109
+ //#endregion
22
110
  //#region src/common/errors.ts
111
+ const lowercaseEventHandlerReg = /^on[a-z]/;
112
+ function assertValidAttrValue(name, value) {
113
+ 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.`);
114
+ if (typeof value === "function") {
115
+ if (name === "content" || /^on/i.test(name) || /Change$/.test(name)) return;
116
+ throw new Error(`The \`${name}\` attribute cannot be a function.`);
117
+ }
118
+ const unrenderable = describeUnrenderable(value);
119
+ if (unrenderable) throw new Error(`The \`${name}\` attribute cannot be ${unrenderable}.`);
120
+ }
121
+ function assertValidTextValue(value) {
122
+ const unrenderable = describeUnrenderable(value);
123
+ if (unrenderable) throw new Error(`Text content cannot be ${unrenderable}.`);
124
+ }
125
+ function describeUnrenderable(value) {
126
+ if (typeof value === "symbol") return "a symbol";
127
+ if (typeof value === "object" && value !== null) {
128
+ let stringified;
129
+ try {
130
+ stringified = `${value}`;
131
+ } catch {
132
+ stringified = "[object Object]";
133
+ }
134
+ 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}\``;
135
+ }
136
+ }
137
+ function assertValidLoopKey(key, seenKeys) {
138
+ 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}"`}.`);
139
+ if (seenKeys) {
140
+ 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.`);
141
+ seenKeys.add(key);
142
+ }
143
+ }
144
+ function assertValidAttrName(name) {
145
+ if (htmlAttrNameReg.test(name)) throw new Error(`Invalid attribute name: ${JSON.stringify(name)}`);
146
+ const suggestion = getWrongAttrSuggestion(name);
147
+ if (suggestion) throw new Error(`\`${name}\` is not a valid attribute, did you mean \`${suggestion}\`?`);
148
+ }
23
149
  function _el_read_error() {
24
150
  throw new Error("Element references can only be read in scripts and event handlers.");
25
151
  }
@@ -44,6 +170,9 @@ function assertExclusiveAttrs(attrs, onError = throwErr) {
44
170
  if (exclusiveAttrs && exclusiveAttrs.length > 1) onError(`The attributes ${joinWithAnd(exclusiveAttrs)} are mutually exclusive.`);
45
171
  }
46
172
  }
173
+ function assertHandlerIsFunction(name, value) {
174
+ 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}".`);
175
+ }
47
176
  function assertValidTagName(tagName) {
48
177
  if (!/^[a-z][a-z0-9._-]*$/i.test(tagName)) throw new Error(`Invalid tag name: "${tagName}". Tag names must start with a letter and contain only letters, numbers, periods, hyphens, and underscores.`);
49
178
  }
@@ -80,56 +209,6 @@ function forUntil(until, from, step, cb) {
80
209
  for (let steps = (until - start) / delta, i = 0; i < steps; i++) cb(start + i * delta);
81
210
  }
82
211
  //#endregion
83
- //#region src/common/helpers.ts
84
- const htmlAttrNameReg = /^[^a-z_]|[^a-z0-9._:-]/i;
85
- function _call(fn, v) {
86
- fn(v);
87
- return v;
88
- }
89
- function stringifyClassObject(name, value) {
90
- return value ? name : "";
91
- }
92
- function stringifyStyleObject(name, value) {
93
- return value || value === 0 ? name + ":" + value : "";
94
- }
95
- const toDelimitedString = function toDelimitedString(val, delimiter, stringify) {
96
- let str = "";
97
- let sep = "";
98
- let part;
99
- if (val) if (typeof val !== "object") str += val;
100
- else if (Array.isArray(val)) for (const v of val) {
101
- part = toDelimitedString(v, delimiter, stringify);
102
- if (part) {
103
- str += sep + part;
104
- sep = delimiter;
105
- }
106
- }
107
- else for (const name in val) {
108
- part = stringify(name, val[name]);
109
- if (part) {
110
- str += sep + part;
111
- sep = delimiter;
112
- }
113
- }
114
- return str;
115
- };
116
- function isEventHandler(name) {
117
- return /^on[A-Z-]/.test(name);
118
- }
119
- function getEventHandlerName(name) {
120
- return name[2] === "-" ? name.slice(3) : name.slice(2).toLowerCase();
121
- }
122
- function isNotVoid(value) {
123
- return value != null && value !== false;
124
- }
125
- function normalizeDynamicRenderer(value) {
126
- if (value) {
127
- if (typeof value === "string") return value;
128
- const normalized = value.content || value.default || value;
129
- if ("id" in normalized) return normalized;
130
- }
131
- }
132
- //#endregion
133
212
  //#region src/common/meta.ts
134
213
  const DYNAMIC_TAG_SCRIPT_REGISTER_ID = "_dynamicTagScript";
135
214
  //#endregion
@@ -144,6 +223,7 @@ function push(opt, item) {
144
223
  //#region src/dom/event.ts
145
224
  const defaultDelegator = /* @__PURE__ */ createDelegator();
146
225
  function _on(element, type, handler) {
226
+ assertHandlerIsFunction("on" + type[0].toUpperCase() + type.slice(1), handler);
147
227
  if (element["$" + type] === void 0) defaultDelegator(element, type, handleDelegated);
148
228
  element["$" + type] = handler || null;
149
229
  }
@@ -791,6 +871,7 @@ function _attr_input_checked_default(scope, nodeAccessor, checked) {
791
871
  function _attr_input_checked(scope, nodeAccessor, checked, checkedChange) {
792
872
  const el = scope[nodeAccessor];
793
873
  const normalizedChecked = isNotVoid(checked);
874
+ assertHandlerIsFunction("checkedChange", checkedChange);
794
875
  scope["ControlledHandler:" + nodeAccessor] = checkedChange;
795
876
  scope["ControlledType:" + nodeAccessor] = checkedChange ? 0 : 5;
796
877
  if (checkedChange && scope["#Gen"] < runId) el.checked = normalizedChecked;
@@ -819,6 +900,7 @@ function _attr_input_checkedValue(scope, nodeAccessor, checkedValue, checkedValu
819
900
  const el = scope[nodeAccessor];
820
901
  const multiple = Array.isArray(checkedValue);
821
902
  const normalizedCheckedValue = scope["ControlledValue:" + nodeAccessor] = multiple ? checkedValue.map(normalizeStrProp) : normalizeStrProp(checkedValue);
903
+ assertHandlerIsFunction("checkedValueChange", checkedValueChange);
822
904
  scope["ControlledHandler:" + nodeAccessor] = checkedValueChange;
823
905
  scope["ControlledType:" + nodeAccessor] = checkedValueChange ? 1 : 5;
824
906
  if (checkedValueChange && scope["#Gen"] < runId) {
@@ -855,6 +937,7 @@ function _attr_input_value_default(scope, nodeAccessor, value) {
855
937
  function _attr_input_value(scope, nodeAccessor, value, valueChange) {
856
938
  const el = scope[nodeAccessor];
857
939
  const normalizedValue = normalizeAttrValue(value) || "";
940
+ assertHandlerIsFunction("valueChange", valueChange);
858
941
  scope["ControlledHandler:" + nodeAccessor] = valueChange;
859
942
  scope["ControlledValue:" + nodeAccessor] = normalizedValue;
860
943
  scope["ControlledType:" + nodeAccessor] = valueChange ? 2 : 5;
@@ -903,8 +986,10 @@ function _attr_select_value(scope, nodeAccessor, value, valueChange) {
903
986
  const existing = scope["#Gen"] < runId;
904
987
  const multiple = Array.isArray(value);
905
988
  const normalizedValue = scope["ControlledValue:" + nodeAccessor] = multiple ? value.map(normalizeStrProp) : normalizeStrProp(value);
989
+ assertHandlerIsFunction("valueChange", valueChange);
906
990
  scope["ControlledHandler:" + nodeAccessor] = valueChange;
907
991
  scope["ControlledType:" + nodeAccessor] = valueChange ? 3 : 5;
992
+ if (valueChange) pendingEffects.unshift(() => assertSelectValueMatchesOption(el, normalizedValue, value), scope);
908
993
  if (valueChange && existing) pendingEffects.unshift(() => setSelectValue(el, normalizedValue, multiple), scope);
909
994
  else _attr_select_value_default(scope, nodeAccessor, normalizedValue);
910
995
  }
@@ -947,11 +1032,19 @@ function setSelectValue(el, value, multiple) {
947
1032
  function getSelectValue(el, multiple) {
948
1033
  return multiple ? Array.from(el.selectedOptions, (opt) => opt.value) : el.value;
949
1034
  }
1035
+ function assertSelectValueMatchesOption(el, normalizedValue, value) {
1036
+ const multiple = Array.isArray(normalizedValue);
1037
+ if (multiple ? normalizedValue.some(Boolean) : normalizedValue) {
1038
+ for (const opt of el.options) if (multiple ? normalizedValue.includes(opt.value) : opt.value === normalizedValue) return;
1039
+ console.error("A controlled `<select>`'s `value` has no matching `<option>`:", value);
1040
+ }
1041
+ }
950
1042
  function _attr_details_or_dialog_open_default(scope, nodeAccessor, open) {
951
1043
  if (scope["#Gen"] === runId) scope[nodeAccessor].open = isNotVoid(open);
952
1044
  }
953
1045
  function _attr_details_or_dialog_open(scope, nodeAccessor, open, openChange) {
954
1046
  const normalizedOpen = scope["ControlledValue:" + nodeAccessor] = isNotVoid(open);
1047
+ assertHandlerIsFunction("openChange", openChange);
955
1048
  scope["ControlledHandler:" + nodeAccessor] = openChange;
956
1049
  scope["ControlledType:" + nodeAccessor] = openChange ? 4 : 5;
957
1050
  if (openChange && scope["#Gen"] < runId) scope[nodeAccessor].open = normalizedOpen;
@@ -1010,9 +1103,11 @@ function updateList(arr, val, push) {
1010
1103
  //#endregion
1011
1104
  //#region src/dom/dom.ts
1012
1105
  function _to_text(value) {
1106
+ assertValidTextValue(value);
1013
1107
  return value || value === 0 ? value + "" : "";
1014
1108
  }
1015
1109
  function _attr(element, name, value) {
1110
+ assertValidAttrValue(name, value);
1016
1111
  setAttribute(element, name, normalizeAttrValue(value));
1017
1112
  }
1018
1113
  function setAttribute(element, name, value) {
@@ -1087,8 +1182,11 @@ function _attrs_partial_content(scope, nodeAccessor, nextAttrs, skip) {
1087
1182
  }
1088
1183
  function attrsInternal(scope, nodeAccessor, nextAttrs) {
1089
1184
  const el = scope[nodeAccessor];
1090
- let events;
1185
+ let events = scope["EventAttributes:" + nodeAccessor];
1091
1186
  let skip;
1187
+ for (const name in events) events[name] = 0;
1188
+ scope["ControlledType:" + nodeAccessor] = 5;
1189
+ scope["ControlledHandler:" + nodeAccessor] = 0;
1092
1190
  switch (el.tagName) {
1093
1191
  case "INPUT":
1094
1192
  if ("checked" in nextAttrs || "checkedChange" in nextAttrs) _attr_input_checked(scope, nodeAccessor, nextAttrs.checked, nextAttrs.checkedChange);
@@ -1127,7 +1225,7 @@ function attrsInternal(scope, nodeAccessor, nextAttrs) {
1127
1225
  _attr_style(el, value);
1128
1226
  break;
1129
1227
  default:
1130
- if (htmlAttrNameReg.test(name)) throw new Error(`Invalid attribute name: ${JSON.stringify(name)}`);
1228
+ assertValidAttrName(name);
1131
1229
  if (isEventHandler(name)) (events ||= scope["EventAttributes:" + nodeAccessor] = {})[getEventHandlerName(name)] = value;
1132
1230
  else if (!(skip?.test(name) || name === "content" && el.tagName !== "META")) _attr(el, name, value);
1133
1231
  break;
@@ -1372,7 +1470,7 @@ function _if(nodeAccessor, ...branchesArgs) {
1372
1470
  while (i < branchesArgs.length) branches.push(_content("", branchesArgs[i++], branchesArgs[i++], branchesArgs[i++])());
1373
1471
  enableBranches();
1374
1472
  return (scope, newBranch) => {
1375
- if (newBranch !== scope[branchAccessor]) setConditionalRenderer(scope, nodeAccessor, branches[scope[branchAccessor] = newBranch], createAndSetupBranch);
1473
+ if (newBranch !== (scope[branchAccessor] ?? (scope["BranchScopes:" + nodeAccessor] && 0))) setConditionalRenderer(scope, nodeAccessor, branches[scope[branchAccessor] = newBranch], createAndSetupBranch);
1376
1474
  };
1377
1475
  }
1378
1476
  function patchDynamicTag(fn) {
@@ -1464,8 +1562,7 @@ function loop(forEach) {
1464
1562
  let hasPotentialMoves;
1465
1563
  var seenKeys = /* @__PURE__ */ new Set();
1466
1564
  forEach(value, (key, args) => {
1467
- 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);
1468
- else seenKeys.add(key);
1565
+ assertValidLoopKey(key, seenKeys);
1469
1566
  let branch = oldLen && (oldScopesByKey ||= oldScopes.reduce((map, scope, i) => map.set(scope["#LoopKey"] ?? i, scope), /* @__PURE__ */ new Map())).get(key);
1470
1567
  if (branch) hasPotentialMoves = oldScopesByKey.delete(key);
1471
1568
  else branch = createAndSetupBranch(scope["$global"], renderer, scope, parentNode);