ilha 0.12.1 → 0.13.0

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, serializeServerManifest, setJsxRuntimeBridge, setServerActionBinder } from "./internal.js";
1
+ import { ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, __ilhaJsxEvent, serializeServerManifest, setJsxRuntimeBridge, setServerActionBinder, setTemplateRenderer } from "./internal.js";
2
2
  import { effect as effect$1, endBatch, setActiveSub, signal, startBatch } from "alien-signals";
3
3
 
4
4
  //#region src/index.ts
@@ -511,15 +511,6 @@ function serializeScriptJson(value) {
511
511
  function escapeStyleText(text) {
512
512
  return text.replace(/</g, "\\3C ");
513
513
  }
514
- function dedentString(str) {
515
- if (str.length === 0 || str[0] !== "\n") return str;
516
- const lines = str.split("\n");
517
- while (lines.length && lines[0].trim() === "") lines.shift();
518
- while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
519
- if (!lines.length) return "";
520
- const indent = Math.min(...lines.filter((l) => l.trim() !== "").map((l) => l.match(/^(\s*)/)[1].length));
521
- return lines.map((l) => l.slice(indent)).join("\n");
522
- }
523
514
  const RAW = Symbol.for("ilha.raw");
524
515
  const SIGNAL_ACCESSOR = Symbol.for("ilha.signalAccessor");
525
516
  const ISLAND = Symbol.for("ilha.island");
@@ -773,18 +764,18 @@ async function resolveAsyncChildren(html, pending) {
773
764
  }
774
765
  return html;
775
766
  }
776
- function resolveSignalSetter(read, value) {
777
- if (typeof value !== "function") return value;
778
- const prevSub = setActiveSub(void 0);
779
- try {
780
- return value(read());
781
- } finally {
782
- setActiveSub(prevSub);
783
- }
784
- }
785
- function markSignalAccessor(fn) {
786
- fn[SIGNAL_ACCESSOR] = true;
787
- const accessor = fn;
767
+ function markSignalAccessor(read, write) {
768
+ read[SIGNAL_ACCESSOR] = true;
769
+ const accessor = read;
770
+ accessor.set = (value) => write(value);
771
+ accessor.update = (fn) => {
772
+ const prevSub = setActiveSub(void 0);
773
+ try {
774
+ write(fn(read()));
775
+ } finally {
776
+ setActiveSub(prevSub);
777
+ }
778
+ };
788
779
  accessor.select = ((...args) => createSelectAccessor(accessor, args));
789
780
  return accessor;
790
781
  }
@@ -870,17 +861,14 @@ function createSelectAccessor(root, input) {
870
861
  if (!Object.is(selected, resolved)) throw new Error("select(): selector must only traverse nested properties or array indexes — derived or transformed values are not supported.");
871
862
  } else path = input;
872
863
  const read = (from) => selector ? path.length === 0 ? selector(from) : getAtPath(from, path) : getAtPath(from, path);
873
- return markSignalAccessor((...args) => {
874
- if (args.length === 0) return read(root());
864
+ return markSignalAccessor(() => read(root()), (value) => {
875
865
  const previousRoot = root();
876
- const previousSelected = read(previousRoot);
877
- const value = resolveSignalSetter(() => previousSelected, args[0]);
878
866
  const next = path.length === 0 && selector ? value : setAtPath({
879
867
  object: previousRoot,
880
868
  path,
881
869
  value
882
870
  });
883
- if (!Object.is(previousRoot, next)) root(() => next);
871
+ if (!Object.is(previousRoot, next)) root.set(next);
884
872
  });
885
873
  }
886
874
  function ilhaRaw(value) {
@@ -950,70 +938,18 @@ const BIND_VALID_KINDS = /* @__PURE__ */ new Set([
950
938
  "group",
951
939
  "this"
952
940
  ]);
953
- const BIND_PREFIX_RE = /\bbind:([a-zA-Z]+)\s*=\s*("|')?$/;
954
- const EVENT_PREFIX_RE = /(?:^|\s)on([a-zA-Z][a-zA-Z0-9-]*)(?::([a-zA-Z][a-zA-Z0-9-]*))?\s*=\s*("|')?$/;
955
941
  const NATIVE_EVENT_MODIFIERS = /* @__PURE__ */ new Set([
956
942
  "abortable",
957
943
  "once",
958
944
  "capture",
959
945
  "passive"
960
946
  ]);
961
- const FULL_VALUE_ATTR_RE = /(?:^|\s)([a-zA-Z_:][a-zA-Z0-9:._-]*)\s*=\s*("|')?$/;
962
- const UNQUOTED_HAZARD_RE = /[\s=]|`/;
963
947
  function resolveTemplateValue(v) {
964
948
  if (isSignalAccessor(v)) return v();
965
949
  if (typeof v === "function") return v();
966
950
  return v;
967
951
  }
968
- function openTagName(source, index) {
969
- const open = source.lastIndexOf("<", index);
970
- if (open === -1) return "element";
971
- return (source.slice(open).match(/^<\s*([a-zA-Z][a-zA-Z0-9:._-]*)/)?.[1] ?? "element").toLowerCase();
972
- }
973
- function isEventAttributePosition(source, index) {
974
- let open = -1;
975
- let quote = null;
976
- for (let i = 0; i < index; i++) {
977
- const char = source[i];
978
- if (open === -1) {
979
- if (char === "<") open = i;
980
- continue;
981
- }
982
- if (quote) {
983
- if (char === quote) quote = null;
984
- } else if (char === "\"" || char === "'") quote = char;
985
- else if (char === ">") open = -1;
986
- }
987
- if (open === -1 || quote !== null) return false;
988
- return !/^\s*[!/?]/.test(source.slice(open + 1, index));
989
- }
990
- function findTagCloseIndex(chunk, initialQuote) {
991
- let quote = initialQuote;
992
- for (let i = 0; i < chunk.length; i++) {
993
- const ch = chunk[i];
994
- if (quote !== null) {
995
- if (ch === quote) quote = null;
996
- } else if (ch === "\"" || ch === "'") quote = ch;
997
- else if (ch === ">") return {
998
- index: i,
999
- quote
1000
- };
1001
- }
1002
- return {
1003
- index: -1,
1004
- quote
1005
- };
1006
- }
1007
- function extractElementStaticValue(prefix) {
1008
- const lastOpen = prefix.lastIndexOf("<");
1009
- if (lastOpen === -1) return null;
1010
- const tagPrefix = prefix.slice(lastOpen);
1011
- if (tagPrefix.includes(">")) return null;
1012
- const m = tagPrefix.match(/\bvalue\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/);
1013
- if (!m) return null;
1014
- return m[2] ?? m[3] ?? m[4] ?? null;
1015
- }
1016
- function emitBindSSR({ kind, index, accessor, prefixForStaticPeek }) {
952
+ function emitBindSSR({ kind, index, accessor, optionValue }) {
1017
953
  const spec = `${kind}:${index}`;
1018
954
  let v;
1019
955
  try {
@@ -1027,183 +963,494 @@ function emitBindSSR({ kind, index, accessor, prefixForStaticPeek }) {
1027
963
  case "open": return [v ? ` open` : ``, spec];
1028
964
  case "files": return [``, spec];
1029
965
  case "this": return [``, spec];
1030
- case "group": {
1031
- const optionValue = extractElementStaticValue(prefixForStaticPeek);
966
+ case "group":
1032
967
  if (optionValue == null) return [``, spec];
1033
968
  return [(Array.isArray(v) ? v.map(String).includes(optionValue) : v != null && String(v) === optionValue) ? ` checked` : ``, spec];
1034
- }
1035
969
  }
1036
970
  }
1037
- function ilhaHtml(strings, ...values) {
1038
- let result = "";
1039
- let stripLeadingQuote = null;
1040
- let pendingBindSpecs = "";
1041
- let pendingEventSpecs = "";
1042
- let pendingQuote = null;
1043
- const ctx = currentRenderCtx();
1044
- for (let i = 0; i < strings.length; i++) {
1045
- let chunk = strings[i];
1046
- if (stripLeadingQuote !== null) {
1047
- if (chunk.startsWith(stripLeadingQuote)) chunk = chunk.slice(1);
1048
- stripLeadingQuote = null;
1049
- }
1050
- if (pendingBindSpecs !== "" || pendingEventSpecs !== "") {
1051
- const { index: gtIdx, quote } = findTagCloseIndex(chunk, pendingQuote);
1052
- if (gtIdx === -1) pendingQuote = quote;
1053
- else {
1054
- const head = chunk.slice(0, gtIdx).replace(/\s*\/\s*$/, "");
1055
- const bindAttr = pendingBindSpecs ? ` data-ilha-bind="${pendingBindSpecs}"` : "";
1056
- const eventAttr = pendingEventSpecs ? ` data-ilha-on="${pendingEventSpecs}"` : "";
1057
- chunk = head + bindAttr + eventAttr + ">" + chunk.slice(gtIdx + 1);
1058
- pendingBindSpecs = "";
1059
- pendingEventSpecs = "";
1060
- pendingQuote = null;
1061
- }
971
+ const TEMPLATE_ATTRIBUTE = Symbol.for("ilha.templateAttribute");
972
+ const TEMPLATE_PARTS = Symbol.for("ilha.templateParts");
973
+ const TEMPLATE_SLOT_START = "ilha:";
974
+ const TEMPLATE_SLOT_END = ":";
975
+ const TEMPLATE_SLOT_RE = /\uE000ilha:(\d+):\uE001/g;
976
+ const templateBlueprintCache = /* @__PURE__ */ new WeakMap();
977
+ const TEMPLATE_PARSE_VOID_TAGS = /* @__PURE__ */ new Set([
978
+ "area",
979
+ "base",
980
+ "br",
981
+ "col",
982
+ "embed",
983
+ "hr",
984
+ "img",
985
+ "input",
986
+ "link",
987
+ "meta",
988
+ "param",
989
+ "source",
990
+ "track",
991
+ "wbr"
992
+ ]);
993
+ const TEMPLATE_PARSE_RAW_TEXT_TAGS = /* @__PURE__ */ new Set([
994
+ "script",
995
+ "style",
996
+ "textarea",
997
+ "title"
998
+ ]);
999
+ function templateSkipWs(s, i) {
1000
+ while (i < s.length && /[\t\n\f\r ]/.test(s[i])) i++;
1001
+ return i;
1002
+ }
1003
+ function templateAsciiLower(source) {
1004
+ return source.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32));
1005
+ }
1006
+ function templateClosingTagAt(lower, index, tag) {
1007
+ const prefix = `</${tag}`;
1008
+ if (!lower.startsWith(prefix, index)) return false;
1009
+ const boundary = lower[index + prefix.length];
1010
+ return boundary === ">" || boundary === "/" || /[\t\n\f\r ]/.test(boundary ?? "");
1011
+ }
1012
+ function templateFindClosingTag(lower, tag, from) {
1013
+ const needle = `</${tag}`;
1014
+ let index = lower.indexOf(needle, from);
1015
+ while (index !== -1) {
1016
+ if (templateClosingTagAt(lower, index, tag)) return index;
1017
+ index = lower.indexOf(needle, index + 1);
1018
+ }
1019
+ return -1;
1020
+ }
1021
+ function templateParseFragment(source) {
1022
+ const children = [];
1023
+ templateParseNodes(source, templateAsciiLower(source), 0, null, children);
1024
+ return {
1025
+ nodeName: "#document-fragment",
1026
+ childNodes: children
1027
+ };
1028
+ }
1029
+ function templateParseNodes(s, lower, i, stop, out) {
1030
+ while (i < s.length) {
1031
+ if (stop && templateClosingTagAt(lower, i, stop)) {
1032
+ const gt = s.indexOf(">", i);
1033
+ return gt === -1 ? s.length : gt + 1;
1062
1034
  }
1063
- if (i >= values.length) {
1064
- result += chunk;
1035
+ if (s.startsWith("<!--", i)) {
1036
+ const end = s.indexOf("-->", i + 4);
1037
+ const close = end === -1 ? s.length : end + 3;
1038
+ out.push({
1039
+ nodeName: "#comment",
1040
+ data: s.slice(i + 4, end === -1 ? s.length : end)
1041
+ });
1042
+ i = close;
1065
1043
  continue;
1066
1044
  }
1067
- const value = values[i];
1068
- const eventMatch = chunk.match(EVENT_PREFIX_RE);
1069
- const eventMatchStart = eventMatch ? chunk.length - eventMatch[0].length : -1;
1070
- if (!eventMatch && typeof value === "function") {
1071
- const ambiguous = chunk.match(/(?:^|\s)on([a-zA-Z][a-zA-Z0-9-]*)(?::[a-zA-Z][a-zA-Z0-9-]*)?\s*=\s*(?:(?:"[^"]*)|(?:'[^']*)|(?:[^\s"'=<>`]+))$/);
1072
- const start = ambiguous ? chunk.length - ambiguous[0].length : -1;
1073
- if (ambiguous && isEventAttributePosition(result + chunk, result.length + start)) throw new Error(`[ilha] on${ambiguous[1]} handler must occupy the entire attribute value. Write on${ambiguous[1]}=\${handler}.`);
1045
+ if (stop === null && s[i] === "<" && s[i + 1] === "/") {
1046
+ const gt = s.indexOf(">", i);
1047
+ i = gt === -1 ? s.length : gt + 1;
1048
+ continue;
1074
1049
  }
1075
- const eventAttributeFragment = eventMatch !== null && chunk.slice(0, eventMatchStart).trim() === "";
1076
- if (eventMatch && (eventAttributeFragment || isEventAttributePosition(result + chunk, result.length + eventMatchStart))) {
1077
- const eventName = eventMatch[1].toLowerCase();
1078
- const rawModifier = eventMatch[2]?.toLowerCase();
1079
- const modifier = rawModifier !== void 0 && NATIVE_EVENT_MODIFIERS.has(rawModifier) ? rawModifier : void 0;
1080
- const openQuote = eventMatch[3] ?? null;
1081
- const nextChunk = strings[i + 1] ?? "";
1082
- if (!(openQuote ? nextChunk.startsWith(openQuote) : /^(?:\s|\/?>|$)/.test(nextChunk))) throw new Error(`[ilha] on${eventName} handler must occupy the entire attribute value. Write on${eventName}=\${handler}.`);
1083
- const cleanPrefix = chunk.slice(0, eventMatchStart).replace(/\s+$/, "");
1084
- result += cleanPrefix;
1085
- typeof value === "function" && SSR_ACTION_STUB in value;
1086
- if (ctx && typeof value === "function") {
1087
- const index = ctx.events.length;
1088
- if (rawModifier && !modifier) {} else {
1089
- ctx.events.push({
1090
- type: eventName,
1091
- handler: value,
1092
- modifier
1093
- });
1094
- pendingEventSpecs += (pendingEventSpecs ? "," : "") + `${eventName}:${index}`;
1050
+ if (s[i] === "<" && /[A-Za-z]/.test(s[i + 1] ?? "")) {
1051
+ const start = i;
1052
+ i++;
1053
+ const tagStart = i;
1054
+ while (i < s.length && /[A-Za-z0-9:-]/.test(s[i])) i++;
1055
+ const tagOrig = s.slice(tagStart, i);
1056
+ const tag = tagOrig.toLowerCase();
1057
+ const attrs = [];
1058
+ const attrLocs = {};
1059
+ i = templateSkipWs(s, i);
1060
+ while (i < s.length && s[i] !== ">" && s[i] !== "/") {
1061
+ const attrStart = i;
1062
+ while (i < s.length && /[^\s=>/]/.test(s[i])) i++;
1063
+ const rawName = s.slice(attrStart, i);
1064
+ if (!rawName) break;
1065
+ const name = rawName;
1066
+ i = templateSkipWs(s, i);
1067
+ let value = "";
1068
+ if (s[i] === "=") {
1069
+ i++;
1070
+ i = templateSkipWs(s, i);
1071
+ if (s[i] === "\"" || s[i] === "'") {
1072
+ const q = s[i];
1073
+ i++;
1074
+ const vs = i;
1075
+ while (i < s.length && s[i] !== q) i++;
1076
+ value = s.slice(vs, i);
1077
+ if (s[i] === q) i++;
1078
+ } else {
1079
+ const vs = i;
1080
+ while (i < s.length && /[^\s>]/.test(s[i])) {
1081
+ if (s[i] === "/") {
1082
+ let j = i + 1;
1083
+ while (j < s.length && /\s/.test(s[j])) j++;
1084
+ if (j >= s.length || s[j] === ">") break;
1085
+ }
1086
+ i++;
1087
+ }
1088
+ value = s.slice(vs, i);
1089
+ }
1090
+ }
1091
+ attrs.push({
1092
+ name,
1093
+ value
1094
+ });
1095
+ attrLocs[name] = {
1096
+ startOffset: attrStart,
1097
+ endOffset: i
1098
+ };
1099
+ i = templateSkipWs(s, i);
1100
+ }
1101
+ if (s[i] === "/") {
1102
+ i++;
1103
+ i = templateSkipWs(s, i);
1104
+ }
1105
+ if (s[i] === ">") i++;
1106
+ const el = {
1107
+ nodeName: tagOrig,
1108
+ tagName: tagOrig,
1109
+ attrs,
1110
+ childNodes: [],
1111
+ sourceCodeLocation: {
1112
+ attrs: attrLocs,
1113
+ startTag: {
1114
+ startOffset: start,
1115
+ endOffset: i
1116
+ }
1095
1117
  }
1118
+ };
1119
+ if (!TEMPLATE_PARSE_VOID_TAGS.has(tag)) {
1120
+ if (TEMPLATE_PARSE_RAW_TEXT_TAGS.has(tag)) {
1121
+ const ci = templateFindClosingTag(lower, tag, i);
1122
+ const textEnd = ci === -1 ? s.length : ci;
1123
+ if (textEnd > i) el.childNodes.push({
1124
+ nodeName: "#text",
1125
+ value: s.slice(i, textEnd)
1126
+ });
1127
+ i = ci === -1 ? s.length : s.indexOf(">", ci) === -1 ? s.length : s.indexOf(">", ci) + 1;
1128
+ } else i = templateParseNodes(s, lower, i, tag, el.childNodes);
1096
1129
  }
1097
- if (openQuote) stripLeadingQuote = openQuote;
1130
+ out.push(el);
1098
1131
  continue;
1099
1132
  }
1100
- const m = chunk.match(BIND_PREFIX_RE);
1101
- if (m && isSignalAccessor(value)) {
1102
- const name = m[1];
1103
- const openQuote = m[2] ?? null;
1104
- if (!BIND_VALID_KINDS.has(name)) {
1105
- result += chunk + interpolateValue(value);
1106
- continue;
1133
+ const next = s.indexOf("<", i);
1134
+ const end = next === -1 ? s.length : next;
1135
+ if (end > i) {
1136
+ out.push({
1137
+ nodeName: "#text",
1138
+ value: s.slice(i, end)
1139
+ });
1140
+ i = end;
1141
+ continue;
1142
+ }
1143
+ out.push({
1144
+ nodeName: "#text",
1145
+ value: s[i]
1146
+ });
1147
+ i++;
1148
+ }
1149
+ return i;
1150
+ }
1151
+ function templateSegments(value) {
1152
+ const out = [];
1153
+ let offset = 0;
1154
+ for (const match of value.matchAll(TEMPLATE_SLOT_RE)) {
1155
+ if (match.index > offset) out.push(value.slice(offset, match.index));
1156
+ out.push({ slot: Number(match[1]) });
1157
+ offset = match.index + match[0].length;
1158
+ }
1159
+ if (offset < value.length) out.push(value.slice(offset));
1160
+ return out;
1161
+ }
1162
+ function templateBlueprint(node, source) {
1163
+ if (node.nodeName === "#text") return {
1164
+ kind: "text",
1165
+ value: templateSegments(node.value)
1166
+ };
1167
+ if (node.nodeName === "#comment") return {
1168
+ kind: "comment",
1169
+ value: templateSegments(node.data)
1170
+ };
1171
+ if (node.nodeName === "#document-fragment") return {
1172
+ kind: "fragment",
1173
+ children: node.childNodes.map((child) => templateBlueprint(child, source)).filter(Boolean)
1174
+ };
1175
+ if (typeof node.tagName === "string") {
1176
+ const props = {};
1177
+ for (const attr of node.attrs ?? []) {
1178
+ const location = node.sourceCodeLocation?.attrs?.[attr.name];
1179
+ const sourceAttr = location ? source.slice(location.startOffset, location.endOffset) : attr.name;
1180
+ const parts = templateSegments(attr.value);
1181
+ parts.quoted = /=\s*["']/.test(sourceAttr);
1182
+ parts.bare = !sourceAttr.includes("=");
1183
+ props[attr.name] = parts;
1184
+ }
1185
+ return {
1186
+ kind: "element",
1187
+ tag: node.tagName,
1188
+ props,
1189
+ children: (node.childNodes ?? []).map((child) => templateBlueprint(child, source)).filter(Boolean),
1190
+ selfClosing: node.sourceCodeLocation?.startTag ? /\/\s*>$/.test(source.slice(node.sourceCodeLocation.startTag.startOffset, node.sourceCodeLocation.startTag.endOffset)) : false
1191
+ };
1192
+ }
1193
+ return null;
1194
+ }
1195
+ function templateCompile(strings) {
1196
+ let source = strings[0] ?? "";
1197
+ for (let i = 0; i < strings.length - 1; i++) source += `${TEMPLATE_SLOT_START}${i}${TEMPLATE_SLOT_END}${strings[i + 1] ?? ""}`;
1198
+ return templateBlueprint(templateParseFragment(source), source) ?? {
1199
+ kind: "fragment",
1200
+ children: []
1201
+ };
1202
+ }
1203
+ function templateBindSegments(parts, values) {
1204
+ let value;
1205
+ if (parts.length === 1 && typeof parts[0] !== "string") value = values[parts[0].slot];
1206
+ else if (parts.some((part) => typeof part !== "string")) value = {
1207
+ [TEMPLATE_PARTS]: true,
1208
+ values: parts.map((part) => typeof part === "string" ? part : values[part.slot])
1209
+ };
1210
+ else value = parts.join("");
1211
+ return {
1212
+ [TEMPLATE_ATTRIBUTE]: true,
1213
+ value,
1214
+ quoted: parts.quoted ?? true,
1215
+ bare: parts.bare ?? false
1216
+ };
1217
+ }
1218
+ function templateBind(node, values) {
1219
+ if (node.kind === "text") {
1220
+ const out = [];
1221
+ for (const part of node.value) if (typeof part === "string") {
1222
+ if (part) out.push({
1223
+ kind: "text",
1224
+ value: part
1225
+ });
1226
+ } else out.push({
1227
+ kind: "dynamic",
1228
+ value: values[part.slot]
1229
+ });
1230
+ return out;
1231
+ }
1232
+ if (node.kind === "comment") {
1233
+ const parts = node.value;
1234
+ if (parts.length === 1 && typeof parts[0] === "string") return {
1235
+ kind: "comment",
1236
+ value: parts[0]
1237
+ };
1238
+ if (parts.every((part) => typeof part === "string")) return {
1239
+ kind: "comment",
1240
+ value: parts.join("")
1241
+ };
1242
+ return {
1243
+ kind: "comment",
1244
+ value: {
1245
+ [TEMPLATE_PARTS]: true,
1246
+ values: parts.map((part) => typeof part === "string" ? part : values[part.slot])
1107
1247
  }
1248
+ };
1249
+ }
1250
+ if (node.kind === "element") {
1251
+ const props = {};
1252
+ for (const [name, parts] of Object.entries(node.props)) props[name] = templateBindSegments(parts, values);
1253
+ return {
1254
+ kind: "element",
1255
+ tag: node.tag,
1256
+ props,
1257
+ children: node.children.flatMap((child) => templateBind(child, values)),
1258
+ selfClosing: node.selfClosing
1259
+ };
1260
+ }
1261
+ return {
1262
+ kind: "fragment",
1263
+ children: node.children.flatMap((child) => templateBind(child, values))
1264
+ };
1265
+ }
1266
+ function htmlTemplate(strings, values) {
1267
+ let compiled = templateBlueprintCache.get(strings);
1268
+ if (!compiled) {
1269
+ compiled = templateCompile(strings);
1270
+ templateBlueprintCache.set(strings, compiled);
1271
+ }
1272
+ const result = templateBind(compiled, values);
1273
+ return Array.isArray(result) ? {
1274
+ kind: "fragment",
1275
+ children: result
1276
+ } : result;
1277
+ }
1278
+ function templateAttribute(value) {
1279
+ return value && typeof value === "object" && TEMPLATE_ATTRIBUTE in value ? value : null;
1280
+ }
1281
+ function templateParts(value) {
1282
+ return value && typeof value === "object" && TEMPLATE_PARTS in value ? value.values : null;
1283
+ }
1284
+ const TEMPLATE_ATTRIBUTE_ALIASES = {
1285
+ className: "class",
1286
+ htmlFor: "for",
1287
+ acceptCharset: "accept-charset",
1288
+ httpEquiv: "http-equiv",
1289
+ accentHeight: "accent-height",
1290
+ alignmentBaseline: "alignment-baseline",
1291
+ baselineShift: "baseline-shift",
1292
+ clipPath: "clip-path",
1293
+ clipRule: "clip-rule",
1294
+ dominantBaseline: "dominant-baseline",
1295
+ fillOpacity: "fill-opacity",
1296
+ fillRule: "fill-rule",
1297
+ floodColor: "flood-color",
1298
+ floodOpacity: "flood-opacity",
1299
+ fontFamily: "font-family",
1300
+ fontSize: "font-size",
1301
+ fontStyle: "font-style",
1302
+ fontWeight: "font-weight",
1303
+ markerEnd: "marker-end",
1304
+ markerMid: "marker-mid",
1305
+ markerStart: "marker-start",
1306
+ stopColor: "stop-color",
1307
+ stopOpacity: "stop-opacity",
1308
+ strokeDasharray: "stroke-dasharray",
1309
+ strokeDashoffset: "stroke-dashoffset",
1310
+ strokeLinecap: "stroke-linecap",
1311
+ strokeLinejoin: "stroke-linejoin",
1312
+ strokeMiterlimit: "stroke-miterlimit",
1313
+ strokeOpacity: "stroke-opacity",
1314
+ strokeWidth: "stroke-width",
1315
+ textAnchor: "text-anchor",
1316
+ vectorEffect: "vector-effect",
1317
+ xlinkHref: "xlink:href"
1318
+ };
1319
+ const TEMPLATE_VOID_ELEMENTS = /* @__PURE__ */ new Set([
1320
+ "area",
1321
+ "base",
1322
+ "br",
1323
+ "col",
1324
+ "embed",
1325
+ "hr",
1326
+ "img",
1327
+ "input",
1328
+ "link",
1329
+ "meta",
1330
+ "param",
1331
+ "source",
1332
+ "track",
1333
+ "wbr"
1334
+ ]);
1335
+ const TEMPLATE_STRING_BOOLEANS = /* @__PURE__ */ new Set([
1336
+ "contenteditable",
1337
+ "draggable",
1338
+ "spellcheck"
1339
+ ]);
1340
+ const TEMPLATE_SAFE_NAME = /^[A-Za-z_:][A-Za-z0-9:._-]*$/;
1341
+ const TEMPLATE_EVENT = /^on([a-z][a-z0-9-]*)(?::([a-z][a-z0-9-]*))?$/;
1342
+ function normalizeTemplateClass(value) {
1343
+ if (Array.isArray(value)) return value.filter(Boolean).join(" ");
1344
+ if (isPlainObject(value)) return Object.entries(value).filter(([, enabled]) => Boolean(enabled)).map(([name]) => name).join(" ");
1345
+ return String(value);
1346
+ }
1347
+ function resolvedAttrText(rawSourceValue) {
1348
+ const attribute = templateAttribute(rawSourceValue);
1349
+ const sourceValue = attribute ? attribute.value : rawSourceValue;
1350
+ if (sourceValue == null) return null;
1351
+ const parts = templateParts(sourceValue);
1352
+ const value = parts ? parts.map((part) => resolveTemplateValue(part) ?? "").join("") : resolveTemplateValue(sourceValue);
1353
+ if (value == null || value === false) return null;
1354
+ return String(value);
1355
+ }
1356
+ function renderTemplateElement(node) {
1357
+ const ctx = currentRenderCtx();
1358
+ let attrs = "";
1359
+ const eventSpecs = [];
1360
+ const bindSpecs = [];
1361
+ for (const [sourceName, rawSourceValue] of Object.entries(node.props)) {
1362
+ const attribute = templateAttribute(rawSourceValue);
1363
+ const sourceValue = attribute ? attribute.value : rawSourceValue;
1364
+ if (sourceName === "children" || sourceName === "key" || sourceValue == null) continue;
1365
+ if (sourceName === "__proto__" || sourceName === "constructor" || sourceName === "prototype") continue;
1366
+ const name = TEMPLATE_ATTRIBUTE_ALIASES[sourceName] ?? (/^on[A-Z]/.test(sourceName) ? sourceName.toLowerCase() : sourceName);
1367
+ if (!TEMPLATE_SAFE_NAME.test(name)) continue;
1368
+ if (name.startsWith("bind:")) {
1369
+ const kind = name.slice(5);
1370
+ if (!BIND_VALID_KINDS.has(kind) || !isSignalAccessor(sourceValue)) continue;
1108
1371
  if (!ctx) {
1109
- const stripped = chunk.slice(0, chunk.length - m[0].length);
1110
- const quote = openQuote ?? "\"";
1111
- result += stripped + name + "=" + quote + interpolateValue(value) + quote;
1112
- if (openQuote) stripLeadingQuote = openQuote;
1372
+ const value = sourceValue();
1373
+ if (kind === "value") attrs += ` value="${escapeHtml(value ?? "")}"`;
1374
+ else if ((kind === "checked" || kind === "open") && value) attrs += ` ${kind}`;
1113
1375
  continue;
1114
1376
  }
1115
- const matchStart = chunk.length - m[0].length;
1116
- const cleanPrefix = chunk.slice(0, matchStart).replace(/\s+$/, "");
1117
1377
  const index = ctx.binds.length;
1118
1378
  ctx.binds.push({
1119
- kind: name,
1120
- accessor: value
1379
+ kind,
1380
+ accessor: sourceValue
1121
1381
  });
1122
- const [valueAttrs, spec] = emitBindSSR({
1123
- kind: name,
1382
+ const [reflected, spec] = emitBindSSR({
1383
+ kind,
1124
1384
  index,
1125
- accessor: value,
1126
- prefixForStaticPeek: cleanPrefix
1385
+ accessor: sourceValue,
1386
+ optionValue: resolvedAttrText(node.props.value)
1127
1387
  });
1128
- result += cleanPrefix + valueAttrs;
1129
- pendingBindSpecs += (pendingBindSpecs ? "," : "") + spec;
1130
- if (openQuote) stripLeadingQuote = openQuote;
1388
+ attrs += reflected;
1389
+ bindSpecs.push(spec);
1131
1390
  continue;
1132
1391
  }
1133
- const attrMatch = chunk.match(FULL_VALUE_ATTR_RE);
1134
- if (attrMatch && isEventAttributePosition(result + chunk, result.length + attrMatch.index)) {
1135
- const attrName = attrMatch[1];
1136
- const openQuote = attrMatch[2] ?? null;
1137
- if (openQuote) stripLeadingQuote = openQuote;
1138
- const cleanPrefix = chunk.slice(0, attrMatch.index).replace(/\s+$/, "");
1139
- if (attrName.length > 2 && /^on/i.test(attrName)) {
1140
- result += cleanPrefix;
1141
- continue;
1142
- }
1143
- if (attrName.toLowerCase() === "srcdoc") {
1144
- result += cleanPrefix;
1145
- continue;
1146
- }
1147
- if (isIsland(value) || isIslandCall(value)) {
1148
- stripLeadingQuote = null;
1149
- result += chunk + interpolateValue(value);
1150
- continue;
1151
- }
1152
- if (isUrlAttributeName(attrName)) {
1153
- if (isRawHtml(value)) {
1154
- result += cleanPrefix + ` ${attrName}="${value.value}"`;
1155
- continue;
1156
- }
1157
- const resolved = resolveTemplateValue(value);
1158
- if (resolved == null || resolved === false) {
1159
- result += cleanPrefix;
1160
- continue;
1161
- }
1162
- const url = String(resolved);
1163
- if (!isSafeUrlAttrValue(openTagName(result + chunk, result.length + attrMatch.index), attrName, url)) {
1164
- result += cleanPrefix;
1165
- continue;
1166
- }
1167
- const quotedUrl = openQuote !== null || UNQUOTED_HAZARD_RE.test(url);
1168
- result += cleanPrefix + ` ${attrName}=` + (quotedUrl ? `"${escapeHtml(url)}"` : escapeHtml(url));
1169
- continue;
1170
- }
1171
- if (attrName.toLowerCase() === "style") {
1172
- if (!isRawHtml(value)) {
1173
- const resolved = resolveTemplateValue(value);
1174
- if (isPlainObject(resolved)) {
1175
- result += cleanPrefix + ` style="${escapeHtml(serializeStyle(resolved))}"`;
1176
- continue;
1177
- }
1178
- }
1179
- }
1180
- if (isRawHtml(value)) {
1181
- stripLeadingQuote = null;
1182
- result += chunk + interpolateValue(value);
1183
- continue;
1184
- }
1185
- const resolved = resolveTemplateValue(value);
1186
- if (resolved == null || resolved === false) {
1187
- result += cleanPrefix;
1188
- continue;
1189
- }
1190
- if (resolved === true) {
1191
- result += cleanPrefix + ` ${attrName}`;
1192
- continue;
1193
- }
1194
- const generic = String(resolved);
1195
- if (openQuote !== null || UNQUOTED_HAZARD_RE.test(generic)) result += cleanPrefix + ` ${attrName}="${escapeHtml(generic)}"`;
1196
- else result += cleanPrefix + ` ${attrName}=${escapeHtml(generic)}`;
1392
+ if (/^on/i.test(name)) {
1393
+ const match = TEMPLATE_EVENT.exec(name.toLowerCase());
1394
+ if (templateParts(sourceValue)?.some((part) => typeof part === "function")) throw new Error(`[ilha] ${name} handler must occupy the entire attribute value. Write ${name}=\${handler}.`);
1395
+ if (!match || typeof sourceValue !== "function") continue;
1396
+ const eventName = match[1];
1397
+ const rawModifier = match[2];
1398
+ const modifier = rawModifier !== void 0 && NATIVE_EVENT_MODIFIERS.has(rawModifier) ? rawModifier : void 0;
1399
+ if (rawModifier && !modifier) continue;
1400
+ const index = __ilhaJsxEvent({
1401
+ type: eventName,
1402
+ handler: sourceValue,
1403
+ modifier
1404
+ });
1405
+ if (index !== void 0) eventSpecs.push(`${eventName}:${index}`);
1406
+ continue;
1407
+ }
1408
+ const lower = name.toLowerCase();
1409
+ if (lower === "srcdoc") continue;
1410
+ const parts = templateParts(sourceValue);
1411
+ let value = parts ? parts.map((part) => resolveTemplateValue(part) ?? "").join("") : resolveTemplateValue(sourceValue);
1412
+ if ((lower.startsWith("aria-") || TEMPLATE_STRING_BOOLEANS.has(lower)) && typeof value === "boolean") value = String(value);
1413
+ if (lower === "class" && !isRawHtml(value)) value = normalizeTemplateClass(value);
1414
+ const structuredStyle = lower === "style" && isPlainObject(value) && !isRawHtml(value);
1415
+ if (structuredStyle) value = serializeStyle(value);
1416
+ if (isUrlAttributeName(lower) && !isRawHtml(value) && !isSafeUrlAttrValue(node.tag, lower, String(value))) continue;
1417
+ if (value === false || value == null) continue;
1418
+ if (attribute?.bare && value === "" || value === true && !TEMPLATE_STRING_BOOLEANS.has(lower) && !lower.startsWith("aria-")) {
1419
+ attrs += ` ${name}`;
1197
1420
  continue;
1198
1421
  }
1199
- result += chunk + interpolateValue(value);
1422
+ const text = isRawHtml(value) ? value.value : String(value);
1423
+ const escaped = isRawHtml(value) ? text : escapeHtml(text);
1424
+ attrs += attribute && !attribute.quoted && !structuredStyle && !/[\s"'`=<>]/.test(text) ? ` ${name}=${escaped}` : ` ${name}="${escaped}"`;
1425
+ }
1426
+ const key = node.props.key;
1427
+ if (key != null && node.props["data-key"] == null) attrs += ` data-key="${escapeHtml(key)}"`;
1428
+ if (bindSpecs.length) attrs += ` data-ilha-bind="${bindSpecs.join(",")}"`;
1429
+ if (eventSpecs.length) attrs += ` data-ilha-on="${eventSpecs.join(",")}"`;
1430
+ if (TEMPLATE_VOID_ELEMENTS.has(node.tag.toLowerCase())) return `<${node.tag}${attrs}${node.selfClosing ? " /" : ""}>`;
1431
+ return `<${node.tag}${attrs}>${node.children.map((child) => renderTemplateNode(child, node.tag)).join("")}</${node.tag}>`;
1432
+ }
1433
+ function renderTemplateNode(node, parentTag) {
1434
+ switch (node.kind) {
1435
+ case "fragment": return node.children.map((child) => renderTemplateNode(child, parentTag)).join("");
1436
+ case "element": return renderTemplateElement(node);
1437
+ case "dynamic": return interpolateValue(node.value);
1438
+ case "comment": {
1439
+ const parts = templateParts(node.value);
1440
+ return `<!--${(parts ? parts.map((part) => resolveTemplateValue(part) ?? "").join("") : String(node.value)).replace(/--/g, "- -")}-->`;
1441
+ }
1442
+ case "text": {
1443
+ const parent = parentTag?.toLowerCase();
1444
+ return parent === "script" || parent === "style" ? node.value : escapeHtml(node.value);
1445
+ }
1200
1446
  }
1201
- if (pendingBindSpecs !== "") result += ` data-ilha-bind="${pendingBindSpecs}"`;
1202
- if (pendingEventSpecs !== "") result += ` data-ilha-on="${pendingEventSpecs}"`;
1203
- return {
1204
- [RAW]: true,
1205
- value: dedentString(result)
1206
- };
1447
+ }
1448
+ function renderTemplate(node) {
1449
+ return ilhaRaw(renderTemplateNode(node));
1450
+ }
1451
+ setTemplateRenderer(renderTemplate);
1452
+ function ilhaHtml(strings, ...values) {
1453
+ return renderTemplate(htmlTemplate(strings, values));
1207
1454
  }
1208
1455
  function isRawHtml(v) {
1209
1456
  if (typeof v !== "object" || v === null) return false;
@@ -1216,9 +1463,8 @@ const contextRegistry = /* @__PURE__ */ new Map();
1216
1463
  function ilhaContextFn(key, initial) {
1217
1464
  if (contextRegistry.has(key)) return contextRegistry.get(key);
1218
1465
  const s = signal(initial);
1219
- const accessor = markSignalAccessor((...args) => {
1220
- if (args.length === 0) return s();
1221
- s(resolveSignalSetter(() => s(), args[0]));
1466
+ const accessor = markSignalAccessor(() => s(), (next) => {
1467
+ s(next);
1222
1468
  });
1223
1469
  contextRegistry.set(key, accessor);
1224
1470
  return accessor;
@@ -1241,9 +1487,8 @@ const ilhaContext = Object.assign(ilhaContextFn, {
1241
1487
  function ilhaSignal(initial) {
1242
1488
  if (currentFrame()?.creating && __DEV__);
1243
1489
  const s = signal(initial);
1244
- return markSignalAccessor((...args) => {
1245
- if (args.length === 0) return s();
1246
- s(resolveSignalSetter(() => s(), args[0]));
1490
+ return markSignalAccessor(() => s(), (next) => {
1491
+ s(next);
1247
1492
  });
1248
1493
  }
1249
1494
  /**
@@ -1353,7 +1598,7 @@ function persist(accessor, key, options = {}) {
1353
1598
  if (value !== null) {
1354
1599
  const prevSub = setActiveSub(void 0);
1355
1600
  try {
1356
- accessor(() => value);
1601
+ accessor.set(value);
1357
1602
  } finally {
1358
1603
  setActiveSub(prevSub);
1359
1604
  }
@@ -1388,12 +1633,8 @@ function persist(accessor, key, options = {}) {
1388
1633
  };
1389
1634
  }
1390
1635
  function createDerivedAccessor(read, write) {
1391
- const accessor = markSignalAccessor((...args) => {
1392
- if (args.length > 0) {
1393
- if (write) write(args[0]);
1394
- return;
1395
- }
1396
- return read().value;
1636
+ const accessor = markSignalAccessor(() => read().value, (value) => {
1637
+ if (write) write(value);
1397
1638
  });
1398
1639
  return new Proxy(accessor, { get(target, prop, receiver) {
1399
1640
  if (prop === "loading" || prop === "value" || prop === "error") return read()[prop];
@@ -1563,8 +1804,8 @@ function applyTemplateBindings(host, binds, phase = "all") {
1563
1804
  const accessor = record.accessor;
1564
1805
  if (spec.kind === "this") {
1565
1806
  if (phase !== "listen") {
1566
- accessor(el);
1567
- cleanups.push(() => accessor(null));
1807
+ accessor.set(el);
1808
+ cleanups.push(() => accessor.set(null));
1568
1809
  }
1569
1810
  continue;
1570
1811
  }
@@ -1592,7 +1833,7 @@ function applyTemplateBindings(host, binds, phase = "all") {
1592
1833
  }
1593
1834
  arr.push(coercedVal);
1594
1835
  } else if (!groupRead.checked && idx !== -1) arr.splice(idx, 1);
1595
- accessor(arr);
1836
+ accessor.set(arr);
1596
1837
  return;
1597
1838
  }
1598
1839
  const currentVal = accessor();
@@ -1601,7 +1842,7 @@ function applyTemplateBindings(host, binds, phase = "all") {
1601
1842
  const n = Number(groupRead);
1602
1843
  coerced = Number.isNaN(n) ? 0 : n;
1603
1844
  } else if (typeof currentVal === "boolean") coerced = Boolean(groupRead);
1604
- accessor(coerced);
1845
+ accessor.set(coerced);
1605
1846
  return;
1606
1847
  }
1607
1848
  const currentVal = accessor();
@@ -1610,7 +1851,7 @@ function applyTemplateBindings(host, binds, phase = "all") {
1610
1851
  const n = Number(raw);
1611
1852
  value = Number.isNaN(n) ? 0 : n;
1612
1853
  } else if (typeof currentVal === "boolean") value = Boolean(raw);
1613
- accessor(value);
1854
+ accessor.set(value);
1614
1855
  };
1615
1856
  el.addEventListener(event, listener);
1616
1857
  cleanups.push(() => el.removeEventListener(event, listener));
@@ -1694,8 +1935,8 @@ function acquireSlot(kind) {
1694
1935
  *
1695
1936
  * A function argument is treated as a lazy initializer:
1696
1937
  * const count = state(() => expensiveInitialValue());
1697
- * To store a function VALUE, return it from the updater wrapper on write:
1698
- * setCallback(() => nextCallback);
1938
+ * To store a function VALUE, pass it to `.set`:
1939
+ * onSave.set(nextCallback);
1699
1940
  */
1700
1941
  function state(init) {
1701
1942
  const frame = currentFrame();
@@ -1707,9 +1948,7 @@ function state(init) {
1707
1948
  const snap = f.ssr ? void 0 : f.instance?.stateSnapshot?.s?.[index];
1708
1949
  const initial = snap === void 0 ? typeof init === "function" ? init() : init : snap;
1709
1950
  const s = signal(initial);
1710
- const acc = markSignalAccessor((...args) => {
1711
- if (args.length === 0) return s();
1712
- const next = resolveSignalSetter(() => s(), args[0]);
1951
+ const acc = markSignalAccessor(() => s(), (next) => {
1713
1952
  s(next);
1714
1953
  });
1715
1954
  f.slots[f.cursor - 1] = {
@@ -2981,4 +3220,4 @@ const mount = mountAll;
2981
3220
  const context = ilhaContext;
2982
3221
 
2983
3222
  //#endregion
2984
- export { action, batch, context, css, derived, each, effect, html, ilha, ilhaSignal, isSafeUrl, isSafeUrlAttrValue, isUrlAttributeName, json, morph, mount, onError, onUncaughtError, persist, raw, serializeStyle, state, untrack };
3223
+ export { action, batch, context, css, derived, each, effect, html, ilha, isSafeUrl, isSafeUrlAttrValue, isUrlAttributeName, json, morph, mount, onError, onUncaughtError, persist, raw, serializeStyle, state, untrack };