@barefootjs/cli 0.33.6 → 0.35.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
@@ -213,6 +213,54 @@ var init_runtime = __esm({
213
213
  }
214
214
  });
215
215
 
216
+ // ../jsx/src/lowering-registry.ts
217
+ function registerLoweringPlugin(plugin) {
218
+ const existing = plugins.findIndex((p) => p.name === plugin.name);
219
+ if (existing >= 0) plugins[existing] = plugin;
220
+ else plugins.push(plugin);
221
+ }
222
+ function getLoweringPlugins() {
223
+ return [...plugins];
224
+ }
225
+ function prepareLoweringMatchers(metadata) {
226
+ const matchers = [];
227
+ for (const plugin of plugins) {
228
+ const matcher = plugin.prepare(metadata);
229
+ if (matcher) matchers.push(matcher);
230
+ }
231
+ return matchers;
232
+ }
233
+ function matchLoweringCall(callee, args2, metadata) {
234
+ for (const matcher of prepareLoweringMatchers(metadata)) {
235
+ const node = matcher(callee, args2);
236
+ if (node) return node;
237
+ }
238
+ return null;
239
+ }
240
+ function loweringNodeChildren(node) {
241
+ if (node.kind === "helper-call") return [...node.args];
242
+ const children2 = [node.base];
243
+ for (const t of node.triples) {
244
+ if (t.guard) children2.push(t.guard);
245
+ children2.push(t.value);
246
+ }
247
+ return children2;
248
+ }
249
+ function isValidHelperId(helper) {
250
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(helper);
251
+ }
252
+ function __resetLoweringPluginsForTest(next = []) {
253
+ plugins.length = 0;
254
+ plugins.push(...next);
255
+ }
256
+ var plugins;
257
+ var init_lowering_registry = __esm({
258
+ "../jsx/src/lowering-registry.ts"() {
259
+ "use strict";
260
+ plugins = [];
261
+ }
262
+ });
263
+
216
264
  // ../jsx/src/expression-parser.ts
217
265
  import ts from "typescript";
218
266
  function extractArrowBodyExpression(source) {
@@ -453,7 +501,7 @@ function convertNode(node, raw) {
453
501
  }
454
502
  if (n === void 0 || Number.isNaN(n)) {
455
503
  const parsedDepth = convertNode(depthNode, raw);
456
- if (checkSupport(parsedDepth, "rendered").supported) {
504
+ if (checkSupport(parsedDepth, "rendered", []).supported) {
457
505
  depthExpr = parsedDepth;
458
506
  flatDepth = 1;
459
507
  } else {
@@ -1200,13 +1248,13 @@ function getUnaryOperatorString(op) {
1200
1248
  return "unknown";
1201
1249
  }
1202
1250
  }
1203
- function isSupported(expr) {
1204
- return checkSupport(expr, "rendered");
1251
+ function isSupported(expr, opts) {
1252
+ return checkSupport(expr, "rendered", opts?.loweringMatchers ?? []);
1205
1253
  }
1206
- function isSupportedValue(expr) {
1207
- return checkSupport(expr, "value");
1254
+ function isSupportedValue(expr, opts) {
1255
+ return checkSupport(expr, "value", opts?.loweringMatchers ?? []);
1208
1256
  }
1209
- function checkSupport(expr, pos) {
1257
+ function checkSupport(expr, pos, matchers) {
1210
1258
  switch (expr.kind) {
1211
1259
  case "unsupported":
1212
1260
  return { supported: false, reason: expr.reason };
@@ -1215,7 +1263,7 @@ function checkSupport(expr, pos) {
1215
1263
  return { supported: false, reason: "Unsupported syntax: ObjectLiteralExpression" };
1216
1264
  }
1217
1265
  for (const prop of expr.properties) {
1218
- const propSupport = checkSupport(prop.kind === "spread" ? prop.expr : prop.value, pos);
1266
+ const propSupport = checkSupport(prop.kind === "spread" ? prop.expr : prop.value, pos, matchers);
1219
1267
  if (!propSupport.supported) return propSupport;
1220
1268
  }
1221
1269
  return { supported: true, level: "L2" };
@@ -1229,7 +1277,7 @@ function checkSupport(expr, pos) {
1229
1277
  return { supported: false, reason: "Standalone arrow functions / regex literals are not supported" };
1230
1278
  case "array-literal": {
1231
1279
  for (const el of expr.elements) {
1232
- const elSupport = checkSupport(el, pos);
1280
+ const elSupport = checkSupport(el, pos, matchers);
1233
1281
  if (!elSupport.supported) return elSupport;
1234
1282
  }
1235
1283
  return { supported: true, level: "L2" };
@@ -1241,24 +1289,33 @@ function checkSupport(expr, pos) {
1241
1289
  reason: `String.prototype.${expr.method} supports only a string pattern + string replacement (the regex form is deferred); use a string pattern or wrap the expression in /* @client */`
1242
1290
  };
1243
1291
  }
1244
- const objSupport = checkSupport(expr.object, pos);
1292
+ const objSupport = checkSupport(expr.object, pos, matchers);
1245
1293
  if (!objSupport.supported) return objSupport;
1246
1294
  for (const arg of expr.args) {
1247
- const argSupport = checkSupport(arg, pos);
1295
+ const argSupport = checkSupport(arg, pos, matchers);
1248
1296
  if (!argSupport.supported) return argSupport;
1249
1297
  }
1250
1298
  if (expr.method === "flat" && expr.depthExpr) {
1251
- const depthSupport = checkSupport(expr.depthExpr, pos);
1299
+ const depthSupport = checkSupport(expr.depthExpr, pos, matchers);
1252
1300
  if (!depthSupport.supported) return depthSupport;
1253
1301
  }
1254
1302
  return { supported: true, level: "L2" };
1255
1303
  }
1256
1304
  case "call": {
1305
+ for (const matcher of matchers) {
1306
+ const node = matcher(expr.callee, expr.args);
1307
+ if (!node) continue;
1308
+ for (const child of loweringNodeChildren(node)) {
1309
+ const childSupport = checkSupport(child, pos, matchers);
1310
+ if (!childSupport.supported) return childSupport;
1311
+ }
1312
+ return { supported: true, level: "L2" };
1313
+ }
1257
1314
  const cb = asCallbackMethodCall(expr);
1258
1315
  if (cb) {
1259
- const objSupport = checkSupport(cb.object, pos);
1316
+ const objSupport = checkSupport(cb.object, pos, matchers);
1260
1317
  if (!objSupport.supported) return objSupport;
1261
- const bodySupport = checkSupport(cb.arrow.body, pos);
1318
+ const bodySupport = checkSupport(cb.arrow.body, pos, matchers);
1262
1319
  if (!bodySupport.supported) {
1263
1320
  return {
1264
1321
  supported: false,
@@ -1267,12 +1324,12 @@ function checkSupport(expr, pos) {
1267
1324
  };
1268
1325
  }
1269
1326
  for (const rest2 of cb.args) {
1270
- const restSupport = checkSupport(rest2, pos);
1327
+ const restSupport = checkSupport(rest2, pos, matchers);
1271
1328
  if (!restSupport.supported) return restSupport;
1272
1329
  }
1273
1330
  return { supported: true, level: "L5" };
1274
1331
  }
1275
- const calleeSupport = checkSupport(expr.callee, pos);
1332
+ const calleeSupport = checkSupport(expr.callee, pos, matchers);
1276
1333
  if (!calleeSupport.supported) {
1277
1334
  return calleeSupport;
1278
1335
  }
@@ -1291,7 +1348,7 @@ function checkSupport(expr, pos) {
1291
1348
  return { supported: true, level: "L1" };
1292
1349
  }
1293
1350
  for (const arg of expr.args) {
1294
- const argSupport = checkSupport(arg, pos);
1351
+ const argSupport = checkSupport(arg, pos, matchers);
1295
1352
  if (!argSupport.supported) {
1296
1353
  return argSupport;
1297
1354
  }
@@ -1299,7 +1356,7 @@ function checkSupport(expr, pos) {
1299
1356
  return { supported: true, level: "L2" };
1300
1357
  }
1301
1358
  case "member": {
1302
- const objSupport = checkSupport(expr.object, pos);
1359
+ const objSupport = checkSupport(expr.object, pos, matchers);
1303
1360
  if (!objSupport.supported) {
1304
1361
  return objSupport;
1305
1362
  }
@@ -1309,16 +1366,16 @@ function checkSupport(expr, pos) {
1309
1366
  return { supported: true, level: "L2" };
1310
1367
  }
1311
1368
  case "index-access": {
1312
- const objSupport = checkSupport(expr.object, pos);
1369
+ const objSupport = checkSupport(expr.object, pos, matchers);
1313
1370
  if (!objSupport.supported) return objSupport;
1314
- const indexSupport = checkSupport(expr.index, pos);
1371
+ const indexSupport = checkSupport(expr.index, pos, matchers);
1315
1372
  if (!indexSupport.supported) return indexSupport;
1316
1373
  return { supported: true, level: "L2" };
1317
1374
  }
1318
1375
  case "binary": {
1319
- const leftSupport = checkSupport(expr.left, pos);
1376
+ const leftSupport = checkSupport(expr.left, pos, matchers);
1320
1377
  if (!leftSupport.supported) return leftSupport;
1321
- const rightSupport = checkSupport(expr.right, pos);
1378
+ const rightSupport = checkSupport(expr.right, pos, matchers);
1322
1379
  if (!rightSupport.supported) return rightSupport;
1323
1380
  if (["===", "==", "!==", "!=", ">", "<", ">=", "<="].includes(expr.op)) {
1324
1381
  return { supported: true, level: "L3" };
@@ -1329,7 +1386,7 @@ function checkSupport(expr, pos) {
1329
1386
  return { supported: false, reason: `Unknown operator: ${expr.op}` };
1330
1387
  }
1331
1388
  case "unary": {
1332
- const argSupport = checkSupport(expr.argument, pos);
1389
+ const argSupport = checkSupport(expr.argument, pos, matchers);
1333
1390
  if (!argSupport.supported) return argSupport;
1334
1391
  if (expr.op === "!") {
1335
1392
  return { supported: true, level: "L4" };
@@ -1340,28 +1397,28 @@ function checkSupport(expr, pos) {
1340
1397
  return { supported: false, reason: `Unsupported unary operator: ${expr.op}` };
1341
1398
  }
1342
1399
  case "logical": {
1343
- const leftSupport = checkSupport(expr.left, pos);
1400
+ const leftSupport = checkSupport(expr.left, pos, matchers);
1344
1401
  if (!leftSupport.supported) return leftSupport;
1345
1402
  if (expr.op === "??" && expr.right.kind === "object-literal" && expr.right.properties.length === 0) {
1346
1403
  return { supported: true, level: "L4" };
1347
1404
  }
1348
- const rightSupport = checkSupport(expr.right, pos);
1405
+ const rightSupport = checkSupport(expr.right, pos, matchers);
1349
1406
  if (!rightSupport.supported) return rightSupport;
1350
1407
  return { supported: true, level: "L4" };
1351
1408
  }
1352
1409
  case "conditional": {
1353
- const testSupport = checkSupport(expr.test, pos);
1410
+ const testSupport = checkSupport(expr.test, pos, matchers);
1354
1411
  if (!testSupport.supported) return testSupport;
1355
- const consSupport = checkSupport(expr.consequent, pos);
1412
+ const consSupport = checkSupport(expr.consequent, pos, matchers);
1356
1413
  if (!consSupport.supported) return consSupport;
1357
- const altSupport = checkSupport(expr.alternate, pos);
1414
+ const altSupport = checkSupport(expr.alternate, pos, matchers);
1358
1415
  if (!altSupport.supported) return altSupport;
1359
1416
  return { supported: true, level: "L4" };
1360
1417
  }
1361
1418
  case "template-literal": {
1362
1419
  for (const part of expr.parts) {
1363
1420
  if (part.type === "expression") {
1364
- const partSupport = checkSupport(part.expr, pos);
1421
+ const partSupport = checkSupport(part.expr, pos, matchers);
1365
1422
  if (!partSupport.supported) return partSupport;
1366
1423
  }
1367
1424
  }
@@ -2139,6 +2196,7 @@ var PARSED_EXPR_KINDS, ARRAY_METHOD_NAMES, SORT_KEY_TYPES, SORT_KEY_TARGETS, SOR
2139
2196
  var init_expression_parser = __esm({
2140
2197
  "../jsx/src/expression-parser.ts"() {
2141
2198
  "use strict";
2199
+ init_lowering_registry();
2142
2200
  PARSED_EXPR_KINDS = [
2143
2201
  "identifier",
2144
2202
  "literal",
@@ -3173,12 +3231,22 @@ function renderLoopBindingAccess(b, base) {
3173
3231
  }
3174
3232
  return parent2;
3175
3233
  }
3176
- function wrapLoopParamAsAccessor(expr, paramName, bindings) {
3234
+ function wrapLoopParamAsAccessor(expr, paramName, bindings, indexParam) {
3235
+ let result2;
3177
3236
  if (bindings && bindings.length > 0) {
3178
- return rewriteLoopBindingRefs(expr, bindings, "__bfItem()");
3237
+ result2 = rewriteLoopBindingRefs(expr, bindings, "__bfItem()");
3238
+ } else {
3239
+ const re = new RegExp(`${ID_BOUNDARY_BEFORE}${escapeIdentifierForRegex(paramName)}(?!\\s*\\()(?!-)${ID_BOUNDARY_AFTER}`, "gu");
3240
+ result2 = replaceInExprContexts(expr, re, () => `${paramName}()`);
3179
3241
  }
3180
- const re = new RegExp(`${ID_BOUNDARY_BEFORE}${escapeIdentifierForRegex(paramName)}(?!\\s*\\()(?!-)${ID_BOUNDARY_AFTER}`, "gu");
3181
- return replaceInExprContexts(expr, re, () => `${paramName}()`);
3242
+ if (indexParam && indexParam !== paramName) {
3243
+ result2 = wrapIndexParamAsAccessor(result2, indexParam);
3244
+ }
3245
+ return result2;
3246
+ }
3247
+ function wrapIndexParamAsAccessor(expr, indexParam) {
3248
+ const re = new RegExp(`${ID_BOUNDARY_BEFORE}${escapeIdentifierForRegex(indexParam)}(?!\\s*\\()(?!-)${ID_BOUNDARY_AFTER}`, "gu");
3249
+ return replaceInExprContexts(expr, re, () => `${indexParam}()`);
3182
3250
  }
3183
3251
  function rewriteLoopBindingRefs(expr, bindings, accessor) {
3184
3252
  const byName = /* @__PURE__ */ new Map();
@@ -3230,7 +3298,7 @@ function wrapExprWithLoopParams(expr, loopParams) {
3230
3298
  let result2 = expr;
3231
3299
  for (const p of loopParams) {
3232
3300
  const spec = typeof p === "string" ? { param: p } : p;
3233
- result2 = wrapLoopParamAsAccessor(result2, spec.param, spec.bindings);
3301
+ result2 = wrapLoopParamAsAccessor(result2, spec.param, spec.bindings, spec.index);
3234
3302
  }
3235
3303
  return result2;
3236
3304
  }
@@ -3535,6 +3603,18 @@ function buildPropAliasMap(params) {
3535
3603
  }
3536
3604
  return map;
3537
3605
  }
3606
+ function resolveBodyDestructuredPropAliases(localConstants, propsObjectName) {
3607
+ const aliases = /* @__PURE__ */ new Map();
3608
+ if (propsObjectName === null) return aliases;
3609
+ for (const c of localConstants) {
3610
+ if (c.isModule) continue;
3611
+ const m = c.parsed;
3612
+ if (m?.kind === "member" && !m.computed && m.object.kind === "identifier" && m.object.name === propsObjectName) {
3613
+ aliases.set(c.name, m.property);
3614
+ }
3615
+ }
3616
+ return aliases;
3617
+ }
3538
3618
  function boundPropLocalNames(b) {
3539
3619
  if (b.propsObjectName !== null) return EMPTY_SET;
3540
3620
  return new Set(b.propsParams.filter((p) => !p.isRest).map((p) => p.name));
@@ -3775,6 +3855,16 @@ function resolveGetterAliases(localConstants, isGetter) {
3775
3855
  }
3776
3856
  return aliases;
3777
3857
  }
3858
+ function collectAliasableGetterNames(signals, memos) {
3859
+ const getterNames = /* @__PURE__ */ new Set();
3860
+ for (const sig of signals) {
3861
+ if (sig.getter && !sig.isModule && !sig.envReader) getterNames.add(sig.getter);
3862
+ }
3863
+ for (const memo of memos) {
3864
+ if (!memo.isModule) getterNames.add(memo.name);
3865
+ }
3866
+ return getterNames;
3867
+ }
3778
3868
  function buildSignalMemoEnv(signals, memos, propsObjectName, localConstants = []) {
3779
3869
  const substitutions = /* @__PURE__ */ new Map();
3780
3870
  for (const s of signals) {
@@ -4063,6 +4153,75 @@ var init_binding_scope = __esm({
4063
4153
  }
4064
4154
  });
4065
4155
 
4156
+ // ../jsx/src/ir-to-client-js/safe-html.ts
4157
+ function safeHtml(expr) {
4158
+ return expr;
4159
+ }
4160
+ function interp(span) {
4161
+ return `\${${span}}`;
4162
+ }
4163
+ function escapedText(expr) {
4164
+ return safeHtml(`escapeText(${expr})`);
4165
+ }
4166
+ function escapedTextOrMarkup(expr) {
4167
+ return safeHtml(`escapeTextOrMarkup(${expr})`);
4168
+ }
4169
+ function branchSlotValue(expr, slotsVar) {
4170
+ return safeHtml(`__bfSlot(${expr}, ${slotsVar})`);
4171
+ }
4172
+ function childrenMarkup(expr) {
4173
+ return safeHtml(`markupOrEmpty(${expr})`);
4174
+ }
4175
+ function joinedMarkup(expr) {
4176
+ return safeHtml(`Array.isArray(${expr}) ? ${expr}.join('') : (${expr} ?? '')`);
4177
+ }
4178
+ function renderChildCall(registryName, propsExpr, tailArgs) {
4179
+ return safeHtml(`renderChild('${registryName}', ${propsExpr}${tailArgs})`);
4180
+ }
4181
+ function dangerousInnerHtml(expr) {
4182
+ return safeHtml(`((${expr}) ?? {}).__html ?? ''`);
4183
+ }
4184
+ function conditionalMarkup(condition, whenTrue, whenFalse) {
4185
+ return safeHtml(`${condition} ? \`${whenTrue}\` : \`${whenFalse}\``);
4186
+ }
4187
+ function mappedRowsMarkup(arrayExpr, method2, params, body2) {
4188
+ return safeHtml(`${arrayExpr}.${method2}(${params} => ${body2}).join('')`);
4189
+ }
4190
+ function isChildrenPassthroughExpr(expr) {
4191
+ return /^([A-Za-z_$][\w$]*\.)?children$/.test(expr.trim());
4192
+ }
4193
+ function spliceChildValue(node, valueExpr, cx2) {
4194
+ if (node.joinArrayChild) return joinedMarkup(valueExpr);
4195
+ if (cx2.branchSlotsVar) return branchSlotValue(valueExpr, cx2.branchSlotsVar);
4196
+ if (node.slotId) {
4197
+ return cx2.markupSlotIds?.has(node.slotId) ? escapedTextOrMarkup(valueExpr) : escapedText(valueExpr);
4198
+ }
4199
+ const resolved = valueExpr.trim().replace(/^\(+|\)+$/g, "");
4200
+ if (isChildrenPassthroughExpr(node.expr) || isChildrenPassthroughExpr(resolved)) {
4201
+ return childrenMarkup(valueExpr);
4202
+ }
4203
+ return escapedText(valueExpr);
4204
+ }
4205
+ var EMPTY_MARKUP;
4206
+ var init_safe_html = __esm({
4207
+ "../jsx/src/ir-to-client-js/safe-html.ts"() {
4208
+ "use strict";
4209
+ EMPTY_MARKUP = safeHtml("''");
4210
+ }
4211
+ });
4212
+
4213
+ // ../jsx/src/ir-to-client-js/markup-slots.ts
4214
+ function markupSlotIdsOf(ctx2) {
4215
+ return new Set(ctx2.dynamicElements.map((e) => e.slotId));
4216
+ }
4217
+ var DYNAMIC_ELEMENT_WRITER_KIND;
4218
+ var init_markup_slots = __esm({
4219
+ "../jsx/src/ir-to-client-js/markup-slots.ts"() {
4220
+ "use strict";
4221
+ DYNAMIC_ELEMENT_WRITER_KIND = "markup";
4222
+ }
4223
+ });
4224
+
4066
4225
  // ../jsx/src/ir-to-client-js/html-template.ts
4067
4226
  function createStringProtector() {
4068
4227
  const strings = [];
@@ -4207,21 +4366,10 @@ function templateAttrExpr(attrName, valExpr, presenceOrUndefined) {
4207
4366
  function escapeAttrValueExpr(valExpr) {
4208
4367
  return `escapeAttr(${valExpr})`;
4209
4368
  }
4210
- function escapeTextSlotExpr(innerExpr, isMarkup = false) {
4211
- return `${isMarkup ? "escapeTextOrMarkup" : "escapeText"}(${innerExpr})`;
4212
- }
4213
- function isChildrenPassthroughExpr(expr) {
4214
- return /^([A-Za-z_$][\w$]*\.)?children$/.test(expr.trim());
4215
- }
4216
- function bareSpliceExpr(node, valueExpr) {
4217
- const resolved = valueExpr.trim().replace(/^\(+|\)+$/g, "");
4218
- const isChildren = isChildrenPassthroughExpr(node.expr) || isChildrenPassthroughExpr(resolved);
4219
- return !node.joinArrayChild && isChildren ? `markupOrEmpty(${valueExpr})` : valueExpr;
4220
- }
4221
4369
  function dangerouslyHtmlChildren(attrs, toExpr) {
4222
4370
  const attr = attrs.find((a) => a.name === "dangerouslySetInnerHTML");
4223
4371
  if (!attr || attr.value.kind !== "expression") return null;
4224
- return `\${((${toExpr(attr.value)}) ?? {}).__html ?? ''}`;
4372
+ return interp(dangerousInnerHtml(toExpr(attr.value)));
4225
4373
  }
4226
4374
  function transformKeyValue(value2, transformExpr) {
4227
4375
  switch (value2.kind) {
@@ -4323,7 +4471,7 @@ function buildSpreadAttrsMergeCall(args2) {
4323
4471
  return `\${spreadAttrs({${objMembers.join(", ")}})}`;
4324
4472
  }
4325
4473
  function itemAnchorTemplate(keyExpr) {
4326
- return `<!--${loopItemMarker("${" + keyExpr + "}")}-->`;
4474
+ return `<!--${loopItemMarker("${escapeCommentText(" + keyExpr + ")}")}-->`;
4327
4475
  }
4328
4476
  function renderPreamble(preamble, opts) {
4329
4477
  let out = "";
@@ -4332,9 +4480,9 @@ function renderPreamble(preamble, opts) {
4332
4480
  const text = opts.textVariant === "template" ? seg.templateText ?? seg.text : seg.text;
4333
4481
  out += opts.transformJs ? opts.transformJs(text) : text;
4334
4482
  } else if (opts.rawLeaf) {
4335
- out += opts.renderLeaf(escapeLeafTextExpressions(seg.ir));
4483
+ out += opts.renderLeaf(seg.ir);
4336
4484
  } else {
4337
- out += "`" + opts.renderLeaf(escapeLeafTextExpressions(seg.ir)) + "`";
4485
+ out += "`" + opts.renderLeaf(seg.ir) + "`";
4338
4486
  }
4339
4487
  }
4340
4488
  return out;
@@ -4376,34 +4524,12 @@ function renderFlatMapProjectionClientBody(inner, restSpreadNames) {
4376
4524
  const chained = applyLoopChain(inner);
4377
4525
  const params = inner.index ? `(${inner.param}, ${inner.index})` : `(${inner.param})`;
4378
4526
  const key = inner.key ? `(${inner.key})` : "undefined";
4379
- const html = inner.children.map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, void 0, void 0)).join("");
4527
+ const html = inner.children.map((c) => irToHtmlTemplate(c, restSpreadNames, 1, void 0, void 0)).join("");
4380
4528
  return `${chained}.map(${params} => ({ k: ${key}, h: \`${html}\` }))`;
4381
4529
  }
4382
- function escapeLeafTextExpressions(ir) {
4383
- switch (ir.type) {
4384
- case "element":
4385
- return { ...ir, children: ir.children.map(escapeLeafTextExpressions) };
4386
- case "fragment":
4387
- return { ...ir, children: ir.children.map(escapeLeafTextExpressions) };
4388
- case "expression": {
4389
- if (ir.expr === "null" || ir.expr === "undefined") return ir;
4390
- if (ir.slotId || ir.expr.trimStart().startsWith("escapeText(")) return ir;
4391
- return { ...ir, expr: `escapeText((${ir.expr}))`, templateExpr: ir.templateExpr ? `escapeText((${ir.templateExpr}))` : ir.templateExpr };
4392
- }
4393
- case "conditional":
4394
- return {
4395
- ...ir,
4396
- whenTrue: escapeLeafTextExpressions(ir.whenTrue),
4397
- whenFalse: ir.whenFalse ? escapeLeafTextExpressions(ir.whenFalse) : ir.whenFalse
4398
- };
4399
- default:
4400
- return ir;
4401
- }
4402
- }
4403
4530
  function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, branchSlotsVar, inHoistedChildren = false) {
4404
4531
  const recurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, inHoistedChildren);
4405
4532
  const wrapExpr = (expr) => wrapExprWithLoopParams(expr, loopParams);
4406
- const wrapInterpolation = (expr) => branchSlotsVar ? `__bfSlot(${expr}, ${branchSlotsVar})` : expr;
4407
4533
  switch (node.type) {
4408
4534
  case "element": {
4409
4535
  const mergeCtx = {
@@ -4446,25 +4572,16 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
4446
4572
  return escapeHtml(node.value);
4447
4573
  case "expression": {
4448
4574
  if (node.expr === "null" || node.expr === "undefined") return "";
4449
- const escapeForClient = (e) => node.escapeInClientTemplate ? `escapeText(${e})` : e;
4450
- if (node.markerless) {
4451
- const bare = escapeForClient(wrapInterpolation(wrapExpr(node.expr)));
4452
- return `\${${bare}}`;
4453
- }
4454
- const inner = escapeForClient(wrapInterpolation(wrapExpr(node.expr)));
4455
- const valueExpr = node.joinArrayChild ? `Array.isArray(${inner}) ? ${inner}.join('') : (${inner} ?? '')` : inner;
4456
- if (node.slotId) {
4457
- const slotted = branchSlotsVar || node.joinArrayChild ? valueExpr : escapeTextSlotExpr(valueExpr);
4458
- return `<!--bf:${node.slotId}-->\${${slotted}}<!--/-->`;
4459
- }
4460
- return `\${${bareSpliceExpr(node, valueExpr)}}`;
4575
+ const hole = interp(spliceChildValue(node, wrapExpr(node.expr), { branchSlotsVar }));
4576
+ if (node.markerless) return hole;
4577
+ return node.slotId ? `<!--bf:${node.slotId}-->${hole}<!--/-->` : hole;
4461
4578
  }
4462
4579
  case "conditional": {
4463
4580
  const trueBranch = recurse(node.whenTrue);
4464
4581
  const falseBranch = recurse(node.whenFalse);
4465
4582
  const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch;
4466
4583
  const falseHtml = node.slotId ? addCondAttrToTemplate(falseBranch, node.slotId) : falseBranch;
4467
- return `\${${wrapExpr(node.condition)} ? \`${trueHtml}\` : \`${falseHtml}\`}`;
4584
+ return interp(conditionalMarkup(wrapExpr(node.condition), trueHtml, falseHtml));
4468
4585
  }
4469
4586
  case "fragment":
4470
4587
  return node.children.map(recurse).join("");
@@ -4499,7 +4616,7 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
4499
4616
  const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(", ")}}` : "{}";
4500
4617
  const keyProp = node.props.find((p) => p.name === "key");
4501
4618
  const keyArg = keyProp ? `, ${attrValueToString(keyProp.value) ?? "undefined"}` : "";
4502
- return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${renderChildScopeArgs(node, keyArg)})}`;
4619
+ return interp(renderChildCall(nameForRegistryRef(node.name), propsExpr, renderChildScopeArgs(node, keyArg)));
4503
4620
  }
4504
4621
  case "loop": {
4505
4622
  const innerRecurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar);
@@ -4517,12 +4634,12 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
4517
4634
  const body2 = renderPreamble(node.flatMapCallback, {
4518
4635
  renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar)
4519
4636
  });
4520
- mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body2}).join('')}`;
4637
+ mapExpr = interp(mappedRowsMarkup(wrappedArray, "flatMap", node.flatMapCallback.params, body2));
4521
4638
  } else if (node.preamble) {
4522
4639
  const preamble = renderPreamble(node.preamble, { textVariant: "client", renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar) });
4523
- mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`;
4640
+ mapExpr = interp(mappedRowsMarkup(wrappedArray, iterMethod, callbackParam, `{ ${preamble} return \`${childTemplate}\` }`));
4524
4641
  } else {
4525
- mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`;
4642
+ mapExpr = interp(mappedRowsMarkup(wrappedArray, iterMethod, callbackParam, `\`${childTemplate}\``));
4526
4643
  }
4527
4644
  return `<!--${loopStartMarker(node.markerId)}-->${mapExpr}<!--${loopEndMarker(node.markerId)}-->`;
4528
4645
  }
@@ -4740,20 +4857,15 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
4740
4857
  return escapeHtml(node.value);
4741
4858
  case "expression": {
4742
4859
  if (node.expr === "null" || node.expr === "undefined") return "";
4743
- const wrapped = wrapExpr(node.expr);
4744
- const value2 = node.joinArrayChild ? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')` : wrapped;
4745
- if (node.slotId) {
4746
- return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value2 : escapeTextSlotExpr(wrapped)}}<!--/-->`;
4747
- }
4748
- const spliced = bareSpliceExpr(node, value2);
4749
- return `\${${node.escapeInClientTemplate ? `escapeText(${spliced})` : spliced}}`;
4860
+ const hole = interp(spliceChildValue(node, wrapExpr(node.expr), {}));
4861
+ return node.slotId ? `<!--bf:${node.slotId}-->${hole}<!--/-->` : hole;
4750
4862
  }
4751
4863
  case "conditional": {
4752
4864
  const trueBranch = recurse(node.whenTrue);
4753
4865
  const falseBranch = recurse(node.whenFalse);
4754
4866
  const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch;
4755
4867
  const falseHtml = node.slotId ? addCondAttrToTemplate(falseBranch, node.slotId) : falseBranch;
4756
- return `\${${wrapExpr(node.condition)} ? \`${trueHtml}\` : \`${falseHtml}\`}`;
4868
+ return interp(conditionalMarkup(wrapExpr(node.condition), trueHtml, falseHtml));
4757
4869
  }
4758
4870
  case "fragment":
4759
4871
  return node.children.map(recurse).join("");
@@ -4778,12 +4890,12 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
4778
4890
  // Leaf `key` stripped — see the irToHtmlTemplate site above.
4779
4891
  renderLeaf: (ir) => irToPlaceholderTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams)
4780
4892
  });
4781
- mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body2}).join('')}`;
4893
+ mapExpr = interp(mappedRowsMarkup(wrappedArray, "flatMap", node.flatMapCallback.params, body2));
4782
4894
  } else if (node.preamble) {
4783
4895
  const preamble = renderPreamble(node.preamble, { textVariant: "client", renderLeaf: (ir) => irToPlaceholderTemplate(ir, restSpreadNames, loopDepth + 1, loopParams) });
4784
- mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`;
4896
+ mapExpr = interp(mappedRowsMarkup(wrappedArray, iterMethod, callbackParam, `{ ${preamble} return \`${childTemplate}\` }`));
4785
4897
  } else {
4786
- mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`;
4898
+ mapExpr = interp(mappedRowsMarkup(wrappedArray, iterMethod, callbackParam, `\`${childTemplate}\``));
4787
4899
  }
4788
4900
  return `<!--${loopStartMarker(node.markerId)}-->${mapExpr}<!--${loopEndMarker(node.markerId)}-->`;
4789
4901
  }
@@ -4982,13 +5094,8 @@ function irToComponentTemplateWithOpts(node, opts) {
4982
5094
  if (node.markerless) return "";
4983
5095
  return `<!--bf:${node.slotId}--><!--/-->`;
4984
5096
  }
4985
- const wrapped = transformExpr(node.expr, node.templateExpr);
4986
- const value2 = node.joinArrayChild ? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')` : wrapped;
4987
- if (node.slotId) {
4988
- const isMarkup = opts.markupSlotIds?.has(node.slotId) ?? false;
4989
- return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value2 : escapeTextSlotExpr(wrapped, isMarkup)}}<!--/-->`;
4990
- }
4991
- return `\${${bareSpliceExpr(node, value2)}}`;
5097
+ const hole = interp(spliceChildValue(node, transformExpr(node.expr, node.templateExpr), { markupSlotIds: opts.markupSlotIds }));
5098
+ return node.slotId ? `<!--bf:${node.slotId}-->${hole}<!--/-->` : hole;
4992
5099
  }
4993
5100
  case "conditional": {
4994
5101
  if (node.clientOnly && node.slotId) {
@@ -4998,7 +5105,7 @@ function irToComponentTemplateWithOpts(node, opts) {
4998
5105
  const falseBranch = recurse(node.whenFalse);
4999
5106
  const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch;
5000
5107
  const falseHtml = node.slotId ? addCondAttrToTemplate(falseBranch, node.slotId) : falseBranch;
5001
- return `\${${transformExpr(node.condition, node.templateCondition)} ? \`${trueHtml}\` : \`${falseHtml}\`}`;
5108
+ return interp(conditionalMarkup(transformExpr(node.condition, node.templateCondition), trueHtml, falseHtml));
5002
5109
  }
5003
5110
  case "fragment":
5004
5111
  return node.children.map(recurse).join("");
@@ -5034,7 +5141,7 @@ function irToComponentTemplateWithOpts(node, opts) {
5034
5141
  const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(", ")}}` : "{}";
5035
5142
  const keyProp = node.props.find((p) => p.name === "key");
5036
5143
  const keyArg = keyProp ? `, ${transformKeyValue(keyProp.value, transformExpr)}` : "";
5037
- return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg})}`;
5144
+ return interp(renderChildCall(nameForRegistryRef(node.name), propsExpr, keyArg));
5038
5145
  }
5039
5146
  case "loop": {
5040
5147
  const innerOpts = { ...opts, loopDepth: loopDepth + 1 };
@@ -5044,7 +5151,7 @@ function irToComponentTemplateWithOpts(node, opts) {
5044
5151
  case "if-statement": {
5045
5152
  const consequent = recurse(node.consequent);
5046
5153
  const alternate = node.alternate ? recurse(node.alternate) : "";
5047
- return `\${${transformExpr(node.condition, node.templateCondition)} ? \`${consequent}\` : \`${alternate}\`}`;
5154
+ return interp(conditionalMarkup(transformExpr(node.condition, node.templateCondition), consequent, alternate));
5048
5155
  }
5049
5156
  case "provider":
5050
5157
  case "async":
@@ -5127,7 +5234,7 @@ function generateCsrTemplate(node, inlinableConstants, ctx2, restSpreadNames, pr
5127
5234
  }
5128
5235
  }
5129
5236
  const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx2, unsafeLocalNames);
5130
- const markupSlotIds = new Set(ctx2.dynamicElements.map((e) => e.slotId));
5237
+ const markupSlotIds = markupSlotIdsOf(ctx2);
5131
5238
  return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1, markupSlotIds, restPropsName: ctx2.restPropsName });
5132
5239
  }
5133
5240
  function mergeCsrNullUnsafe(ctx2, unsafeLocalNames) {
@@ -5308,13 +5415,10 @@ function generateCsrTemplateWithOpts(node, opts) {
5308
5415
  }
5309
5416
  {
5310
5417
  const transformed = transformExpr(node.expr, node.templateExpr);
5311
- const expr = transformed === UNSAFE_TEMPLATE_EXPR ? "''" : transformed;
5312
- const value2 = node.joinArrayChild ? `Array.isArray(${expr}) ? ${expr}.join('') : (${expr} ?? '')` : expr;
5313
- if (node.slotId) {
5314
- const isMarkup = opts.markupSlotIds?.has(node.slotId) ?? false;
5315
- return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value2 : escapeTextSlotExpr(expr, isMarkup)}}<!--/-->`;
5316
- }
5317
- return `\${${bareSpliceExpr(node, value2)}}`;
5418
+ const hole = interp(
5419
+ transformed === UNSAFE_TEMPLATE_EXPR ? EMPTY_MARKUP : spliceChildValue(node, transformed, { markupSlotIds: opts.markupSlotIds })
5420
+ );
5421
+ return node.slotId ? `<!--bf:${node.slotId}-->${hole}<!--/-->` : hole;
5318
5422
  }
5319
5423
  case "conditional": {
5320
5424
  if (node.clientOnly && node.slotId) {
@@ -5324,7 +5428,7 @@ function generateCsrTemplateWithOpts(node, opts) {
5324
5428
  const falseBranch = recurse(node.whenFalse);
5325
5429
  const trueHtml = node.slotId ? addCondAttrToTemplate(trueBranch, node.slotId) : trueBranch;
5326
5430
  const falseHtml = node.slotId ? addCondAttrToTemplate(falseBranch, node.slotId) : falseBranch;
5327
- return `\${${transformExpr(node.condition, node.templateCondition)} ? \`${trueHtml}\` : \`${falseHtml}\`}`;
5431
+ return interp(conditionalMarkup(transformExpr(node.condition, node.templateCondition), trueHtml, falseHtml));
5328
5432
  }
5329
5433
  case "fragment":
5330
5434
  return node.children.map(recurse).join("");
@@ -5368,7 +5472,7 @@ function generateCsrTemplateWithOpts(node, opts) {
5368
5472
  const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(", ")}}` : "{}";
5369
5473
  const keyProp = node.props.find((p) => p.name === "key");
5370
5474
  const keyArg = keyProp ? `, ${transformKeyValue(keyProp.value, transformExpr)}` : "";
5371
- return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${renderChildScopeArgs(node, keyArg)})}`;
5475
+ return interp(renderChildCall(nameForRegistryRef(node.name), propsExpr, renderChildScopeArgs(node, keyArg)));
5372
5476
  }
5373
5477
  case "loop": {
5374
5478
  const childScope = (opts.scope ?? BindingScope.EMPTY).enterLoopRow(node);
@@ -5402,19 +5506,19 @@ function generateCsrTemplateWithOpts(node, opts) {
5402
5506
  // Leaf `key` stripped — see the irToHtmlTemplate site above.
5403
5507
  renderLeaf: (ir) => recurseInLoopBody(stripLeafKeyAttr(ir))
5404
5508
  });
5405
- mapExpr = `\${${iterArrayExpr}.flatMap(${node.flatMapCallback.params} => ${body2}).join('')}`;
5509
+ mapExpr = interp(mappedRowsMarkup(iterArrayExpr, "flatMap", node.flatMapCallback.params, body2));
5406
5510
  } else if (node.preamble) {
5407
5511
  const preamble = renderPreamble(node.preamble, { textVariant: "template", transformJs: (t) => rewritePropsObjectRef(t, propsObjectName ?? null, restPropsName ?? null, { enclosingScope: childScope }), renderLeaf: (ir) => recurseInLoopBody(ir) });
5408
- mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`;
5512
+ mapExpr = interp(mappedRowsMarkup(iterArrayExpr, iterMethod, callbackParam, `{ ${preamble} return \`${childTemplate}\` }`));
5409
5513
  } else {
5410
- mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`;
5514
+ mapExpr = interp(mappedRowsMarkup(iterArrayExpr, iterMethod, callbackParam, `\`${childTemplate}\``));
5411
5515
  }
5412
5516
  return `<!--${loopStartMarker(node.markerId)}-->${mapExpr}<!--${loopEndMarker(node.markerId)}-->`;
5413
5517
  }
5414
5518
  case "if-statement": {
5415
5519
  const consequent = recurse(node.consequent);
5416
5520
  const alternate = node.alternate ? recurse(node.alternate) : "";
5417
- return `\${${transformExpr(node.condition, node.templateCondition)} ? \`${consequent}\` : \`${alternate}\`}`;
5521
+ return interp(conditionalMarkup(transformExpr(node.condition, node.templateCondition), consequent, alternate));
5418
5522
  }
5419
5523
  case "provider":
5420
5524
  case "async":
@@ -5453,6 +5557,8 @@ var init_html_template = __esm({
5453
5557
  init_loop_chain();
5454
5558
  init_child_scope();
5455
5559
  init_binding_scope();
5560
+ init_safe_html();
5561
+ init_markup_slots();
5456
5562
  VOID_ELEMENTS = /* @__PURE__ */ new Set([
5457
5563
  "area",
5458
5564
  "base",
@@ -5560,7 +5666,6 @@ function isNonValuePosition(n, parent2) {
5560
5666
  function collectAstPropRefs(node, propNames, out) {
5561
5667
  walkWithScope(node, (n, parent2, shadowed) => {
5562
5668
  if (shadowed || !propNames.has(n.text)) return;
5563
- if (parent2 && ts7.isShorthandPropertyAssignment(parent2) && parent2.name === n) return;
5564
5669
  if (isNonValuePosition(n, parent2)) return;
5565
5670
  out.add(n.text);
5566
5671
  });
@@ -9745,6 +9850,9 @@ var init_analyzer = __esm({
9745
9850
  "isSSRPortal",
9746
9851
  "findSiblingSlot",
9747
9852
  "cleanupPortalPlaceholder",
9853
+ // Floating-element position tracking (#2848) — same runtime-only shape
9854
+ // as the portal entries above.
9855
+ "trackPosition",
9748
9856
  // Request-scoped environment signal factory (router v0.5) — `createSignal`-
9749
9857
  // shaped, recognised structurally (#2057) so its getter is just a signal
9750
9858
  // getter; the compiler lowers the reader value per adapter via the signal's
@@ -9773,7 +9881,8 @@ var init_analyzer = __esm({
9773
9881
  "createPortal",
9774
9882
  "isSSRPortal",
9775
9883
  "findSiblingSlot",
9776
- "cleanupPortalPlaceholder"
9884
+ "cleanupPortalPlaceholder",
9885
+ "trackPosition"
9777
9886
  ]);
9778
9887
  REACTIVE_PRIMITIVES = /* @__PURE__ */ new Set([
9779
9888
  "createSignal",
@@ -10908,9 +11017,8 @@ function collectBranchLocalPropRefsViaSubstitution(node, ctx2) {
10908
11017
  function visit3(n, parent2) {
10909
11018
  if (ts14.isIdentifier(n) && propDepsMap.has(n.text)) {
10910
11019
  const isObjectKey = parent2 && ts14.isPropertyAssignment(parent2) && parent2.name === n;
10911
- const isShorthand = parent2 && ts14.isShorthandPropertyAssignment(parent2) && parent2.name === n;
10912
11020
  const isAccessName = parent2 && ts14.isPropertyAccessExpression(parent2) && parent2.name === n;
10913
- if (!isObjectKey && !isShorthand && !isAccessName) {
11021
+ if (!isObjectKey && !isAccessName) {
10914
11022
  const deps = propDepsMap.get(n.text);
10915
11023
  if (deps && deps.size > 0) {
10916
11024
  if (!acc) acc = /* @__PURE__ */ new Set();
@@ -11355,12 +11463,7 @@ function lowerFormControlValueSsr(tagName2, attrs, children2) {
11355
11463
  children2.push({
11356
11464
  type: "expression",
11357
11465
  expr,
11358
- // Escaped for client string-building; `expr` stays raw since SSR
11359
- // engines escape text children natively.
11360
- templateExpr: `escapeText(${templateExpr ?? expr})`,
11361
- // Init-scope builders can't just swap in `templateExpr` (its `_p.`
11362
- // binding differs) — see `escapeInClientTemplate`'s docstring.
11363
- escapeInClientTemplate: true,
11466
+ templateExpr,
11364
11467
  typeInfo: null,
11365
11468
  reactive: false,
11366
11469
  slotId: null,
@@ -11377,24 +11480,75 @@ function lowerFormControlValueSsr(tagName2, attrs, children2) {
11377
11480
  `(${expr}) === (${optExpr})`,
11378
11481
  templateExpr !== void 0 || optTemplateExpr !== void 0 ? { templateExpr: `(${templateExpr ?? expr}) === (${optTemplateExpr ?? optExpr})` } : void 0
11379
11482
  );
11483
+ const matchConditions = [];
11484
+ let optionSetIsDynamic = false;
11380
11485
  const distribute = (nodes) => {
11381
11486
  for (const n of nodes) {
11487
+ if (n.type === "text") continue;
11382
11488
  if (n.type === "element" && n.tag === "option") {
11383
- if (n.attrs.some((a) => a.name === "selected")) continue;
11489
+ if (n.attrs.some((a) => a.name === "selected")) {
11490
+ optionSetIsDynamic = true;
11491
+ continue;
11492
+ }
11384
11493
  const optValue = n.attrs.find((a) => a.name === "value");
11385
- if (!optValue) continue;
11494
+ if (!optValue) {
11495
+ optionSetIsDynamic = true;
11496
+ continue;
11497
+ }
11386
11498
  if (optValue.value.kind === "literal") {
11387
- n.attrs.push({ name: "selected", value: selectedForLiteral(optValue.value.value), loc: n.loc });
11499
+ const selected = selectedForLiteral(optValue.value.value);
11500
+ n.attrs.push({ name: "selected", value: selected, loc: n.loc });
11501
+ matchConditions.push(selected);
11388
11502
  } else if (optValue.value.kind === "expression") {
11389
11503
  const selected = selectedForExpr(optValue.value.expr, optValue.value.templateExpr);
11390
11504
  n.attrs.push({ name: "selected", value: selected, loc: n.loc });
11505
+ matchConditions.push(selected);
11506
+ } else {
11507
+ optionSetIsDynamic = true;
11391
11508
  }
11392
- } else if (n.type === "fragment" || n.type === "loop" || n.type === "element" && n.tag === "optgroup") {
11509
+ } else if (n.type === "fragment" || n.type === "element" && n.tag === "optgroup") {
11393
11510
  distribute(n.children);
11511
+ } else if (n.type === "loop") {
11512
+ optionSetIsDynamic = true;
11513
+ distribute(n.children);
11514
+ } else {
11515
+ optionSetIsDynamic = true;
11394
11516
  }
11395
11517
  }
11396
11518
  };
11397
11519
  distribute(children2);
11520
+ if (optionSetIsDynamic || matchConditions.length === 0) return;
11521
+ if (isMultiSelection(attrs)) return;
11522
+ const orExpr = matchConditions.map((c) => `(${c.expr})`).join(" || ");
11523
+ const orTemplateExpr = matchConditions.some((c) => c.templateExpr !== void 0) ? matchConditions.map((c) => `(${c.templateExpr ?? c.expr})`).join(" || ") : void 0;
11524
+ children2.unshift({
11525
+ type: "element",
11526
+ tag: "option",
11527
+ attrs: [
11528
+ { name: "value", value: AttrValueOf.literal(""), loc: valueAttr.loc },
11529
+ { name: "disabled", value: AttrValueOf.booleanAttr(), loc: valueAttr.loc },
11530
+ { name: "hidden", value: AttrValueOf.booleanAttr(), loc: valueAttr.loc },
11531
+ {
11532
+ name: "selected",
11533
+ value: AttrValueOf.expression(
11534
+ `!(${orExpr})`,
11535
+ orTemplateExpr !== void 0 ? { templateExpr: `!(${orTemplateExpr})` } : void 0
11536
+ ),
11537
+ loc: valueAttr.loc
11538
+ }
11539
+ ],
11540
+ events: [],
11541
+ ref: null,
11542
+ children: [],
11543
+ slotId: null,
11544
+ needsScope: false,
11545
+ loc: valueAttr.loc
11546
+ });
11547
+ }
11548
+ function isMultiSelection(attrs) {
11549
+ if (attrs.some((a) => a.name === "multiple")) return true;
11550
+ const sizeAttr = attrs.find((a) => a.name === "size");
11551
+ return sizeAttr?.value.kind === "literal" && Number(sizeAttr.value.value) > 1;
11398
11552
  }
11399
11553
  function transformHtmlElement(node, ctx2, tagName2) {
11400
11554
  const { attrs, events, ref } = processAttributes(
@@ -15732,7 +15886,7 @@ function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx2, options2
15732
15886
  },
15733
15887
  loop: ({ node: n, scope, descend }) => {
15734
15888
  const emitDepth = fixedDepth ?? scope.depth + 1;
15735
- const loopParamsForTemplate = outerLoopParam ? [outerLoopParam, { param: n.param, bindings: n.paramBindings }] : void 0;
15889
+ const loopParamsForTemplate = outerLoopParam ? [outerLoopParam, { param: n.param, bindings: n.paramBindings, index: n.index }] : void 0;
15736
15890
  const template = n.children.map((c) => irToPlaceholderTemplate(c, void 0, emitDepth, loopParamsForTemplate)).join("");
15737
15891
  const refsOuter = outerLoopParam ? identifierPattern(outerLoopParam).test(n.array) : false;
15738
15892
  const bindings = emptyLoopChildBindings();
@@ -16320,7 +16474,7 @@ function collectLoopChildConditionals(node, ctx2, siblingOffsets, loopParam, loo
16320
16474
  const expanded = expandConstantForReactivity(n.condition, ctx2, sourceFreeIds, scope);
16321
16475
  const readsPreamble = preambleNames !== void 0 && preambleNames.size > 0 && anyNameIn(expanded.freeIds ?? extractFreeIdentifiersFromText(expanded.expr), preambleNames);
16322
16476
  if (!readsPreamble && classifyReactivity(expanded.expr, ctx2, loopParam, loopParamBindings, expanded.freeIds).kind === "none") return;
16323
- const loopParamsForCond = loopParam ? [{ param: loopParam, bindings: loopParamBindings }] : void 0;
16477
+ const loopParamsForCond = loopParam ? [{ param: loopParam, bindings: loopParamBindings, index: loopIndex }] : void 0;
16324
16478
  const whenTrueHtml = irToHtmlTemplate(n.whenTrue, void 0, 0, loopParamsForCond, "__slots");
16325
16479
  const whenFalseHtml = irToHtmlTemplate(n.whenFalse, void 0, 0, loopParamsForCond, "__slots");
16326
16480
  conditionals.push({
@@ -17166,6 +17320,9 @@ var init_imports = __esm({
17166
17320
  "escapeAttr",
17167
17321
  "escapeText",
17168
17322
  "escapeTextOrNode",
17323
+ // Whole-item loop conditional's `bf-loop-i:<key>` anchor comment (#1665,
17324
+ // #2795 follow-up) — neutralizes `-` so a key can't close the comment early.
17325
+ "escapeCommentText",
17169
17326
  // JSX-element-as-non-children-prop markup brand (#2651) — `bfMarkup` wraps
17170
17327
  // the compiler-built HTML at the producer (renderChild / initChild props);
17171
17328
  // `escapeTextOrMarkup` unwraps it at the claim-plan-'markup' template slot.
@@ -17213,45 +17370,6 @@ var init_imports = __esm({
17213
17370
  }
17214
17371
  });
17215
17372
 
17216
- // ../jsx/src/lowering-registry.ts
17217
- function registerLoweringPlugin(plugin) {
17218
- const existing = plugins.findIndex((p) => p.name === plugin.name);
17219
- if (existing >= 0) plugins[existing] = plugin;
17220
- else plugins.push(plugin);
17221
- }
17222
- function getLoweringPlugins() {
17223
- return [...plugins];
17224
- }
17225
- function prepareLoweringMatchers(metadata) {
17226
- const matchers = [];
17227
- for (const plugin of plugins) {
17228
- const matcher = plugin.prepare(metadata);
17229
- if (matcher) matchers.push(matcher);
17230
- }
17231
- return matchers;
17232
- }
17233
- function matchLoweringCall(callee, args2, metadata) {
17234
- for (const matcher of prepareLoweringMatchers(metadata)) {
17235
- const node = matcher(callee, args2);
17236
- if (node) return node;
17237
- }
17238
- return null;
17239
- }
17240
- function isValidHelperId(helper) {
17241
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(helper);
17242
- }
17243
- function __resetLoweringPluginsForTest(next = []) {
17244
- plugins.length = 0;
17245
- plugins.push(...next);
17246
- }
17247
- var plugins;
17248
- var init_lowering_registry = __esm({
17249
- "../jsx/src/lowering-registry.ts"() {
17250
- "use strict";
17251
- plugins = [];
17252
- }
17253
- });
17254
-
17255
17373
  // ../jsx/src/relocate.ts
17256
17374
  import ts18 from "typescript";
17257
17375
  function classify(name2, env) {
@@ -18087,7 +18205,7 @@ function emitRegistrationAndHydration(lines, ctx2, _ir, graph, inlinability) {
18087
18205
  const isCommentScope = isFragmentRoot || _ir.root.type === "component";
18088
18206
  const defParts = [`init: init${name2}`];
18089
18207
  if (canGenerateStaticTemplate(_ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
18090
- const markupSlotIds = new Set(ctx2.dynamicElements.map((e) => e.slotId));
18208
+ const markupSlotIds = markupSlotIdsOf(ctx2);
18091
18209
  const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx2.propsObjectName, markupSlotIds, ctx2.restPropsName);
18092
18210
  if (templateHtml) {
18093
18211
  defParts.push(buildTemplateDefPart(ctx2, templateHtml));
@@ -18128,6 +18246,7 @@ var init_emit_registration = __esm({
18128
18246
  init_utils();
18129
18247
  init_compute_inlinability();
18130
18248
  init_html_template();
18249
+ init_markup_slots();
18131
18250
  init_component_scope();
18132
18251
  init_prop_handling();
18133
18252
  }
@@ -19170,11 +19289,11 @@ function nestedLoopIndexAlias(inner, syntheticIndexVar, paramHead, comps, events
19170
19289
  if (!nestedLoopReferencesIndex(inner, comps, events)) return null;
19171
19290
  return `const ${index} = ${syntheticIndexVar}`;
19172
19291
  }
19173
- function buildChildRefBindings(refs, loopParam, loopParamBindings) {
19292
+ function buildChildRefBindings(refs, loopParam, loopParamBindings, loopIndex) {
19174
19293
  if (refs.length === 0) return [];
19175
19294
  return refs.map((r2) => ({
19176
19295
  childSlotId: r2.childSlotId,
19177
- callback: wrapLoopParamAsAccessor(r2.callback, loopParam, loopParamBindings)
19296
+ callback: wrapLoopParamAsAccessor(r2.callback, loopParam, loopParamBindings, loopIndex)
19178
19297
  }));
19179
19298
  }
19180
19299
  function buildStaticChildRefBindings(refs) {
@@ -19205,16 +19324,16 @@ function destructureLoopParam(param, paramBindings) {
19205
19324
  }
19206
19325
  return { head: param, unwrap: "" };
19207
19326
  }
19208
- function buildPreambleRegionPlans(regions, loopParam, loopParamBindings) {
19327
+ function buildPreambleRegionPlans(regions, loopParam, loopParamBindings, loopIndex) {
19209
19328
  if (!regions || regions.length === 0) return [];
19210
19329
  return regions.map((r2) => {
19211
- const wrapped = wrapLoopParamAsAccessor(r2.expr, loopParam, loopParamBindings);
19212
- const valueExpr = r2.joinArrayChild ? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')` : `escapeText(${wrapped})`;
19330
+ const wrapped = wrapLoopParamAsAccessor(r2.expr, loopParam, loopParamBindings, loopIndex);
19331
+ const valueExpr = spliceChildValue({ expr: r2.expr, slotId: r2.slotId, joinArrayChild: r2.joinArrayChild }, wrapped, {});
19213
19332
  return { slotId: r2.slotId, valueExpr };
19214
19333
  });
19215
19334
  }
19216
- function buildComponentPropsExpr2(comp, loopParam, loopParamBindings) {
19217
- const wrap = loopParam ? (expr) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings) : (expr) => expr;
19335
+ function buildComponentPropsExpr2(comp, loopParam, loopParamBindings, loopIndex) {
19336
+ const wrap = loopParam ? (expr) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings, loopIndex) : (expr) => expr;
19218
19337
  const entries2 = comp.props.map((p) => {
19219
19338
  if (p.isEventHandler) {
19220
19339
  const handlerExpr = attrValueToString(p.value) ?? "undefined";
@@ -19263,8 +19382,8 @@ function buildDepthLevels(innerLoops, nestedComps, childEvents) {
19263
19382
  loopInfo: loop
19264
19383
  }));
19265
19384
  }
19266
- function emitEventSetup(ls, indent, elVar, ev, loopParam, loopParamBindings, bodyIsMultiRoot = false) {
19267
- const handler = loopParam ? wrapLoopParamAsAccessor(ev.handler, loopParam, loopParamBindings) : ev.handler;
19385
+ function emitEventSetup(ls, indent, elVar, ev, loopParam, loopParamBindings, bodyIsMultiRoot = false, loopIndex) {
19386
+ const handler = loopParam ? wrapLoopParamAsAccessor(ev.handler, loopParam, loopParamBindings, loopIndex) : ev.handler;
19268
19387
  emitListenerBlock(ls, indent, elVar, ev.childSlotId, "__e", ev.eventName, handler, "dom", bodyIsMultiRoot);
19269
19388
  }
19270
19389
  function buildCompSelector(comp) {
@@ -19275,15 +19394,15 @@ function isTextOnlyConditional(node) {
19275
19394
  const checkNode = (n) => n.type === "text" || n.type === "expression" || n.type === "conditional" && isTextOnlyConditional(n);
19276
19395
  return checkNode(node.whenTrue) && checkNode(node.whenFalse);
19277
19396
  }
19278
- function emitComponentAndEventSetup(ls, indent, elVar, comps, events, loopParam, loopParamBindings, bodyIsMultiRoot = false) {
19279
- const wrap = loopParam ? (expr) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings) : (expr) => expr;
19397
+ function emitComponentAndEventSetup(ls, indent, elVar, comps, events, loopParam, loopParamBindings, bodyIsMultiRoot = false, loopIndex) {
19398
+ const wrap = loopParam ? (expr) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings, loopIndex) : (expr) => expr;
19280
19399
  const upsertFn = bodyIsMultiRoot ? "upsertChildItem" : "upsertChild";
19281
19400
  for (const comp of comps) {
19282
- const propsExpr = buildComponentPropsExpr2(comp, loopParam, loopParamBindings);
19401
+ const propsExpr = buildComponentPropsExpr2(comp, loopParam, loopParamBindings, loopIndex);
19283
19402
  const isTextOnly = comp.children?.length ? comp.children.every((c) => c.type === "expression" || c.type === "text" || isTextOnlyConditional(c)) : false;
19284
19403
  const rawChildrenExpr = isTextOnly ? irChildrenToJsExpr(comp.children) : null;
19285
19404
  const childrenFreeIds = isTextOnly && comp.children ? irChildrenFreeIds(comp.children) : void 0;
19286
- const childrenRefsLoop = loopParam != null && rawChildrenExpr != null && childrenFreeIds != null && exprRefsLoopBinding(childrenFreeIds, { param: loopParam, paramBindings: loopParamBindings });
19405
+ const childrenRefsLoop = loopParam != null && rawChildrenExpr != null && childrenFreeIds != null && (exprRefsLoopBinding(childrenFreeIds, { param: loopParam, paramBindings: loopParamBindings }) || !!loopIndex && childrenFreeIds.has(loopIndex));
19287
19406
  const slotIdLit = comp.slotId ? `'${comp.slotId}'` : "null";
19288
19407
  const keyProp = comp.props.find((p) => p.name === "key");
19289
19408
  const keyArg = keyProp ? `, ${wrap(attrValueToString(keyProp.value) ?? "undefined")}` : ", undefined";
@@ -19296,7 +19415,7 @@ function emitComponentAndEventSetup(ls, indent, elVar, comps, events, loopParam,
19296
19415
  }
19297
19416
  }
19298
19417
  for (const ev of events) {
19299
- emitEventSetup(ls, indent, elVar, ev, loopParam, loopParamBindings, bodyIsMultiRoot);
19418
+ emitEventSetup(ls, indent, elVar, ev, loopParam, loopParamBindings, bodyIsMultiRoot, loopIndex);
19300
19419
  }
19301
19420
  }
19302
19421
  var init_shared = __esm({
@@ -19305,6 +19424,7 @@ var init_shared = __esm({
19305
19424
  init_types();
19306
19425
  init_utils();
19307
19426
  init_html_template();
19427
+ init_safe_html();
19308
19428
  init_event_listener();
19309
19429
  init_component_scope();
19310
19430
  init_src();
@@ -19712,12 +19832,12 @@ function buildBranchInnerLoopsPlan(args2) {
19712
19832
  for (let i = 0; i < innerLoops.length; i++) {
19713
19833
  const inner = innerLoops[i];
19714
19834
  if (!inner.refsOuterParam || !inner.template) continue;
19715
- const wrapInner = (expr) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings);
19716
- const wrapBoth = (expr) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings);
19835
+ const wrapInner = (expr) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index);
19836
+ const wrapBoth = (expr) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index);
19717
19837
  const csl = inner.containerSlotId;
19718
19838
  const containerExpr = csl ? `(${scopeVar}.querySelector('[bf="${csl}"]') ?? ${scopeVar}.querySelector(\`[${BF_HOST}="\${__scopeId}"][${BF_AT}="${csl}"]\`) ?? ${scopeVar})` : `findCondContainer(${scopeVar}, '${condSlotId}')`;
19719
19839
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings);
19720
- const wrappedKey = inner.key ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings) : null;
19840
+ const wrappedKey = inner.key ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings, inner.index) : null;
19721
19841
  const wrapIRNode = (node) => {
19722
19842
  if (node.type === "component") {
19723
19843
  return {
@@ -19894,8 +20014,8 @@ var init_build_loop_child_arm = __esm({
19894
20014
 
19895
20015
  // ../jsx/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts
19896
20016
  function buildReactiveEffectsPlan(args2) {
19897
- const { attrs, texts, conditionals, loopParam, loopParamBindings, profileComponentName } = args2;
19898
- const wrap = (expr) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings);
20017
+ const { attrs, texts, conditionals, loopParam, loopParamBindings, loopIndex, profileComponentName } = args2;
20018
+ const wrap = (expr) => wrapLoopParamAsAccessor(expr, loopParam, loopParamBindings, loopIndex);
19899
20019
  const attrsBySlot = /* @__PURE__ */ new Map();
19900
20020
  for (const attr of attrs) {
19901
20021
  let bucket = attrsBySlot.get(attr.childSlotId);
@@ -19979,6 +20099,7 @@ function buildLoopReactiveEffectsPlan(elem, profileComponentName) {
19979
20099
  conditionals: elem.bindings.conditionals,
19980
20100
  loopParam: elem.param,
19981
20101
  loopParamBindings: elem.paramBindings,
20102
+ loopIndex: elem.index,
19982
20103
  profileComponentName
19983
20104
  });
19984
20105
  }
@@ -20067,10 +20188,10 @@ function buildInnerLoopsPlan(args2) {
20067
20188
  return plan;
20068
20189
  }
20069
20190
  function buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings) {
20070
- const wrapInner = (expr) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings);
20071
- const wrapBoth = (expr) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings);
20191
+ const wrapInner = (expr) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index);
20192
+ const wrapBoth = (expr) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index);
20072
20193
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings);
20073
- const wrappedKey = inner.key ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings) : null;
20194
+ const wrappedKey = inner.key ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings, inner.index) : null;
20074
20195
  const wrapIRNode = (node) => {
20075
20196
  if (node.type === "component") {
20076
20197
  return {
@@ -20101,11 +20222,11 @@ function buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, o
20101
20222
  }));
20102
20223
  const reactiveTexts = inner.bindings.reactiveTexts.map((text) => ({
20103
20224
  slotId: text.slotId,
20104
- wrappedExpression: wrapLoopParamAsAccessor(wrapOuter(text.expression), inner.param, inner.paramBindings),
20225
+ wrappedExpression: wrapLoopParamAsAccessor(wrapOuter(text.expression), inner.param, inner.paramBindings, inner.index),
20105
20226
  insideConditional: !!text.insideConditional
20106
20227
  }));
20107
20228
  const reactiveAttrs = inner.bindings.reactiveAttrs.map((attr) => {
20108
- const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings);
20229
+ const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings, inner.index);
20109
20230
  return {
20110
20231
  slotId: attr.childSlotId,
20111
20232
  attrName: attr.attrName,
@@ -20120,14 +20241,14 @@ function buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, o
20120
20241
  if (inner.preamble) {
20121
20242
  const leafLoopParams = outerLoopParam ? [
20122
20243
  { param: outerLoopParam, bindings: outerLoopParamBindings },
20123
- { param: inner.param, bindings: inner.paramBindings }
20124
- ] : [{ param: inner.param, bindings: inner.paramBindings }];
20244
+ { param: inner.param, bindings: inner.paramBindings, index: inner.index }
20245
+ ] : [{ param: inner.param, bindings: inner.paramBindings, index: inner.index }];
20125
20246
  preludeStatements.push(renderPreamble(inner.preamble, {
20126
20247
  transformJs: (t) => wrapInner(wrapOuter(t)),
20127
20248
  renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, leafLoopParams, void 0)
20128
20249
  }));
20129
20250
  }
20130
- const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings);
20251
+ const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings, inner.index);
20131
20252
  const conditionals = buildLoopChildConditionalsPlan({
20132
20253
  conditionals: inner.bindings.conditionals,
20133
20254
  scopeVar: `__innerEl${uidSuffix}`,
@@ -20186,7 +20307,7 @@ function buildTopLevelCompositePlan(elem, profileComponentName) {
20186
20307
  const nestedComps = elem.nestedComponents;
20187
20308
  const depthLevels = buildDepthLevels(elem.innerLoops ?? [], nestedComps, elem.bindings.events);
20188
20309
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings);
20189
- const wrap = (expr) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings);
20310
+ const wrap = (expr) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings, elem.index);
20190
20311
  const outerCompsByDepth = nestedComps.filter((c) => !c.loopDepth || c.loopDepth === 0);
20191
20312
  return {
20192
20313
  kind: "composite",
@@ -20200,12 +20321,12 @@ function buildTopLevelCompositePlan(elem, profileComponentName) {
20200
20321
  indexParam: elem.index || "__idx",
20201
20322
  mapPreambleWrapped: elem.preamble ? renderPreamble(elem.preamble, {
20202
20323
  transformJs: wrap,
20203
- renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings }], void 0)
20324
+ renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings, index: elem.index }], void 0)
20204
20325
  }) : "",
20205
20326
  template: elem.template,
20206
20327
  outerComps: filterCondCompsOut(outerCompsByDepth, elem.bindings.conditionals),
20207
20328
  outerEvents: elem.bindings.events.filter((ev) => ev.nestedLoops.length === 0),
20208
- childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
20329
+ childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings, elem.index),
20209
20330
  innerLoops: buildInnerLoopsPlan({
20210
20331
  levels: depthLevels,
20211
20332
  parentElVar: "__el",
@@ -20214,12 +20335,14 @@ function buildTopLevelCompositePlan(elem, profileComponentName) {
20214
20335
  }),
20215
20336
  loopParam: elem.param,
20216
20337
  loopParamBindings: elem.paramBindings,
20338
+ loopIndex: elem.index,
20217
20339
  reactiveEffects: hasReactive(elem) ? buildReactiveEffectsPlan({
20218
20340
  attrs: elem.bindings.reactiveAttrs,
20219
20341
  texts: elem.bindings.reactiveTexts,
20220
20342
  conditionals: elem.bindings.conditionals,
20221
20343
  loopParam: elem.param,
20222
20344
  loopParamBindings: elem.paramBindings,
20345
+ loopIndex: elem.index,
20223
20346
  profileComponentName
20224
20347
  }) : null,
20225
20348
  branchClearChildren: false,
@@ -20236,7 +20359,7 @@ function buildBranchCompositePlan(loop, cv, profileComponentName) {
20236
20359
  const childEvents = loop.bindings.events;
20237
20360
  const depthLevels = buildDepthLevels(innerLoops, nestedComps, childEvents);
20238
20361
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings);
20239
- const wrap = (expr) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings);
20362
+ const wrap = (expr) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings, loop.index);
20240
20363
  const outerCompsByDepth = nestedComps.filter((c) => !c.loopDepth || c.loopDepth === 0);
20241
20364
  return {
20242
20365
  kind: "composite",
@@ -20253,12 +20376,12 @@ function buildBranchCompositePlan(loop, cv, profileComponentName) {
20253
20376
  indexParam: loop.index || "__idx",
20254
20377
  mapPreambleWrapped: loop.preamble ? renderPreamble(loop.preamble, {
20255
20378
  transformJs: wrap,
20256
- renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: loop.param, bindings: loop.paramBindings }], void 0)
20379
+ renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: loop.param, bindings: loop.paramBindings, index: loop.index }], void 0)
20257
20380
  }) : "",
20258
20381
  template: loop.template,
20259
20382
  outerComps: filterCondCompsOut(outerCompsByDepth, loop.bindings.conditionals),
20260
20383
  outerEvents: childEvents.filter((ev) => ev.nestedLoops.length === 0),
20261
- childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
20384
+ childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings, loop.index),
20262
20385
  innerLoops: buildInnerLoopsPlan({
20263
20386
  levels: depthLevels,
20264
20387
  parentElVar: "__el",
@@ -20267,12 +20390,14 @@ function buildBranchCompositePlan(loop, cv, profileComponentName) {
20267
20390
  }),
20268
20391
  loopParam: loop.param,
20269
20392
  loopParamBindings: loop.paramBindings,
20393
+ loopIndex: loop.index,
20270
20394
  reactiveEffects: hasReactiveBranch(loop) ? buildReactiveEffectsPlan({
20271
20395
  attrs: loop.bindings.reactiveAttrs,
20272
20396
  texts: loop.bindings.reactiveTexts,
20273
20397
  conditionals: loop.bindings.conditionals,
20274
20398
  loopParam: loop.param,
20275
20399
  loopParamBindings: loop.paramBindings,
20400
+ loopIndex: loop.index,
20276
20401
  profileComponentName
20277
20402
  }) : null,
20278
20403
  branchClearChildren: true,
@@ -20433,7 +20558,7 @@ function wiringOn(branch) {
20433
20558
  if (branch.reactiveTexts && branch.reactiveTexts.length > 0) found.push("reactive text");
20434
20559
  return found;
20435
20560
  }
20436
- function analyzeLazyConditional(cond, indexParam, arms) {
20561
+ function analyzeLazyConditional(cond, arms) {
20437
20562
  for (const [label2, branch] of [["true", cond.whenTrue], ["false", cond.whenFalse]]) {
20438
20563
  const wiring = wiringOn(branch);
20439
20564
  if (wiring.length > 0) {
@@ -20454,9 +20579,6 @@ function analyzeLazyConditional(cond, indexParam, arms) {
20454
20579
  if (!cond.conditionFreeIdentifiers) {
20455
20580
  return NO(`conditional on slot ${cond.slotId}: condition has no analyzable identifier set`);
20456
20581
  }
20457
- if (cond.conditionFreeIdentifiers.has(indexParam)) {
20458
- return NO(`conditional on slot ${cond.slotId}: condition reads the loop index parameter '${indexParam}'`);
20459
- }
20460
20582
  return {
20461
20583
  lazySafe: true,
20462
20584
  facts: {
@@ -20477,7 +20599,7 @@ var init_lazy_conditional = __esm({
20477
20599
 
20478
20600
  // ../jsx/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts
20479
20601
  import ts20 from "typescript";
20480
- function analyzeLazyPreamble(preamble, indexParam, primableNames) {
20602
+ function analyzeLazyPreamble(preamble, primableNames) {
20481
20603
  if (!preamble) return NO_PREAMBLE;
20482
20604
  if (preamble.builderNames.length > 0) {
20483
20605
  return NO2(`map-callback preamble accumulates JSX leaves (${preamble.builderNames.join(", ")})`);
@@ -20519,9 +20641,6 @@ function analyzeLazyPreamble(preamble, indexParam, primableNames) {
20519
20641
  }
20520
20642
  }
20521
20643
  const readNames = extractFreeIdentifiersFromStatementText(text);
20522
- if (readNames.has(indexParam) && !declaredNames.has(indexParam)) {
20523
- return NO2(`map-callback preamble reads the loop index parameter '${indexParam}'`);
20524
- }
20525
20644
  const freeNames = new Set(readNames);
20526
20645
  for (const declared of declaredNames) freeNames.delete(declared);
20527
20646
  return { lazySafe: true, facts: { declaredNames, freeNames } };
@@ -20625,9 +20744,6 @@ function lazyRowEligibility(args2) {
20625
20744
  if (shape.preambleRegionCount > 0) return NO3("row has preamble-patched regions");
20626
20745
  if (shape.hasParamUnwrap) return NO3("destructured loop param without param bindings");
20627
20746
  for (const b of bindings) {
20628
- if (b.referencesIndex) {
20629
- return NO3(`binding on slot ${b.slotId} references the loop index parameter`);
20630
- }
20631
20747
  if (b.opaqueOuterNames.includes(UNKNOWN_IDENTIFIERS)) {
20632
20748
  return NO3(`binding on slot ${b.slotId} has no analyzable identifier set`);
20633
20749
  }
@@ -20708,6 +20824,7 @@ function classifyLazyBinding(args2) {
20708
20824
  }
20709
20825
  if (name2 === indexParam) {
20710
20826
  referencesIndex = true;
20827
+ readsItem = true;
20711
20828
  return;
20712
20829
  }
20713
20830
  if (INERT_BINDING_GLOBALS.has(name2)) return;
@@ -20801,12 +20918,12 @@ function decideLazyRow(args2) {
20801
20918
  const rowLocalNames = /* @__PURE__ */ new Set([loop.param]);
20802
20919
  for (const b of loop.paramBindings ?? []) rowLocalNames.add(b.name);
20803
20920
  const primableNames = /* @__PURE__ */ new Set([...scope.signals.keys(), ...scope.memos]);
20804
- const preambleAnalysis = analyzeLazyPreamble(loop.preamble, args2.indexParam, primableNames);
20921
+ const preambleAnalysis = analyzeLazyPreamble(loop.preamble, primableNames);
20805
20922
  const rawConditionals = loop.bindings.conditionals ?? [];
20806
20923
  const condFacts = [];
20807
20924
  let conditionalRefusal = null;
20808
20925
  for (const cond of rawConditionals) {
20809
- const verdict = analyzeLazyConditional(cond, args2.indexParam, {
20926
+ const verdict = analyzeLazyConditional(cond, {
20810
20927
  whenTrueHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
20811
20928
  whenFalseHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId)
20812
20929
  });
@@ -20957,7 +21074,12 @@ function decideLazyRow(args2) {
20957
21074
  // into `applyItem` above) is counted in the body it actually lands in.
20958
21075
  itemNeedsPreamble: [...attrs, ...texts, ...conditionals].some((b) => b.readsItem && b.readsPreamble),
20959
21076
  outerNeedsPreamble: [...attrs, ...texts, ...conditionals].some((b) => b.readsOuter && b.readsPreamble),
20960
- conditionals
21077
+ conditionals,
21078
+ // From the RAW classification, not the final lists: a binding that
21079
+ // reads the index is always `readsItem` (see `classifyLazyBinding`),
21080
+ // so checking the final lists would say the same thing — raw is just
21081
+ // the more direct source of truth.
21082
+ readsIndex: classified.some((c) => c.referencesIndex)
20961
21083
  },
20962
21084
  decision
20963
21085
  };
@@ -21016,6 +21138,52 @@ var init_build_lazy_row = __esm({
21016
21138
  }
21017
21139
  });
21018
21140
 
21141
+ // ../jsx/src/ir-to-client-js/control-flow/plan/build-plain-row.ts
21142
+ function buildPlainRowCore(inputs) {
21143
+ const { loop, arrayExpr, callSite, flatMapLeafItem, anchored, scope } = inputs;
21144
+ const wrapItem = (expr) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings);
21145
+ const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings);
21146
+ const indexParam = loop.index || "__idx";
21147
+ const mapPreambleWrapped = loop.preamble ? renderPreamble(loop.preamble, {
21148
+ transformJs: wrapItem,
21149
+ renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: loop.param, bindings: loop.paramBindings }], void 0)
21150
+ }) : "";
21151
+ const preambleRegions = buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings, loop.index);
21152
+ const lazyRow = buildLazyRowPlan({
21153
+ loop,
21154
+ arrayExpr,
21155
+ indexParam,
21156
+ paramUnwrap,
21157
+ mapPreambleWrapped,
21158
+ preambleRegionCount: preambleRegions.length,
21159
+ callSite,
21160
+ flatMapLeafItem,
21161
+ anchored,
21162
+ scope
21163
+ }) ?? void 0;
21164
+ const mapPreambleWrappedFinal = !lazyRow && loop.index ? wrapIndexParamAsAccessor(mapPreambleWrapped, loop.index) : mapPreambleWrapped;
21165
+ const templateFinal = !lazyRow && loop.index ? wrapIndexParamAsAccessor(loop.template, loop.index) : loop.template;
21166
+ return {
21167
+ indexParam,
21168
+ paramHead,
21169
+ paramUnwrap,
21170
+ preambleRegions,
21171
+ lazyRow,
21172
+ mapPreambleWrapped: mapPreambleWrappedFinal,
21173
+ template: templateFinal,
21174
+ wrapItem
21175
+ };
21176
+ }
21177
+ var init_build_plain_row = __esm({
21178
+ "../jsx/src/ir-to-client-js/control-flow/plan/build-plain-row.ts"() {
21179
+ "use strict";
21180
+ init_utils();
21181
+ init_shared();
21182
+ init_html_template();
21183
+ init_build_lazy_row();
21184
+ }
21185
+ });
21186
+
21019
21187
  // ../jsx/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts
21020
21188
  function buildBranchLoopPlan(loop, profileComponentName, lazyScope) {
21021
21189
  const containerSlotId = loop.containerSlotId;
@@ -21030,16 +21198,18 @@ function buildBranchLoopPlan(loop, profileComponentName, lazyScope) {
21030
21198
  };
21031
21199
  return composite;
21032
21200
  }
21033
- const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings);
21034
21201
  const hasReactiveEffects = loop.bindings.reactiveAttrs.length > 0 || loop.bindings.reactiveTexts.length > 0 || loop.bindings.conditionals.length > 0;
21035
21202
  const fm = loop.flatMapClient;
21036
21203
  const arrayExpr = fm ? `(${buildChainedArrayExpr(loop)}).flatMap(${fm.params} => ${fm.body})` : buildChainedArrayExpr(loop);
21037
- const indexParam = loop.index || "__idx";
21038
- const mapPreambleWrapped = loop.preamble ? renderPreamble(loop.preamble, {
21039
- transformJs: (t) => wrapLoopParamAsAccessor(t, loop.param, loop.paramBindings),
21040
- renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: loop.param, bindings: loop.paramBindings }], void 0)
21041
- }) : "";
21042
- const preambleRegions = buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings);
21204
+ const core = buildPlainRowCore({
21205
+ loop,
21206
+ arrayExpr,
21207
+ callSite: "branch-plain",
21208
+ flatMapLeafItem: Boolean(fm),
21209
+ anchored: false,
21210
+ scope: lazyScope
21211
+ });
21212
+ const { paramHead, paramUnwrap, indexParam, preambleRegions, lazyRow } = core;
21043
21213
  const plan = {
21044
21214
  kind: "plain",
21045
21215
  rowConstruction: "string-template",
@@ -21054,31 +21224,21 @@ function buildBranchLoopPlan(loop, profileComponentName, lazyScope) {
21054
21224
  indexParam,
21055
21225
  // Wrap loop-param references to signal-accessor form so the preamble
21056
21226
  // matches the template literal's already-wrapped reads (#1065).
21057
- mapPreambleWrapped,
21227
+ mapPreambleWrapped: core.mapPreambleWrapped,
21058
21228
  // Lazy row graph (§9, L3) — undefined for every ineligible loop.
21059
- lazyRow: buildLazyRowPlan({
21060
- loop,
21061
- arrayExpr,
21062
- indexParam,
21063
- paramUnwrap,
21064
- mapPreambleWrapped,
21065
- preambleRegionCount: preambleRegions.length,
21066
- callSite: "branch-plain",
21067
- flatMapLeafItem: Boolean(fm),
21068
- anchored: false,
21069
- scope: lazyScope
21070
- }) ?? void 0,
21071
- template: loop.template,
21229
+ lazyRow,
21230
+ template: core.template,
21072
21231
  reactiveEffects: hasReactiveEffects ? buildReactiveEffectsPlan({
21073
21232
  attrs: loop.bindings.reactiveAttrs,
21074
21233
  texts: loop.bindings.reactiveTexts,
21075
21234
  conditionals: loop.bindings.conditionals,
21076
21235
  loopParam: loop.param,
21077
21236
  loopParamBindings: loop.paramBindings,
21237
+ loopIndex: loop.index,
21078
21238
  profileComponentName
21079
21239
  }) : null,
21080
21240
  eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
21081
- childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
21241
+ childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings, loop.index),
21082
21242
  preambleRegions,
21083
21243
  bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
21084
21244
  profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : void 0
@@ -21092,9 +21252,8 @@ var init_build_branch_loop = __esm({
21092
21252
  init_build_composite_loop();
21093
21253
  init_build_event_delegation();
21094
21254
  init_build_reactive_effects();
21095
- init_build_lazy_row();
21255
+ init_build_plain_row();
21096
21256
  init_shared();
21097
- init_html_template();
21098
21257
  }
21099
21258
  });
21100
21259
 
@@ -21385,7 +21544,7 @@ function emitDynamicTextUpdates(lines, ctx2) {
21385
21544
  const __textSlot = (normalElems[0] ?? conditionalElems[0])?.slotId;
21386
21545
  let writer = "";
21387
21546
  if (normalElems.length > 0) {
21388
- const slots = normalElems.map((elem) => ({ id: elem.slotId, kind: "markup", path: [] }));
21547
+ const slots = normalElems.map((elem) => ({ id: elem.slotId, kind: DYNAMIC_ELEMENT_WRITER_KIND, path: [] }));
21389
21548
  writer = claimWriterVarName(slots, varSlotId);
21390
21549
  lines.push(` const ${writer} = lazySlots(__scope, ${claimPlanLiteral(slots)})`);
21391
21550
  }
@@ -21537,6 +21696,7 @@ var init_emit_reactive = __esm({
21537
21696
  init_utils();
21538
21697
  init_claim_plan();
21539
21698
  init_html_template();
21699
+ init_markup_slots();
21540
21700
  init_date_lowering();
21541
21701
  init_to_locale_date_lowering();
21542
21702
  init_expression_parser();
@@ -21972,6 +22132,7 @@ function stringifyLazyRowLoop(lines, o) {
21972
22132
  lines.push(`${indent}${o.guardContainer ? `if (${o.containerVar}) ` : ""}${call}`);
21973
22133
  const b1 = `${indent} `;
21974
22134
  const b2 = `${indent} `;
22135
+ if (lazyRow.readsIndex) lines.push(`${b1}indexDriven: true,`);
21975
22136
  lines.push(`${b1}createRow: (__e, ${o.indexParam}) => {`);
21976
22137
  lines.push(`${b2}const ${paramHead} = () => __e.item`);
21977
22138
  if (lazyRow.preambleStatements) lines.push(`${b2}${lazyRow.preambleStatements}`);
@@ -21997,6 +22158,7 @@ function stringifyLazyRowLoop(lines, o) {
21997
22158
  } else {
21998
22159
  lines.push(`${b1}applyItem: (__e) => {`);
21999
22160
  lines.push(`${b2}const ${paramHead} = () => __e.item`);
22161
+ if (lazyRow.readsIndex) lines.push(`${b2}const ${o.indexParam} = __e.index`);
22000
22162
  lines.push(`${b2}const __r = __e.refs ?? (__e.refs = [])`);
22001
22163
  lines.push(`${b2}const __l = __e.last ?? (__e.last = [])`);
22002
22164
  if (lazyRow.itemNeedsPreamble && lazyRow.preambleStatements) {
@@ -22019,6 +22181,7 @@ function stringifyLazyRowLoop(lines, o) {
22019
22181
  for (const g of lazyRow.outerPrimeGetters) lines.push(`${b2}${g}()`);
22020
22182
  lines.push(`${b2}for (const __e of __es) {`);
22021
22183
  lines.push(`${b3}const ${paramHead} = () => __e.item`);
22184
+ if (lazyRow.readsIndex) lines.push(`${b3}const ${o.indexParam} = __e.index`);
22022
22185
  lines.push(`${b3}const __r = __e.refs ?? (__e.refs = [])`);
22023
22186
  lines.push(`${b3}const __l = __e.last ?? (__e.last = [])`);
22024
22187
  if (lazyRow.outerNeedsPreamble && lazyRow.preambleStatements) {
@@ -22574,6 +22737,7 @@ function stringifyCompositeLoop(lines, plan) {
22574
22737
  innerLoops,
22575
22738
  loopParam,
22576
22739
  loopParamBindings,
22740
+ loopIndex,
22577
22741
  reactiveEffects,
22578
22742
  childRefs,
22579
22743
  branchClearChildren,
@@ -22607,7 +22771,7 @@ function stringifyCompositeLoop(lines, plan) {
22607
22771
  // variant whose tail needs the row already connected.
22608
22772
  mountRow: true
22609
22773
  });
22610
- emitComponentAndEventSetup(lines, bodyIndent, "__el", compsArr, eventsArr, loopParam, loopParamBindings, bodyIsMultiRoot);
22774
+ emitComponentAndEventSetup(lines, bodyIndent, "__el", compsArr, eventsArr, loopParam, loopParamBindings, bodyIsMultiRoot, loopIndex);
22611
22775
  if (innerLoops.length > 0) {
22612
22776
  stringifyInnerLoops(lines, innerLoops, bodyIndent, pc);
22613
22777
  }
@@ -23048,11 +23212,11 @@ var init_insert = __esm({
23048
23212
  // ../jsx/src/ir-to-client-js/control-flow/plan/build-component-loop.ts
23049
23213
  function buildComponentLoopPlan(elem, profileComponentName) {
23050
23214
  const { name: name2 } = elem.childComponent;
23051
- const propsExpr = buildComponentPropsExpr2(elem.childComponent, elem.param);
23052
- const keyExpr = wrapLoopParamAsAccessor(elem.key || "__idx", elem.param, elem.paramBindings);
23215
+ const propsExpr = buildComponentPropsExpr2(elem.childComponent, elem.param, void 0, elem.index);
23216
+ const keyExpr = wrapLoopParamAsAccessor(elem.key || "__idx", elem.param, elem.paramBindings, elem.index);
23053
23217
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings);
23054
23218
  const mapPreambleWrapped = elem.preamble ? renderPreamble(elem.preamble, {
23055
- transformJs: (text) => wrapLoopParamAsAccessor(text, elem.param, elem.paramBindings),
23219
+ transformJs: (text) => wrapLoopParamAsAccessor(text, elem.param, elem.paramBindings, elem.index),
23056
23220
  renderLeaf: () => {
23057
23221
  internalInvariant(false, "component-root loop received a JSX-bearing preamble \u2014 Phase 1 should have refused it");
23058
23222
  }
@@ -23062,12 +23226,12 @@ function buildComponentLoopPlan(elem, profileComponentName) {
23062
23226
  const isTextOnly = comp.children?.length ? comp.children.every((c) => c.type === "expression" || c.type === "text" || isTextOnlyConditional(c)) : false;
23063
23227
  const rawChildrenExpr = isTextOnly ? irChildrenToJsExpr(comp.children) : null;
23064
23228
  const childrenFreeIds = isTextOnly && comp.children ? irChildrenFreeIds(comp.children) : void 0;
23065
- const childrenRefsLoop = rawChildrenExpr != null && childrenFreeIds != null && childrenFreeIds.has(elem.param);
23229
+ const childrenRefsLoop = rawChildrenExpr != null && childrenFreeIds != null && (childrenFreeIds.has(elem.param) || !!elem.index && childrenFreeIds.has(elem.index));
23066
23230
  return {
23067
23231
  componentName: comp.name,
23068
23232
  selector: buildCompSelector(comp),
23069
- propsExpr: buildComponentPropsExpr2(comp, elem.param),
23070
- childrenTextEffect: childrenRefsLoop ? { wrappedChildren: wrapLoopParamAsAccessor(rawChildrenExpr, elem.param, elem.paramBindings) } : null
23233
+ propsExpr: buildComponentPropsExpr2(comp, elem.param, void 0, elem.index),
23234
+ childrenTextEffect: childrenRefsLoop ? { wrappedChildren: wrapLoopParamAsAccessor(rawChildrenExpr, elem.param, elem.paramBindings, elem.index) } : null
23071
23235
  };
23072
23236
  });
23073
23237
  const hasChildConds = elem.bindings.conditionals.length > 0;
@@ -23094,7 +23258,7 @@ function buildComponentLoopPlan(elem, profileComponentName) {
23094
23258
  // the per-item factory has no `__el` handle to invoke. Still required
23095
23259
  // by the type so the structural invariant (every variant has a
23096
23260
  // `childRefs`) is preserved; populated as empty.
23097
- childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
23261
+ childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings, elem.index),
23098
23262
  profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : void 0,
23099
23263
  childConditionalEffects: hasChildConds ? buildReactiveEffectsPlan({
23100
23264
  attrs: [],
@@ -23102,6 +23266,7 @@ function buildComponentLoopPlan(elem, profileComponentName) {
23102
23266
  conditionals: elem.bindings.conditionals,
23103
23267
  loopParam: elem.param,
23104
23268
  loopParamBindings: elem.paramBindings,
23269
+ loopIndex: elem.index,
23105
23270
  profileComponentName
23106
23271
  }) : null
23107
23272
  };
@@ -23135,8 +23300,6 @@ function buildLoopPlan(elem, opts) {
23135
23300
  return buildPlainLoopPlan(elem, opts.profileComponentName, opts.lazyScope);
23136
23301
  }
23137
23302
  function buildPlainLoopPlan(elem, profileComponentName, lazyScope) {
23138
- const wrap = (expr) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings);
23139
- const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings);
23140
23303
  const hasReactive2 = elem.bindings.reactiveAttrs.length > 0 || elem.bindings.reactiveTexts.length > 0 || elem.bindings.conditionals.length > 0;
23141
23304
  if (elem.flatMapClient) {
23142
23305
  return {
@@ -23164,12 +23327,15 @@ function buildPlainLoopPlan(elem, profileComponentName, lazyScope) {
23164
23327
  };
23165
23328
  }
23166
23329
  const arrayExpr = buildChainedArrayExpr(elem);
23167
- const indexParam = elem.index || "__idx";
23168
- const mapPreambleWrapped = elem.preamble ? renderPreamble(elem.preamble, {
23169
- transformJs: wrap,
23170
- renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings }], void 0)
23171
- }) : "";
23172
- const preambleRegions = buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings);
23330
+ const core = buildPlainRowCore({
23331
+ loop: elem,
23332
+ arrayExpr,
23333
+ callSite: "plain",
23334
+ flatMapLeafItem: false,
23335
+ anchored: elem.bodyIsItemConditional ?? false,
23336
+ scope: lazyScope
23337
+ });
23338
+ const { paramHead, paramUnwrap, indexParam, preambleRegions, lazyRow, wrapItem: wrap } = core;
23173
23339
  return {
23174
23340
  kind: "plain",
23175
23341
  rowConstruction: "string-template",
@@ -23181,29 +23347,21 @@ function buildPlainLoopPlan(elem, profileComponentName, lazyScope) {
23181
23347
  paramHead,
23182
23348
  paramUnwrap,
23183
23349
  indexParam,
23184
- // Lazy row graph (§9, L3). `null` for every ineligible loop, which then
23185
- // keeps the eager emission below byte-for-byte.
23186
- lazyRow: buildLazyRowPlan({
23187
- loop: elem,
23188
- arrayExpr,
23189
- indexParam,
23190
- paramUnwrap,
23191
- mapPreambleWrapped,
23192
- preambleRegionCount: preambleRegions.length,
23193
- callSite: "plain",
23194
- flatMapLeafItem: false,
23195
- anchored: elem.bodyIsItemConditional ?? false,
23196
- scope: lazyScope
23197
- }) ?? void 0,
23350
+ lazyRow,
23198
23351
  // Stage 3 / D4 — js segments get the loop-param accessor wrap; jsx leaves
23199
23352
  // render as HTML-string templates under this loop's param context so a
23200
23353
  // leaf that reads the item (`r`) becomes `r()`.
23201
- mapPreambleWrapped,
23202
- template: elem.template,
23354
+ mapPreambleWrapped: core.mapPreambleWrapped,
23355
+ template: core.template,
23203
23356
  skeletonTemplate: elem.skeletonTemplate,
23204
23357
  skeletonPaths: elem.skeletonPaths,
23205
23358
  reactiveEffects: hasReactive2 ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
23206
- childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
23359
+ // `elem.index` (#2859 follow-up): a ref callback closing over the loop
23360
+ // index (`ref={el => refs[i] = el}`) must see it wrapped the same way
23361
+ // every other reference in this row is — a row with any ref is always
23362
+ // lazy-ineligible (`lazy-row-eligibility.ts`'s `childRefCount` gate), so
23363
+ // there is no shared-string hazard here, unlike `mapPreambleWrapped`.
23364
+ childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings, elem.index),
23207
23365
  preambleRegions,
23208
23366
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
23209
23367
  anchored: elem.bodyIsItemConditional ?? false,
@@ -23211,9 +23369,10 @@ function buildPlainLoopPlan(elem, profileComponentName, lazyScope) {
23211
23369
  // conditional without a key is a BF023 error, but the emitted client JS
23212
23370
  // must still parse — an empty `anchorKeyExpr` would produce
23213
23371
  // `createComment(`bf-loop-i:${}`)` (a SyntaxError that breaks the whole
23214
- // bundle). `elem.index || '__idx'` matches `indexParam` above, so the
23215
- // anchor value stays consistent with the renderItem's own index param.
23216
- anchorKeyExpr: elem.key ? wrap(elem.key) : elem.index || "__idx"
23372
+ // bundle). `indexParam` matches the renderItem head built above, and
23373
+ // (#2859) is bound to an INDEX ACCESSOR at runtime, not a plain number —
23374
+ // call it, same as every other reference to it in this row's body.
23375
+ anchorKeyExpr: elem.key ? wrap(elem.key) : `${indexParam}()`
23217
23376
  };
23218
23377
  }
23219
23378
  function buildStaticLoopPlan(elem, unsafeLocalNames, profileComponentName) {
@@ -23255,7 +23414,7 @@ function buildStaticLoopMaterialize(elem, unsafeLocalNames) {
23255
23414
  return {
23256
23415
  itemTemplate: elem.staticItemTemplate,
23257
23416
  mapPreamble: elem.preamble ? renderPreamble(elem.preamble, {
23258
- renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings }], void 0)
23417
+ renderLeaf: (ir) => irToHtmlTemplate(ir, void 0, 1, [{ param: elem.param, bindings: elem.paramBindings, index: elem.index }], void 0)
23259
23418
  }) : "",
23260
23419
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false
23261
23420
  };
@@ -23266,7 +23425,7 @@ var init_build_loop = __esm({
23266
23425
  init_utils();
23267
23426
  init_shared();
23268
23427
  init_build_reactive_effects();
23269
- init_build_lazy_row();
23428
+ init_build_plain_row();
23270
23429
  init_build_component_loop();
23271
23430
  init_build_composite_loop();
23272
23431
  init_html_template();
@@ -23942,7 +24101,7 @@ function generateTemplateOnlyMount(ir, ctx2) {
23942
24101
  const restSpreadNames = resolveRestSpreadNames(ctx2);
23943
24102
  let templateHtml;
23944
24103
  if (canGenerateStaticTemplate(ir.root, propNamesForStaticCheck, inlinableConstants, unsafeLocalNames)) {
23945
- const markupSlotIds = new Set(ctx2.dynamicElements.map((e) => e.slotId));
24104
+ const markupSlotIds = markupSlotIdsOf(ctx2);
23946
24105
  templateHtml = irToComponentTemplate(ir.root, inlinableConstants, restSpreadNames, ctx2.propsObjectName, markupSlotIds, ctx2.restPropsName);
23947
24106
  }
23948
24107
  if (!templateHtml) {
@@ -23986,6 +24145,7 @@ var init_ir_to_client_js = __esm({
23986
24145
  init_build_references();
23987
24146
  init_init_declarations();
23988
24147
  init_html_template();
24148
+ init_markup_slots();
23989
24149
  init_utils();
23990
24150
  init_emit_registration();
23991
24151
  init_compute_inlinability();
@@ -24644,6 +24804,18 @@ function extractSsrDefaults(metadata) {
24644
24804
  }
24645
24805
  bindings[memo.name] = value2;
24646
24806
  }
24807
+ {
24808
+ const getterNames = collectAliasableGetterNames(metadata.signals, metadata.memos);
24809
+ for (const [alias, origin] of resolveGetterAliases(metadata.localConstants ?? [], (n) => getterNames.has(n))) {
24810
+ if (alias in out) continue;
24811
+ out[alias] = out[origin];
24812
+ }
24813
+ }
24814
+ for (const [local, callerKey] of resolveBodyDestructuredPropAliases(metadata.localConstants ?? [], metadata.propsObjectName)) {
24815
+ if (local in out) continue;
24816
+ const origin = out[callerKey];
24817
+ if (origin) out[local] = origin;
24818
+ }
24647
24819
  if (metadata.propsObjectName !== null) {
24648
24820
  const referenced = /* @__PURE__ */ new Set();
24649
24821
  for (const sig of metadata.signals) {
@@ -24941,6 +25113,8 @@ var init_ssr_defaults = __esm({
24941
25113
  "../jsx/src/ssr-defaults.ts"() {
24942
25114
  "use strict";
24943
25115
  init_analyzer();
25116
+ init_csr_substitute();
25117
+ init_props_binding();
24944
25118
  UNRESOLVED = Symbol("unresolved");
24945
25119
  NO_RETURN = Symbol("no-return");
24946
25120
  }
@@ -27106,6 +27280,15 @@ function emitParsedExpr(expr, emitter) {
27106
27280
  case "literal":
27107
27281
  return emitter.literal(expr.value, expr.literalType);
27108
27282
  case "call": {
27283
+ if (emitter.lowering) {
27284
+ for (const matcher of emitter.lowering.matchers) {
27285
+ const node = matcher(expr.callee, expr.args);
27286
+ if (!node) continue;
27287
+ const rendered = emitter.lowering.render(node, emit);
27288
+ if (rendered !== null) return rendered;
27289
+ break;
27290
+ }
27291
+ }
27109
27292
  const cb = asCallbackMethodCall(expr);
27110
27293
  if (cb) return emitter.callbackMethod(cb.method, cb.object, cb.arrow, cb.args, emit);
27111
27294
  return emitter.call(expr.callee, expr.args, emit);
@@ -30392,6 +30575,7 @@ __export(src_exports, {
30392
30575
  buildSourceMapFromIR: () => buildSourceMapFromIR,
30393
30576
  buildStaticBudget: () => buildStaticBudget,
30394
30577
  buildWhyUpdate: () => buildWhyUpdate,
30578
+ collectAliasableGetterNames: () => collectAliasableGetterNames,
30395
30579
  collectContextConsumers: () => collectContextConsumers,
30396
30580
  collectLoopBoundNames: () => collectLoopBoundNames,
30397
30581
  collectModuleStringConsts: () => collectModuleStringConsts,
@@ -30480,6 +30664,7 @@ __export(src_exports, {
30480
30664
  listComponentFunctions: () => listComponentFunctions,
30481
30665
  listExportedComponents: () => listComponentFunctions,
30482
30666
  lookupStaticRecordLiteral: () => lookupStaticRecordLiteral,
30667
+ loweringNodeChildren: () => loweringNodeChildren,
30483
30668
  makeIdCallRegex: () => makeIdCallRegex,
30484
30669
  matchLoweringCall: () => matchLoweringCall,
30485
30670
  matchQueryHrefCall: () => matchQueryHrefCall,
@@ -30503,7 +30688,9 @@ __export(src_exports, {
30503
30688
  registerBuiltinLoweringPlugins: () => registerBuiltinLoweringPlugins,
30504
30689
  registerLoweringPlugin: () => registerLoweringPlugin,
30505
30690
  resetCompilerCounters: () => resetCompilerCounters,
30691
+ resolveBodyDestructuredPropAliases: () => resolveBodyDestructuredPropAliases,
30506
30692
  resolveDangerousInnerHtml: () => resolveDangerousInnerHtml,
30693
+ resolveGetterAliases: () => resolveGetterAliases,
30507
30694
  resolveSetters: () => resolveSetters,
30508
30695
  resolveStaticLoopSource: () => resolveStaticLoopSource,
30509
30696
  rewriteDynamicImportsInSource: () => rewriteDynamicImportsInSource,
@@ -30523,6 +30710,7 @@ var init_src2 = __esm({
30523
30710
  init_compiler();
30524
30711
  init_ssr_defaults();
30525
30712
  init_props_binding();
30713
+ init_csr_substitute();
30526
30714
  init_component_scope();
30527
30715
  init_ssr_seed_plan();
30528
30716
  init_analyzer();
@@ -32059,7 +32247,7 @@ var bfGoSource, evalGoSource, repropsGoSource, streamingGoSource, bfdevGoSource;
32059
32247
  var init_runtimes_generated = __esm({
32060
32248
  "src/lib/adapters/runtimes.generated.ts"() {
32061
32249
  "use strict";
32062
- bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "math/rand"\n "net/url"\n "os"\n "reflect"\n "regexp"\n "sort"\n "strconv"\n "strings"\n "time"\n "unicode"\n "unicode/utf8"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Nullish coalescing (#2248): JS `??` semantics \u2014 fall back only on\n // nil, keeping present-but-falsy values (`""`, `0`, `false`) that\n // Go\'s truthiness-based `or` would replace.\n "bf_nullish": Nullish,\n\n // Arithmetic\n "bf_add": Add,\n "bf_concat_str": ConcatStr,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n "bf_min": Min,\n "bf_max": Max,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_trim_start": TrimStart,\n "bf_trim_end": TrimEnd,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_split": Split,\n "bf_starts_with": StartsWith,\n "bf_ends_with": EndsWith,\n "bf_replace": Replace,\n "bf_replace_all": ReplaceAll,\n "bf_repeat": Repeat,\n "bf_pad_start": PadStart,\n "bf_pad_end": PadEnd,\n "bf_string": String,\n "bf_raw_html": RawHTML,\n "bf_ternary": Ternary,\n "bf_truthy": Truthy,\n\n // URL query builder (#1897 PostList href helpers): conditional\n // (include, key, value) triples \u2192 "base?k=v&\u2026", mirroring a\n // URLSearchParams builder with guarded `.set()` calls.\n "bf_query": Query,\n\n // Date method lowering (#2274, spec entry "date"): the lowering\n // target for a zero-arg call on a Date-typed prop.\n "bf_date": Date,\n\n // formatDate(date, pattern, tz) lowering (#2324, spec entry\n // "format_date"): the total, locale-free date-pattern formatter \u2014\n // see FormatDate\'s docstring for the full contract.\n "bf_format_date": FormatDate,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n "bf_abs": Abs,\n "bf_to_fixed": ToFixed,\n\n // Array/Slice\n "bf_len": Len,\n "bf_length": Length,\n "bf_is_element": IsValidElement,\n "bf_style_object": StyleObjectToCSS,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_flat": Flat,\n "bf_flat_dynamic": FlatDynamicDepth,\n "bf_flat_map": FlatMap,\n "bf_flat_map_tuple": FlatMapTuple,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n // `bf_map` reuses `Env` (eval.go) \u2014 both build a `map[string]any`\n // from an alternating key/value variadic list. `Env` was written\n // for the evaluator\'s free-var capture map; a JS object-literal\n // lowered to a template action (the `objectLiteral` emitter,\n // #2696 Step 1) needs the exact same construction, so it\'s\n // registered under a second name rather than duplicated.\n "bf_map": Env,\n // `bf_merge` (eval.go) is `bf_map`\'s sibling for a POPULATED\n // object-literal SPREAD (#2696 Step 2, `{ ...t, editing: false }`):\n // a variadic, null-safe shallow merge (a non-map argument is\n // skipped, matching JS\'s null/undefined-spread no-op), later\n // arguments winning \u2014 the `objectLiteral` emitter folds each\n // maximal run of plain properties (a `bf_map` call) and each spread\n // (its own value) through this, in source order.\n "bf_merge": Merge,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n "bf_reduce": Reduce,\n\n // Evaluator-driven higher-order folds (#2018): the comparator / reducer\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_sort / bf_reduce beyond their fixed catalogues. The\n // adapter falls back to bf_sort for a comparator the evaluator can\'t\n // model (e.g. localeCompare). `bf_env` builds the captured-free-var\n // environment passed as the trailing base_env argument.\n "bf_sort_eval": SortEval,\n "bf_reduce_eval": FoldEval,\n "bf_env": Env,\n\n // Evaluator-driven higher-order predicates (#2018, P2): the predicate\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_filter / bf_find / bf_find_index / bf_every / bf_some\n // beyond their field-equality / truthiness catalogues. `bf_find_eval` /\n // `bf_find_index_eval` take a `forward` bool (false \u2192 findLast variants).\n "bf_filter_eval": FilterEval,\n "bf_every_eval": EveryEval,\n "bf_some_eval": SomeEval,\n "bf_find_eval": FindEval,\n "bf_find_index_eval": FindIndexEval,\n // `.flatMap(proj)`: project each element through the serialized\n // projection body, then flatten one level.\n "bf_flat_map_eval": FlatMapEval,\n // Value-producing `.map(cb)` (#2073): project each element, one\n // result per element (no flatten).\n "bf_map_eval": MapEval,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // JSX children passed to an imported child component (#1896):\n // the parent renders the children fragment via a companion\n // define (executed through bf_tmpl from TemplateFuncMap) and\n // injects the result into the child\'s Children field.\n "bf_with_children": WithChildren,\n\n // Per-row props for a child component nested inside a composite\n // loop row (#2445): the parent\'s once-per-slot instance is shared\n // across rows, so a prop that depends on the row is reapplied as a\n // copy inside {{range}}, the props-argument sibling of\n // bf_with_children.\n "bf_with_props": WithProps,\n\n // Per-row props for a child whose CONSTRUCTOR derives a field from\n // the overridden prop (#2448). bf_with_props patches fields on the\n // shared instance and cannot re-run New<Child>Props, so a memo body\n // or a signal initial value computed there would stay at the shared\n // instance\'s one-shot value on every row. This entry looks up the\n // component\'s generated rebuilder and re-runs the real constructor\n // instead. See reprops.go for why the lookup is deferred to execute\n // time rather than merged into this map.\n "bf_reprops": Reprops,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n "bfScopeCommentEnd": ScopeCommentEnd,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n\n // Reverse the loop-row field-access Go-casing before a whole-item\n // spread reaches `bf_spread_attrs` (#2490): a row\'s dot context is\n // necessarily keyed `ID`/`Title`/`DataKind` (the same casing that\n // makes `attrs.id` emit `{{.ID}}`), and `toAttrName`\'s\n // camelCase\u2192kebab conversion mangles an uppercase-led key\n // (`ID` \u2192 `-i-d`). See JSKeys\' docstring for the recovery rule.\n "bf_js_keys": JSKeys,\n\n // Destructure object-rest spread-onto-element residual (#2087):\n // "every field except these" for a struct/map, keyed by json tag.\n "bf_omit": Omit,\n\n // Case-tolerant single-field read off a map or struct (#2087): a\n // `useContext` local whose `createContext` default is object-shaped\n // (e.g. `createContext<{ config: X }>({ config: {} })`) is typed\n // `map[string]interface{}`, and its keys are the SOURCE (JS-cased)\n // property names a `<Ctx.Provider value={{ \u2026 }}>` bakes\n // (`providerObjectValueToGoMap`, go-template-adapter.ts) \u2014 plain\n // `text/template` dot access does an exact-string `MapIndex`, so\n // `ctx.config.label` lowers to nested `bf_get` calls instead of\n // `.Ctx.Config.Label`. Reuses the same `getFieldValue` the\n // project/sort helpers already use for a dynamic field-name lookup;\n // safe on a nil map/interface (returns nil) so a missing Provider or\n // an absent key falls through to `??`\'s fallback.\n "bf_get": getFieldValue,\n }\n}\n\n// Query builds a URL from a base path plus a query string assembled from\n// (include, key, value) triples, in order. A pair is considered only when its\n// `include` flag is true \u2014 mirroring a JS URLSearchParams builder whose\n// `.set(key, value)` calls are each guarded by an `if`. The compiler lowers a\n// conditional `cond ? v : undefined` to the `include` bool and a plain `key: v`\n// to a `true` include; the emptiness check is applied HERE (an included but\n// empty value is dropped), matching the client `queryHref` and the Perl `query`\n// helper. Keys and values use formEscape (application/x-www-form-urlencoded),\n// so the rendered query is byte-for-byte identical to the browser\'s\n// URLSearchParams. An empty query yields the bare base.\n//\n// A value may be a string slice ([]string or []any), which APPENDS one pair per\n// non-empty member (URLSearchParams.append) \u2014 `{tag: [a, b]}` \u2192 `tag=a&tag=b`.\n// A scalar value follows URLSearchParams.set() semantics: repeating a key\n// overwrites the value at the key\'s first position rather than duplicating it\n// (object literals have unique keys, so this is defensive). Trailing args that\n// don\'t complete a triple are ignored.\n//\n// formEscape differs from url.QueryEscape only on `~` (kept by QueryEscape,\n// `%7E` here) and `*` (`%2A` by QueryEscape, kept here).\nfunc Query(base string, triples ...any) string {\n type kv struct{ key, val string }\n pairs := make([]kv, 0, len(triples)/3)\n pos := make(map[string]int)\n for i := 0; i+2 < len(triples); i += 3 {\n include, _ := triples[i].(bool)\n if !include {\n continue\n }\n k := String(triples[i+1])\n if members, ok := asStringSlice(triples[i+2]); ok {\n // Array value \u2192 append each non-empty member; appended pairs never\n // overwrite, so they don\'t participate in the set()-position map.\n for _, m := range members {\n if m == "" {\n continue\n }\n pairs = append(pairs, kv{k, m})\n }\n continue\n }\n v := String(triples[i+2])\n if v == "" {\n continue // omit an included-but-empty value (client / Perl parity)\n }\n if at, ok := pos[k]; ok {\n pairs[at].val = v // set(): overwrite the first occurrence\'s value\n } else {\n pos[k] = len(pairs)\n pairs = append(pairs, kv{k, v})\n }\n }\n var b strings.Builder\n for _, p := range pairs {\n if b.Len() == 0 {\n b.WriteByte(\'?\')\n } else {\n b.WriteByte(\'&\')\n }\n b.WriteString(formEscape(p.key))\n b.WriteByte(\'=\')\n b.WriteString(formEscape(p.val))\n }\n return base + b.String()\n}\n\n// Date implements the `date` helper (spec/template-helpers.md, #2274) \u2014 the\n// lowering target for a zero-arg call on a Date-typed prop\n// (`createdAt.toISOString()`). recv accepts the runtime\'s own `time.Time` /\n// `*time.Time` (however the host framework populated the prop) OR an\n// ISO-8601 string (the wire form a JSON-sourced prop arrives as); either is\n// normalized to UTC before dispatching op, matching the client `Date`\'s own\n// instant semantics regardless of which shape reaches this helper. A nil /\n// unparsable receiver yields this runtime\'s zero value for the requested op\n// (0 for every numeric accessor, "" for toISOString) rather than panicking\n// mid-render \u2014 the same tolerance `String`/`Number` already extend to a nil\n// prop. `getUTCMonth` subtracts 1: Go\'s `time.Month` is 1-based, JS\'s is not\n// (spec entry "date" is explicit that JS wins here).\nfunc Date(recv any, op string) any {\n t, ok := toTime(recv)\n if !ok {\n if op == "toISOString" {\n return ""\n }\n return 0\n }\n t = t.UTC()\n switch op {\n case "getUTCFullYear":\n return t.Year()\n case "getUTCMonth":\n return int(t.Month()) - 1\n case "getUTCDate":\n return t.Day()\n case "getUTCHours":\n return t.Hour()\n case "getUTCMinutes":\n return t.Minute()\n case "getUTCSeconds":\n return t.Second()\n case "getTime":\n return t.UnixMilli()\n case "toISOString":\n return t.Format("2006-01-02T15:04:05.000Z")\n default:\n return 0\n }\n}\n\n// tzOffsetRE matches a fixed UTC offset `\xB1HH:MM` within ECMA-402\'s valid\n// range \u2014 hours 00\u201323, minutes 00\u201359 (`\'+09:00\'`, `\'-05:30\'`) \u2014 one of the\n// three `tz` shapes FormatDate accepts (mirrors OFFSET_RE in\n// packages/client/src/format-date.ts). An out-of-range shape (`\'+25:00\'`)\n// falls through to the tzdata lookup, fails it, and errors \u2014 matching the\n// JS reference\'s RangeError (#2344).\nvar tzOffsetRE = regexp.MustCompile(`^([+-])([01][0-9]|2[0-3]):([0-5][0-9])$`)\n\n// formatDateTokenRE is the longest-match pattern-token alternation (mirrors\n// TOKEN_RE in packages/client/src/format-date.ts). Order matters: MMMM\n// before MMM before MM before M (and dddd before ddd, DD before D) so the\n// longer token wins at a position where a shorter one could also match \u2014\n// Go\'s regexp, like JS\'s, resolves alternation leftmost-first (not POSIX\n// leftmost-longest), so listing the longer alternative first is what makes\n// e.g. "MMMM" consume all four characters instead of "MM" + "MM".\nvar formatDateTokenRE = regexp.MustCompile(`YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D`)\n\n// nameTable section offsets (#2334, mirrors MONTHS_WIDE / MONTHS_ABBR /\n// WEEKDAYS_WIDE / WEEKDAYS_ABBR in packages/client/src/format-date.ts).\nconst (\n monthsWide = 0\n monthsAbbr = 12\n weekdaysWide = 24\n weekdaysAbbr = 31\n)\n\n// formatDateName reads a name-token table entry: index out of range, or a\n// non-string element, both render "" \u2014 the same total, zero-value\n// discipline as an unparseable date (mirrors the JS reference\'s\n// `names[index] ?? ""` fallback, where every table element the vectors ever\n// carry is a string).\nfunc formatDateName(names []any, index int) string {\n if index < 0 || index >= len(names) {\n return ""\n }\n s, ok := names[index].(string)\n if !ok {\n return ""\n }\n return s\n}\n\n// FormatDate implements the `format_date` helper (#2324, #2334, spec entry\n// "format_date") \u2014 the lowering target for\n// `formatDate(date, pattern, tz, names)`\n// (packages/client/src/format-date.ts, the JS-normative reference this must\n// match byte-for-byte). Total and deterministic: no locale, no host\n// timezone, no "now".\n//\n// recv: same receiver contract as the `date` helper above \u2014 the runtime\'s\n// own `time.Time` / `*time.Time`, or an ISO-8601 string \u2014 normalized via\n// `toTime`. A nil / unparseable receiver returns "" (never panics).\n//\n// tz (#2344): "UTC", a range-valid fixed offset `\xB1HH:MM` (shifts by\n// sign*(HH*60+MM) minutes), or a canonical IANA zone name ("Asia/Tokyo")\n// resolved through tzdata via time.LoadLocation \u2014 the zone\'s UTC offset AT\n// THE INSTANT being formatted (DST-aware, historical-transition-aware,\n// seconds precision: pre-standard LMT offsets like Tokyo\'s +09:18:59\n// count). ANY other value \u2014 an unknown zone, a malformed or out-of-range\n// offset ("+9:00", "+25:00"), the empty string or "Local" (LoadLocation\'s\n// implicit-environment aliases) \u2014 returns an ERROR, aborting template\n// execution loudly: the JS reference throws a RangeError there, and a\n// silently substituted timezone is the one failure mode this helper must\n// not have (the pre-#2344 normalize-to-UTC total function is gone). The\n// shifted instant\'s UTC calendar fields (not the original instant\'s) are\n// what pattern tokens read \u2014 the shifted UTC clock face IS the local clock\n// face in that zone, same reasoning as the JS reference.\n//\n// names (#2334): a flat name table in fixed layout \u2014 `[0..11]` wide month\n// names, `[12..23]` abbreviated month names, `[24..30]` wide weekday names\n// (Sunday-first), `[31..37]` abbreviated weekday names. The caller owns the\n// values; this helper only indexes the table.\n//\n// pattern: longest-match token substitution\n// (`YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D`); every other character \u2014 including\n// multi-byte ones like \u5E74/\u6708/\u65E5 \u2014 passes through literally. `YYYY` is\n// `abs(year)` zero-padded to 4 digits, `-`-prefixed for a negative year;\n// `MM`/`DD` zero-pad to 2; `M`/`D` are bare; `MMMM`/`MMM` and `dddd`/`ddd`\n// read the `names` table (weekday computed on the offset-shifted instant,\n// Sunday-first, matching `time.Time.Weekday()`\'s own Sunday=0 encoding).\nfunc FormatDate(recv any, pattern string, tz string, names []any) (string, error) {\n t, ok := toTime(recv)\n if !ok {\n // Receiver contract precedes tz validation (spec receiver-first\n // discipline, mirrored by every port).\n return "", nil\n }\n offsetSeconds := 0\n if tz != "UTC" {\n if m := tzOffsetRE.FindStringSubmatch(tz); m != nil {\n hh, _ := strconv.Atoi(m[2])\n mm, _ := strconv.Atoi(m[3])\n offsetSeconds = (hh*60 + mm) * 60\n if m[1] == "-" {\n offsetSeconds = -offsetSeconds\n }\n } else if tz == "" || tz == "Local" {\n // LoadLocation("") is UTC and LoadLocation("Local") is the host\n // zone \u2014 both implicit-environment reads the contract refuses.\n return "", fmt.Errorf("format_date: unresolvable timeZone %q", tz)\n } else {\n loc, err := time.LoadLocation(tz)\n if err != nil {\n return "", fmt.Errorf("format_date: unresolvable timeZone %q", tz)\n }\n _, offsetSeconds = t.UTC().In(loc).Zone()\n }\n }\n shifted := t.UTC().Add(time.Duration(offsetSeconds) * time.Second)\n year := shifted.Year()\n month := int(shifted.Month())\n day := shifted.Day()\n weekday := int(shifted.Weekday()) // time.Sunday == 0, matching the table\'s Sunday-first layout\n absYear := year\n if absYear < 0 {\n absYear = -absYear\n }\n yyyy := fmt.Sprintf("%04d", absYear)\n if year < 0 {\n yyyy = "-" + yyyy\n }\n out := formatDateTokenRE.ReplaceAllStringFunc(pattern, func(token string) string {\n switch token {\n case "YYYY":\n return yyyy\n case "MMMM":\n return formatDateName(names, monthsWide+month-1)\n case "MMM":\n return formatDateName(names, monthsAbbr+month-1)\n case "MM":\n return fmt.Sprintf("%02d", month)\n case "M":\n return strconv.Itoa(month)\n case "DD":\n return fmt.Sprintf("%02d", day)\n case "D":\n return strconv.Itoa(day)\n case "dddd":\n return formatDateName(names, weekdaysWide+weekday)\n case "ddd":\n return formatDateName(names, weekdaysAbbr+weekday)\n default:\n return token\n }\n })\n return out, nil\n}\n\n// toTime normalizes a `Date` helper receiver to a `time.Time`: the runtime\'s\n// own `time.Time` / `*time.Time`, or an ISO-8601 string parsed with\n// `time.RFC3339Nano` (accepts both the `Z`-suffixed and numeric-offset\n// forms, and any sub-second precision \u2014 including the millisecond precision\n// every value this runtime itself ever produces via `toISOString` above).\n// Anything else (nil, an unparsable string, an unrelated type) reports !ok\n// so `Date` can apply its documented zero-value fallback instead of\n// panicking.\nfunc toTime(recv any) (time.Time, bool) {\n switch v := recv.(type) {\n case time.Time:\n return v, true\n case *time.Time:\n if v == nil {\n return time.Time{}, false\n }\n return *v, true\n case string:\n t, err := time.Parse(time.RFC3339Nano, v)\n if err != nil {\n return time.Time{}, false\n }\n return t, true\n default:\n return time.Time{}, false\n }\n}\n\n// asStringSlice reports whether v is a query *array* value and, if so, returns\n// its members stringified. A compiled template passes a `[]string` field; the\n// golden conformance vectors decode JSON arrays to `[]any`. Anything else is a\n// scalar (false), handled by the set() path.\nfunc asStringSlice(v any) ([]string, bool) {\n switch s := v.(type) {\n case []string:\n return s, true\n case []any:\n out := make([]string, len(s))\n for i, m := range s {\n out[i] = String(m)\n }\n return out, true\n default:\n return nil, false\n }\n}\n\nconst hexUpper = "0123456789ABCDEF"\n\n// formEscape percent-encodes s with the application/x-www-form-urlencoded byte\n// set, matching the browser\'s URLSearchParams serialization (and the Perl\n// `query` helper) so SSR query strings render byte-for-byte identically across\n// adapters. The unreserved set kept verbatim is A-Z a-z 0-9 and `* - . _`; a\n// space becomes `+`; every other byte is `%XX` with uppercase hex. Encoding is\n// byte-wise, so multi-byte UTF-8 is percent-encoded per byte (`\xE9` \u2192 `%C3%A9`).\n//\n// This differs from url.QueryEscape only for `~` (kept by QueryEscape, encoded\n// to `%7E` here) and `*` (encoded to `%2A` by QueryEscape, kept here).\nfunc formEscape(s string) string {\n var b strings.Builder\n for i := 0; i < len(s); i++ {\n c := s[i]\n switch {\n case c >= \'A\' && c <= \'Z\', c >= \'a\' && c <= \'z\', c >= \'0\' && c <= \'9\',\n c == \'*\', c == \'-\', c == \'.\', c == \'_\':\n b.WriteByte(c)\n case c == \' \':\n b.WriteByte(\'+\')\n default:\n b.WriteByte(\'%\')\n b.WriteByte(hexUpper[c>>4])\n b.WriteByte(hexUpper[c&0x0F])\n }\n }\n return b.String()\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// hasUnsafeStyleValue mirrors Hono\'s own CSS-injection guard\n// (`hono/jsx/utils.ts`\'s `hasUnsafeStyleValue` \u2014 the ORACLE this adapter\'s\n// dynamic `style={{...}}` values must match, #2261): a hand-rolled\n// structural scan for characters that could break out of a CSS\n// declaration, NOT real CSSOM property validation. Ported byte-for-byte \u2014\n// every character this scan tests is ASCII, so scanning by byte (Go\n// string indexing) agrees with Hono\'s UTF-16-code-unit scan for every\n// input; a multibyte UTF-8 sequence has no byte in the ASCII range, so it\n// can never spuriously match one of these single-byte comparisons. Skips\n// the reference implementation\'s regex fast-path (a pure optimization \u2014\n// the scan below already returns `false` promptly for a clean value).\nfunc hasUnsafeStyleValue(value string) bool {\n quote := byte(0)\n blockStack := make([]byte, 0, 4)\n for i := 0; i < len(value); i++ {\n c := value[i]\n switch {\n case c == \'\\\\\':\n if i == len(value)-1 {\n return true\n }\n i++\n case quote != 0:\n if c == \'\\n\' || c == \'\\f\' || c == \'\\r\' {\n return true\n }\n if c == quote {\n quote = 0\n }\n case c == \'/\' && i+1 < len(value) && value[i+1] == \'*\':\n end := strings.Index(value[i+2:], "*/")\n if end == -1 {\n return true\n }\n i = i + 2 + end + 1\n case c == \'"\' || c == \'\\\'\':\n quote = c\n case c == \'(\':\n blockStack = append(blockStack, \')\')\n case c == \'[\':\n blockStack = append(blockStack, \']\')\n case c == \'{\' || c == \'}\':\n return true\n case c == \')\' || c == \']\':\n if len(blockStack) == 0 || blockStack[len(blockStack)-1] != c {\n return true\n }\n blockStack = blockStack[:len(blockStack)-1]\n case c == \';\' && len(blockStack) == 0:\n return true\n }\n }\n return quote != 0 || len(blockStack) != 0\n}\n\n// StyleObjectToCSS builds the CSS string for a `style={{...}}` JSX\n// object-literal attribute (#2261) \u2014 `pairs` alternates CSS key (always a\n// compile-time-known literal), then value (`any`, possibly a runtime\n// expression\'s result). A value that fails `hasUnsafeStyleValue` (after\n// JS-`String()`-style stringification) is DROPPED \u2014 the whole `key:value`\n// pair is omitted \u2014 matching Hono\'s oracle behavior exactly, rather than\n// html/template\'s own contextual CSS auto-escaper (which instead emits its\n// `ZgotmplZ` unsafe-content sentinel for the same input). The final joined\n// string is STILL HTML-escaped (mirroring Hono\'s own `escapeToBuffer` call\n// on its accumulated style string) \u2014 a "safe" value can still carry a\n// literal `"`/`\'`/`&` (e.g. a BALANCED-quote CSS string value like\n// `"hello"` passes the structural scan; the quote chars survive into the\n// value) that would otherwise break out of the double-quoted `style="..."`\n// attribute. Returns `template.CSS` (over the escaped result) so\n// html/template treats it as trusted CSS content instead of ALSO applying\n// its own contextual CSS auto-escaper (which would re-derive the exact\n// `ZgotmplZ` divergence this function exists to avoid).\nfunc StyleObjectToCSS(pairs ...any) template.CSS {\n parts := make([]string, 0, len(pairs)/2)\n for i := 0; i+1 < len(pairs); i += 2 {\n key := fmt.Sprint(pairs[i])\n value := String(pairs[i+1])\n if hasUnsafeStyleValue(value) {\n continue\n }\n parts = append(parts, template.HTMLEscapeString(key)+":"+template.HTMLEscapeString(value))\n }\n return template.CSS(strings.Join(parts, ";"))\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// Omit builds a `map[string]any` residual bag from a struct or map value,\n// excluding the given keys \u2014 powers the `{...rest}` spread-onto-element\n// lowering for a destructured `.map()` loop-item\'s object-rest binding\n// (#2087): `.map(({ id, title, ...rest }) => <li {...rest}>)` needs "every\n// field EXCEPT the ones the pattern already destructured out", and a static\n// Go struct type has no way to express "minus a field" \u2014 the exclude set is\n// known at COMPILE TIME (the sibling keys the destructure pattern names), so\n// the compiler passes them here and this does the per-item field-vs-key\n// matching a static type can\'t. The result feeds `bf_spread_attrs`\n// (`SpreadAttrs`), same as a top-level `{...attrs()}` bag.\n//\n// Struct receiver: iterates exported fields via reflection, keyed by each\n// field\'s `json` struct tag (falling back to the Go field name when absent)\n// \u2014 the generated struct\'s json tag is always the ORIGINAL source property\n// name (see `structFieldsFor` / `typeDefinitionToGo` in the Go adapter), so\n// this reproduces the exact JS key `SpreadAttrs`\'s `toAttrName` expects\n// (`"data-priority"`, not a re-derived `"DataPriority"`). A tag of `"-"`\n// (opt-out) is skipped like `encoding/json` does.\n//\n// Map receiver: copies string keys through directly, same exclude/skip\n// rules.\n//\n// Anything else (nil, a non-struct/non-map interface) returns an empty map.\nfunc Omit(item any, excludeKeys ...string) map[string]any {\n exclude := make(map[string]struct{}, len(excludeKeys))\n for _, k := range excludeKeys {\n exclude[k] = struct{}{}\n }\n out := map[string]any{}\n rv := reflect.ValueOf(item)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return out\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Struct:\n rt := rv.Type()\n for i := 0; i < rt.NumField(); i++ {\n field := rt.Field(i)\n if !field.IsExported() {\n continue\n }\n key := field.Name\n if tag, ok := field.Tag.Lookup("json"); ok {\n if comma := strings.Index(tag, ","); comma >= 0 {\n tag = tag[:comma]\n }\n if tag == "-" {\n continue\n }\n if tag != "" {\n key = tag\n }\n }\n if _, skip := exclude[key]; skip {\n continue\n }\n out[key] = rv.Field(i).Interface()\n }\n case reflect.Map:\n for _, k := range rv.MapKeys() {\n if k.Kind() != reflect.String {\n continue\n }\n key := k.String()\n if _, skip := exclude[key]; skip {\n continue\n }\n val := rv.MapIndex(k)\n if val.IsValid() {\n out[key] = val.Interface()\n }\n }\n }\n return out\n}\n\n// goInitialisms mirrors GO_INITIALISMS\n// (packages/adapter-go-template/src/adapter/lib/go-naming.ts) \u2014 the Go-side\n// copy `jsKeyFromGoCasedKey` needs to recognize a whole-word initialism run\n// the same way the TS-side `capitalizeFieldName` produced it (#2490). Keep\n// both lists in sync by hand; there is no shared source between the two\n// languages.\nvar goInitialisms = map[string]struct{}{\n "id": {}, "url": {}, "http": {}, "https": {}, "api": {}, "json": {}, "xml": {},\n "html": {}, "css": {}, "sql": {}, "ip": {}, "tcp": {}, "udp": {}, "dns": {},\n "ssh": {}, "tls": {}, "ssl": {}, "uri": {}, "uid": {}, "uuid": {}, "ascii": {},\n "utf8": {}, "eof": {}, "grpc": {}, "rpc": {}, "cpu": {}, "gpu": {}, "ram": {}, "os": {},\n}\n\n// jsKeyFromGoCasedKey inverts `capitalizeFieldName` (go-naming.ts): recovers\n// the JS-original property name from a Go-cased map key produced by the\n// row\'s field-access contract (#2490). A leading run of 2+ uppercase ASCII\n// letters that, lowercased, is a whole-word Go initialism lowers as a\n// block (`ID` \u2192 `id`, matching `capitalizeFieldName`\'s `id` \u2192 `ID`\n// whole-word branch); otherwise only the first rune lowers (`Title` \u2192\n// `title`, `DataKind` \u2192 `dataKind` \u2014 kebab-casing that back to\n// `data-kind` is `toAttrName`\'s job, not this function\'s). A key whose\n// first rune is already lowercase (not Go-cased) is returned unchanged.\nfunc jsKeyFromGoCasedKey(key string) string {\n if key == "" {\n return key\n }\n first, _ := utf8.DecodeRuneInString(key)\n if !unicode.IsUpper(first) {\n return key\n }\n runLen := 0\n for runLen < len(key) && key[runLen] >= \'A\' && key[runLen] <= \'Z\' {\n runLen++\n }\n // An initialism may carry trailing digits (`utf8` \u2192 `UTF8`); without\n // consuming them the lookup misses and `UTF8` decapitalizes to the\n // wrong `uTF8`.\n for runLen < len(key) && key[runLen] >= \'0\' && key[runLen] <= \'9\' {\n runLen++\n }\n if runLen >= 2 {\n if _, ok := goInitialisms[strings.ToLower(key[:runLen])]; ok {\n return strings.ToLower(key[:runLen]) + key[runLen:]\n }\n }\n return decapitalize(key)\n}\n\n// JSKeys reverses the loop-row Go-casing described above for a WHOLE\n// receiver, keyed by ORIGINAL JS property name \u2014 the counterpart to\n// `SpreadAttrs`\' `bf_js_keys` registration, applied ONLY to the loop-row\n// whole-item spread path (`{...row}` where `row` is the bare `.map()`\n// param, #2490).\n//\n// Struct receiver: prefer each field\'s `json` tag when present (same\n// json-tag recovery `Omit` already does above \u2014 the tag carries the\n// ORIGINAL source property name verbatim, robust to composite/hyphenated\n// names a pure un-casing can\'t reconstruct); fall back to\n// `jsKeyFromGoCasedKey` on the bare field name when no tag is set.\n//\n// Map receiver: `jsKeyFromGoCasedKey` per key.\n//\n// Anything else (nil, a non-struct/non-map interface) returns an empty\n// map rather than panicking.\nfunc JSKeys(item any) map[string]any {\n out := map[string]any{}\n rv := reflect.ValueOf(item)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return out\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Struct:\n rt := rv.Type()\n for i := 0; i < rt.NumField(); i++ {\n field := rt.Field(i)\n if !field.IsExported() {\n continue\n }\n key := jsKeyFromGoCasedKey(field.Name)\n if tag, ok := field.Tag.Lookup("json"); ok {\n if comma := strings.Index(tag, ","); comma >= 0 {\n tag = tag[:comma]\n }\n if tag == "-" {\n continue\n }\n if tag != "" {\n key = tag\n }\n }\n out[key] = rv.Field(i).Interface()\n }\n case reflect.Map:\n for _, k := range rv.MapKeys() {\n if k.Kind() != reflect.String {\n continue\n }\n key := jsKeyFromGoCasedKey(k.String())\n val := rv.MapIndex(k)\n if val.IsValid() {\n out[key] = val.Interface()\n }\n }\n }\n return out\n}\n\n// bfHydrationPayload returns the value to actually `json.Marshal` for a\n// props struct\'s hydration attribute (#2684): when `props` exposes a\n// non-nil `BfCallerProps map[string]interface{}` field (populated by\n// `NewXxxProps` with exactly the keys the caller passed \u2014 see that\n// field\'s doc comment in the generated Props struct for the two-consumers\n// rationale), that map is marshaled INSTEAD OF the struct itself, so the\n// wire payload carries only caller-supplied data \u2014 matching the\n// reference (Hono\'s `serializeHydrationProps`), which only ever\n// serializes caller-passed keys. `ok` is false for a `props` value with\n// no such field (a hand-built Props value, or code generated before this\n// field existed) \u2014 callers fall back to marshaling `props` whole,\n// unchanged from the pre-#2684 behavior. Reused by both `BfPropsAttr` and\n// `ScopeComment` so the two hydration-payload emission sites can\'t drift.\nfunc bfHydrationPayload(props interface{}) (payload interface{}, ok bool) {\n v := reflect.ValueOf(props)\n for v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil, false\n }\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return nil, false\n }\n field := v.FieldByName("BfCallerProps")\n if !field.IsValid() || field.Kind() != reflect.Map || field.IsNil() {\n return nil, false\n }\n m, isMap := field.Interface().(map[string]interface{})\n if !isMap {\n return nil, false\n }\n return m, true\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n // #2684: NOT gated on emptiness \u2014 the reference (Hono) still emits a\n // literal `bf-p="{}"` for a root component that has declared\n // client-tracked props but received none of them from the caller\n // (`JSON.stringify({x: undefined})` drops the key but the object\n // itself, and the attribute, are still real); it only skips the\n // attribute altogether for a component with NO client-tracked props at\n // all, a distinction Go\'s `BfIsRoot`-only gate doesn\'t draw. Matching\n // that finer gate is a separate, pre-existing architectural difference\n // (documented in the PR that introduced this comment), not something\n // this substitution should paper over by guessing at emptiness.\n payload, hasCallerProps := bfHydrationPayload(props)\n if !hasCallerProps {\n payload = props\n }\n\n propsJSON, err := json.Marshal(payload)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// ConcatStr returns a and b concatenated as strings \u2014 the string-typed half\n// of JS `+` (#2168 string-concat-plus). JS `+` is addition when BOTH\n// operands are numeric and concatenation when EITHER is a string; `Add`\n// (above) covers the numeric case, this covers the string one \u2014 `Add`\n// itself can\'t (`toFloat64` returns 0 for a string operand, so `\'Hello, \' +\n// name` silently rendered "0" before this existed).\nfunc ConcatStr(a, b any) string {\n return toString(a) + toString(b)\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Min returns the smaller of a and b (two-arg `Math.min`). Like Mul, it keeps\n// an integer result when both operands are int-like so a CSS value such as\n// `bf_min 100 x` stays `100` rather than `100.000000`. Uses `Number` (not\n// `toFloat64`, which silently zeroes an unrecognized type like a non-numeric\n// string) plus explicit NaN checks, since IEEE-754 `<`/`>` comparisons\n// against NaN are always false and would otherwise let a non-NaN operand\n// win instead of propagating NaN like JS `Math.min`/`Math.max` do.\nfunc Min(a, b any) any {\n av, bv := Number(a), Number(b)\n if math.IsNaN(av) {\n return av\n }\n if math.IsNaN(bv) {\n return bv\n }\n r := av\n if bv < av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Max returns the larger of a and b (two-arg `Math.max`), with the same\n// int-preserving rule and NaN-propagation as Min.\nfunc Max(a, b any) any {\n av, bv := Number(a), Number(b)\n if math.IsNaN(av) {\n return av\n }\n if math.IsNaN(bv) {\n return bv\n }\n r := av\n if bv > av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// TrimStart returns s with leading whitespace removed\n// (String.prototype.trimStart, #2183) \u2014 the one-sided sibling of\n// Trim above, using the same unicode.IsSpace predicate strings.TrimSpace\n// applies to both sides.\nfunc TrimStart(s string) string {\n return strings.TrimLeftFunc(s, unicode.IsSpace)\n}\n\n// TrimEnd returns s with trailing whitespace removed\n// (String.prototype.trimEnd, #2183) \u2014 the one-sided sibling of Trim.\nfunc TrimEnd(s string) string {\n return strings.TrimRightFunc(s, unicode.IsSpace)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Split lowers `String.prototype.split(sep, limit?)` (#1448 Tier B). It\n// wraps `strings.Split` and normalises the result to `[]any` so the\n// slice composes with the array-method surface downstream (`bf_join`,\n// range loops, `bf_len`, \u2026) the same way `bf_slice` / `bf_reverse`\n// results do. Like JS, an empty separator splits into individual UTF-8\n// characters and trailing empty fields are preserved (`"a,".split(",")`\n// \u2192 `["a", ""]`). An optional `limit` caps the number of returned\n// pieces (`"a,b,c".split(",", 2)` \u2192 `["a", "b"]`); a negative limit is\n// ignored (JS would also return every piece \u2014 its ToUint32 wrap makes\n// the limit effectively unbounded). The no-separator form is handled by\n// the adapter (it emits `bf_arr` for the whole-string single element).\nfunc Split(s, sep string, limit ...int) []any {\n parts := strings.Split(s, sep)\n if len(limit) > 0 && limit[0] >= 0 && limit[0] < len(parts) {\n parts = parts[:limit[0]]\n }\n out := make([]any, len(parts))\n for i, p := range parts {\n out[i] = p\n }\n return out\n}\n\n// StartsWith lowers `String.prototype.startsWith(prefix, position?)`\n// (#1448 Tier B). Wraps `strings.HasPrefix`; an empty prefix is always\n// true (JS parity). The optional `position` re-anchors the test to start\n// at that index (clamped to `[0, len]` so it never panics), matching JS\n// `"abc".startsWith("b", 1) === true`.\nfunc StartsWith(s, prefix string, position ...int) bool {\n if len(position) > 0 {\n p := position[0]\n if p < 0 {\n p = 0\n }\n if p > len(s) {\n p = len(s)\n }\n s = s[p:]\n }\n return strings.HasPrefix(s, prefix)\n}\n\n// EndsWith lowers `String.prototype.endsWith(suffix, endPosition?)`\n// (#1448 Tier B). Wraps `strings.HasSuffix`; an empty suffix is always\n// true (JS parity). The optional `endPosition` treats the string as if\n// it were only that many bytes long (clamped to `[0, len]`), matching JS\n// `"abc".endsWith("b", 2) === true`.\nfunc EndsWith(s, suffix string, endPosition ...int) bool {\n if len(endPosition) > 0 {\n e := endPosition[0]\n if e < 0 {\n e = 0\n }\n if e > len(s) {\n e = len(s)\n }\n s = s[:e]\n }\n return strings.HasSuffix(s, suffix)\n}\n\n// Replace lowers the string-pattern form of `String.prototype.replace`\n// (#1448 Tier B). JS replaces only the FIRST occurrence for a string\n// pattern, so the count is 1 (`strings.Replace` with n=1; `ReplaceAll`\n// below is the every-occurrence sibling, `.replaceAll`, #2182). The\n// replacement is treated literally: unlike JS, special replacement\n// patterns like `$&` / `$1` are NOT interpreted (Go and Perl agree on\n// literal replacement, keeping the two template adapters byte-equal;\n// this diverges from the Hono/CSR JS path only for replacement strings\n// that contain `$`-patterns, which are rare in template position).\nfunc Replace(s, old, new string) string {\n return strings.Replace(s, old, new, 1)\n}\n\n// ReplaceAll lowers the string-pattern form of\n// `String.prototype.replaceAll` (#2182): every occurrence, via\n// `strings.ReplaceAll` (equivalent to `strings.Replace` with n=-1).\n// Same literal-replacement caveat as `Replace` above.\nfunc ReplaceAll(s, old, new string) string {\n return strings.ReplaceAll(s, old, new)\n}\n\n// Repeat lowers `String.prototype.repeat(n)` (#1448 Tier B): the\n// receiver concatenated n times. JS throws RangeError for a negative\n// count and `strings.Repeat` panics, so a negative count clamps to the\n// empty string \u2014 SSR templates degrade rather than crash the render.\n// A zero count is the empty string (JS parity).\nfunc Repeat(s string, n int) string {\n if n <= 0 {\n return ""\n }\n return strings.Repeat(s, n)\n}\n\n// padTo lowers the shared body of `String.prototype.padStart` /\n// `padEnd` (#1448 Tier B): pad `s` to `target` code points using `pad`\n// repeated and truncated to fill, prepended (atStart) or appended.\n// Length is measured in runes (not bytes) so the result matches the\n// Perl `bf->pad_*` helpers \u2014 this diverges from JS\'s UTF-16-unit length\n// only for astral-plane input. An empty pad, or a receiver already at\n// least `target` long, returns `s` unchanged (JS parity).\nfunc padTo(s string, target int, pad string, atStart bool) string {\n if pad == "" {\n return s\n }\n sLen := utf8.RuneCountInString(s)\n if sLen >= target {\n return s\n }\n need := target - sLen\n padRunes := []rune(pad)\n fill := make([]rune, 0, need)\n for len(fill) < need {\n for _, r := range padRunes {\n if len(fill) >= need {\n break\n }\n fill = append(fill, r)\n }\n }\n if atStart {\n return string(fill) + s\n }\n return s + string(fill)\n}\n\n// PadStart lowers `String.prototype.padStart(target, pad?)` (#1448 Tier\n// B). The pad string defaults to a single space when omitted.\nfunc PadStart(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, true)\n}\n\n// PadEnd lowers `String.prototype.padEnd(target, pad?)` (#1448 Tier B).\nfunc PadEnd(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, false)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// Ternary returns a when cond is true, else b \u2014 the pipeline-position\n// counterpart of a template {{if}} action. Go templates have no\n// expression-level conditional, so a conditional value sitting in\n// ARGUMENT position (a lowering-node helper arg, e.g. the #2324 union\n// stage\'s locale\u2192pattern ternary) cannot be emitted as an {{if}}\n// fragment; the adapter renders it as `(bf_ternary <cond> <a> <b>)`\n// instead. Both branches are evaluated (function-call semantics) \u2014\n// fine for the value shapes the emitter feeds it, wrong for anything\n// with side effects, which template values never have.\nfunc Ternary(cond bool, a, b any) any {\n if cond {\n return a\n }\n return b\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// RawHTML marks a value as trusted, pre-formatted HTML so html/template\'s\n// contextual escaper emits it verbatim instead of escaping it. It is the SSR\n// half of a dynamic `dangerouslySetInnerHTML={{ __html: expr }}` (#2319) \u2014\n// the one raw-output sink Go lacks as bare template syntax, the counterpart\n// to Blade `{!! !!}`, ERB `<%= %>`, Jinja/MiniJinja `| safe`, Twig `| raw`,\n// Mojolicious `<%== %>`, and Xslate `mark_raw`. The caller owns the value\'s\n// safety (React\'s "dangerously" contract); a nil value renders "".\nfunc RawHTML(v any) template.HTML {\n return template.HTML(String(v))\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// Abs returns the absolute value of v as a float64, mirroring JS\n// `Math.abs`. #2168 math-methods.\nfunc Abs(v any) float64 {\n return math.Abs(Number(v))\n}\n\n// ToFixed formats v with exactly `digits` decimal places, mirroring JS\n// `Number.prototype.toFixed` (zero-padding + half-toward-+Infinity\n// rounding). JS rounds the scaled integer half up (`(2.5).toFixed(0)`\n// is "3"); bare `fmt.Sprintf("%.*f")` rounds half-to-even ("2"), so we\n// scale, round with `Floor(x + 0.5)` (matching `Round`), then format\n// the exact multiple. #1897.\nfunc ToFixed(v any, digits int) string {\n if digits < 0 {\n digits = 0\n }\n n := Number(v)\n // JS toFixed returns the strings "NaN" / "Infinity" / "-Infinity" for\n // non-finite inputs; fmt would render "NaN"/"+Inf"/"-Inf".\n if math.IsNaN(n) {\n return "NaN"\n }\n if math.IsInf(n, 1) {\n return "Infinity"\n }\n if math.IsInf(n, -1) {\n return "-Infinity"\n }\n factor := math.Pow(10, float64(digits))\n rounded := math.Floor(n*factor + 0.5)\n return fmt.Sprintf("%.*f", digits, rounded/factor)\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Length lowers JS `.length`, matching JS semantics per receiver shape\n// (#2255): a slice/array/map counts ELEMENTS (`reflect.Value.Len`, same as\n// `Len` below), but a STRING counts UTF-16 CODE UNITS \u2014 JS\n// `String.prototype.length` counts UTF-16 code units, not bytes (Go\'s\n// native `len`) or codepoints. A codepoint outside the Basic Multilingual\n// Plane (astral, U+10000-U+10FFFF \u2014 e.g. \'\u{1F44D}\') is a surrogate PAIR in\n// UTF-16, so it counts as 2, not 1; `\'\u65E5\u672C\u8A9E\'` is 3 either way (BMP-only).\n// Routed from the `.length` member lowering\'s generic (non-array,\n// non-loop-slice) fallback \u2014 see `member()`\'s `bf_length` call site.\nfunc Length(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.Chan:\n return rv.Len()\n case reflect.String:\n n := 0\n for _, r := range rv.String() {\n if r > 0xFFFF {\n n += 2\n } else {\n n++\n }\n }\n return n\n default:\n return 0\n }\n}\n\n// IsValidElement lowers React/Hono-style `isValidElement(x)` \u2014 the "is this\n// a renderable element (not plain text)?" predicate the `Slot` component\'s\n// `asChild` pattern (#2266) uses to decide whether to merge props into a\n// child ELEMENT (`children.tag`/`children.props`) or fall back to rendering\n// `children` as-is. The JS runtime checks `\'tag\' in x && \'props\' in x`; on\n// Go SSR a passed-through JSX child is represented as pre-rendered markup\n// (a plain string) OR \u2014 where a struct/map shape carrying `Tag`/`Props`\n// (case-insensitively, mirroring `bf_get`\'s field lookup) is available \u2014 an\n// element-shaped value. A plain string/number/bool/nil is never a valid\n// element, so `isValidElement` must NOT be lowered as bare truthiness\n// (previously done via `renderConditionExpr`) \u2014 a truthy non-empty STRING\n// child wrongly took the element-merge branch and panicked dereferencing\n// `.Props` on a string (`can\'t evaluate field Props in type interface {}`).\nfunc IsValidElement(v any) bool {\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return false\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Map:\n hasTag, hasProps := false, false\n for _, k := range rv.MapKeys() {\n key := fmt.Sprintf("%v", k.Interface())\n if strings.EqualFold(key, "tag") {\n hasTag = true\n }\n if strings.EqualFold(key, "props") {\n hasProps = true\n }\n }\n return hasTag && hasProps\n case reflect.Struct:\n return fieldByFoldedName(rv, "tag").IsValid() && fieldByFoldedName(rv, "props").IsValid()\n default:\n return false\n }\n}\n\n// fieldByFoldedName finds a struct field by case-insensitive name match \u2014\n// shared by IsValidElement; mirrors getFieldValue\'s (bf_get) struct-branch\n// lookup so the two case-tolerant field resolutions stay consistent.\nfunc fieldByFoldedName(rv reflect.Value, name string) reflect.Value {\n t := rv.Type()\n for i := 0; i < t.NumField(); i++ {\n if strings.EqualFold(t.Field(i).Name, name) {\n return rv.Field(i)\n }\n }\n return reflect.Value{}\n}\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: SameValueZero element search (matches\n// the evaluator\'s `evalSameValueZero`/`evalIncludes` in eval.go,\n// which back the serialized-callback path) \u2014 numeric types compare\n// by value across int/float64 the way JS\'s single "number" type\n// does, and NaN matches NaN (unlike `===`). This used to be\n// `reflect.DeepEqual`, which is type-strict (`int(2)` != `float64(2)`)\n// and never matches NaN to NaN; that diverged from the evaluator\'s\n// `.includes` and from JS itself, so it was unified here.\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if evalSameValueZero(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// clampSliceRange normalizes JS `.slice(start, end?)` bounds against a\n// receiver of `length` elements (runes, for the string branch of\n// `Slice` below; array elements, for the array branch) \u2014 shared so\n// both branches clamp identically.\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\nfunc clampSliceRange(length, start int, end []int) (int, int) {\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n return start, stop\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A) AND\n// `String.prototype.slice(start, end?)` (the `string-slice`\n// divergence) \u2014 the adapter emits the same `bf_slice` call for both\n// receiver shapes (it can\'t disambiguate string vs. array at compile\n// time), so this helper dispatches at runtime on `reflect.Kind()`,\n// mirroring `Includes` above. The variadic `end` arg lets Go\n// template\'s call dispatcher pass either 2 or 3 arguments; an absent\n// end means "to length".\n//\n// String length/positions are measured in runes, not UTF-16 code\n// units \u2014 the same divergence boundary `padTo` already accepts\n// (differs from JS only for astral-plane input). `start >= end`\n// (after clamping) returns an empty result for either receiver.\n//\n// Any other receiver kind returns an empty `[]any`.\nfunc Slice(items any, start int, end ...int) any {\n v := reflect.ValueOf(items)\n\n if v.Kind() == reflect.String {\n runes := []rune(v.String())\n s, e := clampSliceRange(len(runes), start, end)\n if s >= e {\n return ""\n }\n return string(runes[s:e])\n }\n\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n s, e := clampSliceRange(v.Len(), start, end)\n if s >= e {\n return []any{}\n }\n out := make([]any, 0, e-s)\n for i := s; i < e; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// Flat flattens nested slices/arrays `depth` levels deep. Lowers\n// `Array.prototype.flat(depth?)` (#1448 Tier C). A `depth` of `-1` is the\n// `Infinity` sentinel (flatten fully); `0` (or negative-from-JS, already\n// normalised to 0 at compile time) returns a shallow copy. Non-array\n// elements are kept as-is (JS only flattens nested arrays). A non-array\n// receiver returns an empty `[]any`.\nfunc Flat(items any, depth int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n ev := reflect.ValueOf(el)\n if depth != 0 && (ev.Kind() == reflect.Slice || ev.Kind() == reflect.Array) {\n // `-1` (Infinity) recurses unbounded; a finite depth spends one level.\n next := depth\n if depth > 0 {\n next = depth - 1\n }\n out = append(out, Flat(el, next)...)\n } else {\n out = append(out, el)\n }\n }\n return out\n}\n\n// FlatDynamicDepth coerces `depth` via JS\'s `ToIntegerOrInfinity` and\n// flattens `items` that many levels. Lowers a DYNAMIC `.flat(depth)`\n// (#2094) \u2014 one whose depth isn\'t a compile-time literal, so (unlike\n// `Flat` above) the coercion happens here at render time instead of in the\n// parser.\n//\n// This is a SEPARATE helper from `Flat`/`bf_flat` \u2014 NOT a drop-in\n// replacement \u2014 because `Flat`\'s `depth int` parameter treats `-1` as a\n// compile-time SENTINEL meaning "flatten fully" (the parser\'s own\n// normalisation of a literal `Infinity`). A genuinely dynamic depth value\n// of `-1` means the JS-correct OPPOSITE: `Array.prototype.flat(-1)` never\n// recurses (same as `.flat(0)`, a shallow copy), because\n// `FlattenIntoArray` only recurses when `depth > 0`. Reusing `Flat`\'s int\n// contract for a raw dynamic value would silently invert that case, so\n// this function coerces FIRST \u2014 mapping a real `+Infinity` / huge finite\n// value to `Flat`\'s own `-1` sentinel, and a real negative value to `0` \u2014\n// and only then delegates to `Flat`\'s recursion.\n//\n// Coercion rules (JS `ToIntegerOrInfinity`, mirrored exactly; pinned by the\n// `flat_dynamic` golden-vector cases in\n// packages/adapter-tests/vectors/cases.ts):\n// - the value converts via `ToNumber` first (numeric string / bool /\n// number all coerce; see `flatDepthToFloat`);\n// - a NaN result (including a non-numeric string) \u2192 `0`;\n// - truncates toward zero (`2.7` \u2192 `2`);\n// - negative \u2192 `0`;\n// - `+Infinity` / a huge finite value \u2192 flattens fully.\nfunc FlatDynamicDepth(items any, depth any) []any {\n return Flat(items, coerceFlatDepth(depth))\n}\n\n// coerceFlatDepth implements JS\'s `ToIntegerOrInfinity` for a dynamic\n// `.flat(depth)` argument, returning an int in `Flat`\'s own contract (`-1`\n// = unbounded, `>= 0` = that many levels).\nfunc coerceFlatDepth(depth any) int {\n f, ok := flatDepthToFloat(depth)\n if !ok || math.IsNaN(f) {\n return 0\n }\n if math.IsInf(f, 1) {\n return -1 // Flat\'s "flatten fully" sentinel\n }\n if math.IsInf(f, -1) {\n return 0\n }\n trunc := math.Trunc(f)\n if trunc < 0 {\n return 0\n }\n // A huge finite depth behaves identically to "flatten fully" in\n // practice \u2014 real data bottoms out at its actual nesting depth long\n // before a counter this large would ever reach zero. Capping it here\n // avoids an absurd countdown without needing a second sentinel.\n if trunc > 1_000_000 {\n return -1\n }\n return int(trunc)\n}\n\n// flatDepthToFloat converts a dynamic `.flat(depth)` argument to a float64,\n// mirroring JS\'s `ToNumber` across the value shapes a Go template data\n// model can carry (every numeric kind, bool, numeric string). `ok` is\n// false for a shape `ToNumber` can\'t coerce meaningfully (`nil`, or a\n// non-numeric string) \u2014 `coerceFlatDepth` treats that the same as NaN\n// (\u2192 depth `0`), matching JS.\nfunc flatDepthToFloat(v any) (float64, bool) {\n switch n := v.(type) {\n case nil:\n return 0, false\n case float64:\n return n, true\n case float32:\n return float64(n), true\n case int:\n return float64(n), true\n case int8:\n return float64(n), true\n case int16:\n return float64(n), true\n case int32:\n return float64(n), true\n case int64:\n return float64(n), true\n case uint:\n return float64(n), true\n case uint8:\n return float64(n), true\n case uint16:\n return float64(n), true\n case uint32:\n return float64(n), true\n case uint64:\n return float64(n), true\n case bool:\n if n {\n return 1, true\n }\n return 0, true\n case string:\n s := strings.TrimSpace(n)\n if s == "" {\n return 0, true // JS: Number("") is 0\n }\n f, err := strconv.ParseFloat(s, 64)\n if err != nil {\n return 0, false // not numeric \u2192 NaN path\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// FlatMap projects each element through a `self` / `field` projection and\n// flattens the result one level. Lowers value-returning\n// `Array.prototype.flatMap(fn)` for the field-projection catalogue\n// (#1448 Tier C): `items.flatMap(i => i)` (self) and\n// `items.flatMap(i => i.field)` (field). A projected non-array value is\n// kept as-is (flatMap = map + flat(1)). Non-array receiver \u2192 empty.\nfunc FlatMap(items any, keyKind, keyName string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n projected := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n if keyKind == "field" {\n projected = append(projected, getFieldValue(el, keyName))\n } else {\n projected = append(projected, el)\n }\n }\n return Flat(projected, 1)\n}\n\n// FlatMapTuple lowers an array-literal flatMap projection\n// `items.flatMap(i => [i.a, i.b])` (#1448 Tier C). `specs` is a flat list\n// of (kind, name) pairs, one per array-literal leaf: ("self", "") for the\n// item itself, ("field", "<Name>") for a struct field. For each item it\n// appends every leaf\'s value in order. Unlike the scalar `FlatMap`, the\n// per-item array is flattened only one level (flat(1) removes the literal\n// wrapper), so an array-valued leaf is appended verbatim rather than\n// spread \u2014 which is exactly "append each leaf". Non-array receiver \u2192 empty.\nfunc FlatMapTuple(items any, specs ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n for j := 0; j+1 < len(specs); j += 2 {\n if specs[j] == "field" {\n out = append(out, getFieldValue(el, specs[j+1]))\n } else {\n out = append(out, el)\n }\n }\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// Truthy is the exported form of isTruthy \u2014 JavaScript\'s `Boolean(x)`\n// semantics. Two callers:\n// - generated `NewXxxProps` code lowering a conditional inline-object\n// spread condition on an `interface{}` prop (whose runtime value may be\n// a string, number, bool, \u2026), keeping the spread bag\'s inclusion test\n// faithful to JS rather than string-biased (#1752); and\n// - the `bf_truthy` template FuncMap entry (#2335), which coerces a\n// `bf_ternary` test to a real bool when it isn\'t already a comparison /\n// negation (`Ternary`\'s `cond` parameter is typed `bool`, unlike\n// `{{if}}`\'s built-in truthiness). Uniform across string/number/bool/nil,\n// so a `bf_ternary` test on any prop type can\'t hit a `bool`-vs-`string`\n// comparison error the way a string-only `ne <value> ""` would.\nfunc Truthy(v any) bool { return isTruthy(v) }\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// fieldValue projects item.field for the higher-order predicate\n// helpers. The field name arrives in JS casing; structs resolve via\n// the capitalized Go convention (FieldByName inside getFieldValue),\n// maps via getFieldValue\'s case-variant lookup \u2014 the same dual\n// support Sort/Reduce gained in #1487, extended here so JSON-decoded\n// data (map items) participates instead of being silently skipped.\n// nil-safe: missing fields and nil items project to nil.\nfunc fieldValue(item any, field string) any {\n return getFieldValue(item, capitalize(field))\n}\n\n// Every returns true if every item\'s field is truthy under JS\n// `Boolean(item.field)` semantics. Mirrors JavaScript\'s\n// Array.prototype.every(item => item.field) \u2014 including being\n// vacuously true for an empty receiver.\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if !isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item\'s field is truthy. Mirrors\n// JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n var result []any\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n result = append(result, item)\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// sortKeySpec is one parsed comparison key. A simple comparator has\n// one; a `||`-chained multi-key comparator has several, applied in\n// order as tie-breakers.\ntype sortKeySpec struct {\n kind string // "self" | "field"\n name string // capitalised field name, or "" for "self"\n compareType string // "numeric" | "string" | "auto"\n direction string // "asc" | "desc"\n}\n\n// Sort returns a new stable-sorted slice. Lowers\n// `Array.prototype.sort` / `Array.prototype.toSorted` (#1448 Tier B).\n// Non-mutating \u2014 JS\'s mutate-vs-new distinction is moot in SSR\n// template context (templates render a snapshot).\n//\n// Call shape (the compiler emits one 4-string group per key):\n//\n// bf_sort <items> (<keyKind> <keyName> <compareType> <direction>)+\n//\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field\n// name (e.g. "Price") otherwise\n// compareType: "numeric" | "string" | "auto"\n// direction: "asc" | "desc"\n//\n// The groups cover the accepted comparator catalogue: `a.f - b.f`,\n// `a - b`, `a[.f].localeCompare(b[.f])`, and relational-ternary keys\n// (`a.f > b.f ? 1 : -1` \u2192 "auto"), each `||`-chainable for multi-key\n// tie-breaks. Anything outside refuses at compile time (BF101 from the\n// JSX compiler) and never reaches this helper.\n//\n// "auto" compares numerically when both projected keys parse as\n// numbers, else lexically \u2014 mirroring the Perl `bf->sort` helper\'s\n// `looks_like_number` rule so the two template adapters stay\n// byte-equal. This diverges from JS `<`/`>` only for numeric strings.\n//\n// A future `nulls` knob can extend the per-key group without rewriting\n// existing call sites \u2014 each key already projects before comparing.\nfunc Sort(items any, spec ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return []any{}\n }\n\n // Copy into a fresh []any so the sort is non-mutating regardless\n // of whether the receiver is `[]T` or `[]any`.\n result := make([]any, length)\n for i := 0; i < length; i++ {\n result[i] = v.Index(i).Interface()\n }\n\n keys := parseSortSpec(spec)\n sort.SliceStable(result, func(i, j int) bool {\n for _, k := range keys {\n ki := projectSortKey(result[i], k.kind, k.name)\n kj := projectSortKey(result[j], k.kind, k.name)\n c := compareSortKey(ki, kj, k.compareType)\n if c == 0 {\n continue // tie on this key \u2014 fall through to the next\n }\n if k.direction == "desc" {\n return c > 0\n }\n return c < 0\n }\n return false\n })\n\n return result\n}\n\n// parseSortSpec chunks the variadic operand list into 4-string key\n// groups. A trailing partial group (malformed emit) is ignored rather\n// than panicking \u2014 defensive, mirroring the helper\'s nil-safe stance.\nfunc parseSortSpec(spec []string) []sortKeySpec {\n var keys []sortKeySpec\n for i := 0; i+3 < len(spec); i += 4 {\n keys = append(keys, sortKeySpec{\n kind: spec[i],\n name: spec[i+1],\n compareType: spec[i+2],\n direction: spec[i+3],\n })\n }\n return keys\n}\n\n// compareSortKey returns -1 / 0 / 1 for two projected keys under the\n// given compare type (ascending orientation; the caller flips for\n// "desc"). "string" stringifies both (nil \u2192 "", matching the\n// documented `bf->string(undef) === ""` divergence). "auto" compares\n// numerically when both parse as numbers, else lexically.\nfunc compareSortKey(ki, kj any, compareType string) int {\n switch compareType {\n case "string":\n return strings.Compare(toString(ki), toString(kj))\n case "auto":\n ni, okI := toFloat64WithOK(ki)\n nj, okJ := toFloat64WithOK(kj)\n if okI && okJ {\n return cmpFloat(ni, nj)\n }\n return strings.Compare(toString(ki), toString(kj))\n default: // numeric\n return cmpFloat(toFloat64(ki), toFloat64(kj))\n }\n}\n\nfunc cmpFloat(a, b float64) int {\n if a < b {\n return -1\n }\n if a > b {\n return 1\n }\n return 0\n}\n\n// toFloat64WithOK reports a value\'s numeric float and whether it is\n// number-like. Genuine numeric kinds always qualify; strings qualify\n// when they parse as a float (so the "auto" compare path matches the\n// Perl `looks_like_number` rule). Everything else is non-numeric.\nfunc toFloat64WithOK(v any) (float64, bool) {\n switch n := v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return toFloat64(v), true\n case string:\n f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)\n if err != nil {\n return 0, false\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// projectSortKey reduces an item to the value the comparator\n// actually compares. For `keyKind == "field"` it reads the named\n// struct field; for `keyKind == "self"` (primitive arrays) it\n// returns the item unchanged.\nfunc projectSortKey(item any, keyKind, keyName string) any {\n if keyKind == "field" {\n return getFieldValue(item, keyName)\n }\n return item\n}\n\n// getFieldValue extracts a struct field, map entry, or slice/array/string\n// element using reflection, dispatching on the RUNTIME kind of `item`\n// rather than a compile-time guess about `field`\'s shape (#2491: a\n// dynamic-key element access on a loop row, `tone[k]`, is only known to\n// be string- or number-shaped at execution time \u2014 routing it through a\n// single runtime-polymorphic accessor, mirroring Jinja/minijinja `[]`\n// and Blade\'s `data_get()`, replaces the compile-time either/or guess\n// that broke for one shape or the other). For map/struct receivers it\n// falls back to case-variant lookup so JSON-decoded user data\n// (`map[string]any{"price": 30}`) and PascalCase-emitted test data both\n// resolve under a single key name. (#1487) `field` is `any` (not\n// `string`) precisely so a genuine numeric index (`bf_get $arr $i`,\n// e.g. `selected()[index]`) round-trips as an int rather than being\n// forced through a string conversion \u2014 this is a strict superset of the\n// `index` builtin, not a replacement that narrows numeric-index support.\nfunc getFieldValue(item any, field any) any {\n v := reflect.ValueOf(item)\n // Defensive IsNil guards mirror `SpreadAttrs` \u2014 keeps the helper\n // safe against typed-nil pointer / nil-interface items inside a\n // `[]any` so a single bad row doesn\'t crash the whole sort.\n if v.Kind() == reflect.Interface {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n if v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n\n if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {\n idx, ok := fieldAsIndex(field)\n if !ok || idx < 0 || idx >= v.Len() {\n return nil\n }\n return v.Index(idx).Interface()\n }\n\n if v.Kind() == reflect.String {\n idx, ok := fieldAsIndex(field)\n if !ok {\n return nil\n }\n runes := []rune(v.String())\n if idx < 0 || idx >= len(runes) {\n return nil\n }\n return string(runes[idx])\n }\n\n // From here on only a string-shaped key can match a map or struct\n // field \u2014 a numeric `field` (e.g. a loop index applied to a\n // non-indexable receiver) has no lookup to perform.\n fieldStr, ok := field.(string)\n if !ok {\n return nil\n }\n\n if v.Kind() == reflect.Map {\n keyType := v.Type().Key()\n if keyType.Kind() != reflect.String {\n return nil\n }\n // Convert the lookup string to the map\'s actual key type so\n // maps keyed by a named string type (`type Key string`) don\'t\n // panic with `value of type string is not assignable to type X`.\n lookup := func(s string) (any, bool) {\n k := reflect.ValueOf(s).Convert(keyType)\n if mv := v.MapIndex(k); mv.IsValid() {\n return mv.Interface(), true\n }\n return nil, false\n }\n if r, ok := lookup(fieldStr); ok {\n return r\n }\n if cap := capitalize(fieldStr); cap != fieldStr {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(fieldStr); low != fieldStr {\n if r, ok := lookup(low); ok {\n return r\n }\n }\n // All-lowercase fallback: a Go-initialism field projects as an\n // all-caps key (`id` \u2192 `ID`), and `decapitalize("ID")` only\n // lowers the first char (`iD`), so the JS-keyed map ("id") still\n // misses. Try the fully-lowered key last to resolve it.\n if lower := strings.ToLower(fieldStr); lower != fieldStr && lower != decapitalize(fieldStr) {\n if r, ok := lookup(lower); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(fieldStr)\n if !fieldVal.IsValid() {\n // Case-variant fallback: the evaluator carries the JS field name\n // (`id` / `url`) against a Go-capitalised struct field (`ID` / `URL`),\n // which exact `FieldByName` misses and the initialism rules can\'t be\n // reproduced char-for-char here. Match case-insensitively instead \u2014\n // `FieldByNameFunc` returns the zero Value (\u2192 nil) for an ambiguous\n // match, so it stays safe. The legacy bf_sort/bf_reduce pass an\n // already-capitalised name, so they hit the exact match above and never\n // reach this fallback.\n fieldVal = v.FieldByNameFunc(func(n string) bool { return strings.EqualFold(n, fieldStr) })\n if !fieldVal.IsValid() {\n return nil\n }\n }\n return fieldVal.Interface()\n}\n\n// fieldAsIndex converts a `getFieldValue` key argument to a slice/array/\n// string element index. Accepts genuine numeric kinds (the loop-index\n// case, `bf_get $arr $i`) directly, and a numeric-looking string (a\n// dynamic key that happens to be digits) via `strconv.Atoi` so a\n// string-typed index used against an array-shaped receiver still\n// resolves rather than silently missing.\nfunc fieldAsIndex(field any) (int, bool) {\n if isIntLike(field) {\n return toInt(field), true\n }\n switch n := field.(type) {\n // Only an INTEGRAL float is an index. JS `arr[1.2]` is a property\n // lookup (undefined), not index 1, so truncating here would diverge.\n case float32:\n if float64(n) != math.Trunc(float64(n)) {\n return 0, false\n }\n return int(n), true\n case float64:\n if n != math.Trunc(n) {\n return 0, false\n }\n return int(n), true\n case string:\n i, err := strconv.Atoi(n)\n if err != nil {\n return 0, false\n }\n return i, true\n default:\n return 0, false\n }\n}\n\n// AsMap normalizes a dynamically-typed prop value into a\n// map[string]interface{} for object-valued context bindings\n// (`lowerProviderMapMemberValue`, go-template-adapter.ts). A caller-side\n// `interface{}` field can legally hold ANY string-keyed map kind \u2014 a Go\n// handler modelling `Record<string, string>` naturally passes\n// map[string]string \u2014 so a bare `.(map[string]interface{})` type assertion\n// would silently drop provided values (#2111 review). Returns nil (never an\n// empty map) when the value is absent \u2014 nil interface, typed-nil map or\n// pointer, or any non-map / non-string-keyed value \u2014 so the generated\n// `?? {}` fallback can distinguish "missing" (fall back) from "present but\n// empty" (use as-is). map[string]interface{} passes through without copying.\nfunc AsMap(v any) map[string]interface{} {\n if v == nil {\n return nil\n }\n if m, ok := v.(map[string]interface{}); ok {\n if m == nil {\n return nil\n }\n return m\n }\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n if rv.IsNil() {\n return nil\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map || rv.Type().Key().Kind() != reflect.String || rv.IsNil() {\n return nil\n }\n out := make(map[string]interface{}, rv.Len())\n iter := rv.MapRange()\n for iter.Next() {\n out[iter.Key().String()] = iter.Value().Interface()\n }\n return out\n}\n\n// capitalize uppercases the first character of a string.\nfunc capitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToUpper(s[:1]) + s[1:]\n}\n\n// decapitalize lowercases the first character of a string. Used by\n// `getFieldValue`\'s map-receiver fallback when the projected key\n// name is PascalCase but the receiver carries lowercase JS-style\n// keys (the inverse of the `capitalize` lookup).\nfunc decapitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToLower(s[:1]) + s[1:]\n}\n\n// Reduce folds an array into a scalar via the arithmetic-fold\n// catalogue (#1448 Tier C). It lowers `Array.prototype.reduce(fn, init)`\n// and `Array.prototype.reduceRight(fn, init)` for the shapes\n// `(acc, x) => acc <op> x` and `(acc, x) => acc <op> x.field`:\n//\n// bf_reduce <items> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"\n//\n// direction: "left" (reduce) | "right" (reduceRight). Only changes the\n// result for string concatenation; numeric folds commute.\n//\n// op: "+" | "*"\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field name\n// (e.g. "Duration") otherwise\n// type: "numeric" | "string"\n// init: the fold\'s start value \u2014 the compiler emits the *decoded*\n// seed, so numeric inits arrive as canonical decimal\n// (`1_000`/`0x10` already normalised to `1000`/`16`) that\n// ParseFloat accepts, and string inits arrive as escape-free\n// contents\n//\n// Numeric folds accumulate as float64; each projected key is read via\n// `toFloat64WithOK`, so numeric *strings* ("5" \u2192 5) parse and\n// non-numeric values fold as 0 \u2014 matching Perl\'s\n// `looks_like_number ? $n : 0` so the two template adapters stay\n// byte-equal. String folds concatenate (toString per projected key,\n// matching the documented `bf->string(undef) === ""` convention). The\n// init seeds the accumulator, so an empty receiver returns the init\n// unchanged \u2014 exactly like JS `reduce(fn, init)`. Anything outside the\n// catalogue refuses at compile time (BF101 from the JSX compiler) and\n// never reaches here.\n//\n// Two documented divergences from the JS / Hono path, both rare and\n// mirroring the `bf_sort` "auto" caveat:\n// - float64 stringification differs for sums whose binary expansion\n// isn\'t exact (e.g. 0.1 + 0.2);\n// - numeric-*string* keys fold numerically here, but JS `+`\n// string-concatenates once an operand is a string, so\n// numeric-string data can render differently under CSR.\n//\n// Genuine numbers \u2014 the common SSR case \u2014 agree across all three.\nfunc Reduce(items any, op, keyKind, keyName, typ, init, direction string) any {\n v := reflect.ValueOf(items)\n isSlice := v.Kind() == reflect.Slice || v.Kind() == reflect.Array\n\n // `direction == "right"` (reduceRight) folds right-to-left. Only\n // observable for string concatenation \u2014 numeric sum / product are\n // commutative, so the order doesn\'t change the result there. Build a\n // start/stop/step triple so both folds share one loop shape.\n start, stop, step := 0, 0, 1\n if isSlice {\n stop = v.Len()\n if direction == "right" {\n start, stop, step = v.Len()-1, -1, -1\n }\n }\n\n if typ == "string" {\n acc := init\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n acc += toString(key)\n }\n }\n return acc\n }\n\n // numeric fold\n acc, _ := strconv.ParseFloat(strings.TrimSpace(init), 64)\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n // `toFloat64WithOK` parses numeric *strings* ("5" \u2192 5) and\n // returns 0 for non-numeric values \u2014 mirroring Perl\'s\n // `looks_like_number ? $n : 0` so numeric-string data folds\n // byte-equal across adapters (the same rule `bf_sort`\'s\n // "auto" compare uses). Plain `toFloat64` would zero "5".\n n, _ := toFloat64WithOK(key)\n if op == "*" {\n acc *= n\n } else {\n acc += n\n }\n }\n }\n return acc\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n // Same caller-props-sidecar substitution as BfPropsAttr (#2684) \u2014\n // see that function\'s comment for why this is NOT gated on\n // emptiness.\n payload, hasCallerProps := bfHydrationPayload(props)\n if !hasCallerProps {\n payload = props\n }\n pJSON, err := json.Marshal(payload)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// ScopeCommentEnd emits the paired end marker for a fragment-rooted scope\n// (#2289): a fragment root has no single wrapping element to bound the\n// client\'s scope query, so the range leaks onto later siblings without an\n// explicit terminator. Carries only the scope id \u2014 no `|h=`/`|m=`/props\n// segment, unlike ScopeComment \u2014 since the client only needs it to confirm\n// the range closes on the matching scope (getCommentScopeBoundary in\n// packages/client/src/runtime/scope.ts).\nfunc ScopeCommentEnd(props interface{}) template.HTML {\n scopeID := getStringField(props, "ScopeID")\n return template.HTML("<!--bf-/scope:" + scopeID + "-->")\n}\n\n// TemplateFuncMap returns the helpers that need access to the executing\n// template set itself, closed over the *template.Template the component\n// defines are parsed into. Register it alongside FuncMap BEFORE parsing:\n//\n// t := template.New("")\n// t.Funcs(bf.FuncMap()).Funcs(bf.TemplateFuncMap(t))\n// template.Must(t.Parse(src))\n//\n// bf_tmpl executes a named define from the same set and returns its\n// output \u2014 used for the per-call-site children defines the Go adapter\n// emits when JSX children passed to an imported component contain\n// template actions (nested components, dynamic text) and therefore\n// cannot be baked to a static HTML string (#1896). Reentrant execution\n// of an html/template set from inside a FuncMap function is safe: the\n// escape analysis over every define completes before the outer\n// Execute begins evaluating.\nfunc TemplateFuncMap(t *template.Template) template.FuncMap {\n return template.FuncMap{\n "bf_tmpl": func(name string, data interface{}) (template.HTML, error) {\n var buf bytes.Buffer\n if err := t.ExecuteTemplate(&buf, name, data); err != nil {\n return "", err\n }\n return template.HTML(buf.String()), nil\n },\n }\n}\n\n// WithChildren returns a shallow copy of a component Props struct with its\n// Children field replaced by the given pre-rendered fragment (#1896). The\n// props value stays by-value semantics: callers\' originals are untouched.\n// A props type without a Children field passes through unchanged \u2014 the\n// child template then simply has no children to render, matching the\n// pre-#1896 behaviour.\nfunc WithChildren(props interface{}, children template.HTML) (interface{}, error) {\n v := reflect.ValueOf(props)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return props, nil\n }\n field := v.FieldByName("Children")\n if !field.IsValid() {\n return props, nil\n }\n copyPtr := reflect.New(v.Type())\n copyPtr.Elem().Set(v)\n target := copyPtr.Elem().FieldByName("Children")\n switch {\n case target.Kind() == reflect.Interface:\n target.Set(reflect.ValueOf(children))\n case target.Kind() == reflect.String:\n // Covers both `string` and `template.HTML`-typed fields.\n target.SetString(string(children))\n default:\n return props, fmt.Errorf("bf_with_children: unsupported Children field type %s", target.Type())\n }\n return copyPtr.Elem().Interface(), nil\n}\n\n// WithProps returns a shallow copy of a component Props struct with the\n// given fields overridden (#2445): a child component nested inside a\n// COMPOSITE loop row (row root is a plain element, not the child itself) is\n// constructed ONCE outside `{{range}}` \u2014 every row shares that instance for\n// scope/parent/mount identity \u2014 so a prop that depends on the row\n// (`text={row.label}`) has to be applied per row, at template-execution\n// time, the same way WithChildren applies per-row JSX children to that\n// shared instance. `kv` is a flat name/value list ("Text", .Label, "Count",\n// .N, ...). The props value stays by-value semantics: the caller\'s original\n// is untouched. A name with no matching settable field is left alone for\n// that pair \u2014 the prop routes elsewhere (e.g. a rest bag) and the base\n// instance\'s constructor-built value stands, mirroring WithChildren\'s\n// "props type without a Children field" passthrough.\n//\n// This overrides fields on the ALREADY-CONSTRUCTED instance \u2014 it does not\n// re-run New<Child>Props. A field the child derives FROM the overridden prop\n// at construction time (a memo body, or a signal\'s initial value \u2014 the\n// constructor bakes both) would keep whatever the one-shot constructor\n// computed and never update per row; only the directly-overridden field is\n// correct per row. The compiler therefore does not route that case here: a\n// child with any constructor-derived field gets a generated props rebuilder\n// and the call site emits bf_reprops instead, which re-runs the real\n// constructor per row (#2448, see reprops.go). What reaches this helper is\n// the plain-passthrough case, where patching the field IS the whole update.\n//\n// Still exported and still registered: templates generated before #2448 call\n// it, and it remains the cheaper path when nothing is derived.\nfunc WithProps(props interface{}, kv ...interface{}) (interface{}, error) {\n if len(kv)%2 != 0 {\n return nil, fmt.Errorf("bf_with_props: odd number of key/value arguments (%d)", len(kv))\n }\n v := reflect.ValueOf(props)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return props, nil\n }\n copyPtr := reflect.New(v.Type())\n copyPtr.Elem().Set(v)\n for i := 0; i < len(kv); i += 2 {\n name, ok := kv[i].(string)\n if !ok {\n return nil, fmt.Errorf("bf_with_props: field name at position %d must be a string, got %T", i, kv[i])\n }\n target := copyPtr.Elem().FieldByName(name)\n if !target.IsValid() || !target.CanSet() {\n continue\n }\n if err := setStructFieldValue(target, kv[i+1]); err != nil {\n return nil, fmt.Errorf("bf_with_props: field %s: %w", name, err)\n }\n }\n return copyPtr.Elem().Interface(), nil\n}\n\n// setStructFieldValue assigns val into target, a settable struct field\n// obtained via reflect.Value.FieldByName. Mirrors WithChildren\'s\n// Interface/String branches (a String-kind target covers both `string` and\n// `template.HTML` fields, via String() rather than reflect.Convert \u2014 Go\'s\n// int-to-string conversion produces a rune, not a decimal string) and adds\n// the general assignable/convertible fallback for other field kinds\n// (numeric widening, etc.).\nfunc setStructFieldValue(target reflect.Value, val interface{}) error {\n if val == nil {\n target.Set(reflect.Zero(target.Type()))\n return nil\n }\n switch {\n case target.Kind() == reflect.Interface:\n target.Set(reflect.ValueOf(val))\n return nil\n case target.Kind() == reflect.String:\n target.SetString(String(val))\n return nil\n }\n rv := reflect.ValueOf(val)\n switch {\n case rv.Type().AssignableTo(target.Type()):\n target.Set(rv)\n case rv.Type().ConvertibleTo(target.Type()):\n target.Set(rv.Convert(target.Type()))\n default:\n return fmt.Errorf("cannot assign %T to %s", val, target.Type())\n }\n return nil\n}\n\n// PortalHTML parses and executes a template string with the provided data.\n// Used for rendering dynamic portal content where the template string\n// contains Go template expressions (e.g., {{if .Open}}open{{end}}).\n//\n// The template string is parsed fresh each time to support dynamic content.\n// Standard Go template functions (if, range, eq, etc.) are available.\nfunc PortalHTML(data interface{}, tmplStr string) template.HTML {\n // Create a new template with the FuncMap for custom functions\n t, err := template.New("portal").Funcs(FuncMap()).Parse(tmplStr)\n if err != nil {\n // Return error message as HTML comment for debugging\n return template.HTML("<!-- bfPortalHTML error: " + err.Error() + " -->")\n }\n\n var buf bytes.Buffer\n if err := t.Execute(&buf, data); err != nil {\n return template.HTML("<!-- bfPortalHTML exec error: " + err.Error() + " -->")\n }\n\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Portal Collection\n// =============================================================================\n\n// PortalContent represents a single portal\'s content to be rendered at body end.\ntype PortalContent struct {\n ID string // Unique portal ID for hydration matching\n OwnerID string // Owner scope ID for find() support\n Content template.HTML // Portal HTML content\n}\n\n// PortalCollector collects portal content during template rendering.\n// Portal content is rendered at </body> to avoid z-index issues.\ntype PortalCollector struct {\n portals []PortalContent\n counter int\n}\n\n// NewPortalCollector creates a new PortalCollector.\nfunc NewPortalCollector() *PortalCollector {\n return &PortalCollector{\n portals: []PortalContent{},\n counter: 0,\n }\n}\n\n// Add registers portal content to be rendered at body end.\nfunc (pc *PortalCollector) Add(ownerID string, content template.HTML) string {\n pc.counter++\n id := "bf-portal-" + strconv.Itoa(pc.counter)\n pc.portals = append(pc.portals, PortalContent{\n ID: id,\n OwnerID: ownerID,\n Content: content,\n })\n return "" // Return empty string for template use\n}\n\n// Render outputs all collected portals as HTML.\n// Each portal is wrapped in a div with bf-pi (portal ID) and bf-po (portal owner).\nfunc (pc *PortalCollector) Render() template.HTML {\n if pc == nil || len(pc.portals) == 0 {\n return ""\n }\n var buf strings.Builder\n for _, p := range pc.portals {\n buf.WriteString(`<div bf-pi="`)\n buf.WriteString(p.ID)\n buf.WriteString(`" bf-po="`)\n buf.WriteString(p.OwnerID)\n buf.WriteString(`">`)\n buf.WriteString(string(p.Content))\n buf.WriteString("</div>\\n")\n }\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Script Collection\n// =============================================================================\n\n// ScriptCollector collects client scripts (and modulepreload hints) with\n// deduplication. It preserves insertion order for deterministic output.\n//\n// Preloads share this same collector/struct rather than a parallel\n// PreloadCollector type: the collector is already threaded to every child\n// component via `setScriptsField`/`setScriptsOnSlice`/`setScriptsOnSingle`\n// (the `Scripts` struct field), so a preload registered inside a child\n// automatically survives to the page-level render through that existing\n// propagation \u2014 no separate child-propagation code needed (see #preload\n// task notes).\ntype ScriptCollector struct {\n scripts map[string]bool\n order []string\n\n preloads map[string]bool\n preloadOrder []string\n}\n\n// NewScriptCollector creates a new ScriptCollector.\nfunc NewScriptCollector() *ScriptCollector {\n return &ScriptCollector{\n scripts: make(map[string]bool),\n order: []string{},\n preloads: make(map[string]bool),\n preloadOrder: []string{},\n }\n}\n\n// Register adds a script source to the collection.\n// Duplicate scripts are ignored (only first registration counts).\nfunc (sc *ScriptCollector) Register(src string) string {\n if sc.scripts[src] {\n return "" // Already registered\n }\n sc.scripts[src] = true\n sc.order = append(sc.order, src)\n return "" // Return empty string for template use\n}\n\n// RegisterPreload adds a `<link rel="modulepreload">` href to the\n// collection, mirroring Register\'s dedup/order semantics exactly. Called\n// from a generated template as a no-output statement\n// (`{{.Scripts.RegisterPreload "URL"}}`), never rendered directly \u2014 the\n// `<link>` tag itself is only ever emitted by BfScripts below, so a preload\n// registration never injects a node into a component\'s own template output.\nfunc (sc *ScriptCollector) RegisterPreload(href string) string {\n if sc.preloads[href] {\n return "" // Already registered\n }\n sc.preloads[href] = true\n sc.preloadOrder = append(sc.preloadOrder, href)\n return "" // Return empty string for template use\n}\n\n// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// Preloads returns all registered preload hrefs in insertion order.\nfunc (sc *ScriptCollector) Preloads() []string {\n return sc.preloadOrder\n}\n\n// BfScripts generates `<link rel="modulepreload">` hints followed by\n// `<script type="module">` tags for everything registered on collector.\n// Preloads are always emitted before scripts (a hint that arrives after the\n// script it describes is useless). Returns HTML safe for embedding in\n// templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\n for _, href := range collector.Preloads() {\n result.WriteString(`<link rel="modulepreload" crossorigin href="`)\n result.WriteString(href)\n result.WriteString(`">`)\n result.WriteString("\\n")\n }\n for _, src := range collector.Scripts() {\n result.WriteString(`<script type="module" src="`)\n result.WriteString(src)\n result.WriteString(`"></script>`)\n result.WriteString("\\n")\n }\n return template.HTML(result.String())\n}\n\n// =============================================================================\n// Component Renderer\n// =============================================================================\n\n// RenderContext contains all data needed to render a component page.\n// The layout function receives this context to build the final HTML.\ntype RenderContext struct {\n // ComponentName is the template name being rendered\n ComponentName string\n\n // Props is the component props (for layout to access if needed)\n Props interface{}\n\n // ComponentHTML is the rendered component template output\n ComponentHTML template.HTML\n\n // Portals contains collected portal content to render at body end\n Portals template.HTML\n\n // Scripts contains the collected JS script tags\n Scripts template.HTML\n\n // Title is the page title (defaults to "{ComponentName} - BarefootJS")\n Title string\n\n // Heading is the page heading. Empty string means no heading.\n Heading string\n\n // Extra holds additional user-defined data for the layout\n Extra map[string]interface{}\n}\n\n// LayoutFunc renders the final HTML page given the render context.\ntype LayoutFunc func(ctx *RenderContext) string\n\n// Renderer renders BarefootJS components with a customizable layout.\ntype Renderer struct {\n templates *template.Template\n layout LayoutFunc\n}\n\n// NewRenderer creates a Renderer with the given templates and layout function.\n//\n// Example usage:\n//\n// renderer := bf.NewRenderer(templates, func(ctx *bf.RenderContext) string {\n// return fmt.Sprintf(`<!DOCTYPE html>\n// <html>\n// <head><title>%s</title></head>\n// <body>%s%s</body>\n// </html>`, ctx.Title, ctx.ComponentHTML, ctx.Scripts)\n// })\nfunc NewRenderer(tmpl *template.Template, layout LayoutFunc) *Renderer {\n return &Renderer{\n templates: tmpl,\n layout: layout,\n }\n}\n\n// RenderOptions configures a single render call.\ntype RenderOptions struct {\n // ComponentName is the template name to render (required)\n ComponentName string\n\n // Props is the component props (must be a pointer to struct with Scripts field)\n Props interface{}\n\n // Title is the page title. If empty, defaults to "{ComponentName} - BarefootJS"\n Title string\n\n // Heading is the page heading. If empty, no heading is shown.\n Heading string\n\n // Extra holds additional data to pass to the layout\n Extra map[string]interface{}\n}\n\n// Render renders a component to a full HTML page using the configured layout.\n// Child component props are automatically detected (any slice field with ScopeID/Scripts).\n// renderTemplateErrorPanel formats a Go template execution error into a\n// fragment of HTML that\'s visible in the browser. The panel is\n// HTML-escaped so a faulty template name (anything from `template:\n// "..."`) can\'t smuggle markup back into the page. Keep the styling\n// inline so the panel surfaces even when the project\'s CSS hasn\'t\n// loaded yet (e.g. the failure aborted before the stylesheet links\n// emitted).\n//\n// Surfaced for the #1442 echo repro: a template referencing\n// `.Todo.Done` (instead of the range dot\'s `.Done`) used to fail\n// silently \u2014 Go\'s html/template aborted mid-stream, the partial body\n// flushed as a 200, and the user saw a truncated list with no console\n// signal. With this panel they get the template name, the error\n// message, and a "what to look at" hint inline.\nfunc renderTemplateErrorPanel(componentName string, err error) string {\n return `<div style="margin:1em 0;padding:1em;border:2px solid #d33;background:#fff5f5;color:#900;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.5"><strong style="display:block;margin-bottom:.5em">Template error in <code>` +\n template.HTMLEscapeString(componentName) +\n `</code></strong><pre style="margin:0;white-space:pre-wrap;word-break:break-word">` +\n template.HTMLEscapeString(err.Error()) +\n `</pre><div style="margin-top:.75em;font-size:12px;opacity:.7">Common cause: a JSX expression referenced a name the adapter could not resolve to a struct field. Open the matching <code>dist/templates/*.tmpl</code> for the unresolved reference, then fix the source component.</div></div>`\n}\n\n// renderComponentInto wires a component\'s props (script/portal collectors,\n// child-slot scope ids + hydration, root marking) against the PROVIDED\n// collectors and returns just the component\'s HTML \u2014 no layout. Both Render\n// (one fresh collector pair per page) and RenderFragment (a shared pair across\n// several islands) funnel through here so their wiring stays identical.\nfunc (r *Renderer) renderComponentInto(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n // Inject the shared collectors into the props.\n setScriptsField(opts.Props, scriptCollector)\n setPortalsField(opts.Props, portalCollector)\n\n // Auto-detect and process child component props (slices)\n childSlices := findChildComponentSlices(opts.Props)\n for _, slice := range childSlices {\n setScopeIDsOnSlice(slice)\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScopeIDOnSingle(child)\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n return template.HTML(componentBuf.String())\n}\n\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // One script + portal collector pair for the whole page.\n scriptCollector := NewScriptCollector()\n portalCollector := NewPortalCollector()\n\n componentHTML := r.renderComponentInto(opts, scriptCollector, portalCollector)\n\n // Determine title (default: "{ComponentName} - BarefootJS")\n title := opts.Title\n if title == "" {\n title = opts.ComponentName + " - BarefootJS"\n }\n\n // Heading (empty means no heading)\n heading := opts.Heading\n\n // Build render context\n ctx := &RenderContext{\n ComponentName: opts.ComponentName,\n Props: opts.Props,\n ComponentHTML: componentHTML,\n Portals: portalCollector.Render(),\n Scripts: BfScripts(scriptCollector),\n Title: title,\n Heading: heading,\n Extra: opts.Extra,\n }\n\n return r.layout(ctx)\n}\n\n// RenderFragment renders a single island subtree into the caller-provided\n// script and portal collectors and returns just its HTML \u2014 no page layout.\n//\n// It exists for hand-authored "region shell" pages (the `@barefootjs/router`\n// showcase): a layout that places several independent islands \u2014 e.g. a header\n// ThemeToggle, an `<aside bf-region>` Sidebar, and a `<PageShell>` wrapping the\n// route content \u2014 must collect ALL their scripts and portals into ONE place so\n// the runtime (`barefoot.js`) and each island\'s client JS are emitted exactly\n// once and share a single reactive instance. Render each island with the same\n// collectors, splice the returned HTML into the shell, then emit\n// `BfScripts(sc)` and `pc.Render()` once at the end of the document.\n//\n// Each fragment is treated as its own root (it emits `bf-p` like any top-level\n// island); nested children declared in its props are wired as children, exactly\n// as in Render.\nfunc (r *Renderer) RenderFragment(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n return r.renderComponentInto(opts, scriptCollector, portalCollector)\n}\n\n// setScriptsField sets the Scripts field on a struct using reflection.\nfunc setScriptsField(v interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// setPortalsField sets the Portals field on a struct using reflection.\nfunc setPortalsField(v interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// getStringField extracts a string field from a struct using reflection.\nfunc setBoolField(v interface{}, fieldName string, val bool) {\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Struct {\n return\n }\n field := rv.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n}\n\nfunc getBoolField(v interface{}, fieldName string) bool {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return false\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.Bool {\n return false\n }\n return field.Bool()\n}\n\nfunc getStringField(v interface{}, fieldName string) string {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return ""\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.String {\n return ""\n }\n return field.String()\n}\n\n// scopeIDChars is the alphabet for auto-generated ScopeID suffixes. It\n// mirrors the `randomID` helper the go-template adapter emits into the\n// generated New<Component>Props constructors so runtime-assigned and\n// constructor-assigned ids are indistinguishable.\nconst scopeIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"\n\n// randomScopeSuffix returns a random lowercase-alphanumeric string of\n// length n. math/rand (auto-seeded since Go 1.20) is sufficient here: the\n// suffix only needs to be unique enough to keep a page\'s bf-s scope ids\n// from colliding, not cryptographically unpredictable.\nfunc randomScopeSuffix(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = scopeIDChars[rand.Intn(len(scopeIDChars))]\n }\n return string(b)\n}\n\n// scopeIDPrefix derives the human-readable ScopeID prefix from a child\n// component\'s type, e.g. `TodoItemProps` \u2192 `TodoItem`. Matches the\n// `"<Component>_" + randomID(6)` shape the generated constructors use.\nfunc scopeIDPrefix(t reflect.Type) string {\n for t.Kind() == reflect.Ptr {\n t = t.Elem()\n }\n return strings.TrimSuffix(t.Name(), "Props")\n}\n\n// assignScopeID fills a child component\'s ScopeID with a generated id when\n// the caller left it empty, so application code doesn\'t have to mint scope\n// ids by hand (the parent\'s New<Component>Props constructor does the same\n// for components built through it). A non-empty ScopeID is left untouched,\n// so callers can still pin a stable id when they need one.\nfunc assignScopeID(structVal reflect.Value, prefix string) {\n field := structVal.FieldByName("ScopeID")\n if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.String {\n return\n }\n if field.String() != "" {\n return\n }\n id := randomScopeSuffix(6)\n if prefix != "" {\n id = prefix + "_" + id\n }\n field.SetString(id)\n}\n\n// setScopeIDsOnSlice assigns a generated ScopeID to every child in a slice\n// whose ScopeID is empty.\nfunc setScopeIDsOnSlice(slice interface{}) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n prefix := scopeIDPrefix(v.Type().Elem())\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n assignScopeID(item, prefix)\n }\n }\n}\n\n// setScopeIDOnSingle assigns a generated ScopeID to a single child\n// component when its ScopeID is empty.\nfunc setScopeIDOnSingle(child interface{}) {\n v := reflect.ValueOf(child)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return\n }\n assignScopeID(v, scopeIDPrefix(v.Type()))\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// findSingleChildComponents finds single struct fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findSingleChildComponents(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n\n // Handle pointer to struct\n if field.Kind() == reflect.Ptr {\n if field.IsNil() {\n continue\n }\n field = field.Elem()\n }\n\n // Skip non-struct fields (slices handled by findChildComponentSlices)\n if field.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := field.FieldByName("ScopeID").IsValid()\n hasScripts := field.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Addr().Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSingle sets Scripts on a single struct child component.\nfunc setScriptsOnSingle(child interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// setPortalsOnSingle sets Portals on a single struct child component.\nfunc setPortalsOnSingle(child interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// Nullish implements JS `??` for template use (`bf_nullish`, #2248): returns\n// fallback iff v is nil (untyped nil or a nil pointer/map/slice boxed in the\n// interface), otherwise v \u2014 so present-but-falsy `""`/`0`/`false` are KEPT,\n// unlike the truthiness-based template `or`.\nfunc Nullish(v, fallback any) any {\n if v == nil {\n return fallback\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Interface, reflect.Func, reflect.Chan:\n if rv.IsNil() {\n return fallback\n }\n }\n return v\n}\n\n// ToInt exposes the runtime\'s numeric coercion for generated constructors\n// (#2248): a nillable-lowered numeric prop arrives as `interface{}`, and an\n// untyped Go literal (`Size: 3`) boxes as int even when the prop is\n// float64-shaped \u2014 a direct type assertion would panic where JS accepts the\n// number. Non-numeric values coerce to 0, matching the helpers\' behaviour.\nfunc ToInt(v any) int { return toInt(v) }\n\n// ToFloat64 is ToInt\'s float64 counterpart \u2014 see ToInt.\nfunc ToFloat64(v any) float64 { return toFloat64(v) }\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\nfunc toFloat64(v any) float64 {\n switch n := v.(type) {\n case int:\n return float64(n)\n case int8:\n return float64(n)\n case int16:\n return float64(n)\n case int32:\n return float64(n)\n case int64:\n return float64(n)\n case uint:\n return float64(n)\n case uint8:\n return float64(n)\n case uint16:\n return float64(n)\n case uint32:\n return float64(n)\n case uint64:\n return float64(n)\n case float32:\n return float64(n)\n case float64:\n return n\n default:\n return 0\n }\n}\n\nfunc toInt(v any) int {\n switch n := v.(type) {\n case int:\n return n\n case int8:\n return int(n)\n case int16:\n return int(n)\n case int32:\n return int(n)\n case int64:\n return int(n)\n case uint:\n return int(n)\n case uint8:\n return int(n)\n case uint16:\n return int(n)\n case uint32:\n return int(n)\n case uint64:\n return int(n)\n case float32:\n return int(n)\n case float64:\n return int(n)\n default:\n return 0\n }\n}\n\nfunc isIntLike(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n return true\n default:\n return false\n }\n}\n\nfunc toString(v any) string {\n switch s := v.(type) {\n case string:\n return s\n case int:\n return strconv.Itoa(s)\n case int64:\n return strconv.FormatInt(s, 10)\n case float64:\n return strconv.FormatFloat(s, \'f\', -1, 64)\n case bool:\n return strconv.FormatBool(s)\n default:\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {\n // JS `Array.prototype.toString` is `this.join(\',\')`, applied\n // recursively \u2014 a nested array element stringifies the same\n // way rather than via Go\'s `%v`. Reached via `Join`/`ConcatStr`\n // on an element that is itself an array (e.g. `.flat(0)`\'s\n // shallow copy joined afterwards, #2262).\n parts := make([]string, rv.Len())\n for i := 0; i < rv.Len(); i++ {\n parts[i] = toString(rv.Index(i).Interface())\n }\n return strings.Join(parts, ",")\n }\n return ""\n }\n}\n\n// =============================================================================\n// searchParams() \u2014 request-scoped environment signal (router v0.5, #1922)\n// =============================================================================\n\n// SearchParams is the SSR view of the request query string behind the\n// reactive searchParams() environment signal. The route handler builds it\n// from the request URL and assigns it to the component\'s SearchParams input\n// field; the generated template reads it via `.SearchParams.Get "key"`.\n//\n// The zero value is an empty query (url.Values.Get tolerates a nil map), so a\n// render with no request query \u2014 e.g. the adapter conformance harness, which\n// issues no query string \u2014 resolves every key to "", which the template\'s\n// `or`/`??` fallback turns into the author\'s default.\ntype SearchParams struct {\n values url.Values\n}\n\n// NewSearchParams parses a raw query string (with or without a leading "?")\n// into a SearchParams. A malformed query yields an empty set rather than an\n// error, mirroring the browser\'s URLSearchParams, which never throws on junk.\n//\n// Typical handler use (net/http):\n//\n// in := MyComponentInput{SearchParams: bf.NewSearchParams(r.URL.RawQuery)}\nfunc NewSearchParams(raw string) SearchParams {\n raw = strings.TrimPrefix(raw, "?")\n values, err := url.ParseQuery(raw)\n if err != nil {\n values = url.Values{}\n }\n return SearchParams{values: values}\n}\n\n// Get returns the first value associated with key, or "" when the key is\n// absent. This mirrors url.Values.Get, which also returns "" for a\n// present-but-empty value (`?sort=`). Safe on the zero value (nil map).\n//\n// This is not byte-for-byte URLSearchParams.get under the template\'s `??`\n// lowering. JS distinguishes absent (`null`) from present-but-empty (`""`):\n// `null ?? d` yields the default, but `"" ?? d` keeps the empty string. The\n// Go adapter lowers `??` to the `or` builtin \u2014 Go templates have no\n// null-coalescing operator \u2014 so here BOTH an absent key and a present-but-\n// empty value fall back to the author\'s default. The conformance fixture only\n// exercises the absent-key default, where the two runtimes agree; the\n// empty-string divergence is the same general `?? \u2192 or` limitation that\n// applies to any `x ?? default` the Go adapter lowers.\nfunc (s SearchParams) Get(key string) string {\n return s.values.Get(key)\n}\n';
32250
+ bfGoSource = '// Package bf provides runtime helper functions for BarefootJS Go templates.\n// These functions mirror JavaScript behavior for consistent SSR output.\npackage bf\n\nimport (\n "bytes"\n "encoding/json"\n "fmt"\n "html/template"\n "math"\n "math/rand"\n "net/url"\n "os"\n "reflect"\n "regexp"\n "sort"\n "strconv"\n "strings"\n "time"\n "unicode"\n "unicode/utf8"\n)\n\n// FuncMap returns a template.FuncMap with all BarefootJS helper functions.\n// Usage:\n//\n// tmpl := template.New("").Funcs(bf.FuncMap())\nfunc FuncMap() template.FuncMap {\n return template.FuncMap{\n // Nullish coalescing (#2248): JS `??` semantics \u2014 fall back only on\n // nil, keeping present-but-falsy values (`""`, `0`, `false`) that\n // Go\'s truthiness-based `or` would replace.\n "bf_nullish": Nullish,\n\n // Arithmetic\n "bf_add": Add,\n "bf_concat_str": ConcatStr,\n "bf_sub": Sub,\n "bf_mul": Mul,\n "bf_div": Div,\n "bf_mod": Mod,\n "bf_neg": Neg,\n "bf_min": Min,\n "bf_max": Max,\n\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_trim_start": TrimStart,\n "bf_trim_end": TrimEnd,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_split": Split,\n "bf_starts_with": StartsWith,\n "bf_ends_with": EndsWith,\n "bf_replace": Replace,\n "bf_replace_all": ReplaceAll,\n "bf_repeat": Repeat,\n "bf_pad_start": PadStart,\n "bf_pad_end": PadEnd,\n "bf_string": String,\n "bf_raw_html": RawHTML,\n "bf_ternary": Ternary,\n "bf_truthy": Truthy,\n\n // URL query builder (#1897 PostList href helpers): conditional\n // (include, key, value) triples \u2192 "base?k=v&\u2026", mirroring a\n // URLSearchParams builder with guarded `.set()` calls.\n "bf_query": Query,\n\n // Date method lowering (#2274, spec entry "date"): the lowering\n // target for a zero-arg call on a Date-typed prop.\n "bf_date": Date,\n\n // formatDate(date, pattern, tz) lowering (#2324, spec entry\n // "format_date"): the total, locale-free date-pattern formatter \u2014\n // see FormatDate\'s docstring for the full contract.\n "bf_format_date": FormatDate,\n\n // JSON / numeric primitives \u2014 JS-compat callees registered on\n // the Go adapter\'s `templatePrimitives` map (#1188).\n "bf_json": JSON,\n "bf_number": Number,\n "bf_floor": Floor,\n "bf_ceil": Ceil,\n "bf_round": Round,\n "bf_abs": Abs,\n "bf_to_fixed": ToFixed,\n\n // Array/Slice\n "bf_len": Len,\n "bf_length": Length,\n "bf_is_element": IsValidElement,\n "bf_style_object": StyleObjectToCSS,\n "bf_at": At,\n "bf_includes": Includes,\n "bf_index_of": IndexOf,\n "bf_last_index_of": LastIndexOf,\n "bf_concat": Concat,\n "bf_slice": Slice,\n "bf_reverse": Reverse,\n "bf_flat": Flat,\n "bf_flat_dynamic": FlatDynamicDepth,\n "bf_flat_map": FlatMap,\n "bf_flat_map_tuple": FlatMapTuple,\n "bf_first": First,\n "bf_last": Last,\n "bf_arr": Arr,\n // `bf_map` reuses `Env` (eval.go) \u2014 both build a `map[string]any`\n // from an alternating key/value variadic list. `Env` was written\n // for the evaluator\'s free-var capture map; a JS object-literal\n // lowered to a template action (the `objectLiteral` emitter,\n // #2696 Step 1) needs the exact same construction, so it\'s\n // registered under a second name rather than duplicated.\n "bf_map": Env,\n // `bf_merge` (eval.go) is `bf_map`\'s sibling for a POPULATED\n // object-literal SPREAD (#2696 Step 2, `{ ...t, editing: false }`):\n // a variadic, null-safe shallow merge (a non-map argument is\n // skipped, matching JS\'s null/undefined-spread no-op), later\n // arguments winning \u2014 the `objectLiteral` emitter folds each\n // maximal run of plain properties (a `bf_map` call) and each spread\n // (its own value) through this, in source order.\n "bf_merge": Merge,\n "bf_filter_truthy": FilterTruthy,\n\n // Higher-order Array Methods\n "bf_every": Every,\n "bf_some": Some,\n "bf_filter": Filter,\n "bf_find": Find,\n "bf_find_index": FindIndex,\n "bf_find_last": FindLast,\n "bf_find_last_index": FindLastIndex,\n "bf_sort": Sort,\n "bf_reduce": Reduce,\n\n // Evaluator-driven higher-order folds (#2018): the comparator / reducer\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_sort / bf_reduce beyond their fixed catalogues. The\n // adapter falls back to bf_sort for a comparator the evaluator can\'t\n // model (e.g. localeCompare). `bf_env` builds the captured-free-var\n // environment passed as the trailing base_env argument.\n "bf_sort_eval": SortEval,\n "bf_reduce_eval": FoldEval,\n "bf_env": Env,\n\n // Evaluator-driven higher-order predicates (#2018, P2): the predicate\n // body travels as a serialized ParsedExpr (JSON) evaluated per element,\n // generalizing bf_filter / bf_find / bf_find_index / bf_every / bf_some\n // beyond their field-equality / truthiness catalogues. `bf_find_eval` /\n // `bf_find_index_eval` take a `forward` bool (false \u2192 findLast variants).\n "bf_filter_eval": FilterEval,\n "bf_every_eval": EveryEval,\n "bf_some_eval": SomeEval,\n "bf_find_eval": FindEval,\n "bf_find_index_eval": FindIndexEval,\n // `.flatMap(proj)`: project each element through the serialized\n // projection body, then flatten one level.\n "bf_flat_map_eval": FlatMapEval,\n // Value-producing `.map(cb)` (#2073): project each element, one\n // result per element (no flatten).\n "bf_map_eval": MapEval,\n\n // Comment marker (for hydration)\n "bfComment": Comment,\n "bfEscapeCommentKey": EscapeCommentKey,\n "bfTextStart": TextStart,\n "bfTextEnd": TextEnd,\n\n // Script collection\n "bfScripts": BfScripts,\n\n // Scope attribute value (#1249: bare scope id, no `~` prefix)\n "bfScopeAttr": ScopeAttr,\n\n // Slot-identity markers (#1249): bf-h, bf-m, bf-r\n "bfHydrationAttrs": HydrationAttrs,\n\n // Child component marker (kept for backward compatibility)\n "bfIsChild": IsChild,\n\n // Props attribute for hydration\n "bfPropsAttr": BfPropsAttr,\n\n // Portal HTML rendering (parses and executes template string)\n "bfPortalHTML": PortalHTML,\n\n // JSX children passed to an imported child component (#1896):\n // the parent renders the children fragment via a companion\n // define (executed through bf_tmpl from TemplateFuncMap) and\n // injects the result into the child\'s Children field.\n "bf_with_children": WithChildren,\n\n // Per-row props for a child component nested inside a composite\n // loop row (#2445): the parent\'s once-per-slot instance is shared\n // across rows, so a prop that depends on the row is reapplied as a\n // copy inside {{range}}, the props-argument sibling of\n // bf_with_children.\n "bf_with_props": WithProps,\n\n // Per-row props for a child whose CONSTRUCTOR derives a field from\n // the overridden prop (#2448). bf_with_props patches fields on the\n // shared instance and cannot re-run New<Child>Props, so a memo body\n // or a signal initial value computed there would stay at the shared\n // instance\'s one-shot value on every row. This entry looks up the\n // component\'s generated rebuilder and re-runs the real constructor\n // instead. See reprops.go for why the lookup is deferred to execute\n // time rather than merged into this map.\n "bf_reprops": Reprops,\n\n // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n "bfScopeCommentEnd": ScopeCommentEnd,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n\n // Whole-attribute emission that bypasses html/template\'s contextual\n // auto-escaper (#2743): see Attr\'s docstring.\n "bf_attr": Attr,\n\n // Reverse the loop-row field-access Go-casing before a whole-item\n // spread reaches `bf_spread_attrs` (#2490): a row\'s dot context is\n // necessarily keyed `ID`/`Title`/`DataKind` (the same casing that\n // makes `attrs.id` emit `{{.ID}}`), and `toAttrName`\'s\n // camelCase\u2192kebab conversion mangles an uppercase-led key\n // (`ID` \u2192 `-i-d`). See JSKeys\' docstring for the recovery rule.\n "bf_js_keys": JSKeys,\n\n // Destructure object-rest spread-onto-element residual (#2087):\n // "every field except these" for a struct/map, keyed by json tag.\n "bf_omit": Omit,\n\n // Case-tolerant single-field read off a map or struct (#2087): a\n // `useContext` local whose `createContext` default is object-shaped\n // (e.g. `createContext<{ config: X }>({ config: {} })`) is typed\n // `map[string]interface{}`, and its keys are the SOURCE (JS-cased)\n // property names a `<Ctx.Provider value={{ \u2026 }}>` bakes\n // (`providerObjectValueToGoMap`, go-template-adapter.ts) \u2014 plain\n // `text/template` dot access does an exact-string `MapIndex`, so\n // `ctx.config.label` lowers to nested `bf_get` calls instead of\n // `.Ctx.Config.Label`. Reuses the same `getFieldValue` the\n // project/sort helpers already use for a dynamic field-name lookup;\n // safe on a nil map/interface (returns nil) so a missing Provider or\n // an absent key falls through to `??`\'s fallback.\n "bf_get": getFieldValue,\n }\n}\n\n// Query builds a URL from a base path plus a query string assembled from\n// (include, key, value) triples, in order. A pair is considered only when its\n// `include` flag is true \u2014 mirroring a JS URLSearchParams builder whose\n// `.set(key, value)` calls are each guarded by an `if`. The compiler lowers a\n// conditional `cond ? v : undefined` to the `include` bool and a plain `key: v`\n// to a `true` include; the emptiness check is applied HERE (an included but\n// empty value is dropped), matching the client `queryHref` and the Perl `query`\n// helper. Keys and values use formEscape (application/x-www-form-urlencoded),\n// so the rendered query is byte-for-byte identical to the browser\'s\n// URLSearchParams. An empty query yields the bare base.\n//\n// A value may be a string slice ([]string or []any), which APPENDS one pair per\n// non-empty member (URLSearchParams.append) \u2014 `{tag: [a, b]}` \u2192 `tag=a&tag=b`.\n// A scalar value follows URLSearchParams.set() semantics: repeating a key\n// overwrites the value at the key\'s first position rather than duplicating it\n// (object literals have unique keys, so this is defensive). Trailing args that\n// don\'t complete a triple are ignored.\n//\n// formEscape differs from url.QueryEscape only on `~` (kept by QueryEscape,\n// `%7E` here) and `*` (`%2A` by QueryEscape, kept here).\nfunc Query(base string, triples ...any) string {\n type kv struct{ key, val string }\n pairs := make([]kv, 0, len(triples)/3)\n pos := make(map[string]int)\n for i := 0; i+2 < len(triples); i += 3 {\n include, _ := triples[i].(bool)\n if !include {\n continue\n }\n k := String(triples[i+1])\n if members, ok := asStringSlice(triples[i+2]); ok {\n // Array value \u2192 append each non-empty member; appended pairs never\n // overwrite, so they don\'t participate in the set()-position map.\n for _, m := range members {\n if m == "" {\n continue\n }\n pairs = append(pairs, kv{k, m})\n }\n continue\n }\n v := String(triples[i+2])\n if v == "" {\n continue // omit an included-but-empty value (client / Perl parity)\n }\n if at, ok := pos[k]; ok {\n pairs[at].val = v // set(): overwrite the first occurrence\'s value\n } else {\n pos[k] = len(pairs)\n pairs = append(pairs, kv{k, v})\n }\n }\n var b strings.Builder\n for _, p := range pairs {\n if b.Len() == 0 {\n b.WriteByte(\'?\')\n } else {\n b.WriteByte(\'&\')\n }\n b.WriteString(formEscape(p.key))\n b.WriteByte(\'=\')\n b.WriteString(formEscape(p.val))\n }\n return base + b.String()\n}\n\n// Attr emits one complete `name="value"` HTML attribute as a\n// template.HTMLAttr, HTML-escaping value with template.HTMLEscapeString (the\n// same escaper SpreadAttrs uses) and nothing else \u2014 no URL normalization, no\n// scheme filter. (#2743) html/template infers a URL context from the\n// attribute NAME for a `name="{{pipeline}}"` action (href/src/action/\n// data-*/anything containing src|uri|url/\u2026) and percent-encodes the whole\n// value there, which the JS reference (Hono) never does \u2014 it only\n// HTML-escapes. Returning HTMLAttr for the WHOLE attribute bypasses that\n// contextual inference by design, the same technique SpreadAttrs /\n// HydrationAttrs already use for attributes and StyleObjectToCSS uses for\n// CSS. `name` is a compiler-emitted constant, never data; `value` goes\n// through String (nil \u2192 "").\nfunc Attr(name string, value any) template.HTMLAttr {\n return template.HTMLAttr(name + `="` + template.HTMLEscapeString(String(value)) + `"`)\n}\n\n// Date implements the `date` helper (spec/template-helpers.md, #2274) \u2014 the\n// lowering target for a zero-arg call on a Date-typed prop\n// (`createdAt.toISOString()`). recv accepts the runtime\'s own `time.Time` /\n// `*time.Time` (however the host framework populated the prop) OR an\n// ISO-8601 string (the wire form a JSON-sourced prop arrives as); either is\n// normalized to UTC before dispatching op, matching the client `Date`\'s own\n// instant semantics regardless of which shape reaches this helper. A nil /\n// unparsable receiver yields this runtime\'s zero value for the requested op\n// (0 for every numeric accessor, "" for toISOString) rather than panicking\n// mid-render \u2014 the same tolerance `String`/`Number` already extend to a nil\n// prop. `getUTCMonth` subtracts 1: Go\'s `time.Month` is 1-based, JS\'s is not\n// (spec entry "date" is explicit that JS wins here).\nfunc Date(recv any, op string) any {\n t, ok := toTime(recv)\n if !ok {\n if op == "toISOString" {\n return ""\n }\n return 0\n }\n t = t.UTC()\n switch op {\n case "getUTCFullYear":\n return t.Year()\n case "getUTCMonth":\n return int(t.Month()) - 1\n case "getUTCDate":\n return t.Day()\n case "getUTCHours":\n return t.Hour()\n case "getUTCMinutes":\n return t.Minute()\n case "getUTCSeconds":\n return t.Second()\n case "getTime":\n return t.UnixMilli()\n case "toISOString":\n return t.Format("2006-01-02T15:04:05.000Z")\n default:\n return 0\n }\n}\n\n// tzOffsetRE matches a fixed UTC offset `\xB1HH:MM` within ECMA-402\'s valid\n// range \u2014 hours 00\u201323, minutes 00\u201359 (`\'+09:00\'`, `\'-05:30\'`) \u2014 one of the\n// three `tz` shapes FormatDate accepts (mirrors OFFSET_RE in\n// packages/client/src/format-date.ts). An out-of-range shape (`\'+25:00\'`)\n// falls through to the tzdata lookup, fails it, and errors \u2014 matching the\n// JS reference\'s RangeError (#2344).\nvar tzOffsetRE = regexp.MustCompile(`^([+-])([01][0-9]|2[0-3]):([0-5][0-9])$`)\n\n// formatDateTokenRE is the longest-match pattern-token alternation (mirrors\n// TOKEN_RE in packages/client/src/format-date.ts). Order matters: MMMM\n// before MMM before MM before M (and dddd before ddd, DD before D) so the\n// longer token wins at a position where a shorter one could also match \u2014\n// Go\'s regexp, like JS\'s, resolves alternation leftmost-first (not POSIX\n// leftmost-longest), so listing the longer alternative first is what makes\n// e.g. "MMMM" consume all four characters instead of "MM" + "MM".\nvar formatDateTokenRE = regexp.MustCompile(`YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D`)\n\n// nameTable section offsets (#2334, mirrors MONTHS_WIDE / MONTHS_ABBR /\n// WEEKDAYS_WIDE / WEEKDAYS_ABBR in packages/client/src/format-date.ts).\nconst (\n monthsWide = 0\n monthsAbbr = 12\n weekdaysWide = 24\n weekdaysAbbr = 31\n)\n\n// formatDateName reads a name-token table entry: index out of range, or a\n// non-string element, both render "" \u2014 the same total, zero-value\n// discipline as an unparseable date (mirrors the JS reference\'s\n// `names[index] ?? ""` fallback, where every table element the vectors ever\n// carry is a string).\nfunc formatDateName(names []any, index int) string {\n if index < 0 || index >= len(names) {\n return ""\n }\n s, ok := names[index].(string)\n if !ok {\n return ""\n }\n return s\n}\n\n// FormatDate implements the `format_date` helper (#2324, #2334, spec entry\n// "format_date") \u2014 the lowering target for\n// `formatDate(date, pattern, tz, names)`\n// (packages/client/src/format-date.ts, the JS-normative reference this must\n// match byte-for-byte). Total and deterministic: no locale, no host\n// timezone, no "now".\n//\n// recv: same receiver contract as the `date` helper above \u2014 the runtime\'s\n// own `time.Time` / `*time.Time`, or an ISO-8601 string \u2014 normalized via\n// `toTime`. A nil / unparseable receiver returns "" (never panics).\n//\n// tz (#2344): "UTC", a range-valid fixed offset `\xB1HH:MM` (shifts by\n// sign*(HH*60+MM) minutes), or a canonical IANA zone name ("Asia/Tokyo")\n// resolved through tzdata via time.LoadLocation \u2014 the zone\'s UTC offset AT\n// THE INSTANT being formatted (DST-aware, historical-transition-aware,\n// seconds precision: pre-standard LMT offsets like Tokyo\'s +09:18:59\n// count). ANY other value \u2014 an unknown zone, a malformed or out-of-range\n// offset ("+9:00", "+25:00"), the empty string or "Local" (LoadLocation\'s\n// implicit-environment aliases) \u2014 returns an ERROR, aborting template\n// execution loudly: the JS reference throws a RangeError there, and a\n// silently substituted timezone is the one failure mode this helper must\n// not have (the pre-#2344 normalize-to-UTC total function is gone). The\n// shifted instant\'s UTC calendar fields (not the original instant\'s) are\n// what pattern tokens read \u2014 the shifted UTC clock face IS the local clock\n// face in that zone, same reasoning as the JS reference.\n//\n// names (#2334): a flat name table in fixed layout \u2014 `[0..11]` wide month\n// names, `[12..23]` abbreviated month names, `[24..30]` wide weekday names\n// (Sunday-first), `[31..37]` abbreviated weekday names. The caller owns the\n// values; this helper only indexes the table.\n//\n// pattern: longest-match token substitution\n// (`YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D`); every other character \u2014 including\n// multi-byte ones like \u5E74/\u6708/\u65E5 \u2014 passes through literally. `YYYY` is\n// `abs(year)` zero-padded to 4 digits, `-`-prefixed for a negative year;\n// `MM`/`DD` zero-pad to 2; `M`/`D` are bare; `MMMM`/`MMM` and `dddd`/`ddd`\n// read the `names` table (weekday computed on the offset-shifted instant,\n// Sunday-first, matching `time.Time.Weekday()`\'s own Sunday=0 encoding).\nfunc FormatDate(recv any, pattern string, tz string, names []any) (string, error) {\n t, ok := toTime(recv)\n if !ok {\n // Receiver contract precedes tz validation (spec receiver-first\n // discipline, mirrored by every port).\n return "", nil\n }\n offsetSeconds := 0\n if tz != "UTC" {\n if m := tzOffsetRE.FindStringSubmatch(tz); m != nil {\n hh, _ := strconv.Atoi(m[2])\n mm, _ := strconv.Atoi(m[3])\n offsetSeconds = (hh*60 + mm) * 60\n if m[1] == "-" {\n offsetSeconds = -offsetSeconds\n }\n } else if tz == "" || tz == "Local" {\n // LoadLocation("") is UTC and LoadLocation("Local") is the host\n // zone \u2014 both implicit-environment reads the contract refuses.\n return "", fmt.Errorf("format_date: unresolvable timeZone %q", tz)\n } else {\n loc, err := time.LoadLocation(tz)\n if err != nil {\n return "", fmt.Errorf("format_date: unresolvable timeZone %q", tz)\n }\n _, offsetSeconds = t.UTC().In(loc).Zone()\n }\n }\n shifted := t.UTC().Add(time.Duration(offsetSeconds) * time.Second)\n year := shifted.Year()\n month := int(shifted.Month())\n day := shifted.Day()\n weekday := int(shifted.Weekday()) // time.Sunday == 0, matching the table\'s Sunday-first layout\n absYear := year\n if absYear < 0 {\n absYear = -absYear\n }\n yyyy := fmt.Sprintf("%04d", absYear)\n if year < 0 {\n yyyy = "-" + yyyy\n }\n out := formatDateTokenRE.ReplaceAllStringFunc(pattern, func(token string) string {\n switch token {\n case "YYYY":\n return yyyy\n case "MMMM":\n return formatDateName(names, monthsWide+month-1)\n case "MMM":\n return formatDateName(names, monthsAbbr+month-1)\n case "MM":\n return fmt.Sprintf("%02d", month)\n case "M":\n return strconv.Itoa(month)\n case "DD":\n return fmt.Sprintf("%02d", day)\n case "D":\n return strconv.Itoa(day)\n case "dddd":\n return formatDateName(names, weekdaysWide+weekday)\n case "ddd":\n return formatDateName(names, weekdaysAbbr+weekday)\n default:\n return token\n }\n })\n return out, nil\n}\n\n// toTime normalizes a `Date` helper receiver to a `time.Time`: the runtime\'s\n// own `time.Time` / `*time.Time`, or an ISO-8601 string parsed with\n// `time.RFC3339Nano` (accepts both the `Z`-suffixed and numeric-offset\n// forms, and any sub-second precision \u2014 including the millisecond precision\n// every value this runtime itself ever produces via `toISOString` above).\n// Anything else (nil, an unparsable string, an unrelated type) reports !ok\n// so `Date` can apply its documented zero-value fallback instead of\n// panicking.\nfunc toTime(recv any) (time.Time, bool) {\n switch v := recv.(type) {\n case time.Time:\n return v, true\n case *time.Time:\n if v == nil {\n return time.Time{}, false\n }\n return *v, true\n case string:\n t, err := time.Parse(time.RFC3339Nano, v)\n if err != nil {\n return time.Time{}, false\n }\n return t, true\n default:\n return time.Time{}, false\n }\n}\n\n// asStringSlice reports whether v is a query *array* value and, if so, returns\n// its members stringified. A compiled template passes a `[]string` field; the\n// golden conformance vectors decode JSON arrays to `[]any`. Anything else is a\n// scalar (false), handled by the set() path.\nfunc asStringSlice(v any) ([]string, bool) {\n switch s := v.(type) {\n case []string:\n return s, true\n case []any:\n out := make([]string, len(s))\n for i, m := range s {\n out[i] = String(m)\n }\n return out, true\n default:\n return nil, false\n }\n}\n\nconst hexUpper = "0123456789ABCDEF"\n\n// formEscape percent-encodes s with the application/x-www-form-urlencoded byte\n// set, matching the browser\'s URLSearchParams serialization (and the Perl\n// `query` helper) so SSR query strings render byte-for-byte identically across\n// adapters. The unreserved set kept verbatim is A-Z a-z 0-9 and `* - . _`; a\n// space becomes `+`; every other byte is `%XX` with uppercase hex. Encoding is\n// byte-wise, so multi-byte UTF-8 is percent-encoded per byte (`\xE9` \u2192 `%C3%A9`).\n//\n// This differs from url.QueryEscape only for `~` (kept by QueryEscape, encoded\n// to `%7E` here) and `*` (encoded to `%2A` by QueryEscape, kept here).\nfunc formEscape(s string) string {\n var b strings.Builder\n for i := 0; i < len(s); i++ {\n c := s[i]\n switch {\n case c >= \'A\' && c <= \'Z\', c >= \'a\' && c <= \'z\', c >= \'0\' && c <= \'9\',\n c == \'*\', c == \'-\', c == \'.\', c == \'_\':\n b.WriteByte(c)\n case c == \' \':\n b.WriteByte(\'+\')\n default:\n b.WriteByte(\'%\')\n b.WriteByte(hexUpper[c>>4])\n b.WriteByte(hexUpper[c&0x0F])\n }\n }\n return b.String()\n}\n\n// ScopeAttr returns the bare bf-s scope id (#1249).\nfunc ScopeAttr(props interface{}) string {\n return getStringField(props, "ScopeID")\n}\n\n// HydrationAttrs emits `bf-h="<host>" bf-m="<slot>" bf-r=""` conditionally.\n// See spec/compiler.md "Slot identity".\nfunc HydrationAttrs(props interface{}) template.HTMLAttr {\n parts := []string{}\n if host := getStringField(props, "BfParent"); host != "" {\n parts = append(parts, fmt.Sprintf(`bf-h="%s"`, template.HTMLEscapeString(host)))\n }\n if mount := getStringField(props, "BfMount"); mount != "" {\n parts = append(parts, fmt.Sprintf(`bf-m="%s"`, template.HTMLEscapeString(mount)))\n }\n if !getBoolField(props, "BfIsChild") {\n parts = append(parts, `bf-r=""`)\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// IsChild is a deprecated no-op stub. Child status is signalled by bf-h\n// presence (#1249); use HydrationAttrs instead.\nfunc IsChild(props interface{}) template.HTMLAttr {\n return ""\n}\n\n// svgCamelCaseAttrs mirrors SVG_CAMEL_CASE_ATTRS from\n// packages/client/src/runtime/spread-attrs.ts. SVG XML attribute\n// names are case-sensitive; the default camelCase \u2192 kebab-case\n// rewrite must NOT apply to these or the SVG stops rendering\n// (#1407). Coordinates with the compile-time SVG_CAMEL_TO_KEBAB\n// table in packages/jsx/src/ir-to-client-js/utils.ts: presentation\n// attrs (clipPath, strokeWidth, \u2026) live there and must NOT appear\n// here, or the same JSX prop would lower to clip-path via the\n// explicit-attr path and stay clipPath via the spread path.\nvar svgCamelCaseAttrs = map[string]struct{}{\n "allowReorder": {}, "attributeName": {}, "attributeType": {}, "autoReverse": {},\n "baseFrequency": {}, "baseProfile": {}, "calcMode": {}, "clipPathUnits": {},\n "contentScriptType": {}, "contentStyleType": {}, "diffuseConstant": {}, "edgeMode": {},\n "externalResourcesRequired": {}, "filterRes": {}, "filterUnits": {}, "glyphRef": {},\n "gradientTransform": {}, "gradientUnits": {}, "kernelMatrix": {}, "kernelUnitLength": {},\n "keyPoints": {}, "keySplines": {}, "keyTimes": {}, "lengthAdjust": {}, "limitingConeAngle": {},\n "markerHeight": {}, "markerUnits": {}, "markerWidth": {}, "maskContentUnits": {},\n "maskUnits": {}, "numOctaves": {}, "pathLength": {}, "patternContentUnits": {},\n "patternTransform": {}, "patternUnits": {}, "pointsAtX": {}, "pointsAtY": {}, "pointsAtZ": {},\n "preserveAlpha": {}, "preserveAspectRatio": {}, "primitiveUnits": {}, "refX": {}, "refY": {},\n "repeatCount": {}, "repeatDur": {}, "requiredExtensions": {}, "requiredFeatures": {},\n "specularConstant": {}, "specularExponent": {}, "spreadMethod": {}, "startOffset": {},\n "stdDeviation": {}, "stitchTiles": {}, "surfaceScale": {}, "systemLanguage": {},\n "tableValues": {}, "targetX": {}, "targetY": {}, "textLength": {}, "viewBox": {}, "viewTarget": {},\n "xChannelSelector": {}, "yChannelSelector": {}, "zoomAndPan": {},\n}\n\n// toAttrName mirrors the JSX\u2192HTML attribute-name rewrite from\n// packages/client/src/runtime/spread-attrs.ts. className \u2192 class,\n// htmlFor \u2192 for, SVG camelCase attrs preserved, other camelCase\n// keys lowered to kebab-case.\nfunc toAttrName(key string) string {\n if key == "className" {\n return "class"\n }\n if key == "htmlFor" {\n return "for"\n }\n if _, ok := svgCamelCaseAttrs[key]; ok {\n return key\n }\n // camelCase \u2192 kebab-case: mirror the JS reference exactly\n // (`key.replace(/([A-Z])/g, \'-$1\').toLowerCase()`). The JS shape\n // produces a leading `-` for an initial uppercase letter\n // (`XData` \u2192 `-x-data`); both this Go path and the matching JS\n // runtime are wrong-by-construction for that case (the resulting\n // HTML attribute name is invalid), but keeping them byte-equal\n // avoids silent SSR/CSR divergence (#1411 review).\n var b strings.Builder\n for _, r := range key {\n if r >= \'A\' && r <= \'Z\' {\n b.WriteByte(\'-\')\n b.WriteRune(r + 32)\n } else {\n b.WriteRune(r)\n }\n }\n return b.String()\n}\n\n// hasUnsafeStyleValue mirrors Hono\'s own CSS-injection guard\n// (`hono/jsx/utils.ts`\'s `hasUnsafeStyleValue` \u2014 the ORACLE this adapter\'s\n// dynamic `style={{...}}` values must match, #2261): a hand-rolled\n// structural scan for characters that could break out of a CSS\n// declaration, NOT real CSSOM property validation. Ported byte-for-byte \u2014\n// every character this scan tests is ASCII, so scanning by byte (Go\n// string indexing) agrees with Hono\'s UTF-16-code-unit scan for every\n// input; a multibyte UTF-8 sequence has no byte in the ASCII range, so it\n// can never spuriously match one of these single-byte comparisons. Skips\n// the reference implementation\'s regex fast-path (a pure optimization \u2014\n// the scan below already returns `false` promptly for a clean value).\nfunc hasUnsafeStyleValue(value string) bool {\n quote := byte(0)\n blockStack := make([]byte, 0, 4)\n for i := 0; i < len(value); i++ {\n c := value[i]\n switch {\n case c == \'\\\\\':\n if i == len(value)-1 {\n return true\n }\n i++\n case quote != 0:\n if c == \'\\n\' || c == \'\\f\' || c == \'\\r\' {\n return true\n }\n if c == quote {\n quote = 0\n }\n case c == \'/\' && i+1 < len(value) && value[i+1] == \'*\':\n end := strings.Index(value[i+2:], "*/")\n if end == -1 {\n return true\n }\n i = i + 2 + end + 1\n case c == \'"\' || c == \'\\\'\':\n quote = c\n case c == \'(\':\n blockStack = append(blockStack, \')\')\n case c == \'[\':\n blockStack = append(blockStack, \']\')\n case c == \'{\' || c == \'}\':\n return true\n case c == \')\' || c == \']\':\n if len(blockStack) == 0 || blockStack[len(blockStack)-1] != c {\n return true\n }\n blockStack = blockStack[:len(blockStack)-1]\n case c == \';\' && len(blockStack) == 0:\n return true\n }\n }\n return quote != 0 || len(blockStack) != 0\n}\n\n// StyleObjectToCSS builds the CSS string for a `style={{...}}` JSX\n// object-literal attribute (#2261) \u2014 `pairs` alternates CSS key (always a\n// compile-time-known literal), then value (`any`, possibly a runtime\n// expression\'s result). A value that fails `hasUnsafeStyleValue` (after\n// JS-`String()`-style stringification) is DROPPED \u2014 the whole `key:value`\n// pair is omitted \u2014 matching Hono\'s oracle behavior exactly, rather than\n// html/template\'s own contextual CSS auto-escaper (which instead emits its\n// `ZgotmplZ` unsafe-content sentinel for the same input). The final joined\n// string is STILL HTML-escaped (mirroring Hono\'s own `escapeToBuffer` call\n// on its accumulated style string) \u2014 a "safe" value can still carry a\n// literal `"`/`\'`/`&` (e.g. a BALANCED-quote CSS string value like\n// `"hello"` passes the structural scan; the quote chars survive into the\n// value) that would otherwise break out of the double-quoted `style="..."`\n// attribute. Returns `template.CSS` (over the escaped result) so\n// html/template treats it as trusted CSS content instead of ALSO applying\n// its own contextual CSS auto-escaper (which would re-derive the exact\n// `ZgotmplZ` divergence this function exists to avoid).\nfunc StyleObjectToCSS(pairs ...any) template.CSS {\n parts := make([]string, 0, len(pairs)/2)\n for i := 0; i+1 < len(pairs); i += 2 {\n key := fmt.Sprint(pairs[i])\n value := String(pairs[i+1])\n if hasUnsafeStyleValue(value) {\n continue\n }\n parts = append(parts, template.HTMLEscapeString(key)+":"+template.HTMLEscapeString(value))\n }\n return template.CSS(strings.Join(parts, ";"))\n}\n\n// StyleToCss mirrors styleToCss from\n// packages/client/src/runtime/style.ts. Accepts a string passthrough,\n// or a map (JSON-deserialized object) whose camelCase keys are\n// lowered to kebab-case and joined with `;`. Returns ("", false) for\n// nullish/empty input so callers can omit the attribute entirely.\nfunc StyleToCss(v any) (string, bool) {\n if v == nil {\n return "", false\n }\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return "", false\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n // Non-object: stringify and return as-is, matching the JS\n // `typeof value !== \'object\'` branch.\n s := fmt.Sprint(v)\n if s == "" {\n return "", false\n }\n return s, true\n }\n keys := rv.MapKeys()\n sorted := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sorted = append(sorted, k.String())\n }\n }\n sort.Strings(sorted)\n parts := make([]string, 0, len(sorted))\n for _, k := range sorted {\n val := rv.MapIndex(reflect.ValueOf(k))\n // Skip nil entries (matches the JS `if (v == null) continue`).\n if !val.IsValid() {\n continue\n }\n if val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {\n if val.IsNil() {\n continue\n }\n val = val.Elem()\n }\n prop := toAttrName(k)\n parts = append(parts, fmt.Sprintf("%s:%v", prop, val.Interface()))\n }\n if len(parts) == 0 {\n return "", false\n }\n return strings.Join(parts, ";"), true\n}\n\n// SpreadAttrs lowers a JSX intrinsic-element spread bag (#1407) to\n// an HTML attribute string. Mirrors spreadAttrs from\n// packages/client/src/runtime/spread-attrs.ts so SSR output matches\n// what CSR\'s `applyRestAttrs` writes at hydration.\n//\n// Skip rules: nil/false values, event handlers (`on[A-Z]*`),\n// `children`, `ref`.\n//\n// Key remap: className \u2192 class, htmlFor \u2192 for, SVG camelCase\n// preserved, other camelCase \u2192 kebab-case.\n//\n// `style` is routed through StyleToCss so object literals serialize\n// to a real CSS string instead of Go\'s default `map[k:v]` form.\n//\n// Booleans: true \u2192 bare attribute name, false \u2192 omitted.\n// Other scalar values are HTML-escaped via template.HTMLEscapeString.\n// Returns a `template.HTMLAttr` so html/template emits the result\n// verbatim (the function does its own escaping).\n//\n// Keys are sorted alphabetically before emission for deterministic\n// output. SSR/CSR attribute-order divergence is acceptable per the\n// rest-destructure-object-spread-in-map fixture\'s documented policy\n// \u2014 browsers honor the LAST value when a key is duplicated, so\n// pairing with static attrs (`<div class="x" {...rest}>`) is\n// last-wins regardless of order.\nfunc SpreadAttrs(bag any) template.HTMLAttr {\n if bag == nil {\n return ""\n }\n rv := reflect.ValueOf(bag)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return ""\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map {\n return ""\n }\n keys := rv.MapKeys()\n sortedKeys := make([]string, 0, len(keys))\n for _, k := range keys {\n if k.Kind() == reflect.String {\n sortedKeys = append(sortedKeys, k.String())\n }\n }\n sort.Strings(sortedKeys)\n parts := make([]string, 0, len(sortedKeys))\n for _, key := range sortedKeys {\n // Event handlers \u2014 skip at SSR the same way\n // packages/client/src/runtime/spread-attrs.ts does at\n // hydration. The JS predicate is\n // `key.startsWith(\'on\') && key.length > 2 && key[2] === key[2].toUpperCase()`,\n // which is true for any character whose uppercase form is\n // itself: ASCII A-Z, digits, underscore, and non-letter\n // symbols. Mirror that here by skipping when key[2] is NOT\n // a lowercase ASCII letter \u2014 so `onClick`, `on_custom`, and\n // `on0` all match (#1411 review).\n if len(key) > 2 && key[0] == \'o\' && key[1] == \'n\' && !(key[2] >= \'a\' && key[2] <= \'z\') {\n continue\n }\n // `children` is a JSX construct rendered inside the element,\n // never a DOM attribute. `ref` is intentionally NOT filtered\n // here so output stays byte-equal with the JS reference\n // `spreadAttrs` in packages/client/src/runtime/spread-attrs.ts\n // (which only filters null/false, event handlers, and\n // children) \u2014 aligning Go\'s filter set diverges from JS in\n // the opposite direction. Filtering `ref` consistently across\n // both SSR runtimes is a separate concern tracked alongside\n // the JS `applyRestAttrs` vs `spreadAttrs` mismatch (#1411\n // review).\n if key == "children" {\n continue\n }\n val := rv.MapIndex(reflect.ValueOf(key))\n if !val.IsValid() {\n continue\n }\n // Unwrap interface wrappers (json.Unmarshal produces\n // interface{}-wrapped values for map[string]any).\n v := val\n for v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {\n if v.IsNil() {\n // Skip null entries.\n v = reflect.Value{}\n break\n }\n v = v.Elem()\n }\n if !v.IsValid() {\n continue\n }\n // Boolean values: true \u2192 bare attribute, false \u2192 omitted.\n if v.Kind() == reflect.Bool {\n if !v.Bool() {\n continue\n }\n parts = append(parts, toAttrName(key))\n continue\n }\n // `style` routes through StyleToCss so object literals get a\n // real CSS string. The JS side does the same.\n if key == "style" {\n css, ok := StyleToCss(v.Interface())\n if !ok {\n continue\n }\n parts = append(parts, fmt.Sprintf(`style="%s"`, template.HTMLEscapeString(css)))\n continue\n }\n // Stringify and escape. fmt.Sprint handles numbers, bools-as-\n // strings, and arbitrary stringer types the same way the JS\n // `String(value)` coercion does for the analogous cases.\n s := fmt.Sprint(v.Interface())\n parts = append(parts, fmt.Sprintf(`%s="%s"`, toAttrName(key), template.HTMLEscapeString(s)))\n }\n if len(parts) == 0 {\n return ""\n }\n return template.HTMLAttr(strings.Join(parts, " "))\n}\n\n// Omit builds a `map[string]any` residual bag from a struct or map value,\n// excluding the given keys \u2014 powers the `{...rest}` spread-onto-element\n// lowering for a destructured `.map()` loop-item\'s object-rest binding\n// (#2087): `.map(({ id, title, ...rest }) => <li {...rest}>)` needs "every\n// field EXCEPT the ones the pattern already destructured out", and a static\n// Go struct type has no way to express "minus a field" \u2014 the exclude set is\n// known at COMPILE TIME (the sibling keys the destructure pattern names), so\n// the compiler passes them here and this does the per-item field-vs-key\n// matching a static type can\'t. The result feeds `bf_spread_attrs`\n// (`SpreadAttrs`), same as a top-level `{...attrs()}` bag.\n//\n// Struct receiver: iterates exported fields via reflection, keyed by each\n// field\'s `json` struct tag (falling back to the Go field name when absent)\n// \u2014 the generated struct\'s json tag is always the ORIGINAL source property\n// name (see `structFieldsFor` / `typeDefinitionToGo` in the Go adapter), so\n// this reproduces the exact JS key `SpreadAttrs`\'s `toAttrName` expects\n// (`"data-priority"`, not a re-derived `"DataPriority"`). A tag of `"-"`\n// (opt-out) is skipped like `encoding/json` does.\n//\n// Map receiver: copies string keys through directly, same exclude/skip\n// rules.\n//\n// Anything else (nil, a non-struct/non-map interface) returns an empty map.\nfunc Omit(item any, excludeKeys ...string) map[string]any {\n exclude := make(map[string]struct{}, len(excludeKeys))\n for _, k := range excludeKeys {\n exclude[k] = struct{}{}\n }\n out := map[string]any{}\n rv := reflect.ValueOf(item)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return out\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Struct:\n rt := rv.Type()\n for i := 0; i < rt.NumField(); i++ {\n field := rt.Field(i)\n if !field.IsExported() {\n continue\n }\n key := field.Name\n if tag, ok := field.Tag.Lookup("json"); ok {\n if comma := strings.Index(tag, ","); comma >= 0 {\n tag = tag[:comma]\n }\n if tag == "-" {\n continue\n }\n if tag != "" {\n key = tag\n }\n }\n if _, skip := exclude[key]; skip {\n continue\n }\n out[key] = rv.Field(i).Interface()\n }\n case reflect.Map:\n for _, k := range rv.MapKeys() {\n if k.Kind() != reflect.String {\n continue\n }\n key := k.String()\n if _, skip := exclude[key]; skip {\n continue\n }\n val := rv.MapIndex(k)\n if val.IsValid() {\n out[key] = val.Interface()\n }\n }\n }\n return out\n}\n\n// goInitialisms mirrors GO_INITIALISMS\n// (packages/adapter-go-template/src/adapter/lib/go-naming.ts) \u2014 the Go-side\n// copy `jsKeyFromGoCasedKey` needs to recognize a whole-word initialism run\n// the same way the TS-side `capitalizeFieldName` produced it (#2490). Keep\n// both lists in sync by hand; there is no shared source between the two\n// languages.\nvar goInitialisms = map[string]struct{}{\n "id": {}, "url": {}, "http": {}, "https": {}, "api": {}, "json": {}, "xml": {},\n "html": {}, "css": {}, "sql": {}, "ip": {}, "tcp": {}, "udp": {}, "dns": {},\n "ssh": {}, "tls": {}, "ssl": {}, "uri": {}, "uid": {}, "uuid": {}, "ascii": {},\n "utf8": {}, "eof": {}, "grpc": {}, "rpc": {}, "cpu": {}, "gpu": {}, "ram": {}, "os": {},\n}\n\n// jsKeyFromGoCasedKey inverts `capitalizeFieldName` (go-naming.ts): recovers\n// the JS-original property name from a Go-cased map key produced by the\n// row\'s field-access contract (#2490). A leading run of 2+ uppercase ASCII\n// letters that, lowercased, is a whole-word Go initialism lowers as a\n// block (`ID` \u2192 `id`, matching `capitalizeFieldName`\'s `id` \u2192 `ID`\n// whole-word branch); otherwise only the first rune lowers (`Title` \u2192\n// `title`, `DataKind` \u2192 `dataKind` \u2014 kebab-casing that back to\n// `data-kind` is `toAttrName`\'s job, not this function\'s). A key whose\n// first rune is already lowercase (not Go-cased) is returned unchanged.\nfunc jsKeyFromGoCasedKey(key string) string {\n if key == "" {\n return key\n }\n first, _ := utf8.DecodeRuneInString(key)\n if !unicode.IsUpper(first) {\n return key\n }\n runLen := 0\n for runLen < len(key) && key[runLen] >= \'A\' && key[runLen] <= \'Z\' {\n runLen++\n }\n // An initialism may carry trailing digits (`utf8` \u2192 `UTF8`); without\n // consuming them the lookup misses and `UTF8` decapitalizes to the\n // wrong `uTF8`.\n for runLen < len(key) && key[runLen] >= \'0\' && key[runLen] <= \'9\' {\n runLen++\n }\n if runLen >= 2 {\n if _, ok := goInitialisms[strings.ToLower(key[:runLen])]; ok {\n return strings.ToLower(key[:runLen]) + key[runLen:]\n }\n }\n return decapitalize(key)\n}\n\n// JSKeys reverses the loop-row Go-casing described above for a WHOLE\n// receiver, keyed by ORIGINAL JS property name \u2014 the counterpart to\n// `SpreadAttrs`\' `bf_js_keys` registration, applied ONLY to the loop-row\n// whole-item spread path (`{...row}` where `row` is the bare `.map()`\n// param, #2490).\n//\n// Struct receiver: prefer each field\'s `json` tag when present (same\n// json-tag recovery `Omit` already does above \u2014 the tag carries the\n// ORIGINAL source property name verbatim, robust to composite/hyphenated\n// names a pure un-casing can\'t reconstruct); fall back to\n// `jsKeyFromGoCasedKey` on the bare field name when no tag is set.\n//\n// Map receiver: `jsKeyFromGoCasedKey` per key.\n//\n// Anything else (nil, a non-struct/non-map interface) returns an empty\n// map rather than panicking.\nfunc JSKeys(item any) map[string]any {\n out := map[string]any{}\n rv := reflect.ValueOf(item)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return out\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Struct:\n rt := rv.Type()\n for i := 0; i < rt.NumField(); i++ {\n field := rt.Field(i)\n if !field.IsExported() {\n continue\n }\n key := jsKeyFromGoCasedKey(field.Name)\n if tag, ok := field.Tag.Lookup("json"); ok {\n if comma := strings.Index(tag, ","); comma >= 0 {\n tag = tag[:comma]\n }\n if tag == "-" {\n continue\n }\n if tag != "" {\n key = tag\n }\n }\n out[key] = rv.Field(i).Interface()\n }\n case reflect.Map:\n for _, k := range rv.MapKeys() {\n if k.Kind() != reflect.String {\n continue\n }\n key := jsKeyFromGoCasedKey(k.String())\n val := rv.MapIndex(k)\n if val.IsValid() {\n out[key] = val.Interface()\n }\n }\n }\n return out\n}\n\n// bfHydrationPayload returns the value to actually `json.Marshal` for a\n// props struct\'s hydration attribute (#2684): when `props` exposes a\n// non-nil `BfCallerProps map[string]interface{}` field (populated by\n// `NewXxxProps` with exactly the keys the caller passed \u2014 see that\n// field\'s doc comment in the generated Props struct for the two-consumers\n// rationale), that map is marshaled INSTEAD OF the struct itself, so the\n// wire payload carries only caller-supplied data \u2014 matching the\n// reference (Hono\'s `serializeHydrationProps`), which only ever\n// serializes caller-passed keys. `ok` is false for a `props` value with\n// no such field (a hand-built Props value, or code generated before this\n// field existed) \u2014 callers fall back to marshaling `props` whole,\n// unchanged from the pre-#2684 behavior. Reused by both `BfPropsAttr` and\n// `ScopeComment` so the two hydration-payload emission sites can\'t drift.\nfunc bfHydrationPayload(props interface{}) (payload interface{}, ok bool) {\n v := reflect.ValueOf(props)\n for v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil, false\n }\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return nil, false\n }\n field := v.FieldByName("BfCallerProps")\n if !field.IsValid() || field.Kind() != reflect.Map || field.IsNil() {\n return nil, false\n }\n m, isMap := field.Interface().(map[string]interface{})\n if !isMap {\n return nil, false\n }\n return m, true\n}\n\n// BfPropsAttr returns the bf-p attribute with the JSON-serialized\n// props in flat format. Output format: `bf-p=\'{"propName":value,...}\'`.\n// Only emits the attribute for root components (BfIsRoot == true);\n// child components receive props from their parent via initChild().\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported props rather than silently\n// dropping the bf-p attribute and breaking client-side hydration.\n// Same loud-failure policy as `JSON` \u2014 user data going through\n// `encoding/json` shouldn\'t fail invisibly.\nfunc BfPropsAttr(props interface{}) (template.HTMLAttr, error) {\n // Only root components should emit bf-p\n if !getBoolField(props, "BfIsRoot") {\n return "", nil\n }\n\n // #2684: NOT gated on emptiness \u2014 the reference (Hono) still emits a\n // literal `bf-p="{}"` for a root component that has declared\n // client-tracked props but received none of them from the caller\n // (`JSON.stringify({x: undefined})` drops the key but the object\n // itself, and the attribute, are still real); it only skips the\n // attribute altogether for a component with NO client-tracked props at\n // all, a distinction Go\'s `BfIsRoot`-only gate doesn\'t draw. Matching\n // that finer gate is a separate, pre-existing architectural difference\n // (documented in the PR that introduced this comment), not something\n // this substitution should paper over by guessing at emptiness.\n payload, hasCallerProps := bfHydrationPayload(props)\n if !hasCallerProps {\n payload = props\n }\n\n propsJSON, err := json.Marshal(payload)\n if err != nil {\n return "", err\n }\n\n escaped := template.HTMLEscapeString(string(propsJSON))\n return template.HTMLAttr(`bf-p="` + escaped + `"`), nil\n}\n\n// =============================================================================\n// Arithmetic Operations\n// =============================================================================\n\n// Add returns a + b. Supports int and float64.\nfunc Add(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av + bv\n // Return int if both inputs were int-like\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// ConcatStr returns a and b concatenated as strings \u2014 the string-typed half\n// of JS `+` (#2168 string-concat-plus). JS `+` is addition when BOTH\n// operands are numeric and concatenation when EITHER is a string; `Add`\n// (above) covers the numeric case, this covers the string one \u2014 `Add`\n// itself can\'t (`toFloat64` returns 0 for a string operand, so `\'Hello, \' +\n// name` silently rendered "0" before this existed).\nfunc ConcatStr(a, b any) string {\n return toString(a) + toString(b)\n}\n\n// Sub returns a - b. Supports int and float64.\nfunc Sub(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av - bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Mul returns a * b. Supports int and float64.\nfunc Mul(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n result := av * bv\n if isIntLike(a) && isIntLike(b) && result == float64(int(result)) {\n return int(result)\n }\n return result\n}\n\n// Div returns a / b. Returns float64 to match JavaScript behavior.\n// Returns 0 if b is 0 (instead of panicking).\nfunc Div(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\n if bv == 0 {\n return 0\n }\n return av / bv\n}\n\n// Min returns the smaller of a and b (two-arg `Math.min`). Like Mul, it keeps\n// an integer result when both operands are int-like so a CSS value such as\n// `bf_min 100 x` stays `100` rather than `100.000000`. Uses `Number` (not\n// `toFloat64`, which silently zeroes an unrecognized type like a non-numeric\n// string) plus explicit NaN checks, since IEEE-754 `<`/`>` comparisons\n// against NaN are always false and would otherwise let a non-NaN operand\n// win instead of propagating NaN like JS `Math.min`/`Math.max` do.\nfunc Min(a, b any) any {\n av, bv := Number(a), Number(b)\n if math.IsNaN(av) {\n return av\n }\n if math.IsNaN(bv) {\n return bv\n }\n r := av\n if bv < av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Max returns the larger of a and b (two-arg `Math.max`), with the same\n// int-preserving rule and NaN-propagation as Min.\nfunc Max(a, b any) any {\n av, bv := Number(a), Number(b)\n if math.IsNaN(av) {\n return av\n }\n if math.IsNaN(bv) {\n return bv\n }\n r := av\n if bv > av {\n r = bv\n }\n if isIntLike(a) && isIntLike(b) && r == float64(int(r)) {\n return int(r)\n }\n return r\n}\n\n// Mod returns a % b (modulo). Supports int only.\nfunc Mod(a, b any) int {\n av, bv := toInt(a), toInt(b)\n if bv == 0 {\n return 0\n }\n return av % bv\n}\n\n// Neg returns -a (negation).\nfunc Neg(a any) any {\n if v, ok := a.(int); ok {\n return -v\n }\n return -toFloat64(a)\n}\n\n// =============================================================================\n// String Operations\n// =============================================================================\n\n// Lower returns the lowercase version of s.\nfunc Lower(s string) string {\n return strings.ToLower(s)\n}\n\n// Upper returns the uppercase version of s.\nfunc Upper(s string) string {\n return strings.ToUpper(s)\n}\n\n// Trim returns s with leading and trailing whitespace removed.\nfunc Trim(s string) string {\n return strings.TrimSpace(s)\n}\n\n// TrimStart returns s with leading whitespace removed\n// (String.prototype.trimStart, #2183) \u2014 the one-sided sibling of\n// Trim above, using the same unicode.IsSpace predicate strings.TrimSpace\n// applies to both sides.\nfunc TrimStart(s string) string {\n return strings.TrimLeftFunc(s, unicode.IsSpace)\n}\n\n// TrimEnd returns s with trailing whitespace removed\n// (String.prototype.trimEnd, #2183) \u2014 the one-sided sibling of Trim.\nfunc TrimEnd(s string) string {\n return strings.TrimRightFunc(s, unicode.IsSpace)\n}\n\n// Contains returns true if s contains substr.\nfunc Contains(s, substr string) bool {\n return strings.Contains(s, substr)\n}\n\n// Split lowers `String.prototype.split(sep, limit?)` (#1448 Tier B). It\n// wraps `strings.Split` and normalises the result to `[]any` so the\n// slice composes with the array-method surface downstream (`bf_join`,\n// range loops, `bf_len`, \u2026) the same way `bf_slice` / `bf_reverse`\n// results do. Like JS, an empty separator splits into individual UTF-8\n// characters and trailing empty fields are preserved (`"a,".split(",")`\n// \u2192 `["a", ""]`). An optional `limit` caps the number of returned\n// pieces (`"a,b,c".split(",", 2)` \u2192 `["a", "b"]`); a negative limit is\n// ignored (JS would also return every piece \u2014 its ToUint32 wrap makes\n// the limit effectively unbounded). The no-separator form is handled by\n// the adapter (it emits `bf_arr` for the whole-string single element).\nfunc Split(s, sep string, limit ...int) []any {\n parts := strings.Split(s, sep)\n if len(limit) > 0 && limit[0] >= 0 && limit[0] < len(parts) {\n parts = parts[:limit[0]]\n }\n out := make([]any, len(parts))\n for i, p := range parts {\n out[i] = p\n }\n return out\n}\n\n// StartsWith lowers `String.prototype.startsWith(prefix, position?)`\n// (#1448 Tier B). Wraps `strings.HasPrefix`; an empty prefix is always\n// true (JS parity). The optional `position` re-anchors the test to start\n// at that index (clamped to `[0, len]` so it never panics), matching JS\n// `"abc".startsWith("b", 1) === true`.\nfunc StartsWith(s, prefix string, position ...int) bool {\n if len(position) > 0 {\n p := position[0]\n if p < 0 {\n p = 0\n }\n if p > len(s) {\n p = len(s)\n }\n s = s[p:]\n }\n return strings.HasPrefix(s, prefix)\n}\n\n// EndsWith lowers `String.prototype.endsWith(suffix, endPosition?)`\n// (#1448 Tier B). Wraps `strings.HasSuffix`; an empty suffix is always\n// true (JS parity). The optional `endPosition` treats the string as if\n// it were only that many bytes long (clamped to `[0, len]`), matching JS\n// `"abc".endsWith("b", 2) === true`.\nfunc EndsWith(s, suffix string, endPosition ...int) bool {\n if len(endPosition) > 0 {\n e := endPosition[0]\n if e < 0 {\n e = 0\n }\n if e > len(s) {\n e = len(s)\n }\n s = s[:e]\n }\n return strings.HasSuffix(s, suffix)\n}\n\n// Replace lowers the string-pattern form of `String.prototype.replace`\n// (#1448 Tier B). JS replaces only the FIRST occurrence for a string\n// pattern, so the count is 1 (`strings.Replace` with n=1; `ReplaceAll`\n// below is the every-occurrence sibling, `.replaceAll`, #2182). The\n// replacement is treated literally: unlike JS, special replacement\n// patterns like `$&` / `$1` are NOT interpreted (Go and Perl agree on\n// literal replacement, keeping the two template adapters byte-equal;\n// this diverges from the Hono/CSR JS path only for replacement strings\n// that contain `$`-patterns, which are rare in template position).\nfunc Replace(s, old, new string) string {\n return strings.Replace(s, old, new, 1)\n}\n\n// ReplaceAll lowers the string-pattern form of\n// `String.prototype.replaceAll` (#2182): every occurrence, via\n// `strings.ReplaceAll` (equivalent to `strings.Replace` with n=-1).\n// Same literal-replacement caveat as `Replace` above.\nfunc ReplaceAll(s, old, new string) string {\n return strings.ReplaceAll(s, old, new)\n}\n\n// Repeat lowers `String.prototype.repeat(n)` (#1448 Tier B): the\n// receiver concatenated n times. JS throws RangeError for a negative\n// count and `strings.Repeat` panics, so a negative count clamps to the\n// empty string \u2014 SSR templates degrade rather than crash the render.\n// A zero count is the empty string (JS parity).\nfunc Repeat(s string, n int) string {\n if n <= 0 {\n return ""\n }\n return strings.Repeat(s, n)\n}\n\n// padTo lowers the shared body of `String.prototype.padStart` /\n// `padEnd` (#1448 Tier B): pad `s` to `target` code points using `pad`\n// repeated and truncated to fill, prepended (atStart) or appended.\n// Length is measured in runes (not bytes) so the result matches the\n// Perl `bf->pad_*` helpers \u2014 this diverges from JS\'s UTF-16-unit length\n// only for astral-plane input. An empty pad, or a receiver already at\n// least `target` long, returns `s` unchanged (JS parity).\nfunc padTo(s string, target int, pad string, atStart bool) string {\n if pad == "" {\n return s\n }\n sLen := utf8.RuneCountInString(s)\n if sLen >= target {\n return s\n }\n need := target - sLen\n padRunes := []rune(pad)\n fill := make([]rune, 0, need)\n for len(fill) < need {\n for _, r := range padRunes {\n if len(fill) >= need {\n break\n }\n fill = append(fill, r)\n }\n }\n if atStart {\n return string(fill) + s\n }\n return s + string(fill)\n}\n\n// PadStart lowers `String.prototype.padStart(target, pad?)` (#1448 Tier\n// B). The pad string defaults to a single space when omitted.\nfunc PadStart(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, true)\n}\n\n// PadEnd lowers `String.prototype.padEnd(target, pad?)` (#1448 Tier B).\nfunc PadEnd(s string, target int, pad ...string) string {\n p := " "\n if len(pad) > 0 {\n p = pad[0]\n }\n return padTo(s, target, p, false)\n}\n\n// Join concatenates elements of a slice with sep. Accepts both\n// reflect.Slice (the common case \u2014 `bf_arr` and `bf_filter_truthy`\n// both return `[]any`) AND reflect.Array (fixed-size Go arrays like\n// `[3]string{...}`), mirroring JS `Array.prototype.join` which\n// doesn\'t distinguish between the two. Pre-fix this returned "" for\n// fixed-size arrays passed through template data (Copilot review on\n// #1445).\nfunc Join(items any, sep string) string {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return ""\n }\n\n parts := make([]string, v.Len())\n for i := 0; i < v.Len(); i++ {\n parts[i] = toString(v.Index(i).Interface())\n }\n return strings.Join(parts, sep)\n}\n\n// Ternary returns a when cond is true, else b \u2014 the pipeline-position\n// counterpart of a template {{if}} action. Go templates have no\n// expression-level conditional, so a conditional value sitting in\n// ARGUMENT position (a lowering-node helper arg, e.g. the #2324 union\n// stage\'s locale\u2192pattern ternary) cannot be emitted as an {{if}}\n// fragment; the adapter renders it as `(bf_ternary <cond> <a> <b>)`\n// instead. Both branches are evaluated (function-call semantics) \u2014\n// fine for the value shapes the emitter feeds it, wrong for anything\n// with side effects, which template values never have.\nfunc Ternary(cond bool, a, b any) any {\n if cond {\n return a\n }\n return b\n}\n\n// String returns the string form of v. Mirrors JS `String(v)` for\n// non-nil values via `fmt.Sprintf("%v", ...)`. Diverges from JS on\n// nil: JS `String(null)` is "null", but the template path renders\n// `nil` as the empty string here so an unset prop doesn\'t surface\n// as a literal "null"/"undefined" in user-facing HTML. Document the\n// divergence explicitly so callers don\'t rely on JS-exact parity.\nfunc String(v any) string {\n if v == nil {\n return ""\n }\n return fmt.Sprintf("%v", v)\n}\n\n// RawHTML marks a value as trusted, pre-formatted HTML so html/template\'s\n// contextual escaper emits it verbatim instead of escaping it. It is the SSR\n// half of a dynamic `dangerouslySetInnerHTML={{ __html: expr }}` (#2319) \u2014\n// the one raw-output sink Go lacks as bare template syntax, the counterpart\n// to Blade `{!! !!}`, ERB `<%= %>`, Jinja/MiniJinja `| safe`, Twig `| raw`,\n// Mojolicious `<%== %>`, and Xslate `mark_raw`. The caller owns the value\'s\n// safety (React\'s "dangerously" contract); a nil value renders "".\nfunc RawHTML(v any) template.HTML {\n return template.HTML(String(v))\n}\n\n// JSON returns the JSON encoding of v as a string. Mirrors\n// JS `JSON.stringify(v)` for the V1 single-arg shape (no `replacer`\n// or `space`). Object key order is determined by Go\'s `encoding/json`\n// (alphabetical for maps, declaration order for structs) \u2014 the\n// #1187 contract requires value-compat, not order-compat.\n//\n// Top-level NaN / \xB1Inf are pre-handled to match JS \u2014 JS\'s\n// `JSON.stringify(NaN)` and `JSON.stringify(Infinity)` both produce\n// `"null"`, but Go\'s `encoding/json` rejects them with\n// `UnsupportedValueError`. Without this carve-out the common\n// composition `JSON.stringify(Number("garbage"))` would error\n// instead of emitting `"null"` like JS does. Nested NaN/Inf inside\n// a struct/map still surfaces an error \u2014 covering that needs a\n// custom marshaller; out of V1 scope.\n//\n// Returns the marshal error so a `template.Execute` call fails\n// loudly on cycles / unsupported values rather than silently\n// producing `""` and reintroducing the SSR data-loss class\n// #1187 was filed against. Go\'s text/template treats a non-nil\n// error return from a func as an execution failure.\nfunc JSON(v any) (string, error) {\n if f, ok := v.(float64); ok && (math.IsNaN(f) || math.IsInf(f, 0)) {\n return "null", nil\n }\n b, err := json.Marshal(v)\n if err != nil {\n return "", err\n }\n return string(b), nil\n}\n\n// Number coerces v to a float64. Mirrors JS `Number(v)` semantics:\n// numeric / boolean inputs convert as expected; non-numeric strings\n// and other unsupported shapes return `NaN` (matching JS rather\n// than silently substituting 0, which would mis-shape downstream\n// arithmetic and template-side comparisons). Templates that need\n// a deterministic fallback should compose with the user-side\n// default (e.g. `Number(props.x ?? 0)` in JSX).\nfunc Number(v any) float64 {\n if v == nil {\n return math.NaN()\n }\n switch x := v.(type) {\n case float64:\n return x\n case float32:\n return float64(x)\n case int:\n return float64(x)\n case int32:\n return float64(x)\n case int64:\n return float64(x)\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n f, err := strconv.ParseFloat(x, 64)\n if err != nil {\n return math.NaN()\n }\n return f\n }\n return math.NaN()\n}\n\n// Floor returns the largest integer \u2264 v as a float64. Mirrors JS\n// `Math.floor`. The return type stays float64 so chained primitives\n// (`bf_floor` then `bf_string`) line up with JS\'s number type.\nfunc Floor(v any) float64 {\n return math.Floor(Number(v))\n}\n\n// Abs returns the absolute value of v as a float64, mirroring JS\n// `Math.abs`. #2168 math-methods.\nfunc Abs(v any) float64 {\n return math.Abs(Number(v))\n}\n\n// ToFixed formats v with exactly `digits` decimal places, mirroring JS\n// `Number.prototype.toFixed` (zero-padding + half-toward-+Infinity\n// rounding). JS rounds the scaled integer half up (`(2.5).toFixed(0)`\n// is "3"); bare `fmt.Sprintf("%.*f")` rounds half-to-even ("2"), so we\n// scale, round with `Floor(x + 0.5)` (matching `Round`), then format\n// the exact multiple. #1897.\nfunc ToFixed(v any, digits int) string {\n if digits < 0 {\n digits = 0\n }\n n := Number(v)\n // JS toFixed returns the strings "NaN" / "Infinity" / "-Infinity" for\n // non-finite inputs; fmt would render "NaN"/"+Inf"/"-Inf".\n if math.IsNaN(n) {\n return "NaN"\n }\n if math.IsInf(n, 1) {\n return "Infinity"\n }\n if math.IsInf(n, -1) {\n return "-Infinity"\n }\n factor := math.Pow(10, float64(digits))\n rounded := math.Floor(n*factor + 0.5)\n return fmt.Sprintf("%.*f", digits, rounded/factor)\n}\n\n// Ceil returns the smallest integer \u2265 v as a float64. Mirrors JS\n// `Math.ceil`.\nfunc Ceil(v any) float64 {\n return math.Ceil(Number(v))\n}\n\n// Round returns v rounded to the nearest integer as a float64.\n// Mirrors JS `Math.round` \u2014 half-away-from-zero (Go\'s `math.Round`\n// matches; JS rounds half toward +Infinity which differs at .5\n// negatives; we accept that minor divergence since the conformance\n// contract is value-compat for the common positive case).\nfunc Round(v any) float64 {\n return math.Round(Number(v))\n}\n\n// =============================================================================\n// Array/Slice Operations\n// =============================================================================\n\n// Length lowers JS `.length`, matching JS semantics per receiver shape\n// (#2255): a slice/array/map counts ELEMENTS (`reflect.Value.Len`, same as\n// `Len` below), but a STRING counts UTF-16 CODE UNITS \u2014 JS\n// `String.prototype.length` counts UTF-16 code units, not bytes (Go\'s\n// native `len`) or codepoints. A codepoint outside the Basic Multilingual\n// Plane (astral, U+10000-U+10FFFF \u2014 e.g. \'\u{1F44D}\') is a surrogate PAIR in\n// UTF-16, so it counts as 2, not 1; `\'\u65E5\u672C\u8A9E\'` is 3 either way (BMP-only).\n// Routed from the `.length` member lowering\'s generic (non-array,\n// non-loop-slice) fallback \u2014 see `member()`\'s `bf_length` call site.\nfunc Length(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.Chan:\n return rv.Len()\n case reflect.String:\n n := 0\n for _, r := range rv.String() {\n if r > 0xFFFF {\n n += 2\n } else {\n n++\n }\n }\n return n\n default:\n return 0\n }\n}\n\n// IsValidElement lowers React/Hono-style `isValidElement(x)` \u2014 the "is this\n// a renderable element (not plain text)?" predicate the `Slot` component\'s\n// `asChild` pattern (#2266) uses to decide whether to merge props into a\n// child ELEMENT (`children.tag`/`children.props`) or fall back to rendering\n// `children` as-is. The JS runtime checks `\'tag\' in x && \'props\' in x`; on\n// Go SSR a passed-through JSX child is represented as pre-rendered markup\n// (a plain string) OR \u2014 where a struct/map shape carrying `Tag`/`Props`\n// (case-insensitively, mirroring `bf_get`\'s field lookup) is available \u2014 an\n// element-shaped value. A plain string/number/bool/nil is never a valid\n// element, so `isValidElement` must NOT be lowered as bare truthiness\n// (previously done via `renderConditionExpr`) \u2014 a truthy non-empty STRING\n// child wrongly took the element-merge branch and panicked dereferencing\n// `.Props` on a string (`can\'t evaluate field Props in type interface {}`).\nfunc IsValidElement(v any) bool {\n rv := reflect.ValueOf(v)\n for rv.Kind() == reflect.Interface || rv.Kind() == reflect.Pointer {\n if rv.IsNil() {\n return false\n }\n rv = rv.Elem()\n }\n switch rv.Kind() {\n case reflect.Map:\n hasTag, hasProps := false, false\n for _, k := range rv.MapKeys() {\n key := fmt.Sprintf("%v", k.Interface())\n if strings.EqualFold(key, "tag") {\n hasTag = true\n }\n if strings.EqualFold(key, "props") {\n hasProps = true\n }\n }\n return hasTag && hasProps\n case reflect.Struct:\n return fieldByFoldedName(rv, "tag").IsValid() && fieldByFoldedName(rv, "props").IsValid()\n default:\n return false\n }\n}\n\n// fieldByFoldedName finds a struct field by case-insensitive name match \u2014\n// shared by IsValidElement; mirrors getFieldValue\'s (bf_get) struct-branch\n// lookup so the two case-tolerant field resolutions stay consistent.\nfunc fieldByFoldedName(rv reflect.Value, name string) reflect.Value {\n t := rv.Type()\n for i := 0; i < t.NumField(); i++ {\n if strings.EqualFold(t.Field(i).Name, name) {\n return rv.Field(i)\n }\n }\n return reflect.Value{}\n}\n\n// Len returns the length of a slice, array, map, string, or channel.\n// Returns 0 for nil or unsupported types.\nfunc Len(v any) int {\n if v == nil {\n return 0\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan:\n return rv.Len()\n default:\n return 0\n }\n}\n\n// At returns the element at index i from a slice.\n// Supports negative indices (e.g., -1 for last element).\n// Returns nil if index is out of bounds.\nfunc At(items any, index int) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return nil\n }\n\n // Handle negative indices\n if index < 0 {\n index = length + index\n }\n\n if index < 0 || index >= length {\n return nil\n }\n\n return v.Index(index).Interface()\n}\n\n// Includes returns true if items contains elem. Lowers both\n// `Array.prototype.includes` and `String.prototype.includes` \u2014\n// the adapter can\'t disambiguate the receiver at compile time,\n// so this helper dispatches at runtime on `reflect.Kind()`:\n//\n// - slice/array receiver: SameValueZero element search (matches\n// the evaluator\'s `evalSameValueZero`/`evalIncludes` in eval.go,\n// which back the serialized-callback path) \u2014 numeric types compare\n// by value across int/float64 the way JS\'s single "number" type\n// does, and NaN matches NaN (unlike `===`). This used to be\n// `reflect.DeepEqual`, which is type-strict (`int(2)` != `float64(2)`)\n// and never matches NaN to NaN; that diverged from the evaluator\'s\n// `.includes` and from JS itself, so it was unified here.\n// - string receiver: strings.Contains substring search\n//\n// Anything else returns false (matches the JS semantic where\n// `.includes` is only defined on Array / TypedArray / String).\nfunc Includes(recv any, elem any) bool {\n v := reflect.ValueOf(recv)\n if v.Kind() == reflect.String {\n // JS `String.prototype.includes` accepts only string args;\n // non-string `elem` would TypeError in real JS but our\n // callers have lowered through `convertExpressionToGo`\n // where the arg type is whatever the template binds. Stringify\n // via fmt to keep the helper total.\n needle, ok := elem.(string)\n if !ok {\n needle = fmt.Sprintf("%v", elem)\n }\n return strings.Contains(v.String(), needle)\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if evalSameValueZero(v.Index(i).Interface(), elem) {\n return true\n }\n }\n return false\n}\n\n// IndexOf returns the 0-based position of the first item that\n// DeepEquals `elem`, or -1 if not found. Lowers\n// `Array.prototype.indexOf(x)` (#1448 Tier A). The existing\n// `FindIndex` helper does struct-field equality (used by the\n// higher-order `.find` lowering); this one does value equality\n// against scalar / struct items so callers don\'t have to compose\n// a synthetic predicate.\n//\n// Non-array / non-slice receivers return -1 (matches the JS\n// semantic that `.indexOf` is only defined on Array / TypedArray).\nfunc IndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// LastIndexOf returns the 0-based position of the last item that\n// DeepEquals `elem`, or -1 if not found. Mirrors\n// `Array.prototype.lastIndexOf(x)`. The reverse traversal is the\n// only behavioural difference vs `IndexOf` \u2014 disambiguating a\n// duplicated value\'s first vs last position is the canonical\n// reason a JS author reaches for `lastIndexOf`.\nfunc LastIndexOf(items any, elem any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(v.Index(i).Interface(), elem) {\n return i\n }\n }\n return -1\n}\n\n// Concat merges two arrays (or slices) into a single `[]any`,\n// preserving order: receiver elements first, then `other`\'s.\n// Lowers `Array.prototype.concat(other)` (#1448 Tier A). Non-array\n// operands collapse to an empty source \u2014 matches the JS semantic\n// where `.concat` on a non-Array reads it as a single element only\n// if its `Symbol.isConcatSpreadable` is true; the template-language\n// path doesn\'t have user objects with that flag, so treating\n// non-arrays as empty is the conservative lowering. Variadic\n// `.concat(a, b, c)` is out of scope here (parser gates to a single\n// arg); the helper itself stays binary so a future variadic IR can\n// fold via repeated calls without changing this signature.\nfunc Concat(a, b any) []any {\n flatten := func(v reflect.Value) []any {\n if !v.IsValid() {\n return nil\n }\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n out := make([]any, v.Len())\n for i := 0; i < v.Len(); i++ {\n out[i] = v.Index(i).Interface()\n }\n return out\n }\n left := flatten(reflect.ValueOf(a))\n right := flatten(reflect.ValueOf(b))\n return append(left, right...)\n}\n\n// clampSliceRange normalizes JS `.slice(start, end?)` bounds against a\n// receiver of `length` elements (runes, for the string branch of\n// `Slice` below; array elements, for the array branch) \u2014 shared so\n// both branches clamp identically.\n//\n// JS-compat clamping:\n// - start < 0 \u2192 length + start (e.g. -1 = last index)\n// - end < 0 \u2192 length + end\n// - start < 0 after clamp \u2192 0\n// - end > length \u2192 length\nfunc clampSliceRange(length, start int, end []int) (int, int) {\n if start < 0 {\n start = length + start\n }\n if start < 0 {\n start = 0\n }\n if start > length {\n start = length\n }\n\n stop := length\n if len(end) > 0 {\n stop = end[0]\n if stop < 0 {\n stop = length + stop\n }\n if stop < 0 {\n stop = 0\n }\n if stop > length {\n stop = length\n }\n }\n return start, stop\n}\n\n// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A) AND\n// `String.prototype.slice(start, end?)` (the `string-slice`\n// divergence) \u2014 the adapter emits the same `bf_slice` call for both\n// receiver shapes (it can\'t disambiguate string vs. array at compile\n// time), so this helper dispatches at runtime on `reflect.Kind()`,\n// mirroring `Includes` above. The variadic `end` arg lets Go\n// template\'s call dispatcher pass either 2 or 3 arguments; an absent\n// end means "to length".\n//\n// String length/positions are measured in runes, not UTF-16 code\n// units \u2014 the same divergence boundary `padTo` already accepts\n// (differs from JS only for astral-plane input). `start >= end`\n// (after clamping) returns an empty result for either receiver.\n//\n// Any other receiver kind returns an empty `[]any`.\nfunc Slice(items any, start int, end ...int) any {\n v := reflect.ValueOf(items)\n\n if v.Kind() == reflect.String {\n runes := []rune(v.String())\n s, e := clampSliceRange(len(runes), start, end)\n if s >= e {\n return ""\n }\n return string(runes[s:e])\n }\n\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n s, e := clampSliceRange(v.Len(), start, end)\n if s >= e {\n return []any{}\n }\n out := make([]any, 0, e-s)\n for i := s; i < e; i++ {\n out = append(out, v.Index(i).Interface())\n }\n return out\n}\n\n// Reverse returns a new slice with `items`\'s elements in reverse\n// order. Lowers both `Array.prototype.reverse()` and\n// `Array.prototype.toReversed()` (#1448 Tier A) \u2014 SSR templates\n// render a snapshot, so JS\'s mutate-receiver vs return-new-array\n// distinction has no template-level meaning, and the safer\n// non-mutating shape is used uniformly.\n//\n// Non-array receivers return an empty `[]any`.\nfunc Reverse(items any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n length := v.Len()\n out := make([]any, length)\n for i := 0; i < length; i++ {\n out[length-1-i] = v.Index(i).Interface()\n }\n return out\n}\n\n// Flat flattens nested slices/arrays `depth` levels deep. Lowers\n// `Array.prototype.flat(depth?)` (#1448 Tier C). A `depth` of `-1` is the\n// `Infinity` sentinel (flatten fully); `0` (or negative-from-JS, already\n// normalised to 0 at compile time) returns a shallow copy. Non-array\n// elements are kept as-is (JS only flattens nested arrays). A non-array\n// receiver returns an empty `[]any`.\nfunc Flat(items any, depth int) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n ev := reflect.ValueOf(el)\n if depth != 0 && (ev.Kind() == reflect.Slice || ev.Kind() == reflect.Array) {\n // `-1` (Infinity) recurses unbounded; a finite depth spends one level.\n next := depth\n if depth > 0 {\n next = depth - 1\n }\n out = append(out, Flat(el, next)...)\n } else {\n out = append(out, el)\n }\n }\n return out\n}\n\n// FlatDynamicDepth coerces `depth` via JS\'s `ToIntegerOrInfinity` and\n// flattens `items` that many levels. Lowers a DYNAMIC `.flat(depth)`\n// (#2094) \u2014 one whose depth isn\'t a compile-time literal, so (unlike\n// `Flat` above) the coercion happens here at render time instead of in the\n// parser.\n//\n// This is a SEPARATE helper from `Flat`/`bf_flat` \u2014 NOT a drop-in\n// replacement \u2014 because `Flat`\'s `depth int` parameter treats `-1` as a\n// compile-time SENTINEL meaning "flatten fully" (the parser\'s own\n// normalisation of a literal `Infinity`). A genuinely dynamic depth value\n// of `-1` means the JS-correct OPPOSITE: `Array.prototype.flat(-1)` never\n// recurses (same as `.flat(0)`, a shallow copy), because\n// `FlattenIntoArray` only recurses when `depth > 0`. Reusing `Flat`\'s int\n// contract for a raw dynamic value would silently invert that case, so\n// this function coerces FIRST \u2014 mapping a real `+Infinity` / huge finite\n// value to `Flat`\'s own `-1` sentinel, and a real negative value to `0` \u2014\n// and only then delegates to `Flat`\'s recursion.\n//\n// Coercion rules (JS `ToIntegerOrInfinity`, mirrored exactly; pinned by the\n// `flat_dynamic` golden-vector cases in\n// packages/adapter-tests/vectors/cases.ts):\n// - the value converts via `ToNumber` first (numeric string / bool /\n// number all coerce; see `flatDepthToFloat`);\n// - a NaN result (including a non-numeric string) \u2192 `0`;\n// - truncates toward zero (`2.7` \u2192 `2`);\n// - negative \u2192 `0`;\n// - `+Infinity` / a huge finite value \u2192 flattens fully.\nfunc FlatDynamicDepth(items any, depth any) []any {\n return Flat(items, coerceFlatDepth(depth))\n}\n\n// coerceFlatDepth implements JS\'s `ToIntegerOrInfinity` for a dynamic\n// `.flat(depth)` argument, returning an int in `Flat`\'s own contract (`-1`\n// = unbounded, `>= 0` = that many levels).\nfunc coerceFlatDepth(depth any) int {\n f, ok := flatDepthToFloat(depth)\n if !ok || math.IsNaN(f) {\n return 0\n }\n if math.IsInf(f, 1) {\n return -1 // Flat\'s "flatten fully" sentinel\n }\n if math.IsInf(f, -1) {\n return 0\n }\n trunc := math.Trunc(f)\n if trunc < 0 {\n return 0\n }\n // A huge finite depth behaves identically to "flatten fully" in\n // practice \u2014 real data bottoms out at its actual nesting depth long\n // before a counter this large would ever reach zero. Capping it here\n // avoids an absurd countdown without needing a second sentinel.\n if trunc > 1_000_000 {\n return -1\n }\n return int(trunc)\n}\n\n// flatDepthToFloat converts a dynamic `.flat(depth)` argument to a float64,\n// mirroring JS\'s `ToNumber` across the value shapes a Go template data\n// model can carry (every numeric kind, bool, numeric string). `ok` is\n// false for a shape `ToNumber` can\'t coerce meaningfully (`nil`, or a\n// non-numeric string) \u2014 `coerceFlatDepth` treats that the same as NaN\n// (\u2192 depth `0`), matching JS.\nfunc flatDepthToFloat(v any) (float64, bool) {\n switch n := v.(type) {\n case nil:\n return 0, false\n case float64:\n return n, true\n case float32:\n return float64(n), true\n case int:\n return float64(n), true\n case int8:\n return float64(n), true\n case int16:\n return float64(n), true\n case int32:\n return float64(n), true\n case int64:\n return float64(n), true\n case uint:\n return float64(n), true\n case uint8:\n return float64(n), true\n case uint16:\n return float64(n), true\n case uint32:\n return float64(n), true\n case uint64:\n return float64(n), true\n case bool:\n if n {\n return 1, true\n }\n return 0, true\n case string:\n s := strings.TrimSpace(n)\n if s == "" {\n return 0, true // JS: Number("") is 0\n }\n f, err := strconv.ParseFloat(s, 64)\n if err != nil {\n return 0, false // not numeric \u2192 NaN path\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// FlatMap projects each element through a `self` / `field` projection and\n// flattens the result one level. Lowers value-returning\n// `Array.prototype.flatMap(fn)` for the field-projection catalogue\n// (#1448 Tier C): `items.flatMap(i => i)` (self) and\n// `items.flatMap(i => i.field)` (field). A projected non-array value is\n// kept as-is (flatMap = map + flat(1)). Non-array receiver \u2192 empty.\nfunc FlatMap(items any, keyKind, keyName string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n projected := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n if keyKind == "field" {\n projected = append(projected, getFieldValue(el, keyName))\n } else {\n projected = append(projected, el)\n }\n }\n return Flat(projected, 1)\n}\n\n// FlatMapTuple lowers an array-literal flatMap projection\n// `items.flatMap(i => [i.a, i.b])` (#1448 Tier C). `specs` is a flat list\n// of (kind, name) pairs, one per array-literal leaf: ("self", "") for the\n// item itself, ("field", "<Name>") for a struct field. For each item it\n// appends every leaf\'s value in order. Unlike the scalar `FlatMap`, the\n// per-item array is flattened only one level (flat(1) removes the literal\n// wrapper), so an array-valued leaf is appended verbatim rather than\n// spread \u2014 which is exactly "append each leaf". Non-array receiver \u2192 empty.\nfunc FlatMapTuple(items any, specs ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return []any{}\n }\n out := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n el := v.Index(i).Interface()\n for j := 0; j+1 < len(specs); j += 2 {\n if specs[j] == "field" {\n out = append(out, getFieldValue(el, specs[j+1]))\n } else {\n out = append(out, el)\n }\n }\n }\n return out\n}\n\n// First returns the first element of a slice, or nil if empty.\nfunc First(items any) any {\n return At(items, 0)\n}\n\n// Last returns the last element of a slice, or nil if empty.\nfunc Last(items any) any {\n return At(items, -1)\n}\n\n// Arr builds an []any from variadic args. Used to lower JS array\n// literals like `[a, b]` for the registry Slot\'s\n// `[className, childClass].filter(Boolean).join(\' \')` shape (#1443) \u2014\n// Go templates have no array-literal syntax, so the codegen routes\n// array-literal IR nodes through this helper.\nfunc Arr(items ...any) []any {\n return items\n}\n\n// FilterTruthy returns a new slice containing only truthy items.\n// Mirrors `arr.filter(Boolean)` semantics: drop nil, false, 0, "" \u2014 the\n// same falsy set JavaScript\'s `Boolean(x)` recognises. Used to lower\n// the registry Slot\'s class-merge pattern (#1443); generalising to\n// arbitrary callable predicates would need the callee-resolution path\n// blocked by #1389, so this stays Boolean-specific.\nfunc FilterTruthy(items any) []any {\n v := reflect.ValueOf(items)\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n result := make([]any, 0, v.Len())\n for i := 0; i < v.Len(); i++ {\n raw := v.Index(i).Interface()\n if isTruthy(raw) {\n result = append(result, raw)\n }\n }\n return result\n}\n\n// Truthy is the exported form of isTruthy \u2014 JavaScript\'s `Boolean(x)`\n// semantics. Two callers:\n// - generated `NewXxxProps` code lowering a conditional inline-object\n// spread condition on an `interface{}` prop (whose runtime value may be\n// a string, number, bool, \u2026), keeping the spread bag\'s inclusion test\n// faithful to JS rather than string-biased (#1752); and\n// - the `bf_truthy` template FuncMap entry (#2335), which coerces a\n// `bf_ternary` test to a real bool when it isn\'t already a comparison /\n// negation (`Ternary`\'s `cond` parameter is typed `bool`, unlike\n// `{{if}}`\'s built-in truthiness). Uniform across string/number/bool/nil,\n// so a `bf_ternary` test on any prop type can\'t hit a `bool`-vs-`string`\n// comparison error the way a string-only `ne <value> ""` would.\nfunc Truthy(v any) bool { return isTruthy(v) }\n\n// isTruthy mirrors JavaScript\'s `Boolean(x)` for the value shapes the\n// template path actually receives \u2014 nil / false / 0 / "" are falsy.\n// Other shapes (non-empty maps, slices, structs, true) are truthy, in\n// line with JS\'s "objects are truthy" rule.\nfunc isTruthy(v any) bool {\n if v == nil {\n return false\n }\n switch x := v.(type) {\n case bool:\n return x\n case string:\n return x != ""\n case int:\n return x != 0\n case int8, int16, int32, int64:\n return reflect.ValueOf(v).Int() != 0\n case uint, uint8, uint16, uint32, uint64:\n return reflect.ValueOf(v).Uint() != 0\n case float32:\n // JS `Boolean(NaN)` is false regardless of float width \u2014 the\n // float64 arm below was the only one checking IsNaN, which\n // diverged from JS for `float32` NaN inputs (Copilot review on\n // #1445). Widening to float64 for the IsNaN check keeps the\n // two branches in lock-step.\n return x != 0 && !math.IsNaN(float64(x))\n case float64:\n return x != 0 && !math.IsNaN(x)\n }\n return true\n}\n\n// =============================================================================\n// Higher-order Array Methods\n// =============================================================================\n\n// fieldValue projects item.field for the higher-order predicate\n// helpers. The field name arrives in JS casing; structs resolve via\n// the capitalized Go convention (FieldByName inside getFieldValue),\n// maps via getFieldValue\'s case-variant lookup \u2014 the same dual\n// support Sort/Reduce gained in #1487, extended here so JSON-decoded\n// data (map items) participates instead of being silently skipped.\n// nil-safe: missing fields and nil items project to nil.\nfunc fieldValue(item any, field string) any {\n return getFieldValue(item, capitalize(field))\n}\n\n// Every returns true if every item\'s field is truthy under JS\n// `Boolean(item.field)` semantics. Mirrors JavaScript\'s\n// Array.prototype.every(item => item.field) \u2014 including being\n// vacuously true for an empty receiver.\nfunc Every(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if !isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item\'s field is truthy. Mirrors\n// JavaScript\'s Array.prototype.some(item => item.field).\nfunc Some(items any, field string) bool {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return false\n }\n for i := 0; i < v.Len(); i++ {\n if isTruthy(fieldValue(v.Index(i).Interface(), field)) {\n return true\n }\n }\n return false\n}\n\n// Filter returns items where item.field == value.\n// Mirrors JavaScript\'s Array.prototype.filter(item => item.field === value).\n// Returns []any to allow chaining with other bf_* functions.\nfunc Filter(items any, field string, value any) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n var result []any\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n result = append(result, item)\n }\n }\n return result\n}\n\n// Find returns the first item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.find(item => item.field === value).\nfunc Find(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindIndex returns the index of the first item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findIndex(item => item.field === value).\nfunc FindIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := 0; i < v.Len(); i++ {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// FindLast returns the last item where item.field == value, or nil if not found.\n// Mirrors JavaScript\'s Array.prototype.findLast(item => item.field === value).\nfunc FindLast(items any, field string, value any) any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i).Interface()\n if reflect.DeepEqual(fieldValue(item, field), value) {\n return item\n }\n }\n return nil\n}\n\n// FindLastIndex returns the index of the last item where item.field == value, or -1.\n// Mirrors JavaScript\'s Array.prototype.findLastIndex(item => item.field === value).\nfunc FindLastIndex(items any, field string, value any) int {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return -1\n }\n for i := v.Len() - 1; i >= 0; i-- {\n if reflect.DeepEqual(fieldValue(v.Index(i).Interface(), field), value) {\n return i\n }\n }\n return -1\n}\n\n// sortKeySpec is one parsed comparison key. A simple comparator has\n// one; a `||`-chained multi-key comparator has several, applied in\n// order as tie-breakers.\ntype sortKeySpec struct {\n kind string // "self" | "field"\n name string // capitalised field name, or "" for "self"\n compareType string // "numeric" | "string" | "auto"\n direction string // "asc" | "desc"\n}\n\n// Sort returns a new stable-sorted slice. Lowers\n// `Array.prototype.sort` / `Array.prototype.toSorted` (#1448 Tier B).\n// Non-mutating \u2014 JS\'s mutate-vs-new distinction is moot in SSR\n// template context (templates render a snapshot).\n//\n// Call shape (the compiler emits one 4-string group per key):\n//\n// bf_sort <items> (<keyKind> <keyName> <compareType> <direction>)+\n//\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field\n// name (e.g. "Price") otherwise\n// compareType: "numeric" | "string" | "auto"\n// direction: "asc" | "desc"\n//\n// The groups cover the accepted comparator catalogue: `a.f - b.f`,\n// `a - b`, `a[.f].localeCompare(b[.f])`, and relational-ternary keys\n// (`a.f > b.f ? 1 : -1` \u2192 "auto"), each `||`-chainable for multi-key\n// tie-breaks. Anything outside refuses at compile time (BF101 from the\n// JSX compiler) and never reaches this helper.\n//\n// "auto" compares numerically when both projected keys parse as\n// numbers, else lexically \u2014 mirroring the Perl `bf->sort` helper\'s\n// `looks_like_number` rule so the two template adapters stay\n// byte-equal. This diverges from JS `<`/`>` only for numeric strings.\n//\n// A future `nulls` knob can extend the per-key group without rewriting\n// existing call sites \u2014 each key already projects before comparing.\nfunc Sort(items any, spec ...string) []any {\n v := reflect.ValueOf(items)\n if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {\n return nil\n }\n\n length := v.Len()\n if length == 0 {\n return []any{}\n }\n\n // Copy into a fresh []any so the sort is non-mutating regardless\n // of whether the receiver is `[]T` or `[]any`.\n result := make([]any, length)\n for i := 0; i < length; i++ {\n result[i] = v.Index(i).Interface()\n }\n\n keys := parseSortSpec(spec)\n sort.SliceStable(result, func(i, j int) bool {\n for _, k := range keys {\n ki := projectSortKey(result[i], k.kind, k.name)\n kj := projectSortKey(result[j], k.kind, k.name)\n c := compareSortKey(ki, kj, k.compareType)\n if c == 0 {\n continue // tie on this key \u2014 fall through to the next\n }\n if k.direction == "desc" {\n return c > 0\n }\n return c < 0\n }\n return false\n })\n\n return result\n}\n\n// parseSortSpec chunks the variadic operand list into 4-string key\n// groups. A trailing partial group (malformed emit) is ignored rather\n// than panicking \u2014 defensive, mirroring the helper\'s nil-safe stance.\nfunc parseSortSpec(spec []string) []sortKeySpec {\n var keys []sortKeySpec\n for i := 0; i+3 < len(spec); i += 4 {\n keys = append(keys, sortKeySpec{\n kind: spec[i],\n name: spec[i+1],\n compareType: spec[i+2],\n direction: spec[i+3],\n })\n }\n return keys\n}\n\n// compareSortKey returns -1 / 0 / 1 for two projected keys under the\n// given compare type (ascending orientation; the caller flips for\n// "desc"). "string" stringifies both (nil \u2192 "", matching the\n// documented `bf->string(undef) === ""` divergence). "auto" compares\n// numerically when both parse as numbers, else lexically.\nfunc compareSortKey(ki, kj any, compareType string) int {\n switch compareType {\n case "string":\n return strings.Compare(toString(ki), toString(kj))\n case "auto":\n ni, okI := toFloat64WithOK(ki)\n nj, okJ := toFloat64WithOK(kj)\n if okI && okJ {\n return cmpFloat(ni, nj)\n }\n return strings.Compare(toString(ki), toString(kj))\n default: // numeric\n return cmpFloat(toFloat64(ki), toFloat64(kj))\n }\n}\n\nfunc cmpFloat(a, b float64) int {\n if a < b {\n return -1\n }\n if a > b {\n return 1\n }\n return 0\n}\n\n// toFloat64WithOK reports a value\'s numeric float and whether it is\n// number-like. Genuine numeric kinds always qualify; strings qualify\n// when they parse as a float (so the "auto" compare path matches the\n// Perl `looks_like_number` rule). Everything else is non-numeric.\nfunc toFloat64WithOK(v any) (float64, bool) {\n switch n := v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return toFloat64(v), true\n case string:\n f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)\n if err != nil {\n return 0, false\n }\n return f, true\n default:\n return 0, false\n }\n}\n\n// projectSortKey reduces an item to the value the comparator\n// actually compares. For `keyKind == "field"` it reads the named\n// struct field; for `keyKind == "self"` (primitive arrays) it\n// returns the item unchanged.\nfunc projectSortKey(item any, keyKind, keyName string) any {\n if keyKind == "field" {\n return getFieldValue(item, keyName)\n }\n return item\n}\n\n// getFieldValue extracts a struct field, map entry, or slice/array/string\n// element using reflection, dispatching on the RUNTIME kind of `item`\n// rather than a compile-time guess about `field`\'s shape (#2491: a\n// dynamic-key element access on a loop row, `tone[k]`, is only known to\n// be string- or number-shaped at execution time \u2014 routing it through a\n// single runtime-polymorphic accessor, mirroring Jinja/minijinja `[]`\n// and Blade\'s `data_get()`, replaces the compile-time either/or guess\n// that broke for one shape or the other). For map/struct receivers it\n// falls back to case-variant lookup so JSON-decoded user data\n// (`map[string]any{"price": 30}`) and PascalCase-emitted test data both\n// resolve under a single key name. (#1487) `field` is `any` (not\n// `string`) precisely so a genuine numeric index (`bf_get $arr $i`,\n// e.g. `selected()[index]`) round-trips as an int rather than being\n// forced through a string conversion \u2014 this is a strict superset of the\n// `index` builtin, not a replacement that narrows numeric-index support.\nfunc getFieldValue(item any, field any) any {\n v := reflect.ValueOf(item)\n // Defensive IsNil guards mirror `SpreadAttrs` \u2014 keeps the helper\n // safe against typed-nil pointer / nil-interface items inside a\n // `[]any` so a single bad row doesn\'t crash the whole sort.\n if v.Kind() == reflect.Interface {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n if v.Kind() == reflect.Ptr {\n if v.IsNil() {\n return nil\n }\n v = v.Elem()\n }\n\n if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {\n idx, ok := fieldAsIndex(field)\n if !ok || idx < 0 || idx >= v.Len() {\n return nil\n }\n return v.Index(idx).Interface()\n }\n\n if v.Kind() == reflect.String {\n idx, ok := fieldAsIndex(field)\n if !ok {\n return nil\n }\n runes := []rune(v.String())\n if idx < 0 || idx >= len(runes) {\n return nil\n }\n return string(runes[idx])\n }\n\n // From here on only a string-shaped key can match a map or struct\n // field \u2014 a numeric `field` (e.g. a loop index applied to a\n // non-indexable receiver) has no lookup to perform.\n fieldStr, ok := field.(string)\n if !ok {\n return nil\n }\n\n if v.Kind() == reflect.Map {\n keyType := v.Type().Key()\n if keyType.Kind() != reflect.String {\n return nil\n }\n // Convert the lookup string to the map\'s actual key type so\n // maps keyed by a named string type (`type Key string`) don\'t\n // panic with `value of type string is not assignable to type X`.\n lookup := func(s string) (any, bool) {\n k := reflect.ValueOf(s).Convert(keyType)\n if mv := v.MapIndex(k); mv.IsValid() {\n return mv.Interface(), true\n }\n return nil, false\n }\n if r, ok := lookup(fieldStr); ok {\n return r\n }\n if cap := capitalize(fieldStr); cap != fieldStr {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(fieldStr); low != fieldStr {\n if r, ok := lookup(low); ok {\n return r\n }\n }\n // All-lowercase fallback: a Go-initialism field projects as an\n // all-caps key (`id` \u2192 `ID`), and `decapitalize("ID")` only\n // lowers the first char (`iD`), so the JS-keyed map ("id") still\n // misses. Try the fully-lowered key last to resolve it.\n if lower := strings.ToLower(fieldStr); lower != fieldStr && lower != decapitalize(fieldStr) {\n if r, ok := lookup(lower); ok {\n return r\n }\n }\n return nil\n }\n\n if v.Kind() != reflect.Struct {\n return nil\n }\n\n fieldVal := v.FieldByName(fieldStr)\n if !fieldVal.IsValid() {\n // Case-variant fallback: the evaluator carries the JS field name\n // (`id` / `url`) against a Go-capitalised struct field (`ID` / `URL`),\n // which exact `FieldByName` misses and the initialism rules can\'t be\n // reproduced char-for-char here. Match case-insensitively instead \u2014\n // `FieldByNameFunc` returns the zero Value (\u2192 nil) for an ambiguous\n // match, so it stays safe. The legacy bf_sort/bf_reduce pass an\n // already-capitalised name, so they hit the exact match above and never\n // reach this fallback.\n fieldVal = v.FieldByNameFunc(func(n string) bool { return strings.EqualFold(n, fieldStr) })\n if !fieldVal.IsValid() {\n return nil\n }\n }\n return fieldVal.Interface()\n}\n\n// fieldAsIndex converts a `getFieldValue` key argument to a slice/array/\n// string element index. Accepts genuine numeric kinds (the loop-index\n// case, `bf_get $arr $i`) directly, and a numeric-looking string (a\n// dynamic key that happens to be digits) via `strconv.Atoi` so a\n// string-typed index used against an array-shaped receiver still\n// resolves rather than silently missing.\nfunc fieldAsIndex(field any) (int, bool) {\n if isIntLike(field) {\n return toInt(field), true\n }\n switch n := field.(type) {\n // Only an INTEGRAL float is an index. JS `arr[1.2]` is a property\n // lookup (undefined), not index 1, so truncating here would diverge.\n case float32:\n if float64(n) != math.Trunc(float64(n)) {\n return 0, false\n }\n return int(n), true\n case float64:\n if n != math.Trunc(n) {\n return 0, false\n }\n return int(n), true\n case string:\n i, err := strconv.Atoi(n)\n if err != nil {\n return 0, false\n }\n return i, true\n default:\n return 0, false\n }\n}\n\n// AsMap normalizes a dynamically-typed prop value into a\n// map[string]interface{} for object-valued context bindings\n// (`lowerProviderMapMemberValue`, go-template-adapter.ts). A caller-side\n// `interface{}` field can legally hold ANY string-keyed map kind \u2014 a Go\n// handler modelling `Record<string, string>` naturally passes\n// map[string]string \u2014 so a bare `.(map[string]interface{})` type assertion\n// would silently drop provided values (#2111 review). Returns nil (never an\n// empty map) when the value is absent \u2014 nil interface, typed-nil map or\n// pointer, or any non-map / non-string-keyed value \u2014 so the generated\n// `?? {}` fallback can distinguish "missing" (fall back) from "present but\n// empty" (use as-is). map[string]interface{} passes through without copying.\nfunc AsMap(v any) map[string]interface{} {\n if v == nil {\n return nil\n }\n if m, ok := v.(map[string]interface{}); ok {\n if m == nil {\n return nil\n }\n return m\n }\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n if rv.IsNil() {\n return nil\n }\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Map || rv.Type().Key().Kind() != reflect.String || rv.IsNil() {\n return nil\n }\n out := make(map[string]interface{}, rv.Len())\n iter := rv.MapRange()\n for iter.Next() {\n out[iter.Key().String()] = iter.Value().Interface()\n }\n return out\n}\n\n// capitalize uppercases the first character of a string.\nfunc capitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToUpper(s[:1]) + s[1:]\n}\n\n// decapitalize lowercases the first character of a string. Used by\n// `getFieldValue`\'s map-receiver fallback when the projected key\n// name is PascalCase but the receiver carries lowercase JS-style\n// keys (the inverse of the `capitalize` lookup).\nfunc decapitalize(s string) string {\n if s == "" {\n return s\n }\n return strings.ToLower(s[:1]) + s[1:]\n}\n\n// Reduce folds an array into a scalar via the arithmetic-fold\n// catalogue (#1448 Tier C). It lowers `Array.prototype.reduce(fn, init)`\n// and `Array.prototype.reduceRight(fn, init)` for the shapes\n// `(acc, x) => acc <op> x` and `(acc, x) => acc <op> x.field`:\n//\n// bf_reduce <items> "<op>" "<keyKind>" "<keyName>" "<type>" "<init>" "<direction>"\n//\n// direction: "left" (reduce) | "right" (reduceRight). Only changes the\n// result for string concatenation; numeric folds commute.\n//\n// op: "+" | "*"\n// keyKind: "self" | "field"\n// keyName: "" when keyKind == "self"; capitalised struct field name\n// (e.g. "Duration") otherwise\n// type: "numeric" | "string"\n// init: the fold\'s start value \u2014 the compiler emits the *decoded*\n// seed, so numeric inits arrive as canonical decimal\n// (`1_000`/`0x10` already normalised to `1000`/`16`) that\n// ParseFloat accepts, and string inits arrive as escape-free\n// contents\n//\n// Numeric folds accumulate as float64; each projected key is read via\n// `toFloat64WithOK`, so numeric *strings* ("5" \u2192 5) parse and\n// non-numeric values fold as 0 \u2014 matching Perl\'s\n// `looks_like_number ? $n : 0` so the two template adapters stay\n// byte-equal. String folds concatenate (toString per projected key,\n// matching the documented `bf->string(undef) === ""` convention). The\n// init seeds the accumulator, so an empty receiver returns the init\n// unchanged \u2014 exactly like JS `reduce(fn, init)`. Anything outside the\n// catalogue refuses at compile time (BF101 from the JSX compiler) and\n// never reaches here.\n//\n// Two documented divergences from the JS / Hono path, both rare and\n// mirroring the `bf_sort` "auto" caveat:\n// - float64 stringification differs for sums whose binary expansion\n// isn\'t exact (e.g. 0.1 + 0.2);\n// - numeric-*string* keys fold numerically here, but JS `+`\n// string-concatenates once an operand is a string, so\n// numeric-string data can render differently under CSR.\n//\n// Genuine numbers \u2014 the common SSR case \u2014 agree across all three.\nfunc Reduce(items any, op, keyKind, keyName, typ, init, direction string) any {\n v := reflect.ValueOf(items)\n isSlice := v.Kind() == reflect.Slice || v.Kind() == reflect.Array\n\n // `direction == "right"` (reduceRight) folds right-to-left. Only\n // observable for string concatenation \u2014 numeric sum / product are\n // commutative, so the order doesn\'t change the result there. Build a\n // start/stop/step triple so both folds share one loop shape.\n start, stop, step := 0, 0, 1\n if isSlice {\n stop = v.Len()\n if direction == "right" {\n start, stop, step = v.Len()-1, -1, -1\n }\n }\n\n if typ == "string" {\n acc := init\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n acc += toString(key)\n }\n }\n return acc\n }\n\n // numeric fold\n acc, _ := strconv.ParseFloat(strings.TrimSpace(init), 64)\n if isSlice {\n for i := start; i != stop; i += step {\n key := projectSortKey(v.Index(i).Interface(), keyKind, keyName)\n // `toFloat64WithOK` parses numeric *strings* ("5" \u2192 5) and\n // returns 0 for non-numeric values \u2014 mirroring Perl\'s\n // `looks_like_number ? $n : 0` so numeric-string data folds\n // byte-equal across adapters (the same rule `bf_sort`\'s\n // "auto" compare uses). Plain `toFloat64` would zero "5".\n n, _ := toFloat64WithOK(key)\n if op == "*" {\n acc *= n\n } else {\n acc += n\n }\n }\n }\n return acc\n}\n\n// =============================================================================\n// HTML/Template Helpers\n// =============================================================================\n\n// Comment returns an HTML comment string for hydration markers.\n// The "bf-" prefix is automatically added.\nfunc Comment(content string) template.HTML {\n return template.HTML("<!--bf-" + content + "-->")\n}\n\n// EscapeCommentKey neutralizes a value for splicing into Comment\'s HTML\n// comment content (#2795 follow-up). Comment itself does no escaping \u2014 fine\n// for every other caller (marker IDs like "cond-start:s0", "loop:l0", ...),\n// which are entirely compiler-generated, but the whole-item-conditional\n// loop\'s "loop-i:<key>" anchor carries a user-controlled key. Standard HTML\n// escaping doesn\'t help inside a comment \u2014 only the literal sequence "-->"\n// terminates it early, and &/</>/"/\' are not special there. The key\'s exact\n// text doesn\'t need to round-trip (the client\'s mapArrayAnchored matches\n// items positionally and by its own JS-computed key, never by re-parsing\n// the anchor Comment.nodeValue), so replacing every "-" with the\n// visually-similar U+2010 is sufficient and needs no decoding.\nfunc EscapeCommentKey(v any) string {\n return strings.ReplaceAll(String(v), "-", "\u2010")\n}\n\n// TextStart returns an HTML comment start marker for reactive text expressions.\n// Format: <!--bf:slotId-->\nfunc TextStart(slotId string) template.HTML {\n return template.HTML("<!--bf:" + slotId + "-->")\n}\n\n// TextEnd returns an HTML comment end marker for reactive text expressions.\n// Format: <!--/-->\nfunc TextEnd() template.HTML {\n return "<!--/-->"\n}\n\n// ScopeComment emits a fragment-rooted scope marker. See spec/compiler.md\n// "Slot identity" for the wire format. Loud-fails on marshal errors\n// (same policy as JSON / BfPropsAttr).\nfunc ScopeComment(props interface{}) (template.HTML, error) {\n scopeID := getStringField(props, "ScopeID")\n hostSegment := ""\n if host := getStringField(props, "BfParent"); host != "" {\n mount := getStringField(props, "BfMount")\n hostSegment = "|h=" + host + "|m=" + mount\n }\n propsJSON := ""\n if getBoolField(props, "BfIsRoot") {\n // Same caller-props-sidecar substitution as BfPropsAttr (#2684) \u2014\n // see that function\'s comment for why this is NOT gated on\n // emptiness.\n payload, hasCallerProps := bfHydrationPayload(props)\n if !hasCallerProps {\n payload = props\n }\n pJSON, err := json.Marshal(payload)\n if err != nil {\n return "", err\n }\n propsJSON = "|" + string(pJSON)\n }\n return template.HTML("<!--bf-scope:" + scopeID + hostSegment + propsJSON + "-->"), nil\n}\n\n// ScopeCommentEnd emits the paired end marker for a fragment-rooted scope\n// (#2289): a fragment root has no single wrapping element to bound the\n// client\'s scope query, so the range leaks onto later siblings without an\n// explicit terminator. Carries only the scope id \u2014 no `|h=`/`|m=`/props\n// segment, unlike ScopeComment \u2014 since the client only needs it to confirm\n// the range closes on the matching scope (getCommentScopeBoundary in\n// packages/client/src/runtime/scope.ts).\nfunc ScopeCommentEnd(props interface{}) template.HTML {\n scopeID := getStringField(props, "ScopeID")\n return template.HTML("<!--bf-/scope:" + scopeID + "-->")\n}\n\n// TemplateFuncMap returns the helpers that need access to the executing\n// template set itself, closed over the *template.Template the component\n// defines are parsed into. Register it alongside FuncMap BEFORE parsing:\n//\n// t := template.New("")\n// t.Funcs(bf.FuncMap()).Funcs(bf.TemplateFuncMap(t))\n// template.Must(t.Parse(src))\n//\n// bf_tmpl executes a named define from the same set and returns its\n// output \u2014 used for the per-call-site children defines the Go adapter\n// emits when JSX children passed to an imported component contain\n// template actions (nested components, dynamic text) and therefore\n// cannot be baked to a static HTML string (#1896). Reentrant execution\n// of an html/template set from inside a FuncMap function is safe: the\n// escape analysis over every define completes before the outer\n// Execute begins evaluating.\nfunc TemplateFuncMap(t *template.Template) template.FuncMap {\n return template.FuncMap{\n "bf_tmpl": func(name string, data interface{}) (template.HTML, error) {\n var buf bytes.Buffer\n if err := t.ExecuteTemplate(&buf, name, data); err != nil {\n return "", err\n }\n return template.HTML(buf.String()), nil\n },\n }\n}\n\n// WithChildren returns a shallow copy of a component Props struct with its\n// Children field replaced by the given pre-rendered fragment (#1896). The\n// props value stays by-value semantics: callers\' originals are untouched.\n// A props type without a Children field passes through unchanged \u2014 the\n// child template then simply has no children to render, matching the\n// pre-#1896 behaviour.\nfunc WithChildren(props interface{}, children template.HTML) (interface{}, error) {\n v := reflect.ValueOf(props)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return props, nil\n }\n field := v.FieldByName("Children")\n if !field.IsValid() {\n return props, nil\n }\n copyPtr := reflect.New(v.Type())\n copyPtr.Elem().Set(v)\n target := copyPtr.Elem().FieldByName("Children")\n switch {\n case target.Kind() == reflect.Interface:\n target.Set(reflect.ValueOf(children))\n case target.Kind() == reflect.String:\n // Covers both `string` and `template.HTML`-typed fields.\n target.SetString(string(children))\n default:\n return props, fmt.Errorf("bf_with_children: unsupported Children field type %s", target.Type())\n }\n return copyPtr.Elem().Interface(), nil\n}\n\n// WithProps returns a shallow copy of a component Props struct with the\n// given fields overridden (#2445): a child component nested inside a\n// COMPOSITE loop row (row root is a plain element, not the child itself) is\n// constructed ONCE outside `{{range}}` \u2014 every row shares that instance for\n// scope/parent/mount identity \u2014 so a prop that depends on the row\n// (`text={row.label}`) has to be applied per row, at template-execution\n// time, the same way WithChildren applies per-row JSX children to that\n// shared instance. `kv` is a flat name/value list ("Text", .Label, "Count",\n// .N, ...). The props value stays by-value semantics: the caller\'s original\n// is untouched. A name with no matching settable field is left alone for\n// that pair \u2014 the prop routes elsewhere (e.g. a rest bag) and the base\n// instance\'s constructor-built value stands, mirroring WithChildren\'s\n// "props type without a Children field" passthrough.\n//\n// This overrides fields on the ALREADY-CONSTRUCTED instance \u2014 it does not\n// re-run New<Child>Props. A field the child derives FROM the overridden prop\n// at construction time (a memo body, or a signal\'s initial value \u2014 the\n// constructor bakes both) would keep whatever the one-shot constructor\n// computed and never update per row; only the directly-overridden field is\n// correct per row. The compiler therefore does not route that case here: a\n// child with any constructor-derived field gets a generated props rebuilder\n// and the call site emits bf_reprops instead, which re-runs the real\n// constructor per row (#2448, see reprops.go). What reaches this helper is\n// the plain-passthrough case, where patching the field IS the whole update.\n//\n// Still exported and still registered: templates generated before #2448 call\n// it, and it remains the cheaper path when nothing is derived.\nfunc WithProps(props interface{}, kv ...interface{}) (interface{}, error) {\n if len(kv)%2 != 0 {\n return nil, fmt.Errorf("bf_with_props: odd number of key/value arguments (%d)", len(kv))\n }\n v := reflect.ValueOf(props)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return props, nil\n }\n copyPtr := reflect.New(v.Type())\n copyPtr.Elem().Set(v)\n for i := 0; i < len(kv); i += 2 {\n name, ok := kv[i].(string)\n if !ok {\n return nil, fmt.Errorf("bf_with_props: field name at position %d must be a string, got %T", i, kv[i])\n }\n target := copyPtr.Elem().FieldByName(name)\n if !target.IsValid() || !target.CanSet() {\n continue\n }\n if err := setStructFieldValue(target, kv[i+1]); err != nil {\n return nil, fmt.Errorf("bf_with_props: field %s: %w", name, err)\n }\n }\n return copyPtr.Elem().Interface(), nil\n}\n\n// setStructFieldValue assigns val into target, a settable struct field\n// obtained via reflect.Value.FieldByName. Mirrors WithChildren\'s\n// Interface/String branches (a String-kind target covers both `string` and\n// `template.HTML` fields, via String() rather than reflect.Convert \u2014 Go\'s\n// int-to-string conversion produces a rune, not a decimal string) and adds\n// the general assignable/convertible fallback for other field kinds\n// (numeric widening, etc.).\nfunc setStructFieldValue(target reflect.Value, val interface{}) error {\n if val == nil {\n target.Set(reflect.Zero(target.Type()))\n return nil\n }\n switch {\n case target.Kind() == reflect.Interface:\n target.Set(reflect.ValueOf(val))\n return nil\n case target.Kind() == reflect.String:\n target.SetString(String(val))\n return nil\n }\n rv := reflect.ValueOf(val)\n switch {\n case rv.Type().AssignableTo(target.Type()):\n target.Set(rv)\n case rv.Type().ConvertibleTo(target.Type()):\n target.Set(rv.Convert(target.Type()))\n default:\n return fmt.Errorf("cannot assign %T to %s", val, target.Type())\n }\n return nil\n}\n\n// PortalHTML parses and executes a template string with the provided data.\n// Used for rendering dynamic portal content where the template string\n// contains Go template expressions (e.g., {{if .Open}}open{{end}}).\n//\n// The template string is parsed fresh each time to support dynamic content.\n// Standard Go template functions (if, range, eq, etc.) are available.\nfunc PortalHTML(data interface{}, tmplStr string) template.HTML {\n // Create a new template with the FuncMap for custom functions\n t, err := template.New("portal").Funcs(FuncMap()).Parse(tmplStr)\n if err != nil {\n // Return error message as HTML comment for debugging\n return template.HTML("<!-- bfPortalHTML error: " + err.Error() + " -->")\n }\n\n var buf bytes.Buffer\n if err := t.Execute(&buf, data); err != nil {\n return template.HTML("<!-- bfPortalHTML exec error: " + err.Error() + " -->")\n }\n\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Portal Collection\n// =============================================================================\n\n// PortalContent represents a single portal\'s content to be rendered at body end.\ntype PortalContent struct {\n ID string // Unique portal ID for hydration matching\n OwnerID string // Owner scope ID for find() support\n Content template.HTML // Portal HTML content\n}\n\n// PortalCollector collects portal content during template rendering.\n// Portal content is rendered at </body> to avoid z-index issues.\ntype PortalCollector struct {\n portals []PortalContent\n counter int\n}\n\n// NewPortalCollector creates a new PortalCollector.\nfunc NewPortalCollector() *PortalCollector {\n return &PortalCollector{\n portals: []PortalContent{},\n counter: 0,\n }\n}\n\n// Add registers portal content to be rendered at body end.\nfunc (pc *PortalCollector) Add(ownerID string, content template.HTML) string {\n pc.counter++\n id := "bf-portal-" + strconv.Itoa(pc.counter)\n pc.portals = append(pc.portals, PortalContent{\n ID: id,\n OwnerID: ownerID,\n Content: content,\n })\n return "" // Return empty string for template use\n}\n\n// Render outputs all collected portals as HTML.\n// Each portal is wrapped in a div with bf-pi (portal ID) and bf-po (portal owner).\nfunc (pc *PortalCollector) Render() template.HTML {\n if pc == nil || len(pc.portals) == 0 {\n return ""\n }\n var buf strings.Builder\n for _, p := range pc.portals {\n buf.WriteString(`<div bf-pi="`)\n buf.WriteString(p.ID)\n buf.WriteString(`" bf-po="`)\n buf.WriteString(p.OwnerID)\n buf.WriteString(`">`)\n buf.WriteString(string(p.Content))\n buf.WriteString("</div>\\n")\n }\n return template.HTML(buf.String())\n}\n\n// =============================================================================\n// Script Collection\n// =============================================================================\n\n// ScriptCollector collects client scripts (and modulepreload hints) with\n// deduplication. It preserves insertion order for deterministic output.\n//\n// Preloads share this same collector/struct rather than a parallel\n// PreloadCollector type: the collector is already threaded to every child\n// component via `setScriptsField`/`setScriptsOnSlice`/`setScriptsOnSingle`\n// (the `Scripts` struct field), so a preload registered inside a child\n// automatically survives to the page-level render through that existing\n// propagation \u2014 no separate child-propagation code needed (see #preload\n// task notes).\ntype ScriptCollector struct {\n scripts map[string]bool\n order []string\n\n preloads map[string]bool\n preloadOrder []string\n}\n\n// NewScriptCollector creates a new ScriptCollector.\nfunc NewScriptCollector() *ScriptCollector {\n return &ScriptCollector{\n scripts: make(map[string]bool),\n order: []string{},\n preloads: make(map[string]bool),\n preloadOrder: []string{},\n }\n}\n\n// Register adds a script source to the collection.\n// Duplicate scripts are ignored (only first registration counts).\nfunc (sc *ScriptCollector) Register(src string) string {\n if sc.scripts[src] {\n return "" // Already registered\n }\n sc.scripts[src] = true\n sc.order = append(sc.order, src)\n return "" // Return empty string for template use\n}\n\n// RegisterPreload adds a `<link rel="modulepreload">` href to the\n// collection, mirroring Register\'s dedup/order semantics exactly. Called\n// from a generated template as a no-output statement\n// (`{{.Scripts.RegisterPreload "URL"}}`), never rendered directly \u2014 the\n// `<link>` tag itself is only ever emitted by BfScripts below, so a preload\n// registration never injects a node into a component\'s own template output.\nfunc (sc *ScriptCollector) RegisterPreload(href string) string {\n if sc.preloads[href] {\n return "" // Already registered\n }\n sc.preloads[href] = true\n sc.preloadOrder = append(sc.preloadOrder, href)\n return "" // Return empty string for template use\n}\n\n// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// Preloads returns all registered preload hrefs in insertion order.\nfunc (sc *ScriptCollector) Preloads() []string {\n return sc.preloadOrder\n}\n\n// BfScripts generates `<link rel="modulepreload">` hints followed by\n// `<script type="module">` tags for everything registered on collector.\n// Preloads are always emitted before scripts (a hint that arrives after the\n// script it describes is useless). Returns HTML safe for embedding in\n// templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\n for _, href := range collector.Preloads() {\n result.WriteString(`<link rel="modulepreload" crossorigin href="`)\n result.WriteString(href)\n result.WriteString(`">`)\n result.WriteString("\\n")\n }\n for _, src := range collector.Scripts() {\n result.WriteString(`<script type="module" src="`)\n result.WriteString(src)\n result.WriteString(`"></script>`)\n result.WriteString("\\n")\n }\n return template.HTML(result.String())\n}\n\n// =============================================================================\n// Component Renderer\n// =============================================================================\n\n// RenderContext contains all data needed to render a component page.\n// The layout function receives this context to build the final HTML.\ntype RenderContext struct {\n // ComponentName is the template name being rendered\n ComponentName string\n\n // Props is the component props (for layout to access if needed)\n Props interface{}\n\n // ComponentHTML is the rendered component template output\n ComponentHTML template.HTML\n\n // Portals contains collected portal content to render at body end\n Portals template.HTML\n\n // Scripts contains the collected JS script tags\n Scripts template.HTML\n\n // Title is the page title (defaults to "{ComponentName} - BarefootJS")\n Title string\n\n // Heading is the page heading. Empty string means no heading.\n Heading string\n\n // Extra holds additional user-defined data for the layout\n Extra map[string]interface{}\n}\n\n// LayoutFunc renders the final HTML page given the render context.\ntype LayoutFunc func(ctx *RenderContext) string\n\n// Renderer renders BarefootJS components with a customizable layout.\ntype Renderer struct {\n templates *template.Template\n layout LayoutFunc\n}\n\n// NewRenderer creates a Renderer with the given templates and layout function.\n//\n// Example usage:\n//\n// renderer := bf.NewRenderer(templates, func(ctx *bf.RenderContext) string {\n// return fmt.Sprintf(`<!DOCTYPE html>\n// <html>\n// <head><title>%s</title></head>\n// <body>%s%s</body>\n// </html>`, ctx.Title, ctx.ComponentHTML, ctx.Scripts)\n// })\nfunc NewRenderer(tmpl *template.Template, layout LayoutFunc) *Renderer {\n return &Renderer{\n templates: tmpl,\n layout: layout,\n }\n}\n\n// RenderOptions configures a single render call.\ntype RenderOptions struct {\n // ComponentName is the template name to render (required)\n ComponentName string\n\n // Props is the component props (must be a pointer to struct with Scripts field)\n Props interface{}\n\n // Title is the page title. If empty, defaults to "{ComponentName} - BarefootJS"\n Title string\n\n // Heading is the page heading. If empty, no heading is shown.\n Heading string\n\n // Extra holds additional data to pass to the layout\n Extra map[string]interface{}\n}\n\n// Render renders a component to a full HTML page using the configured layout.\n// Child component props are automatically detected (any slice field with ScopeID/Scripts).\n// renderTemplateErrorPanel formats a Go template execution error into a\n// fragment of HTML that\'s visible in the browser. The panel is\n// HTML-escaped so a faulty template name (anything from `template:\n// "..."`) can\'t smuggle markup back into the page. Keep the styling\n// inline so the panel surfaces even when the project\'s CSS hasn\'t\n// loaded yet (e.g. the failure aborted before the stylesheet links\n// emitted).\n//\n// Surfaced for the #1442 echo repro: a template referencing\n// `.Todo.Done` (instead of the range dot\'s `.Done`) used to fail\n// silently \u2014 Go\'s html/template aborted mid-stream, the partial body\n// flushed as a 200, and the user saw a truncated list with no console\n// signal. With this panel they get the template name, the error\n// message, and a "what to look at" hint inline.\nfunc renderTemplateErrorPanel(componentName string, err error) string {\n return `<div style="margin:1em 0;padding:1em;border:2px solid #d33;background:#fff5f5;color:#900;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.5"><strong style="display:block;margin-bottom:.5em">Template error in <code>` +\n template.HTMLEscapeString(componentName) +\n `</code></strong><pre style="margin:0;white-space:pre-wrap;word-break:break-word">` +\n template.HTMLEscapeString(err.Error()) +\n `</pre><div style="margin-top:.75em;font-size:12px;opacity:.7">Common cause: a JSX expression referenced a name the adapter could not resolve to a struct field. Open the matching <code>dist/templates/*.tmpl</code> for the unresolved reference, then fix the source component.</div></div>`\n}\n\n// renderComponentInto wires a component\'s props (script/portal collectors,\n// child-slot scope ids + hydration, root marking) against the PROVIDED\n// collectors and returns just the component\'s HTML \u2014 no layout. Both Render\n// (one fresh collector pair per page) and RenderFragment (a shared pair across\n// several islands) funnel through here so their wiring stays identical.\nfunc (r *Renderer) renderComponentInto(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n // Inject the shared collectors into the props.\n setScriptsField(opts.Props, scriptCollector)\n setPortalsField(opts.Props, portalCollector)\n\n // Auto-detect and process child component props (slices)\n childSlices := findChildComponentSlices(opts.Props)\n for _, slice := range childSlices {\n setScopeIDsOnSlice(slice)\n setScriptsOnSlice(slice, scriptCollector)\n setPortalsOnSlice(slice, portalCollector)\n setBoolOnSlice(slice, "BfIsChild", true)\n }\n\n // Auto-detect and process single child component props\n singleChildren := findSingleChildComponents(opts.Props)\n for _, child := range singleChildren {\n setScopeIDOnSingle(child)\n setScriptsOnSingle(child, scriptCollector)\n setPortalsOnSingle(child, portalCollector)\n setBoolField(child, "BfIsChild", true)\n }\n\n // Mark the root component so BfPropsAttr emits bf-p only for it\n setBoolField(opts.Props, "BfIsRoot", true)\n\n // Render the component template.\n //\n // Errors here are NOT silently dropped. The original implementation\n // ignored the return value of `ExecuteTemplate`, which masked a real\n // onboarding failure mode: a template referencing a non-existent\n // field (`.Todo.Done` instead of the range dot\'s `.Done`) caused\n // html/template to abort mid-stream, the partial output got\n // returned, and the HTTP server happily flushed a 200 with a\n // truncated body. No error log, no signal \u2014 the user just saw a\n // blank list (#1442 echo TodoApp repro).\n //\n // Now we capture the error and replace the partial output with a\n // visible inline panel (dev mode) or a fenced error comment\n // (production), so the cause is on-screen and grep-able in logs.\n // Either way the renderer also writes to stderr so structured log\n // aggregators see it.\n var componentBuf strings.Builder\n if err := r.templates.ExecuteTemplate(&componentBuf, opts.ComponentName, opts.Props); err != nil {\n fmt.Fprintf(os.Stderr, "barefoot: template %q failed to render: %v\\n", opts.ComponentName, err)\n // Preserve whatever the template did manage to emit before\n // failing (Go\'s text/template flushes incrementally), but\n // follow it with a clearly-marked error block so the user\n // notices something is wrong instead of seeing a silent\n // truncation.\n componentBuf.WriteString(renderTemplateErrorPanel(opts.ComponentName, err))\n }\n\n return template.HTML(componentBuf.String())\n}\n\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // One script + portal collector pair for the whole page.\n scriptCollector := NewScriptCollector()\n portalCollector := NewPortalCollector()\n\n componentHTML := r.renderComponentInto(opts, scriptCollector, portalCollector)\n\n // Determine title (default: "{ComponentName} - BarefootJS")\n title := opts.Title\n if title == "" {\n title = opts.ComponentName + " - BarefootJS"\n }\n\n // Heading (empty means no heading)\n heading := opts.Heading\n\n // Build render context\n ctx := &RenderContext{\n ComponentName: opts.ComponentName,\n Props: opts.Props,\n ComponentHTML: componentHTML,\n Portals: portalCollector.Render(),\n Scripts: BfScripts(scriptCollector),\n Title: title,\n Heading: heading,\n Extra: opts.Extra,\n }\n\n return r.layout(ctx)\n}\n\n// RenderFragment renders a single island subtree into the caller-provided\n// script and portal collectors and returns just its HTML \u2014 no page layout.\n//\n// It exists for hand-authored "region shell" pages (the `@barefootjs/router`\n// showcase): a layout that places several independent islands \u2014 e.g. a header\n// ThemeToggle, an `<aside bf-region>` Sidebar, and a `<PageShell>` wrapping the\n// route content \u2014 must collect ALL their scripts and portals into ONE place so\n// the runtime (`barefoot.js`) and each island\'s client JS are emitted exactly\n// once and share a single reactive instance. Render each island with the same\n// collectors, splice the returned HTML into the shell, then emit\n// `BfScripts(sc)` and `pc.Render()` once at the end of the document.\n//\n// Each fragment is treated as its own root (it emits `bf-p` like any top-level\n// island); nested children declared in its props are wired as children, exactly\n// as in Render.\nfunc (r *Renderer) RenderFragment(opts RenderOptions, scriptCollector *ScriptCollector, portalCollector *PortalCollector) template.HTML {\n return r.renderComponentInto(opts, scriptCollector, portalCollector)\n}\n\n// setScriptsField sets the Scripts field on a struct using reflection.\nfunc setScriptsField(v interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// setPortalsField sets the Portals field on a struct using reflection.\nfunc setPortalsField(v interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return\n }\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n}\n\n// getStringField extracts a string field from a struct using reflection.\nfunc setBoolField(v interface{}, fieldName string, val bool) {\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Ptr {\n rv = rv.Elem()\n }\n if rv.Kind() != reflect.Struct {\n return\n }\n field := rv.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n}\n\nfunc getBoolField(v interface{}, fieldName string) bool {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return false\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.Bool {\n return false\n }\n return field.Bool()\n}\n\nfunc getStringField(v interface{}, fieldName string) string {\n val := reflect.ValueOf(v)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return ""\n }\n field := val.FieldByName(fieldName)\n if !field.IsValid() || field.Kind() != reflect.String {\n return ""\n }\n return field.String()\n}\n\n// scopeIDChars is the alphabet for auto-generated ScopeID suffixes. It\n// mirrors the `randomID` helper the go-template adapter emits into the\n// generated New<Component>Props constructors so runtime-assigned and\n// constructor-assigned ids are indistinguishable.\nconst scopeIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"\n\n// randomScopeSuffix returns a random lowercase-alphanumeric string of\n// length n. math/rand (auto-seeded since Go 1.20) is sufficient here: the\n// suffix only needs to be unique enough to keep a page\'s bf-s scope ids\n// from colliding, not cryptographically unpredictable.\nfunc randomScopeSuffix(n int) string {\n b := make([]byte, n)\n for i := range b {\n b[i] = scopeIDChars[rand.Intn(len(scopeIDChars))]\n }\n return string(b)\n}\n\n// scopeIDPrefix derives the human-readable ScopeID prefix from a child\n// component\'s type, e.g. `TodoItemProps` \u2192 `TodoItem`. Matches the\n// `"<Component>_" + randomID(6)` shape the generated constructors use.\nfunc scopeIDPrefix(t reflect.Type) string {\n for t.Kind() == reflect.Ptr {\n t = t.Elem()\n }\n return strings.TrimSuffix(t.Name(), "Props")\n}\n\n// assignScopeID fills a child component\'s ScopeID with a generated id when\n// the caller left it empty, so application code doesn\'t have to mint scope\n// ids by hand (the parent\'s New<Component>Props constructor does the same\n// for components built through it). A non-empty ScopeID is left untouched,\n// so callers can still pin a stable id when they need one.\nfunc assignScopeID(structVal reflect.Value, prefix string) {\n field := structVal.FieldByName("ScopeID")\n if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.String {\n return\n }\n if field.String() != "" {\n return\n }\n id := randomScopeSuffix(6)\n if prefix != "" {\n id = prefix + "_" + id\n }\n field.SetString(id)\n}\n\n// setScopeIDsOnSlice assigns a generated ScopeID to every child in a slice\n// whose ScopeID is empty.\nfunc setScopeIDsOnSlice(slice interface{}) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n prefix := scopeIDPrefix(v.Type().Elem())\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n assignScopeID(item, prefix)\n }\n }\n}\n\n// setScopeIDOnSingle assigns a generated ScopeID to a single child\n// component when its ScopeID is empty.\nfunc setScopeIDOnSingle(child interface{}) {\n v := reflect.ValueOf(child)\n if v.Kind() == reflect.Ptr {\n v = v.Elem()\n }\n if v.Kind() != reflect.Struct {\n return\n }\n assignScopeID(v, scopeIDPrefix(v.Type()))\n}\n\n// findChildComponentSlices finds slice fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findChildComponentSlices(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n if field.Kind() != reflect.Slice || field.Len() == 0 {\n continue\n }\n\n elem := field.Index(0)\n if elem.Kind() == reflect.Ptr {\n elem = elem.Elem()\n }\n if elem.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := elem.FieldByName("ScopeID").IsValid()\n hasScripts := elem.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSlice sets Scripts on all items in a slice.\nfunc setScriptsOnSlice(slice interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// setBoolOnSlice sets a bool field on all items in a slice.\nfunc setBoolOnSlice(slice interface{}, fieldName string, val bool) {\n v := reflect.ValueOf(slice)\n if v.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName(fieldName)\n if field.IsValid() && field.CanSet() && field.Kind() == reflect.Bool {\n field.SetBool(val)\n }\n }\n }\n}\n\n// setPortalsOnSlice sets Portals on all items in a slice.\nfunc setPortalsOnSlice(slice interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(slice)\n if val.Kind() != reflect.Slice {\n return\n }\n for i := 0; i < val.Len(); i++ {\n item := val.Index(i)\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() == reflect.Struct {\n field := item.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n }\n}\n\n// findSingleChildComponents finds single struct fields containing child component props.\n// Child props are identified by having ScopeID and Scripts fields.\nfunc findSingleChildComponents(props interface{}) []interface{} {\n var result []interface{}\n\n val := reflect.ValueOf(props)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() != reflect.Struct {\n return result\n }\n\n for i := 0; i < val.NumField(); i++ {\n field := val.Field(i)\n\n // Handle pointer to struct\n if field.Kind() == reflect.Ptr {\n if field.IsNil() {\n continue\n }\n field = field.Elem()\n }\n\n // Skip non-struct fields (slices handled by findChildComponentSlices)\n if field.Kind() != reflect.Struct {\n continue\n }\n\n hasScopeID := field.FieldByName("ScopeID").IsValid()\n hasScripts := field.FieldByName("Scripts").IsValid()\n\n if hasScopeID && hasScripts {\n result = append(result, field.Addr().Interface())\n }\n }\n\n return result\n}\n\n// setScriptsOnSingle sets Scripts on a single struct child component.\nfunc setScriptsOnSingle(child interface{}, collector *ScriptCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Scripts")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// setPortalsOnSingle sets Portals on a single struct child component.\nfunc setPortalsOnSingle(child interface{}, collector *PortalCollector) {\n val := reflect.ValueOf(child)\n if val.Kind() == reflect.Ptr {\n val = val.Elem()\n }\n if val.Kind() == reflect.Struct {\n field := val.FieldByName("Portals")\n if field.IsValid() && field.CanSet() {\n field.Set(reflect.ValueOf(collector))\n }\n }\n}\n\n// Nullish implements JS `??` for template use (`bf_nullish`, #2248): returns\n// fallback iff v is nil (untyped nil or a nil pointer/map/slice boxed in the\n// interface), otherwise v \u2014 so present-but-falsy `""`/`0`/`false` are KEPT,\n// unlike the truthiness-based template `or`.\nfunc Nullish(v, fallback any) any {\n if v == nil {\n return fallback\n }\n rv := reflect.ValueOf(v)\n switch rv.Kind() {\n case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Interface, reflect.Func, reflect.Chan:\n if rv.IsNil() {\n return fallback\n }\n }\n return v\n}\n\n// ToInt exposes the runtime\'s numeric coercion for generated constructors\n// (#2248): a nillable-lowered numeric prop arrives as `interface{}`, and an\n// untyped Go literal (`Size: 3`) boxes as int even when the prop is\n// float64-shaped \u2014 a direct type assertion would panic where JS accepts the\n// number. Non-numeric values coerce to 0, matching the helpers\' behaviour.\nfunc ToInt(v any) int { return toInt(v) }\n\n// ToFloat64 is ToInt\'s float64 counterpart \u2014 see ToInt.\nfunc ToFloat64(v any) float64 { return toFloat64(v) }\n\n// =============================================================================\n// Internal Helpers\n// =============================================================================\n\nfunc toFloat64(v any) float64 {\n switch n := v.(type) {\n case int:\n return float64(n)\n case int8:\n return float64(n)\n case int16:\n return float64(n)\n case int32:\n return float64(n)\n case int64:\n return float64(n)\n case uint:\n return float64(n)\n case uint8:\n return float64(n)\n case uint16:\n return float64(n)\n case uint32:\n return float64(n)\n case uint64:\n return float64(n)\n case float32:\n return float64(n)\n case float64:\n return n\n default:\n return 0\n }\n}\n\nfunc toInt(v any) int {\n switch n := v.(type) {\n case int:\n return n\n case int8:\n return int(n)\n case int16:\n return int(n)\n case int32:\n return int(n)\n case int64:\n return int(n)\n case uint:\n return int(n)\n case uint8:\n return int(n)\n case uint16:\n return int(n)\n case uint32:\n return int(n)\n case uint64:\n return int(n)\n case float32:\n return int(n)\n case float64:\n return int(n)\n default:\n return 0\n }\n}\n\nfunc isIntLike(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n return true\n default:\n return false\n }\n}\n\nfunc toString(v any) string {\n switch s := v.(type) {\n case string:\n return s\n case int:\n return strconv.Itoa(s)\n case int64:\n return strconv.FormatInt(s, 10)\n case float64:\n return strconv.FormatFloat(s, \'f\', -1, 64)\n case bool:\n return strconv.FormatBool(s)\n default:\n rv := reflect.ValueOf(v)\n if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {\n // JS `Array.prototype.toString` is `this.join(\',\')`, applied\n // recursively \u2014 a nested array element stringifies the same\n // way rather than via Go\'s `%v`. Reached via `Join`/`ConcatStr`\n // on an element that is itself an array (e.g. `.flat(0)`\'s\n // shallow copy joined afterwards, #2262).\n parts := make([]string, rv.Len())\n for i := 0; i < rv.Len(); i++ {\n parts[i] = toString(rv.Index(i).Interface())\n }\n return strings.Join(parts, ",")\n }\n return ""\n }\n}\n\n// =============================================================================\n// searchParams() \u2014 request-scoped environment signal (router v0.5, #1922)\n// =============================================================================\n\n// SearchParams is the SSR view of the request query string behind the\n// reactive searchParams() environment signal. The route handler builds it\n// from the request URL and assigns it to the component\'s SearchParams input\n// field; the generated template reads it via `.SearchParams.Get "key"`.\n//\n// The zero value is an empty query (url.Values.Get tolerates a nil map), so a\n// render with no request query \u2014 e.g. the adapter conformance harness, which\n// issues no query string \u2014 resolves every key to "", which the template\'s\n// `or`/`??` fallback turns into the author\'s default.\ntype SearchParams struct {\n values url.Values\n}\n\n// NewSearchParams parses a raw query string (with or without a leading "?")\n// into a SearchParams. A malformed query yields an empty set rather than an\n// error, mirroring the browser\'s URLSearchParams, which never throws on junk.\n//\n// Typical handler use (net/http):\n//\n// in := MyComponentInput{SearchParams: bf.NewSearchParams(r.URL.RawQuery)}\nfunc NewSearchParams(raw string) SearchParams {\n raw = strings.TrimPrefix(raw, "?")\n values, err := url.ParseQuery(raw)\n if err != nil {\n values = url.Values{}\n }\n return SearchParams{values: values}\n}\n\n// Get returns the first value associated with key, or "" when the key is\n// absent. This mirrors url.Values.Get, which also returns "" for a\n// present-but-empty value (`?sort=`). Safe on the zero value (nil map).\n//\n// This is not byte-for-byte URLSearchParams.get under the template\'s `??`\n// lowering. JS distinguishes absent (`null`) from present-but-empty (`""`):\n// `null ?? d` yields the default, but `"" ?? d` keeps the empty string. The\n// Go adapter lowers `??` to the `or` builtin \u2014 Go templates have no\n// null-coalescing operator \u2014 so here BOTH an absent key and a present-but-\n// empty value fall back to the author\'s default. The conformance fixture only\n// exercises the absent-key default, where the two runtimes agree; the\n// empty-string divergence is the same general `?? \u2192 or` limitation that\n// applies to any `x ?? default` the Go adapter lowers.\nfunc (s SearchParams) Get(key string) string {\n return s.values.Get(key)\n}\n';
32063
32251
  evalGoSource = 'package bf\n\nimport (\n "encoding/json"\n "errors"\n "math"\n "reflect"\n "regexp"\n "sort"\n "strconv"\n "strings"\n "unicode/utf8"\n)\n\n// =============================================================================\n// Lightweight ParsedExpr evaluator (issue #2018)\n// =============================================================================\n//\n// Templates cannot carry a lambda in expression position, which is why the\n// adapter historically special-cased higher-order callbacks (reduce / sort /\n// map / filter / find) into fixed shapes (bf_sort\'s comparator catalogue,\n// bf_reduce\'s +/* fold). This evaluator replaces that ad-hoc list: a callback\n// BODY is carried as a pure `ParsedExpr` subtree (the same structured IR the\n// compiler already produces) and evaluated here against an environment\n// (`{acc, item, \u2026captured free vars}`).\n//\n// Scope: higher-order callback bodies only. Ordinary expressions stay lowered\n// to template-native syntax \u2014 this is NOT a general expression engine.\n//\n// The accepted pure subset and its semantics (evaluation order, coercion,\n// equality, allowed operators / builtins) are documented in spec/compiler.md\n// ("ParsedExpr Evaluator Semantics") and pinned isomorphically by the golden\n// vectors in packages/adapter-tests/vectors/eval-vectors.json (shared\n// with the Perl evaluator). The coercion rules below are the literal JS rules\n// (ToNumber / ToString / ToBoolean) \u2014 deliberately NOT the divergent\n// bf->string / bf_reduce helper conventions \u2014 so the contract is unambiguous\n// and the backends stay byte-isomorphic.\n//\n// String semantics operate on Unicode code points / UTF-8 bytes: `.length`\n// counts code points and relational `<`/`>` compares byte order. This equals\n// the JS reference (UTF-16 code units) across the BMP \u2014 the range template\n// data uses \u2014 and matches the Perl evaluator exactly (the primary "same input\n// \u2192 same output" contract is between the two SSR backends). Astral-plane\n// characters (where JS counts a surrogate pair as length 2 and orders by\n// surrogate code units) are a documented divergence region, alongside the\n// already-documented non-ASCII relational / localeCompare carve-outs\n// (spec/compiler.md). Both backends stay equal; only the JS reference differs,\n// and no corpus vector exercises the astral range.\n\n// EvalExpr evaluates a pure ParsedExpr (carried as its JSON encoding) against\n// env. The result is a value in the JSON domain: a number (Go int or float64 \u2014\n// both are the single JS number type; e.g. `.length` returns int, arithmetic\n// returns float64), string, bool, nil, []any, or map[string]any. A malformed\n// tree yields nil.\nfunc EvalExpr(exprJSON string, env map[string]any) any {\n var node any\n if err := json.Unmarshal([]byte(exprJSON), &node); err != nil {\n return nil\n }\n return EvalNode(node, env)\n}\n\n// EvalNode evaluates an already-decoded ParsedExpr node (a map[string]any with\n// a "kind" discriminator) against env. Exported so callers that already hold a\n// decoded tree (e.g. the golden-vector harness) skip a re-marshal.\nfunc EvalNode(node any, env map[string]any) any {\n n, ok := node.(map[string]any)\n if !ok {\n return nil\n }\n kind, _ := n["kind"].(string)\n switch kind {\n case "literal":\n return n["value"]\n\n case "identifier":\n name, _ := n["name"].(string)\n return env[name]\n\n case "binary":\n op, _ := n["op"].(string)\n return evalBinary(op, EvalNode(n["left"], env), EvalNode(n["right"], env))\n\n case "unary":\n op, _ := n["op"].(string)\n return evalUnary(op, EvalNode(n["argument"], env))\n\n case "logical":\n op, _ := n["op"].(string)\n left := EvalNode(n["left"], env)\n switch op {\n case "&&":\n if !evalTruthy(left) {\n return left\n }\n return EvalNode(n["right"], env)\n case "||":\n if evalTruthy(left) {\n return left\n }\n return EvalNode(n["right"], env)\n case "??":\n if left == nil {\n return EvalNode(n["right"], env)\n }\n return left\n }\n return nil\n\n case "conditional":\n if evalTruthy(EvalNode(n["test"], env)) {\n return EvalNode(n["consequent"], env)\n }\n return EvalNode(n["alternate"], env)\n\n case "member":\n prop, _ := n["property"].(string)\n return evalReadProperty(EvalNode(n["object"], env), prop)\n\n case "index-access":\n return evalReadIndex(EvalNode(n["object"], env), EvalNode(n["index"], env))\n\n case "call":\n // A nested `.map(cb)` / `.filter(cb)` callback call (#2094): syntactically\n // a `call` whose callee is `<recv>.map`/`<recv>.filter` and whose first\n // argument is an `arrow` \u2014 the SAME shape `asCallbackMethodCall`\n // recognizes at compile time, and the shape the `eval-vectors.json`\n // golden corpus itself carries (it stores the genuine `ParsedExpr`, not\n // a bespoke wrapper). Checked BEFORE the builtin-callee gate below,\n // since `<recv>.map` would otherwise resolve to a non-builtin member\n // callee and refuse.\n if method, objNode, arrowNode, ok := evalArrayCallbackCall(n); ok {\n return evalArrayCallback(method, objNode, arrowNode, env)\n }\n name := evalBuiltinName(n["callee"])\n if name == "" {\n return nil\n }\n rawArgs, _ := n["args"].([]any)\n args := make([]any, len(rawArgs))\n for i, a := range rawArgs {\n args[i] = EvalNode(a, env)\n }\n return evalCallBuiltin(name, args)\n\n case "template-literal":\n parts, _ := n["parts"].([]any)\n var sb strings.Builder\n for _, p := range parts {\n pm, _ := p.(map[string]any)\n if t, _ := pm["type"].(string); t == "string" {\n s, _ := pm["value"].(string)\n sb.WriteString(s)\n } else {\n sb.WriteString(evalToString(EvalNode(pm["expr"], env)))\n }\n }\n return sb.String()\n\n case "array-literal":\n elems, _ := n["elements"].([]any)\n out := make([]any, len(elems))\n for i, e := range elems {\n out[i] = EvalNode(e, env)\n }\n return out\n\n case "object-literal":\n // Each entry carries its own "kind" ("prop" | "spread", #2696 Step\n // 2) \u2014 the SAME encoding `toEvalNode`\'s object-literal case emits\n // AND the raw ParsedExpr shape the eval-vectors.json golden corpus\n // carries, so this one decoder serves both sources. A "spread"\n // evaluates its "expr" and shallow-merges the result\'s own keys (a\n // non-map result \u2014 including a null/undefined JS spread source \u2014 is\n // a no-op, via Merge\'s same skip-non-map tolerance); a "prop" sets\n // one key. Later entries win on a shared key, in source order,\n // matching JS object-spread exactly.\n props, _ := n["properties"].([]any)\n out := make(map[string]any, len(props))\n for _, p := range props {\n pm, _ := p.(map[string]any)\n if kind, _ := pm["kind"].(string); kind == "spread" {\n spread, _ := EvalNode(pm["expr"], env).(map[string]any)\n for k, v := range spread {\n out[k] = v\n }\n continue\n }\n key, _ := pm["key"].(string)\n out[key] = EvalNode(pm["value"], env)\n }\n return out\n\n case "array-method":\n // `.includes(x)` / `.join(sep?)` are the `array-method` shapes the\n // evaluator executes (the JS reference\'s `evaluate` "array-method" arm,\n // eval-reference.ts). A nested `.map`/`.filter` is NOT an\n // `array-method` node \u2014 it reaches the `call` case above (it carries\n // an `arrow` callback, not a plain `args` list). Every other\n // array/string method (`slice`, `flat`, \u2026) is refused upstream\n // (BF101) and never reaches here.\n method, _ := n["method"].(string)\n rawArgs, _ := n["args"].([]any)\n if method == "includes" && len(rawArgs) == 1 {\n return evalIncludes(EvalNode(n["object"], env), EvalNode(rawArgs[0], env))\n }\n if method == "join" && len(rawArgs) <= 1 {\n sep := ","\n if len(rawArgs) == 1 {\n sep = evalToString(EvalNode(rawArgs[0], env))\n }\n return evalJoin(EvalNode(n["object"], env), sep)\n }\n return nil\n }\n // arrow-fn / higher-order / unsupported: a callback body containing these\n // is refused upstream (BF101); never reached here.\n return nil\n}\n\n// evalArrayCallbackCall reports whether the decoded `call` node `n` is a\n// nested `.map(cb)` / `.filter(cb)` callback call (#2094): its callee is a\n// non-computed member `<recv>.map`/`<recv>.filter` and its first argument is\n// an `arrow` node. Returns the method name, the (still-encoded) receiver\n// object node, and the (still-encoded) arrow node.\nfunc evalArrayCallbackCall(n map[string]any) (method string, object any, arrow map[string]any, ok bool) {\n callee, _ := n["callee"].(map[string]any)\n if callee == nil || callee["kind"] != "member" {\n return "", nil, nil, false\n }\n if computed, _ := callee["computed"].(bool); computed {\n return "", nil, nil, false\n }\n prop, _ := callee["property"].(string)\n if prop != "map" && prop != "filter" {\n return "", nil, nil, false\n }\n rawArgs, _ := n["args"].([]any)\n if len(rawArgs) == 0 {\n return "", nil, nil, false\n }\n arrowNode, _ := rawArgs[0].(map[string]any)\n if arrowNode == nil || arrowNode["kind"] != "arrow" {\n return "", nil, nil, false\n }\n return prop, callee["object"], arrowNode, true\n}\n\n// evalArrayCallback executes a nested `.map`/`.filter` callback call: evaluates\n// the receiver, then evaluates the arrow body per element in a CHILD env that\n// binds the arrow\'s first param to the element and (when the arrow declares a\n// second param) the second to the integer index \u2014 both 1- and 2-param arrows\n// are supported. `map` keeps one result per element (order-preserving);\n// `filter` keeps the elements whose body evaluates truthy. A non-array\n// receiver degrades to nil (unreachable for a body the compiler validated,\n// since the receiver of a nested `.map`/`.filter` is itself gated upstream).\nfunc evalArrayCallback(method string, objectNode any, arrowNode map[string]any, env map[string]any) any {\n arr := toAnySlice(EvalNode(objectNode, env))\n if arr == nil {\n return nil\n }\n rawParams, _ := arrowNode["params"].([]any)\n params := make([]string, len(rawParams))\n for i, p := range rawParams {\n params[i], _ = p.(string)\n }\n body := arrowNode["body"]\n callCb := func(item any, index int) any {\n inner := make(map[string]any, len(env)+2)\n for k, v := range env {\n inner[k] = v\n }\n if len(params) > 0 {\n inner[params[0]] = item\n }\n if len(params) > 1 {\n inner[params[1]] = index\n }\n return EvalNode(body, inner)\n }\n if method == "map" {\n out := make([]any, len(arr))\n for i, item := range arr {\n out[i] = callCb(item, i)\n }\n return out\n }\n out := []any{}\n for i, item := range arr {\n if evalTruthy(callCb(item, i)) {\n out = append(out, item)\n }\n }\n return out\n}\n\n// evalJoin implements `.join(sep)`: elements ToString\'d and joined; a\n// null/undefined element ToStrings to the empty string (matching JS\n// `Array.prototype.join`, which skips null/undefined rather than rendering\n// the literal string "null"/"undefined"). A non-array receiver degrades to\n// the empty string (unreachable for a validated body).\nfunc evalJoin(obj any, sep string) string {\n arr := toAnySlice(obj)\n if arr == nil {\n return ""\n }\n parts := make([]string, len(arr))\n for i, el := range arr {\n if el == nil {\n parts[i] = ""\n continue\n }\n parts[i] = evalToString(el)\n }\n return strings.Join(parts, sep)\n}\n\n// ---------------------------------------------------------------------------\n// JS coercion primitives (ToNumber / ToString / ToBoolean), pinned so the\n// evaluator matches the JS reference. These are JS-faithful and intentionally\n// distinct from the bf->string / Number helpers, which diverge (null \u2192 "",\n// null/"" \u2192 NaN) for SSR-survival reasons that do not apply to the evaluator.\n// ---------------------------------------------------------------------------\n\n// jsDecimalNumberRe matches the JS StringToNumber decimal numeric literal\n// grammar (ASCII digits only): optional sign, then integer/fraction digits,\n// then an optional exponent. It deliberately excludes underscore digit\n// separators, radix prefixes (0x/0o/0b), and hex-float forms \u2014 none of which\n// are valid JS decimal numeric literals.\nvar jsDecimalNumberRe = regexp.MustCompile(`^[+-]?(?:[0-9]+\\.?[0-9]*|\\.[0-9]+)(?:[eE][+-]?[0-9]+)?$`)\n\nfunc evalToNumber(v any) float64 {\n switch x := v.(type) {\n case nil:\n return 0\n case bool:\n if x {\n return 1\n }\n return 0\n case string:\n t := strings.TrimSpace(x)\n if t == "" {\n return 0\n }\n // Exact JS Infinity spellings (case-sensitive, no other aliases like\n // "infinity"/"inf" are valid JS numeric strings).\n switch t {\n case "Infinity", "+Infinity":\n return math.Inf(1)\n case "-Infinity":\n return math.Inf(-1)\n }\n // Decimal / exponent numeric strings parse JS-faithfully, including\n // overflow: strconv.ParseFloat rejects underscores, hex-floats, and\n // non-canonical "inf"/"nan" spellings via the anchored decimal-grammar\n // gate below, so those correctly yield NaN. The radix-prefixed forms\n // JS Number() also accepts ("0x10" / "0o17" / "0b101") are a\n // documented divergence region: they fail the decimal grammar (a\n // leading "0x"/"0o"/"0b" is not a valid decimal literal) and yield\n // NaN here, as they do in the Perl evaluator (looks_like_number is\n // false for them), so Go==Perl while differing from the JS\n // reference. Template data carries JSON numbers, not radix-string\n // literals, so this never arises in practice.\n if !jsDecimalNumberRe.MatchString(t) {\n return math.NaN()\n }\n f, err := strconv.ParseFloat(t, 64)\n if err == nil {\n return f\n }\n if errors.Is(err, strconv.ErrRange) {\n // ParseFloat still returns the correctly-signed \xB1Inf (or a\n // subnormal) as its best-effort value on overflow/underflow;\n // JS Number() on an overflowing decimal literal yields \xB1Infinity\n // (e.g. "1e1000" -> +Infinity), so surface that value as-is.\n return f\n }\n return math.NaN()\n default:\n if evalIsNumeric(v) {\n return toFloat64(v)\n }\n return math.NaN()\n }\n}\n\n// evalIsNumeric reports whether v is one of the Go numeric types (all of\n// which are the single JS "number" type to the evaluator).\nfunc evalIsNumeric(v any) bool {\n switch v.(type) {\n case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:\n return true\n }\n return false\n}\n\nfunc evalToString(v any) string {\n if v == nil {\n return "null"\n }\n // JS spells the non-finite doubles "Infinity" / "-Infinity" / "NaN"; the\n // runtime String() helper (fmt %v) would render "+Inf" / "-Inf" / "NaN",\n // so the non-finite cases are pinned here to stay JS-faithful (and match\n // the Perl evaluator\'s _to_string). Strings, bools and nil are JS-faithful\n // through String() (nil is already handled above as "null").\n //\n // Finite-number formatting is a documented divergence region. Go\'s fmt is\n // shortest-round-trip (it matches JS\'s *digits*, e.g. 0.1+0.2 \u2192\n // "0.30000000000000004"), but its exponent threshold/padding differ from\n // JS Number::toString for very large / very small magnitudes (Go renders\n // 1e6 as "1e+06" where JS keeps "1000000", and "1e-07" vs JS "1e-7").\n // Perl\'s `%.15g` instead diverges on *precision* (the pinned helper-vector\n // "0.3" case). A fully JS-faithful Number::toString is not reimplemented\n // here because Perl has no shortest-round-trip formatter, so the three\n // could never all agree; the common integer / short-decimal range \u2014 what\n // arithmetic over template data produces \u2014 renders identically across all\n // three. Only the \xB10 sign is normalised below, since it is cheap and the\n // realisticly-reachable case.\n if f, ok := v.(float64); ok {\n if math.IsNaN(f) {\n return "NaN"\n }\n if math.IsInf(f, 1) {\n return "Infinity"\n }\n if math.IsInf(f, -1) {\n return "-Infinity"\n }\n // JS `String(-0)` is "0", but fmt %v renders Go\'s negative zero as\n // "-0". `f == 0` matches both \xB10, normalising to JS\'s spelling. (A\n // unary `-` on a zero operand is the way the evaluator can produce -0.)\n if f == 0 {\n return "0"\n }\n }\n // Non-primitive operands (arrays / objects) in ToString position are\n // outside the evaluator subset \u2014 the JS reference refuses them, and the\n // compiler gates such a callback body with BF101 before it ever reaches\n // the runtime. This evaluator runs already-validated bodies, so it does\n // not re-reject here; String() (fmt %v) is a best-effort fallback for that\n // unreachable path, never exercised by the in-subset corpus.\n return String(v)\n}\n\nfunc evalTruthy(v any) bool {\n return isTruthy(v)\n}\n\n// ---------------------------------------------------------------------------\n// Operators\n// ---------------------------------------------------------------------------\n\nfunc evalBinary(op string, l, r any) any {\n switch op {\n case "+":\n // JS `+`: string concatenation once either operand is a string,\n // numeric addition otherwise.\n if _, lok := l.(string); lok {\n return evalToString(l) + evalToString(r)\n }\n if _, rok := r.(string); rok {\n return evalToString(l) + evalToString(r)\n }\n return evalToNumber(l) + evalToNumber(r)\n case "-":\n return evalToNumber(l) - evalToNumber(r)\n case "*":\n return evalToNumber(l) * evalToNumber(r)\n case "/":\n return evalToNumber(l) / evalToNumber(r)\n case "%":\n return math.Mod(evalToNumber(l), evalToNumber(r))\n case "<", "<=", ">", ">=":\n return evalRelational(op, l, r)\n case "===":\n return evalStrictEq(l, r)\n case "!==":\n return !evalStrictEq(l, r)\n }\n // Loose equality / bitwise / shift are out of the subset.\n return nil\n}\n\nfunc evalRelational(op string, l, r any) bool {\n // JS Abstract Relational Comparison: both strings \u2192 compare by code\n // unit; otherwise coerce both to numbers (a NaN operand makes every\n // comparison false).\n var c int\n ls, lok := l.(string)\n rs, rok := r.(string)\n if lok && rok {\n if ls < rs {\n c = -1\n } else if ls > rs {\n c = 1\n }\n } else {\n ln := evalToNumber(l)\n rn := evalToNumber(r)\n if math.IsNaN(ln) || math.IsNaN(rn) {\n return false\n }\n if ln < rn {\n c = -1\n } else if ln > rn {\n c = 1\n }\n }\n switch op {\n case "<":\n return c < 0\n case "<=":\n return c <= 0\n case ">":\n return c > 0\n case ">=":\n return c >= 0\n }\n return false\n}\n\nfunc evalStrictEq(l, r any) bool {\n // Strict `===`: equal JS type and value, no coercion. All numeric Go\n // types are the single JS "number" type, so int 2 === float64 2.\n lnum := evalIsNumeric(l)\n rnum := evalIsNumeric(r)\n if lnum && rnum {\n lf, rf := toFloat64(l), toFloat64(r)\n if math.IsNaN(lf) || math.IsNaN(rf) {\n return false\n }\n return lf == rf\n }\n if lnum != rnum {\n return false\n }\n switch lv := l.(type) {\n case nil:\n return r == nil\n case string:\n rv, ok := r.(string)\n return ok && rv == lv\n case bool:\n rv, ok := r.(bool)\n return ok && rv == lv\n }\n // Non-primitive operands (arrays / objects) are outside the subset: the JS\n // reference refuses `===` on them and the compiler gates such a body with\n // BF101 upstream, so this is unreachable for an in-subset corpus. The\n // runtime trusts that gate rather than re-validating, returning false\n // here (it does not attempt JS reference identity, which templates can\'t\n // model anyway).\n return false\n}\n\n// evalSameValueZero implements `Array.prototype.includes`\'s membership\n// comparison: `===` except `NaN` equals itself (JS\'s SameValueZero). Reuses\n// evalStrictEq \u2014 the one divergence, both operands NaN, is checked first;\n// every other pair (including "one NaN, one not") falls through to the same\n// equality `evalBinary`\'s `===` uses, so the two operators stay in lockstep.\nfunc evalSameValueZero(a, b any) bool {\n if evalIsNumeric(a) && evalIsNumeric(b) {\n af, bf := toFloat64(a), toFloat64(b)\n if math.IsNaN(af) && math.IsNaN(bf) {\n return true\n }\n }\n return evalStrictEq(a, b)\n}\n\n// evalIncludes implements `.includes(needle)`, shared between\n// `Array.prototype.includes` (SameValueZero membership over a slice/array\n// receiver) and `String.prototype.includes` (substring search), mirroring\n// the receiver-type dispatch the SSR template lowering already does\n// (`bf_includes`). Any other receiver type is not a JS `.includes` target;\n// this degrades to false rather than panicking (there is no receiver here\n// for which JS itself would throw), matching the JS reference\n// (eval-reference.ts `includes`).\nfunc evalIncludes(obj, needle any) bool {\n if arr := toAnySlice(obj); arr != nil {\n for _, el := range arr {\n if evalSameValueZero(el, needle) {\n return true\n }\n }\n return false\n }\n if s, ok := obj.(string); ok {\n return strings.Contains(s, evalToString(needle))\n }\n return false\n}\n\nfunc evalUnary(op string, v any) any {\n switch op {\n case "!":\n return !evalTruthy(v)\n case "-":\n return -evalToNumber(v)\n case "+":\n return evalToNumber(v)\n }\n return nil\n}\n\n// ---------------------------------------------------------------------------\n// Built-in calls (the deterministic allowlist). Locale-sensitive builtins\n// (localeCompare) are deliberately excluded to keep the backends isomorphic.\n// ---------------------------------------------------------------------------\n\n// evalBuiltinName resolves a `call` callee to its builtin name (e.g.\n// "Math.max"), or "" if the callee is not an allowlisted builtin reference.\nfunc evalBuiltinName(callee any) string {\n cm, ok := callee.(map[string]any)\n if !ok {\n return ""\n }\n switch cm["kind"] {\n case "identifier":\n name, _ := cm["name"].(string)\n return name\n case "member":\n if computed, _ := cm["computed"].(bool); computed {\n return ""\n }\n obj, _ := cm["object"].(map[string]any)\n if obj == nil || obj["kind"] != "identifier" {\n return ""\n }\n objName, _ := obj["name"].(string)\n prop, _ := cm["property"].(string)\n return objName + "." + prop\n }\n return ""\n}\n\n// evalMathRound rounds a half toward +Infinity (JS Math.round: 2.5\u21923,\n// -2.5\u2192-2), matching the existing `round` helper rather than Go\'s math.Round.\nfunc evalMathRound(n float64) float64 {\n // floor(n+0.5) yields +0 for x in [-0.5, -0], where JS Math.round returns\n // -0. That sign is only observable through a subsequent division\n // (`1 / Math.round(-0.5)` is -Infinity in JS, +Infinity here) \u2014 through\n // ToString both \xB10 render "0". It is left as +0 deliberately: the two SSR\n // backends must stay equal, and Perl can\'t reproduce the -0 divisor sign\n // without fragile, version-dependent zero handling (its native `/` even\n // dies on a zero divisor). So Math.round\'s -0 is a JS-reference-only\n // divergence region, like the astral-plane / radix-string carve-outs.\n return math.Floor(n + 0.5)\n}\n\nfunc evalCallBuiltin(name string, args []any) any {\n switch name {\n case "Math.max":\n if len(args) == 0 {\n return math.Inf(-1)\n }\n m := evalToNumber(args[0])\n for _, a := range args[1:] {\n m = math.Max(m, evalToNumber(a))\n }\n return m\n case "Math.min":\n if len(args) == 0 {\n return math.Inf(1)\n }\n m := evalToNumber(args[0])\n for _, a := range args[1:] {\n m = math.Min(m, evalToNumber(a))\n }\n return m\n case "Math.abs":\n return math.Abs(evalToNumber(arg0(args)))\n case "Math.floor":\n return math.Floor(evalToNumber(arg0(args)))\n case "Math.ceil":\n return math.Ceil(evalToNumber(arg0(args)))\n case "Math.round":\n return evalMathRound(evalToNumber(arg0(args)))\n case "String":\n return evalToString(arg0(args))\n case "Number":\n return evalToNumber(arg0(args))\n case "Boolean":\n return evalTruthy(arg0(args))\n }\n // Any other callee is outside the subset (refused upstream).\n return nil\n}\n\nfunc arg0(args []any) any {\n if len(args) == 0 {\n return nil\n }\n return args[0]\n}\n\n// ---------------------------------------------------------------------------\n// Member / index access\n// ---------------------------------------------------------------------------\n\nfunc evalReadProperty(obj any, key string) any {\n switch o := obj.(type) {\n case string:\n if key == "length" {\n // Code-point count (== JS UTF-16 length across the BMP, == Perl\n // `length`). RuneCountInString avoids the []rune allocation since\n // this can run inside comparator / reducer evaluation.\n return utf8.RuneCountInString(o)\n }\n return nil\n case []any:\n if key == "length" {\n return len(o)\n }\n return nil\n case map[string]any:\n // Case-variant lookup: a callback body reads the raw JS property\n // (`t.duration`), but test data / Go-keyed maps may carry PascalCase\n // keys (`{"Duration": \u2026}`). Reuse the field reader the sort / reduce\n // helpers use \u2014 it resolves `duration` \u2192 `Duration` and reads a\n // genuinely missing key as null (the backends\' single absent value).\n return getFieldValue(o, key)\n case nil:\n return nil\n default:\n // Real template data (Go structs): reuse the field reader the sort /\n // reduce helpers use, which handles case-variant keys.\n return getFieldValue(obj, key)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Evaluator-driven higher-order folds (the generalization of bf_reduce /\n// bf_sort onto the evaluator)\n//\n// These prove the evaluator subsumes the special-cased callback catalogue:\n// the callback BODY is carried as a pure ParsedExpr (JSON) and evaluated per\n// element against an environment, so the op restriction (bf_reduce\'s +/*),\n// the acc-canonical form, and the comparator pattern restriction (bf_sort)\n// all disappear \u2014 any pure reducer / comparator body works. They are the\n// runtime half of the integration; the compiler-side emit migration (carrying\n// callback bodies as ParsedExpr) and the byte-equal divergence decision for\n// the string-`localeCompare` sort path (won\'t-fix for byte-equal SSR, see\n// spec/compiler.md Known limitations) are the remaining follow-up.\n// ---------------------------------------------------------------------------\n\n// toAnySlice copies a reflect-iterable receiver into a fresh []any, returning\n// nil for a non-slice/array (matching the bf_sort / bf_reduce nil-tolerance).\nfunc toAnySlice(items any) []any {\n v := reflect.ValueOf(items)\n // A nil interface yields an invalid Value (Kind() == Invalid, not a\n // panic), so the Slice/Array guard below already tolerates nil; the\n // explicit IsValid check just documents the nil-tolerance intent.\n if !v.IsValid() || (v.Kind() != reflect.Slice && v.Kind() != reflect.Array) {\n return nil\n }\n out := make([]any, v.Len())\n for i := range out {\n out[i] = v.Index(i).Interface()\n }\n return out\n}\n\n// FoldEval folds items into a value via the ParsedExpr evaluator. The reducer\n// body is a pure ParsedExpr (JSON) evaluated against `{accName: acc, itemName:\n// item}` plus the captured free vars in `baseEnv` for each element; `init`\n// seeds the accumulator and `direction` is "left" (reduce) or "right"\n// (reduceRight). This is the evaluator-based generalization of bf_reduce \u2014 any\n// reducer body, not just the `+`/`*` arithmetic catalogue, and `acc` may\n// appear anywhere in the body. `baseEnv` may be nil when the body captures no\n// outer references; the accName / itemName keys shadow any same-named base key.\nfunc FoldEval(items any, bodyJSON, accName, itemName string, init any, direction string, baseEnv map[string]any) any {\n var body any\n if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {\n return init\n }\n arr := toAnySlice(items)\n if direction == "right" {\n for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {\n arr[i], arr[j] = arr[j], arr[i]\n }\n }\n acc := init\n // Seed the env from the captured free vars once; acc / item are\n // overwritten each iteration (constant base keys carry through).\n env := make(map[string]any, len(baseEnv)+2)\n for k, v := range baseEnv {\n env[k] = v\n }\n for _, item := range arr {\n env[accName] = acc\n env[itemName] = item\n acc = EvalNode(body, env)\n }\n return acc\n}\n\n// SortEval returns a new stable-sorted slice ordered by a ParsedExpr\n// comparator body (JSON) evaluated against `{paramA: a, paramB: b}` plus the\n// captured free vars in `baseEnv` to a number (negative / zero / positive,\n// like a JS comparator). This is the evaluator-based generalization of bf_sort\n// \u2014 any comparator body, not just the subtraction / relational-ternary\n// catalogue. `baseEnv` may be nil. Non-mutating.\nfunc SortEval(items any, cmpJSON, paramA, paramB string, baseEnv map[string]any) []any {\n arr := toAnySlice(items)\n if arr == nil {\n return nil\n }\n var cmp any\n if err := json.Unmarshal([]byte(cmpJSON), &cmp); err != nil {\n return arr\n }\n // One env seeded from the captured free vars; the two operand keys are\n // overwritten per comparison (the comparator runs synchronously).\n env := make(map[string]any, len(baseEnv)+2)\n for k, v := range baseEnv {\n env[k] = v\n }\n sort.SliceStable(arr, func(i, j int) bool {\n env[paramA] = arr[i]\n env[paramB] = arr[j]\n return evalToNumber(EvalNode(cmp, env)) < 0\n })\n return arr\n}\n\n// ---------------------------------------------------------------------------\n// Evaluator-driven higher-order predicates (#2018, P2) \u2014 the generalization\n// of bf_filter / bf_find / bf_find_index / bf_every / bf_some onto the\n// evaluator. The predicate BODY travels as a pure ParsedExpr (JSON) and is\n// evaluated per element against `{param: item}` plus the captured free vars in\n// `baseEnv`, lifting the field-equality / truthiness restriction of the\n// special-cased helpers to any pure predicate. `baseEnv` may be nil.\n// ---------------------------------------------------------------------------\n\n// decodeEvalBody unmarshals a serialized ParsedExpr body; ok is false on bad\n// JSON (only reachable by a corrupt emit \u2014 the adapter always emits valid JSON).\nfunc decodeEvalBody(bodyJSON string) (any, bool) {\n var body any\n if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil {\n return nil, false\n }\n return body, true\n}\n\n// seedPredEnv copies the captured free vars into a fresh env with room for the\n// single predicate param, which the callers overwrite per element.\nfunc seedPredEnv(baseEnv map[string]any) map[string]any {\n env := make(map[string]any, len(baseEnv)+1)\n for k, v := range baseEnv {\n env[k] = v\n }\n return env\n}\n\n// FilterEval returns a new slice of the elements for which the predicate body\n// evaluates truthy \u2014 the evaluator generalization of bf_filter. Returns a\n// non-nil empty slice when nothing matches (so a downstream `range` / `bf_join`\n// sees a real slice); returns nil only on a bad body.\nfunc FilterEval(items any, predJSON, param string, baseEnv map[string]any) []any {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return nil\n }\n env := seedPredEnv(baseEnv)\n out := []any{}\n for _, item := range toAnySlice(items) {\n env[param] = item\n if evalTruthy(EvalNode(pred, env)) {\n out = append(out, item)\n }\n }\n return out\n}\n\n// EveryEval reports whether every element satisfies the predicate (vacuously\n// true for an empty receiver, like JS) \u2014 the generalization of bf_every.\nfunc EveryEval(items any, predJSON, param string, baseEnv map[string]any) bool {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return false\n }\n env := seedPredEnv(baseEnv)\n for _, item := range toAnySlice(items) {\n env[param] = item\n if !evalTruthy(EvalNode(pred, env)) {\n return false\n }\n }\n return true\n}\n\n// SomeEval reports whether any element satisfies the predicate (false for an\n// empty receiver, like JS) \u2014 the generalization of bf_some.\nfunc SomeEval(items any, predJSON, param string, baseEnv map[string]any) bool {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return false\n }\n env := seedPredEnv(baseEnv)\n for _, item := range toAnySlice(items) {\n env[param] = item\n if evalTruthy(EvalNode(pred, env)) {\n return true\n }\n }\n return false\n}\n\n// FindEval returns the first element satisfying the predicate, or nil when none\n// does \u2014 the generalization of bf_find. `forward` false searches from the end\n// (findLast).\nfunc FindEval(items any, predJSON, param string, forward bool, baseEnv map[string]any) any {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return nil\n }\n env := seedPredEnv(baseEnv)\n arr := toAnySlice(items)\n for n := range arr {\n i := n\n if !forward {\n i = len(arr) - 1 - n\n }\n env[param] = arr[i]\n if evalTruthy(EvalNode(pred, env)) {\n return arr[i]\n }\n }\n return nil\n}\n\n// FindIndexEval returns the index of the first element satisfying the predicate,\n// or -1 when none does \u2014 the generalization of bf_find_index. `forward` false\n// searches from the end (findLastIndex).\nfunc FindIndexEval(items any, predJSON, param string, forward bool, baseEnv map[string]any) int {\n pred, ok := decodeEvalBody(predJSON)\n if !ok {\n return -1\n }\n env := seedPredEnv(baseEnv)\n arr := toAnySlice(items)\n for n := range arr {\n i := n\n if !forward {\n i = len(arr) - 1 - n\n }\n env[param] = arr[i]\n if evalTruthy(EvalNode(pred, env)) {\n return i\n }\n }\n return -1\n}\n\n// FlatMapEval projects each element through the projection body (a pure\n// ParsedExpr JSON evaluated against `{param: item}` + baseEnv) and flattens the\n// results one level \u2014 the evaluator generalization of bf_flat_map /\n// bf_flat_map_tuple. A projection that yields a slice contributes its elements;\n// any other value contributes itself (matching JS `.flatMap`, where a non-array\n// return is kept as a single element). Returns a non-nil empty slice on a bad\n// body so a downstream `range` / `bf_join` sees a real slice.\nfunc FlatMapEval(items any, projJSON, param string, baseEnv map[string]any) []any {\n proj, ok := decodeEvalBody(projJSON)\n if !ok {\n return []any{}\n }\n env := seedPredEnv(baseEnv)\n out := []any{}\n for _, item := range toAnySlice(items) {\n env[param] = item\n v := EvalNode(proj, env)\n // Flatten any slice/array kind one level, not just []any: real Go\n // template data projects a field like `i.tags` to a typed slice\n // (`[]string` / `[]int`), which `toAnySlice` normalizes to []any. A\n // non-slice value (string / number / struct / nil) contributes itself,\n // matching JS `.flatMap` (a non-array return is kept as one element).\n if sub := toAnySlice(v); sub != nil {\n out = append(out, sub...)\n } else {\n out = append(out, v)\n }\n }\n return out\n}\n\n// MapEval projects each element through the projection body (a pure ParsedExpr\n// JSON evaluated against `{param: item}` + baseEnv), keeping one result per\n// element \u2014 the value-producing `.map(cb)` lowering (#2073). Unlike\n// FlatMapEval there is no flatten: a projection yielding a slice contributes\n// that slice as a single element, matching JS `.map`. Returns a non-nil empty\n// slice on a bad body so a downstream `range` / `bf_join` sees a real slice.\nfunc MapEval(items any, projJSON, param string, baseEnv map[string]any) []any {\n proj, ok := decodeEvalBody(projJSON)\n if !ok {\n return []any{}\n }\n env := seedPredEnv(baseEnv)\n out := []any{}\n for _, item := range toAnySlice(items) {\n env[param] = item\n out = append(out, EvalNode(proj, env))\n }\n return out\n}\n\n// Env builds the captured-free-var environment for FoldEval / SortEval from a\n// flat key, value, key, value, \u2026 argument list \u2014 the adapter emits\n// `bf_env "k1" v1 "k2" v2 \u2026` for the free variables a callback body references\n// beyond its own params. An odd trailing key with no value is ignored; with no\n// pairs it returns an empty (non-nil) map, the no-capture case. A non-string\n// key (only reachable by a malformed template call, never by the adapter\'s\n// quoted-literal emit) is skipped rather than collapsed into an `env[""]` slot.\nfunc Env(pairs ...any) map[string]any {\n env := make(map[string]any, len(pairs)/2)\n for i := 0; i+1 < len(pairs); i += 2 {\n key, ok := pairs[i].(string)\n if !ok {\n continue\n }\n env[key] = pairs[i+1]\n }\n return env\n}\n\n// Merge shallow-merges any number of maps, later arguments winning on a\n// shared key \u2014 `bf_map`\'s sibling for a POPULATED object-literal SPREAD\n// (#2696 Step 2, `{ ...t, editing: false }`), registered as `bf_merge` in\n// FuncMap (bf.go). A non-`map[string]any` argument (nil included) is\n// SKIPPED rather than panicking \u2014 this is the template-action-emit path\'s\n// null-safety counterpart to `toEvalNode`\'s object-literal spread handling\n// (a null/undefined JS spread source is a no-op, and this also tolerates any\n// other non-object value the same way, never panicking on stray input).\n// With zero maps to merge it returns a non-nil empty map (never nil), same\n// as `Env` with no pairs.\nfunc Merge(maps ...any) map[string]any {\n out := make(map[string]any, len(maps))\n for _, m := range maps {\n mm, ok := m.(map[string]any)\n if !ok {\n continue\n }\n for k, v := range mm {\n out[k] = v\n }\n }\n return out\n}\n\nfunc evalReadIndex(obj any, index any) any {\n switch o := obj.(type) {\n case []any:\n f := evalToNumber(index)\n i := int(f)\n if float64(i) != f || i < 0 || i >= len(o) {\n return nil\n }\n return o[i]\n case map[string]any:\n return o[evalToString(index)]\n case nil:\n return nil\n default:\n return getFieldValue(obj, evalToString(index))\n }\n}\n';
32064
32252
  repropsGoSource = '// Per-row child props reconstruction (#2448).\n//\n// `bf_with_props` (#2445) overrides fields on a child\'s already-constructed\n// shared instance by reflection. That is correct for a plain passthrough prop\n// and WRONG for anything the child\'s constructor DERIVES from it: a\n// `createMemo` body and a `createSignal` initial value are both baked into the\n// struct once by `New<Child>Props`, and reflection cannot re-run that.\n//\n// This file is the fix. Instead of patching fields, the parent asks the child\n// to REBUILD its props: reconstruct the constructor Input from the base\n// instance, apply the row\'s overrides, re-run the real constructor. Every\n// derived field recomputes because the real Go code runs again.\n//\n// The constructor cannot be called from a template directly \u2014 `html/template`\n// has no expression language and can only call FuncMap entries. So the\n// compiler emits, per component, a closure that does the rebuild in generated\n// Go (typed field assignments, no reflection), and registers it here from the\n// generated package\'s `init()`. `FuncMap()` gains exactly ONE fixed entry,\n// `bf_reprops`, so `t.Funcs(bf.FuncMap())` keeps working unchanged.\n//\n// The registry is consulted at template EXECUTE time, never at `Funcs()` time.\n// That is load-bearing, not incidental: Go initializes a package\'s variables\n// BEFORE its `init()` functions, so an app that builds its template set in a\n// package-level var \u2014\n//\n// var tmpl = template.Must(template.New("").Funcs(bf.FuncMap()).ParseGlob(...))\n//\n// \u2014 calls `FuncMap()` while the registry is still empty. Merging the\n// constructors into `FuncMap()`\'s return value would fail that app at parse\n// time with `function "bf_new_Badge" not defined`. Looking them up behind one\n// fixed entry, at execute time, makes the ordering irrelevant.\npackage bf\n\nimport (\n "fmt"\n "reflect"\n "sync"\n)\n\n// RepropsFunc rebuilds a child component\'s props from a base instance plus a\n// flat name/value override list (`"Text", .Label, "N", .N, \u2026`), by re-running\n// the component\'s generated constructor.\n//\n// Names in `kv` are the Go FIELD names the PARENT computed from the JSX\n// attribute (`n=` \u2192 `"N"`). A generated implementation maps those onto its own\n// constructor Input, which is what makes an aliased destructure\n// (`{ n: count }`, whose field is `Count`) land correctly \u2014 the mapping lives\n// in generated code that knows both sides.\n//\n// Identity fields (ScopeID / BfParent / BfMount) MUST be carried over from the\n// base rather than re-derived: `New<Child>Props` mints a random ScopeID when\n// given an empty one, so a naive re-run would give every row its own scope and\n// break hydration. Fields that live only on Props and never on Input (Scripts,\n// BfIsChild, BfDataKey) must be carried over for the same reason.\ntype RepropsFunc func(base interface{}, kv ...interface{}) (interface{}, error)\n\nvar (\n repropsMu sync.RWMutex\n repropsRegistry = map[string]RepropsFunc{}\n)\n\n// RegisterReprops registers a component\'s props rebuilder under its component\n// name. Called from the generated package\'s `init()`; re-registering the same\n// name replaces the previous entry, so a rebuilt components file in a\n// long-lived dev process wins.\nfunc RegisterReprops(name string, fn RepropsFunc) {\n repropsMu.Lock()\n defer repropsMu.Unlock()\n repropsRegistry[name] = fn\n}\n\n// Reprops is the `bf_reprops` FuncMap entry: rebuild `base` with the row\'s\n// overrides applied, by re-running the named component\'s constructor.\n//\n// {{template "Badge" (bf_reprops "Badge" $.BadgeSlot0 "Text" .Label "N" .N)}}\n//\n// An unregistered name is an error rather than a silent passthrough: the\n// compiler only emits this call for a component whose rebuilder it also\n// emitted, so a missing entry means the generated package was not linked in,\n// and falling back to the stale shared instance would reintroduce exactly the\n// silently-wrong output this exists to prevent.\nfunc Reprops(name string, base interface{}, kv ...interface{}) (interface{}, error) {\n if len(kv)%2 != 0 {\n return nil, fmt.Errorf("bf_reprops: odd number of key/value arguments (%d) for %q", len(kv), name)\n }\n // Field names are validated HERE, not in each generated rebuilder, so the\n // generated `name, _ := kv[i].(string)` is safe by construction. Rejecting\n // them matches `bf_with_props`: a non-string name would otherwise degrade\n // to `""`, match no case, and silently drop the override \u2014 the same class\n // of silent misrender this whole path exists to remove.\n for i := 0; i < len(kv); i += 2 {\n if _, ok := kv[i].(string); !ok {\n return nil, fmt.Errorf(\n "bf_reprops: %s field name at position %d must be a string, got %T", name, i, kv[i])\n }\n }\n repropsMu.RLock()\n fn, ok := repropsRegistry[name]\n repropsMu.RUnlock()\n if !ok {\n return nil, fmt.Errorf(\n "bf_reprops: no props rebuilder registered for %q \u2014 is the generated components package linked into this binary?",\n name,\n )\n }\n return fn(base, kv...)\n}\n\n// RepropsTypeError is the error a generated rebuilder returns when handed a\n// value that is not its own props struct. Kept here so the generated code\n// stays a fixed shape instead of formatting its own message.\nfunc RepropsTypeError(name string, base interface{}) error {\n return fmt.Errorf("bf_reprops: the %s rebuilder got %T, not its own props struct", name, base)\n}\n\n// RepropsUnknownFieldError is what a generated rebuilder returns for a field\n// name it has no case for.\n//\n// Unlike `bf_with_props`, whose unknown-field passthrough is load-bearing (a\n// prop routed into a rest bag has no named field to set), a rebuilder is only\n// emitted for components with NO rest bag and no spread slot, and the compiler\n// emits a case for every prop the parent can override. So an unknown name here\n// is a compiler gap, and dropping it silently would leave the override\n// unapplied \u2014 the failure mode this path replaced.\nfunc RepropsUnknownFieldError(component, field string) error {\n return fmt.Errorf("bf_reprops: %s has no overridable field %q", component, field)\n}\n\n// RepropsAssign writes `val` into `*target`, which a generated rebuilder passes\n// as a pointer to one field of its constructor Input (`&in.N`).\n//\n// This is the one place the rebuild uses reflection, and it does so on purpose:\n// it delegates to the SAME `setStructFieldValue` that `bf_with_props` uses, so\n// a prop that already assigned correctly under the old helper assigns\n// identically under this one. Re-deriving the conversion rules per Go type in\n// the generator would be more code and would drift from that behaviour.\n//\n// `field` names the field for the error message; `component` names the owner.\nfunc RepropsAssign(component, field string, target interface{}, val interface{}) error {\n p := reflect.ValueOf(target)\n if p.Kind() != reflect.Ptr || p.IsNil() {\n return fmt.Errorf("bf_reprops: %s.%s: target must be a non-nil pointer, got %T", component, field, target)\n }\n if err := setStructFieldValue(p.Elem(), val); err != nil {\n return fmt.Errorf("bf_reprops: %s.%s: %w", component, field, err)\n }\n return nil\n}\n\n// RepropsRegistered reports whether a rebuilder is registered for `name`.\n// For tests and diagnostics.\nfunc RepropsRegistered(name string) bool {\n repropsMu.RLock()\n defer repropsMu.RUnlock()\n _, ok := repropsRegistry[name]\n return ok\n}\n';
32065
32253
  streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers