@barefootjs/cli 0.18.4 → 0.18.7
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 +1210 -347
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -448,6 +448,9 @@ function convertNode(node, raw) {
|
|
|
448
448
|
if (callee.property === "trim") {
|
|
449
449
|
return { kind: "array-method", method: "trim", object: callee.object, args: args2 };
|
|
450
450
|
}
|
|
451
|
+
if (callee.property === "trimStart" || callee.property === "trimEnd") {
|
|
452
|
+
return { kind: "array-method", method: callee.property, object: callee.object, args: args2 };
|
|
453
|
+
}
|
|
451
454
|
if (callee.property === "toFixed") {
|
|
452
455
|
return { kind: "array-method", method: "toFixed", object: callee.object, args: args2 };
|
|
453
456
|
}
|
|
@@ -473,24 +476,25 @@ function convertNode(node, raw) {
|
|
|
473
476
|
}
|
|
474
477
|
return { kind: "array-method", method: callee.property, object: callee.object, args: args2 };
|
|
475
478
|
}
|
|
476
|
-
if (callee.property === "replace") {
|
|
479
|
+
if (callee.property === "replace" || callee.property === "replaceAll") {
|
|
480
|
+
const method2 = callee.property;
|
|
477
481
|
if (args2.length < 2) {
|
|
478
482
|
return {
|
|
479
483
|
kind: "unsupported",
|
|
480
484
|
raw,
|
|
481
|
-
reason:
|
|
485
|
+
reason: `\`.${method2}(${args2.length === 0 ? "" : "pattern"})\` needs both a pattern and a replacement \u2014 JS coerces the missing argument to the string "undefined", a degenerate result. Pass both arguments, or pre-compute the value before the template.`
|
|
482
486
|
};
|
|
483
487
|
}
|
|
484
488
|
const patternNode = node.arguments[0];
|
|
485
489
|
if (patternNode && ts.isRegularExpressionLiteral(patternNode)) {
|
|
486
|
-
return { kind: "array-method", method:
|
|
490
|
+
return { kind: "array-method", method: method2, object: callee.object, args: args2 };
|
|
487
491
|
}
|
|
488
492
|
const badArg = args2[0].kind === "unsupported" || args2[0].kind === "object-literal" ? args2[0] : args2[1].kind === "unsupported" || args2[1].kind === "object-literal" ? args2[1] : void 0;
|
|
489
493
|
if (badArg) {
|
|
490
494
|
const reason2 = badArg.kind === "unsupported" ? badArg.reason : "Unsupported syntax: ObjectLiteralExpression";
|
|
491
495
|
return { kind: "unsupported", raw, reason: reason2 };
|
|
492
496
|
}
|
|
493
|
-
return { kind: "array-method", method:
|
|
497
|
+
return { kind: "array-method", method: method2, object: callee.object, args: args2 };
|
|
494
498
|
}
|
|
495
499
|
if (callee.property === "repeat") {
|
|
496
500
|
return { kind: "array-method", method: "repeat", object: callee.object, args: args2 };
|
|
@@ -524,7 +528,7 @@ function convertNode(node, raw) {
|
|
|
524
528
|
if (ts.isPropertyAccessExpression(node)) {
|
|
525
529
|
const object = convertNode(node.expression, raw);
|
|
526
530
|
const property = node.name.text;
|
|
527
|
-
return { kind: "member", object, property, computed: false };
|
|
531
|
+
return { kind: "member", object, property, computed: false, optional: !!node.questionDotToken };
|
|
528
532
|
}
|
|
529
533
|
if (ts.isElementAccessExpression(node)) {
|
|
530
534
|
const object = convertNode(node.expression, raw);
|
|
@@ -533,10 +537,10 @@ function convertNode(node, raw) {
|
|
|
533
537
|
return { kind: "unsupported", raw, reason: "Element access with no index expression" };
|
|
534
538
|
}
|
|
535
539
|
if (ts.isNumericLiteral(argNode)) {
|
|
536
|
-
return { kind: "member", object, property: argNode.text, computed: true };
|
|
540
|
+
return { kind: "member", object, property: argNode.text, computed: true, optional: !!node.questionDotToken };
|
|
537
541
|
}
|
|
538
542
|
if (ts.isStringLiteral(argNode)) {
|
|
539
|
-
return { kind: "member", object, property: argNode.text, computed: true };
|
|
543
|
+
return { kind: "member", object, property: argNode.text, computed: true, optional: !!node.questionDotToken };
|
|
540
544
|
}
|
|
541
545
|
const index = convertNode(argNode, raw);
|
|
542
546
|
if (index.kind === "unsupported" || index.kind === "object-literal") return index;
|
|
@@ -1051,7 +1055,7 @@ function substituteDestructuredFields(expr, fieldMap, syntheticParam, restName)
|
|
|
1051
1055
|
if (entry === void 0) return e;
|
|
1052
1056
|
let node = { kind: "identifier", name: syntheticParam };
|
|
1053
1057
|
for (const segment of entry.path) {
|
|
1054
|
-
node = { kind: "member", object: node, property: segment, computed: false };
|
|
1058
|
+
node = { kind: "member", object: node, property: segment, computed: false, optional: false };
|
|
1055
1059
|
}
|
|
1056
1060
|
if (entry.defaultExpr) {
|
|
1057
1061
|
return { kind: "logical", op: "??", left: node, right: entry.defaultExpr };
|
|
@@ -1066,10 +1070,11 @@ function substituteDestructuredFields(expr, fieldMap, syntheticParam, restName)
|
|
|
1066
1070
|
kind: "member",
|
|
1067
1071
|
object: { kind: "identifier", name: syntheticParam },
|
|
1068
1072
|
property: e.property,
|
|
1069
|
-
computed: false
|
|
1073
|
+
computed: false,
|
|
1074
|
+
optional: e.optional
|
|
1070
1075
|
};
|
|
1071
1076
|
}
|
|
1072
|
-
return { kind: "member", object: walk(e.object), property: e.property, computed: e.computed };
|
|
1077
|
+
return { kind: "member", object: walk(e.object), property: e.property, computed: e.computed, optional: e.optional };
|
|
1073
1078
|
case "index-access":
|
|
1074
1079
|
return { kind: "index-access", object: walk(e.object), index: walk(e.index) };
|
|
1075
1080
|
case "binary":
|
|
@@ -1192,10 +1197,10 @@ function checkSupport(expr) {
|
|
|
1192
1197
|
return { supported: true, level: "L2" };
|
|
1193
1198
|
}
|
|
1194
1199
|
case "array-method": {
|
|
1195
|
-
if (expr.method === "replace" && expr.args[0]?.kind === "regex") {
|
|
1200
|
+
if ((expr.method === "replace" || expr.method === "replaceAll") && expr.args[0]?.kind === "regex") {
|
|
1196
1201
|
return {
|
|
1197
1202
|
supported: false,
|
|
1198
|
-
reason:
|
|
1203
|
+
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 */`
|
|
1199
1204
|
};
|
|
1200
1205
|
}
|
|
1201
1206
|
const objSupport = checkSupport(expr.object);
|
|
@@ -1548,7 +1553,7 @@ function inlineBinding(expr, name2, value2) {
|
|
|
1548
1553
|
case "call":
|
|
1549
1554
|
return { kind: "call", callee: walk(e.callee, enclosing), args: e.args.map((a) => walk(a, enclosing)) };
|
|
1550
1555
|
case "member":
|
|
1551
|
-
return { kind: "member", object: walk(e.object, enclosing), property: e.property, computed: e.computed };
|
|
1556
|
+
return { kind: "member", object: walk(e.object, enclosing), property: e.property, computed: e.computed, optional: e.optional };
|
|
1552
1557
|
case "index-access":
|
|
1553
1558
|
return { kind: "index-access", object: walk(e.object, enclosing), index: walk(e.index, enclosing) };
|
|
1554
1559
|
case "binary":
|
|
@@ -1795,7 +1800,7 @@ function materializeGetterCalls(expr, names) {
|
|
|
1795
1800
|
alternate: rw(expr.alternate)
|
|
1796
1801
|
};
|
|
1797
1802
|
case "member":
|
|
1798
|
-
return { kind: "member", object: rw(expr.object), property: expr.property, computed: expr.computed };
|
|
1803
|
+
return { kind: "member", object: rw(expr.object), property: expr.property, computed: expr.computed, optional: expr.optional };
|
|
1799
1804
|
case "index-access":
|
|
1800
1805
|
return { kind: "index-access", object: rw(expr.object), index: rw(expr.index) };
|
|
1801
1806
|
case "template-literal":
|
|
@@ -2173,18 +2178,17 @@ var init_expression_parser = __esm({
|
|
|
2173
2178
|
// `startsWith` / `endsWith` are no longer here — both lower via the
|
|
2174
2179
|
// `array-method` IR + `bf_starts_with` / `bf_ends_with` (Go) and
|
|
2175
2180
|
// `bf->starts_with` / `bf->ends_with` (Mojo). See #1448 Tier B.
|
|
2176
|
-
// `replace`
|
|
2177
|
-
// the `array-method` IR + `bf_replace`
|
|
2178
|
-
//
|
|
2179
|
-
//
|
|
2180
|
-
//
|
|
2181
|
+
// `replace` / `replaceAll` are no longer here — the string-pattern
|
|
2182
|
+
// form of each lowers via the `array-method` IR + `bf_replace` /
|
|
2183
|
+
// `bf_replace_all` (Go) / `bf->replace` / `bf->replace_all` (Mojo);
|
|
2184
|
+
// the regex-pattern form of EITHER is refused at the parse arm below
|
|
2185
|
+
// (it would need the per-adapter regex-flavour decision).
|
|
2181
2186
|
// `repeat` is no longer here — `String.prototype.repeat(n)` lowers via
|
|
2182
2187
|
// the `array-method` IR + `bf_repeat` (Go) / `bf->repeat` (Mojo).
|
|
2183
2188
|
// See #1448 Tier B.
|
|
2184
2189
|
// `padStart` / `padEnd` are no longer here — both lower via the
|
|
2185
2190
|
// `array-method` IR + `bf_pad_start` / `bf_pad_end` (Go) and
|
|
2186
2191
|
// `bf->pad_start` / `bf->pad_end` (Mojo). See #1448 Tier B.
|
|
2187
|
-
"replaceAll",
|
|
2188
2192
|
"charAt",
|
|
2189
2193
|
"charCodeAt",
|
|
2190
2194
|
"codePointAt",
|
|
@@ -2607,12 +2611,89 @@ var init_dom_prop = __esm({
|
|
|
2607
2611
|
}
|
|
2608
2612
|
});
|
|
2609
2613
|
|
|
2614
|
+
// ../shared/src/html-entities.ts
|
|
2615
|
+
function decodeEntities(text) {
|
|
2616
|
+
return text.replace(/&(#[xX]?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, body2) => {
|
|
2617
|
+
if (body2[0] === "#") {
|
|
2618
|
+
const isHex = body2[1] === "x" || body2[1] === "X";
|
|
2619
|
+
const digits = body2.slice(isHex ? 2 : 1);
|
|
2620
|
+
if (!isHex && !/^[0-9]+$/.test(digits)) return match;
|
|
2621
|
+
const code = parseInt(digits, isHex ? 16 : 10);
|
|
2622
|
+
if (!Number.isFinite(code) || code > 1114111 || code >= 55296 && code <= 57343) {
|
|
2623
|
+
return match;
|
|
2624
|
+
}
|
|
2625
|
+
return String.fromCodePoint(code);
|
|
2626
|
+
}
|
|
2627
|
+
return NAMED_ENTITIES[body2] ?? match;
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
function escapeHtml(text) {
|
|
2631
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2632
|
+
}
|
|
2633
|
+
var NAMED_ENTITIES;
|
|
2634
|
+
var init_html_entities = __esm({
|
|
2635
|
+
"../shared/src/html-entities.ts"() {
|
|
2636
|
+
"use strict";
|
|
2637
|
+
NAMED_ENTITIES = {
|
|
2638
|
+
amp: "&",
|
|
2639
|
+
lt: "<",
|
|
2640
|
+
gt: ">",
|
|
2641
|
+
quot: '"',
|
|
2642
|
+
apos: "'",
|
|
2643
|
+
nbsp: "\xA0",
|
|
2644
|
+
copy: "\xA9",
|
|
2645
|
+
reg: "\xAE",
|
|
2646
|
+
trade: "\u2122",
|
|
2647
|
+
deg: "\xB0",
|
|
2648
|
+
plusmn: "\xB1",
|
|
2649
|
+
times: "\xD7",
|
|
2650
|
+
divide: "\xF7",
|
|
2651
|
+
middot: "\xB7",
|
|
2652
|
+
bull: "\u2022",
|
|
2653
|
+
hellip: "\u2026",
|
|
2654
|
+
ndash: "\u2013",
|
|
2655
|
+
mdash: "\u2014",
|
|
2656
|
+
lsquo: "\u2018",
|
|
2657
|
+
rsquo: "\u2019",
|
|
2658
|
+
ldquo: "\u201C",
|
|
2659
|
+
rdquo: "\u201D",
|
|
2660
|
+
laquo: "\xAB",
|
|
2661
|
+
raquo: "\xBB",
|
|
2662
|
+
sect: "\xA7",
|
|
2663
|
+
para: "\xB6",
|
|
2664
|
+
dagger: "\u2020",
|
|
2665
|
+
Dagger: "\u2021",
|
|
2666
|
+
euro: "\u20AC",
|
|
2667
|
+
pound: "\xA3",
|
|
2668
|
+
yen: "\xA5",
|
|
2669
|
+
cent: "\xA2",
|
|
2670
|
+
sup1: "\xB9",
|
|
2671
|
+
sup2: "\xB2",
|
|
2672
|
+
sup3: "\xB3",
|
|
2673
|
+
frac12: "\xBD",
|
|
2674
|
+
frac14: "\xBC",
|
|
2675
|
+
frac34: "\xBE",
|
|
2676
|
+
larr: "\u2190",
|
|
2677
|
+
uarr: "\u2191",
|
|
2678
|
+
rarr: "\u2192",
|
|
2679
|
+
darr: "\u2193",
|
|
2680
|
+
harr: "\u2194",
|
|
2681
|
+
minus: "\u2212",
|
|
2682
|
+
infin: "\u221E",
|
|
2683
|
+
ne: "\u2260",
|
|
2684
|
+
le: "\u2264",
|
|
2685
|
+
ge: "\u2265"
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
|
|
2610
2690
|
// ../shared/src/index.ts
|
|
2611
2691
|
var init_src = __esm({
|
|
2612
2692
|
"../shared/src/index.ts"() {
|
|
2613
2693
|
"use strict";
|
|
2614
2694
|
init_markers();
|
|
2615
2695
|
init_dom_prop();
|
|
2696
|
+
init_html_entities();
|
|
2616
2697
|
}
|
|
2617
2698
|
});
|
|
2618
2699
|
|
|
@@ -2663,13 +2744,20 @@ function attrValueToString(value2, opts) {
|
|
|
2663
2744
|
return null;
|
|
2664
2745
|
}
|
|
2665
2746
|
}
|
|
2747
|
+
function applyObjectIterationWrap(node, arrayExpr) {
|
|
2748
|
+
if (node.objectIteration === "entries") return `Object.entries(${arrayExpr})`;
|
|
2749
|
+
if (node.objectIteration === "keys") return `Object.keys(${arrayExpr})`;
|
|
2750
|
+
if (node.objectIteration === "values") return `Object.values(${arrayExpr})`;
|
|
2751
|
+
return arrayExpr;
|
|
2752
|
+
}
|
|
2666
2753
|
function buildChainedArrayExpr(elem) {
|
|
2667
|
-
|
|
2754
|
+
const chained = buildLoopChainExpr({
|
|
2668
2755
|
base: elem.array,
|
|
2669
2756
|
sortComparator: elem.sortComparator,
|
|
2670
2757
|
filterPredicate: elem.filterPredicate,
|
|
2671
2758
|
chainOrder: elem.chainOrder
|
|
2672
2759
|
});
|
|
2760
|
+
return applyObjectIterationWrap(elem, chained);
|
|
2673
2761
|
}
|
|
2674
2762
|
function loopOffsetTerms(offset2) {
|
|
2675
2763
|
if (!offset2) return [];
|
|
@@ -3309,6 +3397,38 @@ function extractFreeIdentifiersFromText(text) {
|
|
|
3309
3397
|
const expr = ts4.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression;
|
|
3310
3398
|
return extractFreeIdentifiersFromNode(expr);
|
|
3311
3399
|
}
|
|
3400
|
+
function extractFreeIdentifiersFromStatementText(text) {
|
|
3401
|
+
if (!text || text.trim().length === 0) return /* @__PURE__ */ new Set();
|
|
3402
|
+
const sf = ts4.createSourceFile(
|
|
3403
|
+
"__free_ids_stmt__.ts",
|
|
3404
|
+
text,
|
|
3405
|
+
ts4.ScriptTarget.Latest,
|
|
3406
|
+
/* setParentNodes */
|
|
3407
|
+
true,
|
|
3408
|
+
ts4.ScriptKind.TS
|
|
3409
|
+
);
|
|
3410
|
+
return extractFreeIdentifiersFromNode(sf);
|
|
3411
|
+
}
|
|
3412
|
+
function extractFreeIdentifiersFromTemplateText(template) {
|
|
3413
|
+
if (!template || template.length === 0) return /* @__PURE__ */ new Set();
|
|
3414
|
+
const sf = ts4.createSourceFile(
|
|
3415
|
+
"__free_ids_template__.ts",
|
|
3416
|
+
`(\`${template}\`);`,
|
|
3417
|
+
ts4.ScriptTarget.Latest,
|
|
3418
|
+
/* setParentNodes */
|
|
3419
|
+
true,
|
|
3420
|
+
ts4.ScriptKind.TS
|
|
3421
|
+
);
|
|
3422
|
+
const stmt = sf.statements[0];
|
|
3423
|
+
if (!stmt || !ts4.isExpressionStatement(stmt)) return /* @__PURE__ */ new Set();
|
|
3424
|
+
const expr = ts4.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression;
|
|
3425
|
+
if (!ts4.isTemplateExpression(expr)) return /* @__PURE__ */ new Set();
|
|
3426
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3427
|
+
for (const span of expr.templateSpans) {
|
|
3428
|
+
for (const id2 of extractFreeIdentifiersFromNode(span.expression)) ids.add(id2);
|
|
3429
|
+
}
|
|
3430
|
+
return ids;
|
|
3431
|
+
}
|
|
3312
3432
|
function extractMemoBodyExpr(computation) {
|
|
3313
3433
|
const arrowMatch = computation.match(/^\(\)\s*=>\s*(.+)$/s);
|
|
3314
3434
|
if (!arrowMatch) return computation;
|
|
@@ -3435,6 +3555,24 @@ function applyIterationShape(node, arrayExpr, indexParam) {
|
|
|
3435
3555
|
callbackParam: `(${node.param})`
|
|
3436
3556
|
};
|
|
3437
3557
|
}
|
|
3558
|
+
if (node.objectIteration === "entries") {
|
|
3559
|
+
return {
|
|
3560
|
+
array: `Object.entries(${arrayExpr})`,
|
|
3561
|
+
callbackParam: node.index ? `([${node.index}, ${node.param}])` : `(${node.param}${indexParam})`
|
|
3562
|
+
};
|
|
3563
|
+
}
|
|
3564
|
+
if (node.objectIteration === "keys") {
|
|
3565
|
+
return {
|
|
3566
|
+
array: `Object.keys(${arrayExpr})`,
|
|
3567
|
+
callbackParam: `(${node.param})`
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
if (node.objectIteration === "values") {
|
|
3571
|
+
return {
|
|
3572
|
+
array: `Object.values(${arrayExpr})`,
|
|
3573
|
+
callbackParam: `(${node.param})`
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3438
3576
|
return { array: arrayExpr, callbackParam: `(${node.param}${indexParam})` };
|
|
3439
3577
|
}
|
|
3440
3578
|
function childrenPropEntry(children2, recurse) {
|
|
@@ -3490,7 +3628,7 @@ function renderTemplateAttrPart(attr, attrName, wrap, restSpreadNames) {
|
|
|
3490
3628
|
case "boolean-attr":
|
|
3491
3629
|
return attrName;
|
|
3492
3630
|
case "literal":
|
|
3493
|
-
return `${attrName}="${v.value}"`;
|
|
3631
|
+
return `${attrName}="${escapeHtml(v.value)}"`;
|
|
3494
3632
|
case "expression": {
|
|
3495
3633
|
const valExpr = wrap(v.expr);
|
|
3496
3634
|
return templateAttrExpr(attrName, valExpr, v.presenceOrUndefined);
|
|
@@ -3606,7 +3744,7 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
|
|
|
3606
3744
|
return `<${node.tag}${attrs ? " " + attrs : ""} />`;
|
|
3607
3745
|
}
|
|
3608
3746
|
case "text":
|
|
3609
|
-
return node.value;
|
|
3747
|
+
return escapeHtml(node.value);
|
|
3610
3748
|
case "expression":
|
|
3611
3749
|
if (node.expr === "null" || node.expr === "undefined") return "";
|
|
3612
3750
|
if (node.slotId) {
|
|
@@ -3709,7 +3847,7 @@ function buildLoopSkeletonTemplate(node, safe) {
|
|
|
3709
3847
|
const v = a.value;
|
|
3710
3848
|
switch (v.kind) {
|
|
3711
3849
|
case "literal":
|
|
3712
|
-
attrParts.push(`${toHTMLAttrName(a.name)}="${v.value}"`);
|
|
3850
|
+
attrParts.push(`${toHTMLAttrName(a.name)}="${escapeHtml(v.value)}"`);
|
|
3713
3851
|
break;
|
|
3714
3852
|
case "boolean-attr":
|
|
3715
3853
|
attrParts.push(toHTMLAttrName(a.name));
|
|
@@ -3741,7 +3879,7 @@ function buildLoopSkeletonTemplate(node, safe) {
|
|
|
3741
3879
|
return `<${node.tag}${attrs ? " " + attrs : ""} />`;
|
|
3742
3880
|
}
|
|
3743
3881
|
case "text":
|
|
3744
|
-
return node.value;
|
|
3882
|
+
return escapeHtml(node.value);
|
|
3745
3883
|
case "expression":
|
|
3746
3884
|
if (node.expr === "null" || node.expr === "undefined") return "";
|
|
3747
3885
|
if (!node.slotId) {
|
|
@@ -3773,6 +3911,100 @@ function buildLoopSkeletonTemplate(node, safe) {
|
|
|
3773
3911
|
return assertNever(node);
|
|
3774
3912
|
}
|
|
3775
3913
|
}
|
|
3914
|
+
function skeletonForceCloseGroup(tag) {
|
|
3915
|
+
return SKELETON_PATH_FORCE_CLOSE_GROUPS.findIndex((group) => group.has(tag));
|
|
3916
|
+
}
|
|
3917
|
+
function computeSkeletonSlotPaths(node, safe) {
|
|
3918
|
+
const state2 = { elementPaths: /* @__PURE__ */ new Map(), textMarkerPaths: /* @__PURE__ */ new Map(), bailed: false };
|
|
3919
|
+
walkSkeletonPathNode(node, [], safe, state2, /* @__PURE__ */ new Set());
|
|
3920
|
+
if (state2.bailed) return null;
|
|
3921
|
+
return { elementPaths: state2.elementPaths, textMarkerPaths: state2.textMarkerPaths };
|
|
3922
|
+
}
|
|
3923
|
+
function walkSkeletonPathNode(node, path25, safe, state2, forceCloseAncestors) {
|
|
3924
|
+
if (state2.bailed || node.type !== "element") return;
|
|
3925
|
+
if (SKELETON_PATH_HAZARD_TAGS.has(node.tag)) {
|
|
3926
|
+
state2.bailed = true;
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
const groupIdx = skeletonForceCloseGroup(node.tag);
|
|
3930
|
+
if (groupIdx >= 0 && forceCloseAncestors.has(groupIdx)) {
|
|
3931
|
+
state2.bailed = true;
|
|
3932
|
+
return;
|
|
3933
|
+
}
|
|
3934
|
+
const flatChildren = flattenSkeletonChildren(node.children);
|
|
3935
|
+
if (VOID_ELEMENTS.has(node.tag) && flatChildren.length > 0) {
|
|
3936
|
+
state2.bailed = true;
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3939
|
+
if (node.tag === "tr" && hasForeignTableRowContent(flatChildren)) {
|
|
3940
|
+
state2.bailed = true;
|
|
3941
|
+
return;
|
|
3942
|
+
}
|
|
3943
|
+
if (node.slotId) state2.elementPaths.set(node.slotId, path25);
|
|
3944
|
+
const nextAncestors = groupIdx >= 0 ? /* @__PURE__ */ new Set([...forceCloseAncestors, groupIdx]) : forceCloseAncestors;
|
|
3945
|
+
walkSkeletonPathChildren(flatChildren, path25, safe, state2, nextAncestors);
|
|
3946
|
+
}
|
|
3947
|
+
function hasForeignTableRowContent(children2) {
|
|
3948
|
+
for (const child of children2) {
|
|
3949
|
+
if (child.type === "text") {
|
|
3950
|
+
if (child.value.trim() !== "") return true;
|
|
3951
|
+
continue;
|
|
3952
|
+
}
|
|
3953
|
+
if (child.type === "element" && child.tag !== "td" && child.tag !== "th") {
|
|
3954
|
+
return true;
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
return false;
|
|
3958
|
+
}
|
|
3959
|
+
function flattenSkeletonChildren(children2) {
|
|
3960
|
+
const out = [];
|
|
3961
|
+
for (const child of children2) {
|
|
3962
|
+
if (child.type === "fragment") {
|
|
3963
|
+
out.push(...flattenSkeletonChildren(child.children));
|
|
3964
|
+
} else {
|
|
3965
|
+
out.push(child);
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
return out;
|
|
3969
|
+
}
|
|
3970
|
+
function walkSkeletonPathChildren(children2, parentPath, safe, state2, forceCloseAncestors) {
|
|
3971
|
+
let idx = 0;
|
|
3972
|
+
let pendingText = false;
|
|
3973
|
+
for (const child of children2) {
|
|
3974
|
+
if (state2.bailed) return;
|
|
3975
|
+
switch (child.type) {
|
|
3976
|
+
case "text": {
|
|
3977
|
+
if (child.value === "") continue;
|
|
3978
|
+
if (!pendingText) idx += 1;
|
|
3979
|
+
pendingText = true;
|
|
3980
|
+
continue;
|
|
3981
|
+
}
|
|
3982
|
+
case "expression": {
|
|
3983
|
+
if (child.expr === "null" || child.expr === "undefined") continue;
|
|
3984
|
+
if (!child.slotId || !safe.reactiveTextSlotIds.has(child.slotId)) {
|
|
3985
|
+
state2.bailed = true;
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
state2.textMarkerPaths.set(child.slotId, [...parentPath, idx]);
|
|
3989
|
+
idx += 2;
|
|
3990
|
+
pendingText = false;
|
|
3991
|
+
continue;
|
|
3992
|
+
}
|
|
3993
|
+
case "element": {
|
|
3994
|
+
walkSkeletonPathNode(child, [...parentPath, idx], safe, state2, forceCloseAncestors);
|
|
3995
|
+
idx += 1;
|
|
3996
|
+
pendingText = false;
|
|
3997
|
+
continue;
|
|
3998
|
+
}
|
|
3999
|
+
case "fragment":
|
|
4000
|
+
continue;
|
|
4001
|
+
// already flattened
|
|
4002
|
+
default:
|
|
4003
|
+
state2.bailed = true;
|
|
4004
|
+
return;
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
3776
4008
|
function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParams) {
|
|
3777
4009
|
const recurse = (n) => irToPlaceholderTemplate(n, restSpreadNames, loopDepth, loopParams);
|
|
3778
4010
|
const wrapExpr = (expr) => wrapExprWithLoopParams(expr, loopParams);
|
|
@@ -3793,7 +4025,7 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
|
|
|
3793
4025
|
return `<${node.tag}${attrs ? " " + attrs : ""} />`;
|
|
3794
4026
|
}
|
|
3795
4027
|
case "text":
|
|
3796
|
-
return node.value;
|
|
4028
|
+
return escapeHtml(node.value);
|
|
3797
4029
|
case "expression":
|
|
3798
4030
|
if (node.expr === "null" || node.expr === "undefined") return "";
|
|
3799
4031
|
if (node.slotId) {
|
|
@@ -3988,7 +4220,7 @@ function irToComponentTemplateWithOpts(node, opts) {
|
|
|
3988
4220
|
return templateAttrExpr(keyName, transformExpr(tmplStr));
|
|
3989
4221
|
}
|
|
3990
4222
|
case "literal":
|
|
3991
|
-
return `${keyName}="${v.value}"`;
|
|
4223
|
+
return `${keyName}="${escapeHtml(v.value)}"`;
|
|
3992
4224
|
default:
|
|
3993
4225
|
return "";
|
|
3994
4226
|
}
|
|
@@ -3998,7 +4230,7 @@ function irToComponentTemplateWithOpts(node, opts) {
|
|
|
3998
4230
|
case "boolean-attr":
|
|
3999
4231
|
return attrName;
|
|
4000
4232
|
case "literal":
|
|
4001
|
-
return `${attrName}="${v.value}"`;
|
|
4233
|
+
return `${attrName}="${escapeHtml(v.value)}"`;
|
|
4002
4234
|
case "expression":
|
|
4003
4235
|
return templateAttrExpr(attrName, transformExpr(v.expr, v.templateExpr), v.presenceOrUndefined);
|
|
4004
4236
|
case "template": {
|
|
@@ -4023,7 +4255,7 @@ function irToComponentTemplateWithOpts(node, opts) {
|
|
|
4023
4255
|
return `<${node.tag}${attrs ? " " + attrs : ""} />`;
|
|
4024
4256
|
}
|
|
4025
4257
|
case "text":
|
|
4026
|
-
return node.value;
|
|
4258
|
+
return escapeHtml(node.value);
|
|
4027
4259
|
case "expression":
|
|
4028
4260
|
if (node.expr === "null" || node.expr === "undefined") return "";
|
|
4029
4261
|
if (node.slotId) {
|
|
@@ -4294,7 +4526,7 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
4294
4526
|
case "boolean-attr":
|
|
4295
4527
|
return attrName;
|
|
4296
4528
|
case "literal":
|
|
4297
|
-
return `${attrName}="${v.value}"`;
|
|
4529
|
+
return `${attrName}="${escapeHtml(v.value)}"`;
|
|
4298
4530
|
case "expression":
|
|
4299
4531
|
return templateAttrExpr(attrName, transformExpr(v.expr, v.templateExpr), v.presenceOrUndefined);
|
|
4300
4532
|
case "template": {
|
|
@@ -4319,7 +4551,7 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
4319
4551
|
return `<${node.tag}${attrs ? " " + attrs : ""} />`;
|
|
4320
4552
|
}
|
|
4321
4553
|
case "text":
|
|
4322
|
-
return node.value;
|
|
4554
|
+
return escapeHtml(node.value);
|
|
4323
4555
|
case "expression":
|
|
4324
4556
|
if (node.expr === "null" || node.expr === "undefined") return "";
|
|
4325
4557
|
if (node.clientOnly && node.slotId) {
|
|
@@ -4389,7 +4621,26 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
4389
4621
|
return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ", undefined" : "")}${slotArg})}`;
|
|
4390
4622
|
}
|
|
4391
4623
|
case "loop": {
|
|
4392
|
-
|
|
4624
|
+
const boundHere = new Set(opts.loopBoundNames ?? []);
|
|
4625
|
+
if (node.paramBindings && node.paramBindings.length > 0) {
|
|
4626
|
+
for (const b of node.paramBindings) boundHere.add(b.name);
|
|
4627
|
+
} else if (!node.param.startsWith("[") && !node.param.startsWith("{")) {
|
|
4628
|
+
boundHere.add(node.param);
|
|
4629
|
+
}
|
|
4630
|
+
if (node.index) boundHere.add(node.index);
|
|
4631
|
+
const childEnv = {
|
|
4632
|
+
...env,
|
|
4633
|
+
substitutions: new Map([...env.substitutions].filter(([name2]) => !boundHere.has(name2)))
|
|
4634
|
+
};
|
|
4635
|
+
const recurseInLoopBody = (n) => generateCsrTemplateWithOpts(n, {
|
|
4636
|
+
...opts,
|
|
4637
|
+
insideLoop: true,
|
|
4638
|
+
loopDepth: loopDepth + 1,
|
|
4639
|
+
inHoistedChildren: false,
|
|
4640
|
+
loopBoundNames: boundHere,
|
|
4641
|
+
csrEnv: childEnv
|
|
4642
|
+
});
|
|
4643
|
+
let childTemplate = node.children.map(recurseInLoopBody).join("");
|
|
4393
4644
|
if (node.bodyIsItemConditional && node.key) {
|
|
4394
4645
|
childTemplate = `${itemAnchorTemplate(node.key)}${childTemplate}`;
|
|
4395
4646
|
}
|
|
@@ -4403,7 +4654,7 @@ function generateCsrTemplateWithOpts(node, opts) {
|
|
|
4403
4654
|
if (node.flatMapCallback) {
|
|
4404
4655
|
let body2 = node.flatMapCallback.templateBody ?? node.flatMapCallback.body;
|
|
4405
4656
|
for (const frag of node.flatMapCallback.fragments) {
|
|
4406
|
-
const renderedIr =
|
|
4657
|
+
const renderedIr = recurseInLoopBody(frag.ir);
|
|
4407
4658
|
body2 = body2.replace(frag.placeholder, `\`${renderedIr}\``);
|
|
4408
4659
|
}
|
|
4409
4660
|
body2 = applyPropsRewrite(body2, propsObjectName ?? null);
|
|
@@ -4445,7 +4696,7 @@ function isSimplePropExpression(expr, propNames) {
|
|
|
4445
4696
|
if (expr.includes("()")) return false;
|
|
4446
4697
|
return true;
|
|
4447
4698
|
}
|
|
4448
|
-
var VOID_ELEMENTS, UNSAFE_TEMPLATE_EXPR;
|
|
4699
|
+
var VOID_ELEMENTS, UNSAFE_TEMPLATE_EXPR, SKELETON_PATH_HAZARD_TAGS, SKELETON_PATH_FORCE_CLOSE_GROUPS;
|
|
4449
4700
|
var init_html_template = __esm({
|
|
4450
4701
|
"../jsx/src/ir-to-client-js/html-template.ts"() {
|
|
4451
4702
|
"use strict";
|
|
@@ -4473,6 +4724,33 @@ var init_html_template = __esm({
|
|
|
4473
4724
|
"wbr"
|
|
4474
4725
|
]);
|
|
4475
4726
|
UNSAFE_TEMPLATE_EXPR = "undefined";
|
|
4727
|
+
SKELETON_PATH_HAZARD_TAGS = /* @__PURE__ */ new Set([
|
|
4728
|
+
"table",
|
|
4729
|
+
"thead",
|
|
4730
|
+
"tbody",
|
|
4731
|
+
"tfoot",
|
|
4732
|
+
"caption",
|
|
4733
|
+
"colgroup",
|
|
4734
|
+
"col",
|
|
4735
|
+
"select",
|
|
4736
|
+
"optgroup",
|
|
4737
|
+
"p",
|
|
4738
|
+
"pre",
|
|
4739
|
+
"textarea",
|
|
4740
|
+
"listing",
|
|
4741
|
+
"template",
|
|
4742
|
+
"math"
|
|
4743
|
+
// MathML foreign-content: breakout tags pop content back out, same class of hazard as SVG.
|
|
4744
|
+
]);
|
|
4745
|
+
SKELETON_PATH_FORCE_CLOSE_GROUPS = [
|
|
4746
|
+
/* @__PURE__ */ new Set(["a"]),
|
|
4747
|
+
/* @__PURE__ */ new Set(["button"]),
|
|
4748
|
+
/* @__PURE__ */ new Set(["form"]),
|
|
4749
|
+
/* @__PURE__ */ new Set(["option"]),
|
|
4750
|
+
/* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]),
|
|
4751
|
+
/* @__PURE__ */ new Set(["dd", "dt"]),
|
|
4752
|
+
/* @__PURE__ */ new Set(["li"])
|
|
4753
|
+
];
|
|
4476
4754
|
}
|
|
4477
4755
|
});
|
|
4478
4756
|
|
|
@@ -5123,6 +5401,7 @@ import fs from "node:fs";
|
|
|
5123
5401
|
function needsTypeBasedDetection(source) {
|
|
5124
5402
|
if (REACTIVE_BRAND_PACKAGES.some((pkg) => source.includes(pkg))) return true;
|
|
5125
5403
|
if (/\.map\s*\(/.test(source)) return true;
|
|
5404
|
+
if (source.includes("createSelector")) return true;
|
|
5126
5405
|
return false;
|
|
5127
5406
|
}
|
|
5128
5407
|
function findBrandPackageImportLoc(sourceFile, filePath) {
|
|
@@ -7412,6 +7691,7 @@ var init_analyzer = __esm({
|
|
|
7412
7691
|
"createEffect",
|
|
7413
7692
|
"createDisposableEffect",
|
|
7414
7693
|
"createMemo",
|
|
7694
|
+
"createSelector",
|
|
7415
7695
|
"createRoot",
|
|
7416
7696
|
"onCleanup",
|
|
7417
7697
|
"onMount",
|
|
@@ -8078,8 +8358,13 @@ function exprHasFunctionCalls(expr) {
|
|
|
8078
8358
|
return found;
|
|
8079
8359
|
}
|
|
8080
8360
|
function rewriteBarePropRefs2(text, expr, ctx2) {
|
|
8081
|
-
|
|
8361
|
+
let propNames = getDestructuredPropNames(ctx2);
|
|
8082
8362
|
if (!propNames) return void 0;
|
|
8363
|
+
if (ctx2.loopParams.size > 0) {
|
|
8364
|
+
const filtered = new Set([...propNames].filter((n) => !ctx2.loopParams.has(n)));
|
|
8365
|
+
if (filtered.size === 0) return void 0;
|
|
8366
|
+
propNames = filtered;
|
|
8367
|
+
}
|
|
8083
8368
|
const extraPropRefs = collectBranchLocalPropRefsViaSubstitution(expr, ctx2);
|
|
8084
8369
|
return rewriteBarePropRefs(text, expr, propNames, extraPropRefs);
|
|
8085
8370
|
}
|
|
@@ -8138,6 +8423,7 @@ function createTransformContext(analyzer) {
|
|
|
8138
8423
|
isRoot: true,
|
|
8139
8424
|
insideComponentChildren: false,
|
|
8140
8425
|
loopParams: /* @__PURE__ */ new Set(),
|
|
8426
|
+
loopDepth: 0,
|
|
8141
8427
|
patterns: {
|
|
8142
8428
|
signals: analyzer.signals.map((s) => ({
|
|
8143
8429
|
getter: s.getter,
|
|
@@ -8232,35 +8518,37 @@ function makeBindingEnv(ctx2) {
|
|
|
8232
8518
|
function parseValueExpr(trimmed) {
|
|
8233
8519
|
return parseExpression(trimmed.startsWith("{") ? `(${trimmed})` : trimmed);
|
|
8234
8520
|
}
|
|
8235
|
-
function attachParsedExpressions(node) {
|
|
8521
|
+
function attachParsedExpressions(node, analyzer, bound = EMPTY_BOUND) {
|
|
8522
|
+
const parse = (trimmed) => resolveCallbackMethodFunctionReferences(parseExpression(trimmed), analyzer, bound);
|
|
8523
|
+
const parseValue = (trimmed) => resolveCallbackMethodFunctionReferences(parseValueExpr(trimmed), analyzer, bound);
|
|
8236
8524
|
if (node.type === "expression") {
|
|
8237
8525
|
const trimmed = node.expr.trim();
|
|
8238
|
-
if (trimmed) node.parsed =
|
|
8526
|
+
if (trimmed) node.parsed = parse(trimmed);
|
|
8239
8527
|
} else if (node.type === "conditional" || node.type === "if-statement") {
|
|
8240
8528
|
const trimmed = node.condition.trim();
|
|
8241
|
-
if (trimmed) node.parsedCondition =
|
|
8529
|
+
if (trimmed) node.parsedCondition = parse(trimmed);
|
|
8242
8530
|
}
|
|
8243
8531
|
if (node.type === "element") {
|
|
8244
8532
|
for (const attr of node.attrs) {
|
|
8245
8533
|
if (attr.value.kind === "expression") {
|
|
8246
8534
|
const trimmed = attr.value.expr.trim();
|
|
8247
|
-
if (trimmed) attr.value.parsed =
|
|
8535
|
+
if (trimmed) attr.value.parsed = parseValue(trimmed);
|
|
8248
8536
|
} else if (attr.value.kind === "spread") {
|
|
8249
8537
|
const trimmed = attr.value.expr.trim();
|
|
8250
|
-
if (trimmed) attr.value.parsed =
|
|
8538
|
+
if (trimmed) attr.value.parsed = parse(trimmed);
|
|
8251
8539
|
}
|
|
8252
8540
|
}
|
|
8253
8541
|
} else if (node.type === "component") {
|
|
8254
8542
|
for (const prop of node.props) {
|
|
8255
8543
|
if (prop.value.kind === "expression") {
|
|
8256
8544
|
const trimmed = prop.value.expr.trim();
|
|
8257
|
-
if (trimmed) prop.value.parsed =
|
|
8545
|
+
if (trimmed) prop.value.parsed = parseValue(trimmed);
|
|
8258
8546
|
}
|
|
8259
8547
|
}
|
|
8260
8548
|
} else if (node.type === "provider") {
|
|
8261
8549
|
if (node.valueProp.value.kind === "expression") {
|
|
8262
8550
|
const trimmed = node.valueProp.value.expr.trim();
|
|
8263
|
-
if (trimmed) node.valueProp.value.parsed =
|
|
8551
|
+
if (trimmed) node.valueProp.value.parsed = parseValue(trimmed);
|
|
8264
8552
|
}
|
|
8265
8553
|
}
|
|
8266
8554
|
switch (node.type) {
|
|
@@ -8268,40 +8556,43 @@ function attachParsedExpressions(node) {
|
|
|
8268
8556
|
case "component":
|
|
8269
8557
|
case "fragment":
|
|
8270
8558
|
case "provider":
|
|
8271
|
-
for (const child of node.children) attachParsedExpressions(child);
|
|
8559
|
+
for (const child of node.children) attachParsedExpressions(child, analyzer, bound);
|
|
8272
8560
|
break;
|
|
8273
8561
|
case "async":
|
|
8274
|
-
attachParsedExpressions(node.fallback);
|
|
8275
|
-
for (const child of node.children) attachParsedExpressions(child);
|
|
8562
|
+
attachParsedExpressions(node.fallback, analyzer, bound);
|
|
8563
|
+
for (const child of node.children) attachParsedExpressions(child, analyzer, bound);
|
|
8276
8564
|
break;
|
|
8277
8565
|
case "loop": {
|
|
8278
8566
|
const trimmedArray = node.array.trim();
|
|
8279
|
-
if (trimmedArray) node.arrayParsed =
|
|
8280
|
-
|
|
8567
|
+
if (trimmedArray) node.arrayParsed = parse(trimmedArray);
|
|
8568
|
+
const loopBound = new Set(bound);
|
|
8569
|
+
loopBound.add(node.param);
|
|
8570
|
+
if (node.index) loopBound.add(node.index);
|
|
8571
|
+
for (const child of node.children) attachParsedExpressions(child, analyzer, loopBound);
|
|
8281
8572
|
if (node.childComponent) {
|
|
8282
|
-
for (const child of node.childComponent.children) attachParsedExpressions(child);
|
|
8573
|
+
for (const child of node.childComponent.children) attachParsedExpressions(child, analyzer, loopBound);
|
|
8283
8574
|
}
|
|
8284
8575
|
for (const nested of node.nestedComponents ?? []) {
|
|
8285
|
-
for (const child of nested.children) attachParsedExpressions(child);
|
|
8576
|
+
for (const child of nested.children) attachParsedExpressions(child, analyzer, loopBound);
|
|
8286
8577
|
}
|
|
8287
8578
|
for (const frag of node.flatMapCallback?.fragments ?? []) {
|
|
8288
|
-
attachParsedExpressions(frag.ir);
|
|
8579
|
+
attachParsedExpressions(frag.ir, analyzer, loopBound);
|
|
8289
8580
|
}
|
|
8290
8581
|
break;
|
|
8291
8582
|
}
|
|
8292
8583
|
case "conditional":
|
|
8293
|
-
attachParsedExpressions(node.whenTrue);
|
|
8294
|
-
attachParsedExpressions(node.whenFalse);
|
|
8584
|
+
attachParsedExpressions(node.whenTrue, analyzer, bound);
|
|
8585
|
+
attachParsedExpressions(node.whenFalse, analyzer, bound);
|
|
8295
8586
|
break;
|
|
8296
8587
|
case "if-statement":
|
|
8297
|
-
attachParsedExpressions(node.consequent);
|
|
8298
|
-
if (node.alternate) attachParsedExpressions(node.alternate);
|
|
8588
|
+
attachParsedExpressions(node.consequent, analyzer, bound);
|
|
8589
|
+
if (node.alternate) attachParsedExpressions(node.alternate, analyzer, bound);
|
|
8299
8590
|
break;
|
|
8300
8591
|
}
|
|
8301
8592
|
}
|
|
8302
8593
|
function jsxToIR(analyzer) {
|
|
8303
8594
|
const root2 = buildIRRoot(analyzer);
|
|
8304
|
-
if (root2) attachParsedExpressions(root2);
|
|
8595
|
+
if (root2) attachParsedExpressions(root2, analyzer);
|
|
8305
8596
|
return root2;
|
|
8306
8597
|
}
|
|
8307
8598
|
function buildIRRoot(analyzer) {
|
|
@@ -8776,7 +9067,12 @@ function transformText(node, ctx2) {
|
|
|
8776
9067
|
}
|
|
8777
9068
|
return {
|
|
8778
9069
|
type: "text",
|
|
8779
|
-
|
|
9070
|
+
// JSX decodes character references at parse time (`©` IS the
|
|
9071
|
+
// text `©`), so the IR carries the DECODED value — the semantics —
|
|
9072
|
+
// and each adapter re-escapes for its own emission context.
|
|
9073
|
+
// Decode AFTER whitespace normalization: ` ` yields U+00A0,
|
|
9074
|
+
// which `\s+` would otherwise collapse into a plain space.
|
|
9075
|
+
value: decodeEntities(text),
|
|
8780
9076
|
loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath)
|
|
8781
9077
|
};
|
|
8782
9078
|
}
|
|
@@ -9339,6 +9635,16 @@ function isIteratorShapeCall(node) {
|
|
|
9339
9635
|
if (name2 !== "entries" && name2 !== "keys" && name2 !== "values") return null;
|
|
9340
9636
|
return { array: node.expression.expression, shape: name2 };
|
|
9341
9637
|
}
|
|
9638
|
+
function isObjectIteratorCall(node) {
|
|
9639
|
+
if (!ts11.isCallExpression(node)) return null;
|
|
9640
|
+
if (!ts11.isPropertyAccessExpression(node.expression)) return null;
|
|
9641
|
+
if (!ts11.isIdentifier(node.expression.expression)) return null;
|
|
9642
|
+
if (node.expression.expression.text !== "Object") return null;
|
|
9643
|
+
if (node.arguments.length !== 1) return null;
|
|
9644
|
+
const name2 = node.expression.name.text;
|
|
9645
|
+
if (name2 !== "entries" && name2 !== "keys" && name2 !== "values") return null;
|
|
9646
|
+
return { object: node.arguments[0], shape: name2 };
|
|
9647
|
+
}
|
|
9342
9648
|
function extractSortComparator(callback, _method, ctx2) {
|
|
9343
9649
|
const outerRaw = ctx2.getJS(callback);
|
|
9344
9650
|
const unsupported = () => ({
|
|
@@ -9382,8 +9688,22 @@ function extractSortComparator(callback, _method, ctx2) {
|
|
|
9382
9688
|
};
|
|
9383
9689
|
}
|
|
9384
9690
|
function resolveSortComparatorIdentifier(name2, ctx2) {
|
|
9385
|
-
const constInfo = findLocalConst(name2, ctx2);
|
|
9386
|
-
const fnInfo = findLocalFunction(name2, ctx2);
|
|
9691
|
+
const constInfo = findLocalConst(name2, ctx2.analyzer);
|
|
9692
|
+
const fnInfo = findLocalFunction(name2, ctx2.analyzer);
|
|
9693
|
+
if (constInfo && fnInfo) return null;
|
|
9694
|
+
if (constInfo) {
|
|
9695
|
+
const ast = parseConstInitializer(constInfo);
|
|
9696
|
+
return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
|
|
9697
|
+
}
|
|
9698
|
+
if (fnInfo) {
|
|
9699
|
+
const ast = parseFunctionInfoAsExpr(fnInfo);
|
|
9700
|
+
return ast && (ts11.isArrowFunction(ast) || ts11.isFunctionExpression(ast)) ? ast : null;
|
|
9701
|
+
}
|
|
9702
|
+
return null;
|
|
9703
|
+
}
|
|
9704
|
+
function resolveCallbackMethodFunctionReferenceIdentifier(name2, analyzer) {
|
|
9705
|
+
const constInfo = findLocalConst(name2, analyzer);
|
|
9706
|
+
const fnInfo = findLocalFunction(name2, analyzer);
|
|
9387
9707
|
if (constInfo && fnInfo) return null;
|
|
9388
9708
|
if (constInfo) {
|
|
9389
9709
|
const ast = parseConstInitializer(constInfo);
|
|
@@ -9395,6 +9715,56 @@ function resolveSortComparatorIdentifier(name2, ctx2) {
|
|
|
9395
9715
|
}
|
|
9396
9716
|
return null;
|
|
9397
9717
|
}
|
|
9718
|
+
function resolveCallbackMethodFunctionReferences(expr, analyzer, bound = EMPTY_BOUND) {
|
|
9719
|
+
function visit3(e, bound2) {
|
|
9720
|
+
switch (e.kind) {
|
|
9721
|
+
case "literal":
|
|
9722
|
+
case "identifier":
|
|
9723
|
+
case "regex":
|
|
9724
|
+
case "unsupported":
|
|
9725
|
+
return e;
|
|
9726
|
+
case "call": {
|
|
9727
|
+
const callee = visit3(e.callee, bound2);
|
|
9728
|
+
const args2 = e.args.map((a) => visit3(a, bound2));
|
|
9729
|
+
if (callee.kind === "member" && !callee.computed && CALLBACK_METHODS.has(callee.property) && args2[0]?.kind === "identifier" && !bound2.has(args2[0].name)) {
|
|
9730
|
+
const resolved = resolveCallbackMethodFunctionReferenceIdentifier(args2[0].name, analyzer);
|
|
9731
|
+
const arrow = resolved ? tsNodeToParsedExpr(resolved) : null;
|
|
9732
|
+
if (arrow && arrow.kind === "arrow") args2[0] = arrow;
|
|
9733
|
+
}
|
|
9734
|
+
return { ...e, callee, args: args2 };
|
|
9735
|
+
}
|
|
9736
|
+
case "member":
|
|
9737
|
+
return { ...e, object: visit3(e.object, bound2) };
|
|
9738
|
+
case "index-access":
|
|
9739
|
+
return { ...e, object: visit3(e.object, bound2), index: visit3(e.index, bound2) };
|
|
9740
|
+
case "binary":
|
|
9741
|
+
case "logical":
|
|
9742
|
+
return { ...e, left: visit3(e.left, bound2), right: visit3(e.right, bound2) };
|
|
9743
|
+
case "unary":
|
|
9744
|
+
return { ...e, argument: visit3(e.argument, bound2) };
|
|
9745
|
+
case "conditional":
|
|
9746
|
+
return { ...e, test: visit3(e.test, bound2), consequent: visit3(e.consequent, bound2), alternate: visit3(e.alternate, bound2) };
|
|
9747
|
+
case "template-literal":
|
|
9748
|
+
return { ...e, parts: e.parts.map((p) => p.type === "expression" ? { ...p, expr: visit3(p.expr, bound2) } : p) };
|
|
9749
|
+
case "array-literal":
|
|
9750
|
+
return { ...e, elements: e.elements.map((el) => visit3(el, bound2)) };
|
|
9751
|
+
case "array-method":
|
|
9752
|
+
return {
|
|
9753
|
+
...e,
|
|
9754
|
+
object: visit3(e.object, bound2),
|
|
9755
|
+
args: e.args.map((a) => visit3(a, bound2)),
|
|
9756
|
+
...e.method === "flat" && e.depthExpr ? { depthExpr: visit3(e.depthExpr, bound2) } : {}
|
|
9757
|
+
};
|
|
9758
|
+
case "object-literal":
|
|
9759
|
+
return { ...e, properties: e.properties.map((p) => ({ ...p, value: visit3(p.value, bound2) })) };
|
|
9760
|
+
case "arrow": {
|
|
9761
|
+
const inner = e.params.length === 0 ? bound2 : /* @__PURE__ */ new Set([...bound2, ...e.params]);
|
|
9762
|
+
return { ...e, body: visit3(e.body, inner) };
|
|
9763
|
+
}
|
|
9764
|
+
}
|
|
9765
|
+
}
|
|
9766
|
+
return visit3(expr, bound);
|
|
9767
|
+
}
|
|
9398
9768
|
function extractFilterPredicate(callback, ctx2) {
|
|
9399
9769
|
if (!ts11.isArrowFunction(callback)) return { result: null };
|
|
9400
9770
|
if (callback.parameters.length < 1) return { result: null };
|
|
@@ -9741,6 +10111,7 @@ function extractItemConditionalKey(cond) {
|
|
|
9741
10111
|
}
|
|
9742
10112
|
function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
9743
10113
|
const isNested = ctx2.loopParams.size > 0;
|
|
10114
|
+
const depth = ctx2.loopDepth;
|
|
9744
10115
|
const propAccess = node.expression;
|
|
9745
10116
|
const mapSource = propAccess.expression;
|
|
9746
10117
|
let array = "";
|
|
@@ -9753,6 +10124,7 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
9753
10124
|
let templateMapPreamble;
|
|
9754
10125
|
let typedMapPreamble;
|
|
9755
10126
|
let iterationShape;
|
|
10127
|
+
let objectIteration;
|
|
9756
10128
|
const setArray = (node2) => {
|
|
9757
10129
|
array = ctx2.getJS(node2);
|
|
9758
10130
|
templateArray = rewriteBarePropRefs2(array, node2, ctx2);
|
|
@@ -9767,6 +10139,12 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
9767
10139
|
} else if (iteratorInfo.shape === "keys") {
|
|
9768
10140
|
iterationShape = "keys";
|
|
9769
10141
|
}
|
|
10142
|
+
} else {
|
|
10143
|
+
const objectIteratorInfo = isObjectIteratorCall(mapSource);
|
|
10144
|
+
if (objectIteratorInfo) {
|
|
10145
|
+
chainSource = objectIteratorInfo.object;
|
|
10146
|
+
objectIteration = objectIteratorInfo.shape;
|
|
10147
|
+
}
|
|
9770
10148
|
}
|
|
9771
10149
|
const filterInfo = isFilterCall(chainSource);
|
|
9772
10150
|
const sortInfo = isSortCall(chainSource);
|
|
@@ -9865,13 +10243,11 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
9865
10243
|
setArray(innerSort.array);
|
|
9866
10244
|
}
|
|
9867
10245
|
} else {
|
|
9868
|
-
|
|
9869
|
-
arrayExpr = filterInfo.array;
|
|
10246
|
+
setArray(filterInfo.array);
|
|
9870
10247
|
}
|
|
9871
10248
|
}
|
|
9872
10249
|
} else {
|
|
9873
|
-
|
|
9874
|
-
arrayExpr = chainSource;
|
|
10250
|
+
setArray(chainSource);
|
|
9875
10251
|
}
|
|
9876
10252
|
const callback = node.arguments[0];
|
|
9877
10253
|
let param = "item";
|
|
@@ -9888,7 +10264,8 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
9888
10264
|
if (firstParam.type) {
|
|
9889
10265
|
paramType = firstParam.type.getText(ctx2.sourceFile);
|
|
9890
10266
|
}
|
|
9891
|
-
|
|
10267
|
+
const isEntriesShape = iterationShape === "entries" || objectIteration === "entries";
|
|
10268
|
+
if (isEntriesShape && ts11.isArrayBindingPattern(firstParam.name)) {
|
|
9892
10269
|
const elements2 = firstParam.name.elements.filter(
|
|
9893
10270
|
(el) => !ts11.isOmittedExpression(el)
|
|
9894
10271
|
);
|
|
@@ -9922,7 +10299,7 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
9922
10299
|
}
|
|
9923
10300
|
}
|
|
9924
10301
|
}
|
|
9925
|
-
if (callback.parameters.length > 1 && iterationShape !== "entries") {
|
|
10302
|
+
if (callback.parameters.length > 1 && iterationShape !== "entries" && objectIteration !== "entries") {
|
|
9926
10303
|
const secondParam = callback.parameters[1];
|
|
9927
10304
|
index = secondParam.name.getText(ctx2.sourceFile);
|
|
9928
10305
|
if (secondParam.type) {
|
|
@@ -9935,6 +10312,7 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
9935
10312
|
ctx2.loopParams.add(param);
|
|
9936
10313
|
}
|
|
9937
10314
|
if (index) ctx2.loopParams.add(index);
|
|
10315
|
+
ctx2.loopDepth++;
|
|
9938
10316
|
const tryTransformRenderableBody = (expr) => {
|
|
9939
10317
|
if (!ts11.isBinaryExpression(expr)) return;
|
|
9940
10318
|
const op = expr.operatorToken.kind;
|
|
@@ -10025,6 +10403,7 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
10025
10403
|
ctx2.loopParams.delete(param);
|
|
10026
10404
|
}
|
|
10027
10405
|
if (index) ctx2.loopParams.delete(index);
|
|
10406
|
+
ctx2.loopDepth--;
|
|
10028
10407
|
}
|
|
10029
10408
|
if (children2.length === 0 && !flatMapCallback) {
|
|
10030
10409
|
return null;
|
|
@@ -10053,7 +10432,7 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
10053
10432
|
const callsReactive = exprCallsReactiveGetters(arrayExpr, ctx2);
|
|
10054
10433
|
const hasCalls = exprHasFunctionCalls(arrayExpr);
|
|
10055
10434
|
const isDirectPropArray = method2 !== "flatMap" && isArrayExprDirectPropRef(arrayExpr, ctx2);
|
|
10056
|
-
const isStaticArray = !isSignalOrMemoArray(array, ctx2) && !isDirectPropArray && !hasCalls;
|
|
10435
|
+
const isStaticArray = !isSignalOrMemoArray(array, ctx2) && !isDirectPropArray && !hasCalls && !objectIteration;
|
|
10057
10436
|
const nestedComponents = collectNestedComponents(children2).filter((c) => c.name !== childComponent?.name);
|
|
10058
10437
|
return {
|
|
10059
10438
|
type: "loop",
|
|
@@ -10087,6 +10466,8 @@ function transformMapCall(node, ctx2, isClientOnly = false, method2 = "map") {
|
|
|
10087
10466
|
sortComparator,
|
|
10088
10467
|
chainOrder,
|
|
10089
10468
|
iterationShape,
|
|
10469
|
+
objectIteration,
|
|
10470
|
+
depth,
|
|
10090
10471
|
clientOnly: isClientOnly || void 0,
|
|
10091
10472
|
mapPreamble,
|
|
10092
10473
|
templateMapPreamble,
|
|
@@ -10326,7 +10707,7 @@ function getAttributeValue(attr, ctx2) {
|
|
|
10326
10707
|
return AttrValueOf.booleanAttr();
|
|
10327
10708
|
}
|
|
10328
10709
|
if (ts11.isStringLiteral(attr.initializer)) {
|
|
10329
|
-
return AttrValueOf.literal(attr.initializer.text);
|
|
10710
|
+
return AttrValueOf.literal(decodeEntities(attr.initializer.text));
|
|
10330
10711
|
}
|
|
10331
10712
|
if (ts11.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
10332
10713
|
let expr = attr.initializer.expression;
|
|
@@ -10426,7 +10807,7 @@ function parseTemplateLiteral(expr, ctx2) {
|
|
|
10426
10807
|
}
|
|
10427
10808
|
function tryResolveTemplateSpanFromConst(expr, ctx2) {
|
|
10428
10809
|
if (ts11.isIdentifier(expr)) {
|
|
10429
|
-
const constInfo = findLocalConst(expr.text, ctx2);
|
|
10810
|
+
const constInfo = findLocalConst(expr.text, ctx2.analyzer);
|
|
10430
10811
|
if (!constInfo) return null;
|
|
10431
10812
|
const ast = parseConstInitializer(constInfo);
|
|
10432
10813
|
if (!ast) return null;
|
|
@@ -10437,7 +10818,7 @@ function tryResolveTemplateSpanFromConst(expr, ctx2) {
|
|
|
10437
10818
|
}
|
|
10438
10819
|
if (ts11.isElementAccessExpression(expr)) {
|
|
10439
10820
|
if (!ts11.isIdentifier(expr.expression)) return null;
|
|
10440
|
-
const constInfo = findLocalConst(expr.expression.text, ctx2);
|
|
10821
|
+
const constInfo = findLocalConst(expr.expression.text, ctx2.analyzer);
|
|
10441
10822
|
if (!constInfo) return null;
|
|
10442
10823
|
const ast = parseConstInitializer(constInfo);
|
|
10443
10824
|
if (!ast || !ts11.isObjectLiteralExpression(ast)) return null;
|
|
@@ -10459,15 +10840,15 @@ function tryResolveTemplateSpanFromConst(expr, ctx2) {
|
|
|
10459
10840
|
}
|
|
10460
10841
|
return null;
|
|
10461
10842
|
}
|
|
10462
|
-
function findLocalConst(name2,
|
|
10463
|
-
const matches =
|
|
10843
|
+
function findLocalConst(name2, analyzer) {
|
|
10844
|
+
const matches = analyzer.localConstants.filter((c) => c.name === name2);
|
|
10464
10845
|
if (matches.length === 0) return void 0;
|
|
10465
10846
|
const fnScoped = matches.filter((c) => !c.isModule);
|
|
10466
10847
|
const pool = fnScoped.length > 0 ? fnScoped : matches;
|
|
10467
10848
|
return pool[pool.length - 1];
|
|
10468
10849
|
}
|
|
10469
|
-
function findLocalFunction(name2,
|
|
10470
|
-
const matches =
|
|
10850
|
+
function findLocalFunction(name2, analyzer) {
|
|
10851
|
+
const matches = analyzer.localFunctions.filter((f) => f.name === name2);
|
|
10471
10852
|
if (matches.length === 0) return void 0;
|
|
10472
10853
|
const fnScoped = matches.filter((f) => !f.isModule);
|
|
10473
10854
|
const pool = fnScoped.length > 0 ? fnScoped : matches;
|
|
@@ -10502,7 +10883,8 @@ function hasDynamicTagBinding(name2, sourceFile) {
|
|
|
10502
10883
|
return found;
|
|
10503
10884
|
}
|
|
10504
10885
|
function tryResolveIdentifierAsTemplateLiteral(ident, ctx2) {
|
|
10505
|
-
|
|
10886
|
+
if (ctx2.loopParams.has(ident.text)) return null;
|
|
10887
|
+
const constInfo = findLocalConst(ident.text, ctx2.analyzer);
|
|
10506
10888
|
if (!constInfo) return null;
|
|
10507
10889
|
const ast = parseConstInitializer(constInfo);
|
|
10508
10890
|
if (!ast) return null;
|
|
@@ -10594,8 +10976,8 @@ function tryDesugarInterleaveTaggedTemplate(expr, ctx2) {
|
|
|
10594
10976
|
return rewritten ?? expr;
|
|
10595
10977
|
}
|
|
10596
10978
|
function resolveInterleaveTagIdentifier(name2, ctx2) {
|
|
10597
|
-
const constInfo = findLocalConst(name2, ctx2);
|
|
10598
|
-
const fnInfo = findLocalFunction(name2, ctx2);
|
|
10979
|
+
const constInfo = findLocalConst(name2, ctx2.analyzer);
|
|
10980
|
+
const fnInfo = findLocalFunction(name2, ctx2.analyzer);
|
|
10599
10981
|
if (constInfo && fnInfo) return null;
|
|
10600
10982
|
if (constInfo) {
|
|
10601
10983
|
const ast = parseConstInitializer(constInfo);
|
|
@@ -11094,7 +11476,7 @@ function buildIfStatementChain(analyzer, ctx2) {
|
|
|
11094
11476
|
}
|
|
11095
11477
|
return alternate;
|
|
11096
11478
|
}
|
|
11097
|
-
var CLIENT_DIRECTIVE_INTERIOR_RE2, BLOCK_COMMENT_RE2, constInitializerCache, functionInfoExprCache;
|
|
11479
|
+
var CLIENT_DIRECTIVE_INTERIOR_RE2, BLOCK_COMMENT_RE2, EMPTY_BOUND, constInitializerCache, functionInfoExprCache;
|
|
11098
11480
|
var init_jsx_to_ir = __esm({
|
|
11099
11481
|
"../jsx/src/jsx-to-ir.ts"() {
|
|
11100
11482
|
"use strict";
|
|
@@ -11113,6 +11495,7 @@ var init_jsx_to_ir = __esm({
|
|
|
11113
11495
|
init_src();
|
|
11114
11496
|
CLIENT_DIRECTIVE_INTERIOR_RE2 = /^\s*@client\s*$/;
|
|
11115
11497
|
BLOCK_COMMENT_RE2 = /\/\*([\s\S]*?)\*\//g;
|
|
11498
|
+
EMPTY_BOUND = /* @__PURE__ */ new Set();
|
|
11116
11499
|
constInitializerCache = /* @__PURE__ */ new WeakMap();
|
|
11117
11500
|
functionInfoExprCache = /* @__PURE__ */ new WeakMap();
|
|
11118
11501
|
}
|
|
@@ -11345,6 +11728,12 @@ function collectLoopChildEventsWithNesting(node, initialNestingStack = []) {
|
|
|
11345
11728
|
depth: scope.nestingStack.length + 1,
|
|
11346
11729
|
array: l.array,
|
|
11347
11730
|
param: l.param,
|
|
11731
|
+
// Event-delegation metadata only (see the field comment on
|
|
11732
|
+
// `NestedLoop.index`) — threaded for type-completeness (#2218)
|
|
11733
|
+
// so this record stays a structurally-valid `NestedLoop`, even
|
|
11734
|
+
// though delegated-handler index binding is handled separately
|
|
11735
|
+
// by `indexBindingLine` in `stringify/event-delegation.ts` (#2189).
|
|
11736
|
+
index: l.index,
|
|
11348
11737
|
key: l.key,
|
|
11349
11738
|
markerId: l.markerId,
|
|
11350
11739
|
containerSlotId: scope.lastElementSlotId,
|
|
@@ -11441,89 +11830,239 @@ var init_reactivity = __esm({
|
|
|
11441
11830
|
}
|
|
11442
11831
|
});
|
|
11443
11832
|
|
|
11444
|
-
// ../jsx/src/ir-to-client-js/
|
|
11445
|
-
function
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
11449
|
-
|
|
11450
|
-
|
|
11451
|
-
|
|
11452
|
-
case "text":
|
|
11453
|
-
return 0;
|
|
11454
|
-
case "expression":
|
|
11455
|
-
return EMPTY_RENDER_EXPRS.has(node.expr.trim()) ? 0 : null;
|
|
11456
|
-
case "loop":
|
|
11457
|
-
if (node.bodyIsItemConditional || node.method === "flatMap") return null;
|
|
11458
|
-
return `(${buildLoopChainExpr({
|
|
11459
|
-
base: node.array,
|
|
11460
|
-
sortComparator: node.sortComparator,
|
|
11461
|
-
filterPredicate: node.filterPredicate,
|
|
11462
|
-
chainOrder: node.chainOrder
|
|
11463
|
-
})}).length`;
|
|
11464
|
-
case "conditional": {
|
|
11465
|
-
const t = domElementCount(node.whenTrue);
|
|
11466
|
-
const f = domElementCount(node.whenFalse);
|
|
11467
|
-
if (t === null || f === null) return null;
|
|
11468
|
-
if (typeof t === "number" && typeof f === "number" && t === f) return t;
|
|
11469
|
-
return `(${node.condition} ? ${t} : ${f})`;
|
|
11470
|
-
}
|
|
11471
|
-
case "fragment":
|
|
11472
|
-
return sumElementCounts(node.children);
|
|
11473
|
-
default:
|
|
11474
|
-
return null;
|
|
11833
|
+
// ../jsx/src/ir-to-client-js/control-flow/stringify/template-parse.ts
|
|
11834
|
+
function templateRootIsSvg(template) {
|
|
11835
|
+
const stripped = stripLeadingNonContent(template);
|
|
11836
|
+
const m = stripped.match(/^<\s*([A-Za-z][A-Za-z0-9-]*)/);
|
|
11837
|
+
if (m) {
|
|
11838
|
+
const tag = m[1];
|
|
11839
|
+
if (SVG_ROOT_TAGS.has(tag)) return true;
|
|
11840
|
+
return SVG_ROOT_TAGS.has(tag.toLowerCase());
|
|
11475
11841
|
}
|
|
11842
|
+
const branches = extractConditionalBranchTemplates(stripped);
|
|
11843
|
+
if (branches === null || branches.length === 0) return false;
|
|
11844
|
+
return branches.every(templateRootIsSvg);
|
|
11476
11845
|
}
|
|
11477
|
-
function
|
|
11478
|
-
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11846
|
+
function multiRootTemplateNeedsSvgWrap(template) {
|
|
11847
|
+
const m = stripLeadingNonContent(template).match(/^<\s*([A-Za-z][A-Za-z0-9-]*)/);
|
|
11848
|
+
if (m && m[1].toLowerCase() === "svg") return false;
|
|
11849
|
+
return templateRootIsSvg(template);
|
|
11850
|
+
}
|
|
11851
|
+
function stripLeadingNonContent(template) {
|
|
11852
|
+
let s = template.trimStart();
|
|
11853
|
+
while (s.startsWith("<!--")) {
|
|
11854
|
+
const end2 = s.indexOf("-->");
|
|
11855
|
+
if (end2 < 0) return s;
|
|
11856
|
+
s = s.slice(end2 + 3).trimStart();
|
|
11485
11857
|
}
|
|
11486
|
-
|
|
11487
|
-
const parts = staticCount > 0 ? [String(staticCount), ...dynamic] : dynamic;
|
|
11488
|
-
return parts.length === 1 ? parts[0] : `(${parts.join(" + ")})`;
|
|
11858
|
+
return s;
|
|
11489
11859
|
}
|
|
11490
|
-
function
|
|
11491
|
-
|
|
11860
|
+
function extractConditionalBranchTemplates(template) {
|
|
11861
|
+
if (!template.startsWith("${")) return null;
|
|
11862
|
+
const exprEnd = findInterpolationEnd(template, 2);
|
|
11863
|
+
if (exprEnd < 0) return null;
|
|
11864
|
+
const trailing = stripLeadingNonContent(template.slice(exprEnd + 1));
|
|
11865
|
+
if (trailing.length > 0) return null;
|
|
11866
|
+
const expr = template.slice(2, exprEnd);
|
|
11867
|
+
return findTopLevelTemplateLiterals(expr);
|
|
11492
11868
|
}
|
|
11493
|
-
function
|
|
11494
|
-
|
|
11495
|
-
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
if (preceding.length > 0 && !offsets.has(child)) {
|
|
11499
|
-
offsets.set(child, [...preceding]);
|
|
11500
|
-
}
|
|
11501
|
-
preceding.push(child);
|
|
11502
|
-
} else if (child.type === "fragment" || child.type === "provider" || child.type === "async") {
|
|
11503
|
-
recordRun(child.children, preceding);
|
|
11504
|
-
} else {
|
|
11505
|
-
preceding.push(child);
|
|
11506
|
-
}
|
|
11507
|
-
}
|
|
11508
|
-
};
|
|
11509
|
-
const containerVisit = ({ node, descend }) => {
|
|
11510
|
-
recordRun(node.children, []);
|
|
11511
|
-
descend();
|
|
11512
|
-
};
|
|
11513
|
-
walkIR(root2, null, {
|
|
11514
|
-
element: containerVisit,
|
|
11515
|
-
component: containerVisit,
|
|
11516
|
-
fragment: containerVisit,
|
|
11517
|
-
provider: containerVisit,
|
|
11518
|
-
async: containerVisit
|
|
11519
|
-
// `loop` / `conditional` / `if-statement` are not flat sibling
|
|
11520
|
-
// containers (their children are item bodies / branches), and leaves
|
|
11521
|
-
// (text / expression / slot) have no children — all rely on walkIR's
|
|
11522
|
-
// default descent with the same scope.
|
|
11523
|
-
});
|
|
11524
|
-
return offsets;
|
|
11869
|
+
function emitTemplateCloneInline(template) {
|
|
11870
|
+
if (templateRootIsSvg(template)) {
|
|
11871
|
+
return `const __tpl = document.createElement('template'); __tpl.innerHTML = \`<svg>${template}</svg>\`; return __tpl.content.firstElementChild.firstElementChild.cloneNode(true)`;
|
|
11872
|
+
}
|
|
11873
|
+
return `const __tpl = document.createElement('template'); __tpl.innerHTML = \`${template}\`; return __tpl.content.firstElementChild.cloneNode(true)`;
|
|
11525
11874
|
}
|
|
11526
|
-
function
|
|
11875
|
+
function emitHoistedTemplateDecl(lines, indent, tplVar, skeletonTemplate) {
|
|
11876
|
+
const isSvg = templateRootIsSvg(skeletonTemplate);
|
|
11877
|
+
const html = isSvg ? `<svg>${skeletonTemplate}</svg>` : skeletonTemplate;
|
|
11878
|
+
lines.push(`${indent}const ${tplVar} = document.createElement('template')`);
|
|
11879
|
+
lines.push(`${indent}${tplVar}.innerHTML = \`${html}\``);
|
|
11880
|
+
}
|
|
11881
|
+
function hoistedCloneExpr(tplVar, skeletonTemplate) {
|
|
11882
|
+
return templateRootIsSvg(skeletonTemplate) ? `${tplVar}.content.firstElementChild.firstElementChild.cloneNode(true)` : `${tplVar}.content.firstElementChild.cloneNode(true)`;
|
|
11883
|
+
}
|
|
11884
|
+
function emitTemplateCloneLines(template, indent) {
|
|
11885
|
+
if (templateRootIsSvg(template)) {
|
|
11886
|
+
return [
|
|
11887
|
+
`${indent}const __tpl = document.createElement('template')`,
|
|
11888
|
+
`${indent}__tpl.innerHTML = \`<svg>${template}</svg>\``,
|
|
11889
|
+
`${indent}return __tpl.content.firstElementChild.firstElementChild.cloneNode(true)`
|
|
11890
|
+
];
|
|
11891
|
+
}
|
|
11892
|
+
return [
|
|
11893
|
+
`${indent}const __tpl = document.createElement('template')`,
|
|
11894
|
+
`${indent}__tpl.innerHTML = \`${template}\``,
|
|
11895
|
+
`${indent}return __tpl.content.firstElementChild.cloneNode(true)`
|
|
11896
|
+
];
|
|
11897
|
+
}
|
|
11898
|
+
function emitLoopItemElementSetup(lines, opts) {
|
|
11899
|
+
const { template, bodyIsMultiRoot, indent, singleRootLayout } = opts;
|
|
11900
|
+
const innerIndent = indent + " ";
|
|
11901
|
+
if (bodyIsMultiRoot) {
|
|
11902
|
+
lines.push(`${indent}let __el, __extras`);
|
|
11903
|
+
lines.push(`${indent}if (__existing) {`);
|
|
11904
|
+
lines.push(`${innerIndent}__el = __existing`);
|
|
11905
|
+
lines.push(`${indent}} else {`);
|
|
11906
|
+
for (const ln of emitMultiRootTemplateCloneLines(template, innerIndent, "__el", "__extras")) {
|
|
11907
|
+
lines.push(ln);
|
|
11908
|
+
}
|
|
11909
|
+
lines.push(`${innerIndent}__el.__bfExtras = __extras`);
|
|
11910
|
+
lines.push(`${indent}}`);
|
|
11911
|
+
return;
|
|
11912
|
+
}
|
|
11913
|
+
if (singleRootLayout === "inline") {
|
|
11914
|
+
const cloneExpr = emitTemplateCloneInline(template);
|
|
11915
|
+
lines.push(`${indent}const __el = __existing ?? (() => { ${cloneExpr} })()`);
|
|
11916
|
+
return;
|
|
11917
|
+
}
|
|
11918
|
+
lines.push(`${indent}const __el = __existing ?? (() => {`);
|
|
11919
|
+
for (const ln of emitTemplateCloneLines(template, innerIndent)) lines.push(ln);
|
|
11920
|
+
lines.push(`${indent}})()`);
|
|
11921
|
+
}
|
|
11922
|
+
function emitMultiRootTemplateCloneLines(template, indent, varEl, varExtras) {
|
|
11923
|
+
const isSvg = multiRootTemplateNeedsSvgWrap(template);
|
|
11924
|
+
const innerHtmlExpr = isSvg ? `\`<svg>${template}</svg>\`` : `\`${template}\``;
|
|
11925
|
+
const parentExpr = isSvg ? `__tpl.content.firstElementChild` : `__tpl.content`;
|
|
11926
|
+
return [
|
|
11927
|
+
`${indent}const __tpl = document.createElement('template')`,
|
|
11928
|
+
`${indent}__tpl.innerHTML = ${innerHtmlExpr}`,
|
|
11929
|
+
`${indent}${varEl} = ${parentExpr}.firstElementChild.cloneNode(true)`,
|
|
11930
|
+
`${indent}${varExtras} = []`,
|
|
11931
|
+
`${indent}{ let __sib = ${parentExpr}.firstElementChild.nextElementSibling; while (__sib) { ${varExtras}.push(__sib.cloneNode(true)); __sib = __sib.nextElementSibling } }`
|
|
11932
|
+
];
|
|
11933
|
+
}
|
|
11934
|
+
var SVG_ROOT_TAGS;
|
|
11935
|
+
var init_template_parse = __esm({
|
|
11936
|
+
"../jsx/src/ir-to-client-js/control-flow/stringify/template-parse.ts"() {
|
|
11937
|
+
"use strict";
|
|
11938
|
+
init_js_scanner();
|
|
11939
|
+
SVG_ROOT_TAGS = /* @__PURE__ */ new Set([
|
|
11940
|
+
"svg",
|
|
11941
|
+
"path",
|
|
11942
|
+
"circle",
|
|
11943
|
+
"rect",
|
|
11944
|
+
"line",
|
|
11945
|
+
"polyline",
|
|
11946
|
+
"polygon",
|
|
11947
|
+
"ellipse",
|
|
11948
|
+
"text",
|
|
11949
|
+
"tspan",
|
|
11950
|
+
"textPath",
|
|
11951
|
+
"g",
|
|
11952
|
+
"defs",
|
|
11953
|
+
"use",
|
|
11954
|
+
"symbol",
|
|
11955
|
+
"switch",
|
|
11956
|
+
"clipPath",
|
|
11957
|
+
"mask",
|
|
11958
|
+
"marker",
|
|
11959
|
+
"pattern",
|
|
11960
|
+
"linearGradient",
|
|
11961
|
+
"radialGradient",
|
|
11962
|
+
"stop",
|
|
11963
|
+
"image",
|
|
11964
|
+
"foreignObject",
|
|
11965
|
+
"filter",
|
|
11966
|
+
"feBlend",
|
|
11967
|
+
"feColorMatrix",
|
|
11968
|
+
"feComposite",
|
|
11969
|
+
"feFlood",
|
|
11970
|
+
"feGaussianBlur",
|
|
11971
|
+
"feMerge",
|
|
11972
|
+
"feMergeNode",
|
|
11973
|
+
"feMorphology",
|
|
11974
|
+
"feOffset",
|
|
11975
|
+
"feTurbulence",
|
|
11976
|
+
"animate",
|
|
11977
|
+
"animateTransform",
|
|
11978
|
+
"animateMotion"
|
|
11979
|
+
]);
|
|
11980
|
+
}
|
|
11981
|
+
});
|
|
11982
|
+
|
|
11983
|
+
// ../jsx/src/ir-to-client-js/collect-elements.ts
|
|
11984
|
+
function domElementCount(node) {
|
|
11985
|
+
switch (node.type) {
|
|
11986
|
+
case "element":
|
|
11987
|
+
case "component":
|
|
11988
|
+
case "provider":
|
|
11989
|
+
case "async":
|
|
11990
|
+
return 1;
|
|
11991
|
+
case "text":
|
|
11992
|
+
return 0;
|
|
11993
|
+
case "expression":
|
|
11994
|
+
return EMPTY_RENDER_EXPRS.has(node.expr.trim()) ? 0 : null;
|
|
11995
|
+
case "loop":
|
|
11996
|
+
if (node.bodyIsItemConditional || node.method === "flatMap") return null;
|
|
11997
|
+
return `(${buildLoopChainExpr({
|
|
11998
|
+
base: node.array,
|
|
11999
|
+
sortComparator: node.sortComparator,
|
|
12000
|
+
filterPredicate: node.filterPredicate,
|
|
12001
|
+
chainOrder: node.chainOrder
|
|
12002
|
+
})}).length`;
|
|
12003
|
+
case "conditional": {
|
|
12004
|
+
const t = domElementCount(node.whenTrue);
|
|
12005
|
+
const f = domElementCount(node.whenFalse);
|
|
12006
|
+
if (t === null || f === null) return null;
|
|
12007
|
+
if (typeof t === "number" && typeof f === "number" && t === f) return t;
|
|
12008
|
+
return `(${node.condition} ? ${t} : ${f})`;
|
|
12009
|
+
}
|
|
12010
|
+
case "fragment":
|
|
12011
|
+
return sumElementCounts(node.children);
|
|
12012
|
+
default:
|
|
12013
|
+
return null;
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
function sumElementCounts(nodes) {
|
|
12017
|
+
let staticCount = 0;
|
|
12018
|
+
const dynamic = [];
|
|
12019
|
+
for (const n of nodes) {
|
|
12020
|
+
const c = domElementCount(n);
|
|
12021
|
+
if (c === null) return null;
|
|
12022
|
+
if (typeof c === "number") staticCount += c;
|
|
12023
|
+
else dynamic.push(c);
|
|
12024
|
+
}
|
|
12025
|
+
if (dynamic.length === 0) return staticCount;
|
|
12026
|
+
const parts = staticCount > 0 ? [String(staticCount), ...dynamic] : dynamic;
|
|
12027
|
+
return parts.length === 1 ? parts[0] : `(${parts.join(" + ")})`;
|
|
12028
|
+
}
|
|
12029
|
+
function legacyElementCount(node) {
|
|
12030
|
+
return node.type === "element" || node.type === "component" || node.type === "provider" || node.type === "async" || node.type === "text" || node.type === "expression" && !node.reactive || node.type === "conditional" ? 1 : 0;
|
|
12031
|
+
}
|
|
12032
|
+
function computeLoopSiblingOffsets(root2) {
|
|
12033
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
12034
|
+
const recordRun = (children2, preceding) => {
|
|
12035
|
+
for (const child of children2) {
|
|
12036
|
+
if (child.type === "loop") {
|
|
12037
|
+
if (preceding.length > 0 && !offsets.has(child)) {
|
|
12038
|
+
offsets.set(child, [...preceding]);
|
|
12039
|
+
}
|
|
12040
|
+
preceding.push(child);
|
|
12041
|
+
} else if (child.type === "fragment" || child.type === "provider" || child.type === "async") {
|
|
12042
|
+
recordRun(child.children, preceding);
|
|
12043
|
+
} else {
|
|
12044
|
+
preceding.push(child);
|
|
12045
|
+
}
|
|
12046
|
+
}
|
|
12047
|
+
};
|
|
12048
|
+
const containerVisit = ({ node, descend }) => {
|
|
12049
|
+
recordRun(node.children, []);
|
|
12050
|
+
descend();
|
|
12051
|
+
};
|
|
12052
|
+
walkIR(root2, null, {
|
|
12053
|
+
element: containerVisit,
|
|
12054
|
+
component: containerVisit,
|
|
12055
|
+
fragment: containerVisit,
|
|
12056
|
+
provider: containerVisit,
|
|
12057
|
+
async: containerVisit
|
|
12058
|
+
// `loop` / `conditional` / `if-statement` are not flat sibling
|
|
12059
|
+
// containers (their children are item bodies / branches), and leaves
|
|
12060
|
+
// (text / expression / slot) have no children — all rely on walkIR's
|
|
12061
|
+
// default descent with the same scope.
|
|
12062
|
+
});
|
|
12063
|
+
return offsets;
|
|
12064
|
+
}
|
|
12065
|
+
function resolveLoopOffset(preceding) {
|
|
11527
12066
|
if (!preceding || preceding.length === 0) return void 0;
|
|
11528
12067
|
let staticCount = 0;
|
|
11529
12068
|
const dynamicTerms = [];
|
|
@@ -11610,11 +12149,13 @@ function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx2, options2
|
|
|
11610
12149
|
arrayFreeIdentifiers: n.arrayFreeIdentifiers,
|
|
11611
12150
|
param: n.param,
|
|
11612
12151
|
paramBindings: n.paramBindings,
|
|
12152
|
+
index: n.index,
|
|
11613
12153
|
key: n.key,
|
|
11614
12154
|
markerId: n.markerId,
|
|
11615
12155
|
bodyIsMultiRoot: n.bodyIsMultiRoot,
|
|
11616
12156
|
bodyIsItemConditional: n.bodyIsItemConditional,
|
|
11617
12157
|
iterationShape: n.iterationShape,
|
|
12158
|
+
objectIteration: n.objectIteration,
|
|
11618
12159
|
containerSlotId: scope.parentSlotId,
|
|
11619
12160
|
template,
|
|
11620
12161
|
mapPreamble: n.mapPreamble,
|
|
@@ -11790,6 +12331,7 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
|
|
|
11790
12331
|
let template = "";
|
|
11791
12332
|
let staticItemTemplate;
|
|
11792
12333
|
let skeletonTemplate;
|
|
12334
|
+
let skeletonPaths;
|
|
11793
12335
|
if (l.childComponent) {
|
|
11794
12336
|
template = "";
|
|
11795
12337
|
if (l.isStaticArray && l.children[0]) {
|
|
@@ -11809,10 +12351,14 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
|
|
|
11809
12351
|
if (l.isStaticArray) {
|
|
11810
12352
|
staticItemTemplate = useElementReconciliation ? irToPlaceholderTemplate(l.children[0], buildRestSpreadNames(ctx2), 0) : irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx2), 0);
|
|
11811
12353
|
} else if (!useElementReconciliation && !l.bodyIsMultiRoot && !l.bodyIsItemConditional) {
|
|
11812
|
-
|
|
12354
|
+
const skeletonSafeSlots = {
|
|
11813
12355
|
reactiveAttrKeys: new Set(bindings.reactiveAttrs.map((a) => `${a.childSlotId}::${a.attrName}`)),
|
|
11814
12356
|
reactiveTextSlotIds: new Set(bindings.reactiveTexts.map((t) => t.slotId))
|
|
11815
|
-
}
|
|
12357
|
+
};
|
|
12358
|
+
skeletonTemplate = buildLoopSkeletonTemplate(l.children[0], skeletonSafeSlots) ?? void 0;
|
|
12359
|
+
if (skeletonTemplate && !templateRootIsSvg(skeletonTemplate)) {
|
|
12360
|
+
skeletonPaths = computeSkeletonSlotPaths(l.children[0], skeletonSafeSlots) ?? void 0;
|
|
12361
|
+
}
|
|
11816
12362
|
}
|
|
11817
12363
|
}
|
|
11818
12364
|
ctx2.loopElements.push({
|
|
@@ -11828,9 +12374,11 @@ function collectElements(node, ctx2, siblingOffsets, insideConditional = false)
|
|
|
11828
12374
|
bodyIsMultiRoot: l.bodyIsMultiRoot,
|
|
11829
12375
|
bodyIsItemConditional: l.bodyIsItemConditional,
|
|
11830
12376
|
iterationShape: l.iterationShape,
|
|
12377
|
+
objectIteration: l.objectIteration,
|
|
11831
12378
|
template,
|
|
11832
12379
|
staticItemTemplate,
|
|
11833
12380
|
skeletonTemplate,
|
|
12381
|
+
skeletonPaths,
|
|
11834
12382
|
childEventHandlers: childHandlers,
|
|
11835
12383
|
bindings,
|
|
11836
12384
|
childComponent: l.childComponent,
|
|
@@ -12025,6 +12573,7 @@ function collectBranchLoops(node, ctx2, siblingOffsets) {
|
|
|
12025
12573
|
bodyIsMultiRoot: n.bodyIsMultiRoot,
|
|
12026
12574
|
bodyIsItemConditional: n.bodyIsItemConditional,
|
|
12027
12575
|
iterationShape: n.iterationShape,
|
|
12576
|
+
objectIteration: n.objectIteration,
|
|
12028
12577
|
template: childTemplate,
|
|
12029
12578
|
containerSlotId: containerSlot,
|
|
12030
12579
|
mapPreamble: n.mapPreamble ?? null,
|
|
@@ -12149,6 +12698,7 @@ var init_collect_elements = __esm({
|
|
|
12149
12698
|
init_utils();
|
|
12150
12699
|
init_reactivity();
|
|
12151
12700
|
init_html_template();
|
|
12701
|
+
init_template_parse();
|
|
12152
12702
|
init_prop_handling();
|
|
12153
12703
|
init_walker();
|
|
12154
12704
|
init_loop_chain();
|
|
@@ -12724,6 +13274,7 @@ var init_imports = __esm({
|
|
|
12724
13274
|
"__slot",
|
|
12725
13275
|
"__bfSlot",
|
|
12726
13276
|
"__bfText",
|
|
13277
|
+
"tAfter",
|
|
12727
13278
|
// Profile mode (#1690, SR3) — turn-boundary markers around event handlers.
|
|
12728
13279
|
"beginTurn",
|
|
12729
13280
|
"endTurn"
|
|
@@ -14557,9 +15108,49 @@ var init_event_listener = __esm({
|
|
|
14557
15108
|
// ../jsx/src/ir-to-client-js/control-flow/shared.ts
|
|
14558
15109
|
function loopKeyFn(loop) {
|
|
14559
15110
|
if (loop.key === null) return "null";
|
|
14560
|
-
const params =
|
|
15111
|
+
const params = `${loop.param}${loop.index ? `, ${loop.index}` : ""}`;
|
|
14561
15112
|
return `(${params}) => String(${loop.key})`;
|
|
14562
15113
|
}
|
|
15114
|
+
function nestedLoopReferencesIndex(inner, comps, events) {
|
|
15115
|
+
const index = inner.index;
|
|
15116
|
+
if (!index) return false;
|
|
15117
|
+
const exprRefs = (text, precomputed) => {
|
|
15118
|
+
if (precomputed) return precomputed.has(index);
|
|
15119
|
+
if (!text) return false;
|
|
15120
|
+
return extractFreeIdentifiersFromText(text).has(index);
|
|
15121
|
+
};
|
|
15122
|
+
if (exprRefs(inner.key)) return true;
|
|
15123
|
+
for (const t of inner.bindings.reactiveTexts) {
|
|
15124
|
+
if (exprRefs(t.expression, t.freeIdentifiers)) return true;
|
|
15125
|
+
}
|
|
15126
|
+
for (const a of inner.bindings.reactiveAttrs) {
|
|
15127
|
+
if (exprRefs(a.expression, a.freeIdentifiers)) return true;
|
|
15128
|
+
}
|
|
15129
|
+
if (inner.mapPreamble && extractFreeIdentifiersFromStatementText(inner.mapPreamble).has(index)) return true;
|
|
15130
|
+
if (inner.template && extractFreeIdentifiersFromTemplateText(inner.template).has(index)) return true;
|
|
15131
|
+
for (const ev of events) {
|
|
15132
|
+
if (exprRefs(ev.handler)) return true;
|
|
15133
|
+
}
|
|
15134
|
+
for (const r2 of inner.bindings.refs) {
|
|
15135
|
+
if (exprRefs(r2.callback)) return true;
|
|
15136
|
+
}
|
|
15137
|
+
for (const c of comps) {
|
|
15138
|
+
for (const p of c.props) {
|
|
15139
|
+
if (exprRefs(attrValueToString(p.value))) return true;
|
|
15140
|
+
}
|
|
15141
|
+
if (c.children?.length) {
|
|
15142
|
+
const childrenExpr = irChildrenToJsExpr(c.children);
|
|
15143
|
+
if (childrenExpr && exprRefs(childrenExpr)) return true;
|
|
15144
|
+
}
|
|
15145
|
+
}
|
|
15146
|
+
return false;
|
|
15147
|
+
}
|
|
15148
|
+
function nestedLoopIndexAlias(inner, syntheticIndexVar, paramHead, comps, events) {
|
|
15149
|
+
const index = inner.index;
|
|
15150
|
+
if (!index || index === paramHead) return null;
|
|
15151
|
+
if (!nestedLoopReferencesIndex(inner, comps, events)) return null;
|
|
15152
|
+
return `const ${index} = ${syntheticIndexVar}`;
|
|
15153
|
+
}
|
|
14563
15154
|
function buildChildRefBindings(refs, loopParam, loopParamBindings) {
|
|
14564
15155
|
if (refs.length === 0) return [];
|
|
14565
15156
|
return refs.map((r2) => ({
|
|
@@ -14689,6 +15280,7 @@ var init_shared = __esm({
|
|
|
14689
15280
|
init_event_listener();
|
|
14690
15281
|
init_component_scope();
|
|
14691
15282
|
init_src();
|
|
15283
|
+
init_csr_substitute();
|
|
14692
15284
|
}
|
|
14693
15285
|
});
|
|
14694
15286
|
|
|
@@ -14752,6 +15344,7 @@ function buildOuterNestedPlan(elem, comp) {
|
|
|
14752
15344
|
}
|
|
14753
15345
|
function buildInnerLoopNestedPlan(elem, innerLoop, innerComps) {
|
|
14754
15346
|
const outerIndexParam = elem.index || "__idx";
|
|
15347
|
+
const innerIndexParam = innerLoop.index || "__innerIdx";
|
|
14755
15348
|
const comps = innerComps.map((comp) => ({
|
|
14756
15349
|
componentName: comp.name,
|
|
14757
15350
|
selector: buildCompSelector(comp),
|
|
@@ -14768,7 +15361,8 @@ function buildInnerLoopNestedPlan(elem, innerLoop, innerComps) {
|
|
|
14768
15361
|
innerContainerSlotId: innerLoop.containerSlotId ?? null,
|
|
14769
15362
|
innerArrayExpr: innerLoop.array,
|
|
14770
15363
|
innerParam: innerLoop.param,
|
|
14771
|
-
|
|
15364
|
+
innerIndexParam,
|
|
15365
|
+
innerOffsetExpr: buildLoopChildIndexExpr(innerIndexParam, innerLoop.offset),
|
|
14772
15366
|
innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
|
|
14773
15367
|
depth: innerLoop.depth,
|
|
14774
15368
|
comps
|
|
@@ -14785,9 +15379,14 @@ function buildComponentRootedInnerLoopPlan(elem, innerLoop, innerComps) {
|
|
|
14785
15379
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
14786
15380
|
outerArrayExpr: elem.array,
|
|
14787
15381
|
outerParam: elem.param,
|
|
15382
|
+
// Declared index names only (#2231) — the zip shape never indexes by
|
|
15383
|
+
// position, so there's no synthetic fallback and index-less loops keep
|
|
15384
|
+
// byte-identical output.
|
|
15385
|
+
outerIndexParam: elem.index,
|
|
14788
15386
|
outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
|
|
14789
15387
|
innerArrayExpr: innerLoop.array,
|
|
14790
15388
|
innerParam: innerLoop.param,
|
|
15389
|
+
innerIndexParam: innerLoop.index,
|
|
14791
15390
|
innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
|
|
14792
15391
|
depth: innerLoop.depth,
|
|
14793
15392
|
comps
|
|
@@ -14888,6 +15487,7 @@ function emitInnerLoopNested(lines, plan) {
|
|
|
14888
15487
|
innerContainerSlotId,
|
|
14889
15488
|
innerArrayExpr,
|
|
14890
15489
|
innerParam,
|
|
15490
|
+
innerIndexParam,
|
|
14891
15491
|
innerOffsetExpr,
|
|
14892
15492
|
innerPreludeStatements,
|
|
14893
15493
|
depth,
|
|
@@ -14906,7 +15506,7 @@ function emitInnerLoopNested(lines, plan) {
|
|
|
14906
15506
|
} else {
|
|
14907
15507
|
lines.push(` const __ic = __outerEl`);
|
|
14908
15508
|
}
|
|
14909
|
-
lines.push(` ${innerArrayExpr}.forEach((${innerParam},
|
|
15509
|
+
lines.push(` ${innerArrayExpr}.forEach((${innerParam}, ${innerIndexParam}) => {`);
|
|
14910
15510
|
lines.push(` const __innerEl = __ic.children[${innerOffsetExpr}]`);
|
|
14911
15511
|
lines.push(` if (!__innerEl) return`);
|
|
14912
15512
|
for (const stmt of innerPreludeStatements) {
|
|
@@ -14927,9 +15527,11 @@ function emitComponentRootedInnerLoop(lines, plan) {
|
|
|
14927
15527
|
containerVar,
|
|
14928
15528
|
outerArrayExpr,
|
|
14929
15529
|
outerParam,
|
|
15530
|
+
outerIndexParam,
|
|
14930
15531
|
outerPreludeStatements,
|
|
14931
15532
|
innerArrayExpr,
|
|
14932
15533
|
innerParam,
|
|
15534
|
+
innerIndexParam,
|
|
14933
15535
|
innerPreludeStatements,
|
|
14934
15536
|
depth,
|
|
14935
15537
|
comps
|
|
@@ -14943,11 +15545,11 @@ function emitComponentRootedInnerLoop(lines, plan) {
|
|
|
14943
15545
|
lines.push(` const ${scopesVar(i)} = qsaChildScopes(${containerVar}, ${comp.selector})`);
|
|
14944
15546
|
lines.push(` let ${cursorVar(i)} = 0`);
|
|
14945
15547
|
});
|
|
14946
|
-
lines.push(` ${outerArrayExpr}.forEach((${outerParam}) => {`);
|
|
15548
|
+
lines.push(` ${outerArrayExpr}.forEach((${outerParam}${outerIndexParam ? `, ${outerIndexParam}` : ""}) => {`);
|
|
14947
15549
|
for (const stmt of outerPreludeStatements) {
|
|
14948
15550
|
lines.push(` ${stmt}`);
|
|
14949
15551
|
}
|
|
14950
|
-
lines.push(` ${innerArrayExpr}.forEach((${innerParam}) => {`);
|
|
15552
|
+
lines.push(` ${innerArrayExpr}.forEach((${innerParam}${innerIndexParam ? `, ${innerIndexParam}` : ""}) => {`);
|
|
14951
15553
|
for (const stmt of innerPreludeStatements) {
|
|
14952
15554
|
lines.push(` ${stmt}`);
|
|
14953
15555
|
}
|
|
@@ -15110,6 +15712,13 @@ function buildBranchInnerLoopsPlan(args2) {
|
|
|
15110
15712
|
...ev,
|
|
15111
15713
|
handler: wrapInner(ev.handler)
|
|
15112
15714
|
}));
|
|
15715
|
+
const indexAlias = nestedLoopIndexAlias(
|
|
15716
|
+
inner,
|
|
15717
|
+
`__bidxbr_${i}`,
|
|
15718
|
+
paramHead,
|
|
15719
|
+
inner.childComponents ?? [],
|
|
15720
|
+
inner.bindings.events
|
|
15721
|
+
);
|
|
15113
15722
|
const reactiveTexts = inner.bindings.reactiveTexts.map((text) => ({
|
|
15114
15723
|
slotId: text.slotId,
|
|
15115
15724
|
wrappedExpression: wrapBoth(text.expression),
|
|
@@ -15126,6 +15735,7 @@ function buildBranchInnerLoopsPlan(args2) {
|
|
|
15126
15735
|
keyFn: loopKeyFn(inner),
|
|
15127
15736
|
paramHead,
|
|
15128
15737
|
paramUnwrap,
|
|
15738
|
+
indexAlias,
|
|
15129
15739
|
wrappedTemplate: inner.template,
|
|
15130
15740
|
wrappedKey,
|
|
15131
15741
|
keyDepth: 1,
|
|
@@ -15381,7 +15991,7 @@ function buildInnerLoopsPlan(args2) {
|
|
|
15381
15991
|
const containerExpr = inner.containerSlotId ? `qsa(${parentElVar}, '[bf="${inner.containerSlotId}"]')` : parentElVar;
|
|
15382
15992
|
const refsParent = !!outerLoopParam && (inner.arrayFreeIdentifiers?.has(outerLoopParam) ?? false);
|
|
15383
15993
|
const useReactive = refsParent && !!inner.template;
|
|
15384
|
-
const emit = useReactive ? buildReactiveEmit(inner, level, wrapOuter) : buildStaticEmit(inner, level);
|
|
15994
|
+
const emit = useReactive ? buildReactiveEmit(inner, level, wrapOuter, uidSuffix) : buildStaticEmit(inner, level, uidSuffix);
|
|
15385
15995
|
const arrayExpr = useReactive ? wrapOuter(inner.array) : inner.array;
|
|
15386
15996
|
const childLevelsPlan = childLevels.length > 0 ? buildInnerLoopsPlan({
|
|
15387
15997
|
levels: childLevels,
|
|
@@ -15408,7 +16018,7 @@ function buildInnerLoopsPlan(args2) {
|
|
|
15408
16018
|
}
|
|
15409
16019
|
return plan;
|
|
15410
16020
|
}
|
|
15411
|
-
function buildReactiveEmit(inner, level, wrapOuter) {
|
|
16021
|
+
function buildReactiveEmit(inner, level, wrapOuter, uidSuffix) {
|
|
15412
16022
|
const wrapInner = (expr) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings);
|
|
15413
16023
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings);
|
|
15414
16024
|
const wrappedKey = inner.key ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings) : null;
|
|
@@ -15455,6 +16065,8 @@ function buildReactiveEmit(inner, level, wrapOuter) {
|
|
|
15455
16065
|
};
|
|
15456
16066
|
});
|
|
15457
16067
|
const preludeStatements = [];
|
|
16068
|
+
const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, paramHead, level.comps, level.events);
|
|
16069
|
+
if (indexAlias) preludeStatements.push(indexAlias);
|
|
15458
16070
|
if (paramUnwrap) preludeStatements.push(paramUnwrap);
|
|
15459
16071
|
if (inner.mapPreamble) preludeStatements.push(wrapInner(wrapOuter(inner.mapPreamble)));
|
|
15460
16072
|
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings);
|
|
@@ -15473,8 +16085,11 @@ function buildReactiveEmit(inner, level, wrapOuter) {
|
|
|
15473
16085
|
childRefs
|
|
15474
16086
|
};
|
|
15475
16087
|
}
|
|
15476
|
-
function buildStaticEmit(inner, level) {
|
|
15477
|
-
const preludeStatements =
|
|
16088
|
+
function buildStaticEmit(inner, level, uidSuffix) {
|
|
16089
|
+
const preludeStatements = [];
|
|
16090
|
+
const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, inner.param, level.comps, level.events);
|
|
16091
|
+
if (indexAlias) preludeStatements.push(indexAlias);
|
|
16092
|
+
if (inner.mapPreamble) preludeStatements.push(inner.mapPreamble);
|
|
15478
16093
|
return {
|
|
15479
16094
|
mode: "static",
|
|
15480
16095
|
rawKey: inner.key ?? null,
|
|
@@ -15631,6 +16246,7 @@ function buildDynamicLoopDelegationPlan(elem, profileComponentName) {
|
|
|
15631
16246
|
param: elem.param,
|
|
15632
16247
|
paramBindings: elem.paramBindings,
|
|
15633
16248
|
key: elem.key,
|
|
16249
|
+
index: elem.index,
|
|
15634
16250
|
mapPreamble: elem.mapPreamble ?? null
|
|
15635
16251
|
})
|
|
15636
16252
|
};
|
|
@@ -15647,6 +16263,7 @@ function buildBranchLoopDelegationPlan(loop, cv, profileComponentName) {
|
|
|
15647
16263
|
param: loop.param,
|
|
15648
16264
|
paramBindings: loop.paramBindings,
|
|
15649
16265
|
key: loop.key,
|
|
16266
|
+
index: loop.index,
|
|
15650
16267
|
mapPreamble: loop.mapPreamble ?? null
|
|
15651
16268
|
})
|
|
15652
16269
|
};
|
|
@@ -15665,7 +16282,8 @@ function buildStaticArrayDelegationPlan(elem, profileComponentName) {
|
|
|
15665
16282
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
15666
16283
|
param: elem.param,
|
|
15667
16284
|
mapPreamble: elem.mapPreamble ?? null,
|
|
15668
|
-
offset: elem.offset ?? null
|
|
16285
|
+
offset: elem.offset ?? null,
|
|
16286
|
+
indexParam: elem.index ?? null
|
|
15669
16287
|
}
|
|
15670
16288
|
};
|
|
15671
16289
|
}
|
|
@@ -15680,7 +16298,8 @@ function buildKeyedOrIndexLookup(args2) {
|
|
|
15680
16298
|
paramBindings: args2.paramBindings,
|
|
15681
16299
|
keyWithItem,
|
|
15682
16300
|
mapPreamble: args2.mapPreamble,
|
|
15683
|
-
hasBindings
|
|
16301
|
+
hasBindings,
|
|
16302
|
+
indexParam: args2.index
|
|
15684
16303
|
};
|
|
15685
16304
|
}
|
|
15686
16305
|
return {
|
|
@@ -15688,7 +16307,8 @@ function buildKeyedOrIndexLookup(args2) {
|
|
|
15688
16307
|
arrayExpr: args2.array,
|
|
15689
16308
|
param: args2.param,
|
|
15690
16309
|
mapPreamble: args2.mapPreamble,
|
|
15691
|
-
hasBindings
|
|
16310
|
+
hasBindings,
|
|
16311
|
+
indexParam: args2.index
|
|
15692
16312
|
};
|
|
15693
16313
|
}
|
|
15694
16314
|
var init_build_event_delegation = __esm({
|
|
@@ -16040,151 +16660,6 @@ var init_emit_reactive = __esm({
|
|
|
16040
16660
|
}
|
|
16041
16661
|
});
|
|
16042
16662
|
|
|
16043
|
-
// ../jsx/src/ir-to-client-js/control-flow/stringify/template-parse.ts
|
|
16044
|
-
function templateRootIsSvg(template) {
|
|
16045
|
-
const stripped = stripLeadingNonContent(template);
|
|
16046
|
-
const m = stripped.match(/^<\s*([A-Za-z][A-Za-z0-9-]*)/);
|
|
16047
|
-
if (m) {
|
|
16048
|
-
const tag = m[1];
|
|
16049
|
-
if (SVG_ROOT_TAGS.has(tag)) return true;
|
|
16050
|
-
return SVG_ROOT_TAGS.has(tag.toLowerCase());
|
|
16051
|
-
}
|
|
16052
|
-
const branches = extractConditionalBranchTemplates(stripped);
|
|
16053
|
-
if (branches === null || branches.length === 0) return false;
|
|
16054
|
-
return branches.every(templateRootIsSvg);
|
|
16055
|
-
}
|
|
16056
|
-
function stripLeadingNonContent(template) {
|
|
16057
|
-
let s = template.trimStart();
|
|
16058
|
-
while (s.startsWith("<!--")) {
|
|
16059
|
-
const end2 = s.indexOf("-->");
|
|
16060
|
-
if (end2 < 0) return s;
|
|
16061
|
-
s = s.slice(end2 + 3).trimStart();
|
|
16062
|
-
}
|
|
16063
|
-
return s;
|
|
16064
|
-
}
|
|
16065
|
-
function extractConditionalBranchTemplates(template) {
|
|
16066
|
-
if (!template.startsWith("${")) return null;
|
|
16067
|
-
const exprEnd = findInterpolationEnd(template, 2);
|
|
16068
|
-
if (exprEnd < 0) return null;
|
|
16069
|
-
const trailing = stripLeadingNonContent(template.slice(exprEnd + 1));
|
|
16070
|
-
if (trailing.length > 0) return null;
|
|
16071
|
-
const expr = template.slice(2, exprEnd);
|
|
16072
|
-
return findTopLevelTemplateLiterals(expr);
|
|
16073
|
-
}
|
|
16074
|
-
function emitTemplateCloneInline(template) {
|
|
16075
|
-
if (templateRootIsSvg(template)) {
|
|
16076
|
-
return `const __tpl = document.createElement('template'); __tpl.innerHTML = \`<svg>${template}</svg>\`; return __tpl.content.firstElementChild.firstElementChild.cloneNode(true)`;
|
|
16077
|
-
}
|
|
16078
|
-
return `const __tpl = document.createElement('template'); __tpl.innerHTML = \`${template}\`; return __tpl.content.firstElementChild.cloneNode(true)`;
|
|
16079
|
-
}
|
|
16080
|
-
function emitHoistedTemplateDecl(lines, indent, tplVar, skeletonTemplate) {
|
|
16081
|
-
const isSvg = templateRootIsSvg(skeletonTemplate);
|
|
16082
|
-
const html = isSvg ? `<svg>${skeletonTemplate}</svg>` : skeletonTemplate;
|
|
16083
|
-
lines.push(`${indent}const ${tplVar} = document.createElement('template')`);
|
|
16084
|
-
lines.push(`${indent}${tplVar}.innerHTML = \`${html}\``);
|
|
16085
|
-
}
|
|
16086
|
-
function hoistedCloneExpr(tplVar, skeletonTemplate) {
|
|
16087
|
-
return templateRootIsSvg(skeletonTemplate) ? `${tplVar}.content.firstElementChild.firstElementChild.cloneNode(true)` : `${tplVar}.content.firstElementChild.cloneNode(true)`;
|
|
16088
|
-
}
|
|
16089
|
-
function emitTemplateCloneLines(template, indent) {
|
|
16090
|
-
if (templateRootIsSvg(template)) {
|
|
16091
|
-
return [
|
|
16092
|
-
`${indent}const __tpl = document.createElement('template')`,
|
|
16093
|
-
`${indent}__tpl.innerHTML = \`<svg>${template}</svg>\``,
|
|
16094
|
-
`${indent}return __tpl.content.firstElementChild.firstElementChild.cloneNode(true)`
|
|
16095
|
-
];
|
|
16096
|
-
}
|
|
16097
|
-
return [
|
|
16098
|
-
`${indent}const __tpl = document.createElement('template')`,
|
|
16099
|
-
`${indent}__tpl.innerHTML = \`${template}\``,
|
|
16100
|
-
`${indent}return __tpl.content.firstElementChild.cloneNode(true)`
|
|
16101
|
-
];
|
|
16102
|
-
}
|
|
16103
|
-
function emitLoopItemElementSetup(lines, opts) {
|
|
16104
|
-
const { template, bodyIsMultiRoot, indent, singleRootLayout } = opts;
|
|
16105
|
-
const innerIndent = indent + " ";
|
|
16106
|
-
if (bodyIsMultiRoot) {
|
|
16107
|
-
lines.push(`${indent}let __el, __extras`);
|
|
16108
|
-
lines.push(`${indent}if (__existing) {`);
|
|
16109
|
-
lines.push(`${innerIndent}__el = __existing`);
|
|
16110
|
-
lines.push(`${indent}} else {`);
|
|
16111
|
-
for (const ln of emitMultiRootTemplateCloneLines(template, innerIndent, "__el", "__extras")) {
|
|
16112
|
-
lines.push(ln);
|
|
16113
|
-
}
|
|
16114
|
-
lines.push(`${innerIndent}__el.__bfExtras = __extras`);
|
|
16115
|
-
lines.push(`${indent}}`);
|
|
16116
|
-
return;
|
|
16117
|
-
}
|
|
16118
|
-
if (singleRootLayout === "inline") {
|
|
16119
|
-
const cloneExpr = emitTemplateCloneInline(template);
|
|
16120
|
-
lines.push(`${indent}const __el = __existing ?? (() => { ${cloneExpr} })()`);
|
|
16121
|
-
return;
|
|
16122
|
-
}
|
|
16123
|
-
lines.push(`${indent}const __el = __existing ?? (() => {`);
|
|
16124
|
-
for (const ln of emitTemplateCloneLines(template, innerIndent)) lines.push(ln);
|
|
16125
|
-
lines.push(`${indent}})()`);
|
|
16126
|
-
}
|
|
16127
|
-
function emitMultiRootTemplateCloneLines(template, indent, varEl, varExtras) {
|
|
16128
|
-
const isSvg = templateRootIsSvg(template);
|
|
16129
|
-
const innerHtmlExpr = isSvg ? `\`<svg>${template}</svg>\`` : `\`${template}\``;
|
|
16130
|
-
const parentExpr = isSvg ? `__tpl.content.firstElementChild` : `__tpl.content`;
|
|
16131
|
-
return [
|
|
16132
|
-
`${indent}const __tpl = document.createElement('template')`,
|
|
16133
|
-
`${indent}__tpl.innerHTML = ${innerHtmlExpr}`,
|
|
16134
|
-
`${indent}${varEl} = ${parentExpr}.firstElementChild.cloneNode(true)`,
|
|
16135
|
-
`${indent}${varExtras} = []`,
|
|
16136
|
-
`${indent}{ let __sib = ${parentExpr}.firstElementChild.nextElementSibling; while (__sib) { ${varExtras}.push(__sib.cloneNode(true)); __sib = __sib.nextElementSibling } }`
|
|
16137
|
-
];
|
|
16138
|
-
}
|
|
16139
|
-
var SVG_ROOT_TAGS;
|
|
16140
|
-
var init_template_parse = __esm({
|
|
16141
|
-
"../jsx/src/ir-to-client-js/control-flow/stringify/template-parse.ts"() {
|
|
16142
|
-
"use strict";
|
|
16143
|
-
init_js_scanner();
|
|
16144
|
-
SVG_ROOT_TAGS = /* @__PURE__ */ new Set([
|
|
16145
|
-
"svg",
|
|
16146
|
-
"path",
|
|
16147
|
-
"circle",
|
|
16148
|
-
"rect",
|
|
16149
|
-
"line",
|
|
16150
|
-
"polyline",
|
|
16151
|
-
"polygon",
|
|
16152
|
-
"ellipse",
|
|
16153
|
-
"text",
|
|
16154
|
-
"tspan",
|
|
16155
|
-
"textPath",
|
|
16156
|
-
"g",
|
|
16157
|
-
"defs",
|
|
16158
|
-
"use",
|
|
16159
|
-
"symbol",
|
|
16160
|
-
"switch",
|
|
16161
|
-
"clipPath",
|
|
16162
|
-
"mask",
|
|
16163
|
-
"marker",
|
|
16164
|
-
"pattern",
|
|
16165
|
-
"linearGradient",
|
|
16166
|
-
"radialGradient",
|
|
16167
|
-
"stop",
|
|
16168
|
-
"image",
|
|
16169
|
-
"foreignObject",
|
|
16170
|
-
"filter",
|
|
16171
|
-
"feBlend",
|
|
16172
|
-
"feColorMatrix",
|
|
16173
|
-
"feComposite",
|
|
16174
|
-
"feFlood",
|
|
16175
|
-
"feGaussianBlur",
|
|
16176
|
-
"feMerge",
|
|
16177
|
-
"feMergeNode",
|
|
16178
|
-
"feMorphology",
|
|
16179
|
-
"feOffset",
|
|
16180
|
-
"feTurbulence",
|
|
16181
|
-
"animate",
|
|
16182
|
-
"animateTransform",
|
|
16183
|
-
"animateMotion"
|
|
16184
|
-
]);
|
|
16185
|
-
}
|
|
16186
|
-
});
|
|
16187
|
-
|
|
16188
16663
|
// ../jsx/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts
|
|
16189
16664
|
function stringifyBranchEventBindings(lines, plan, indent) {
|
|
16190
16665
|
for (const slot of plan) {
|
|
@@ -16206,6 +16681,9 @@ function stringifyBranchInnerLoops(lines, plan, indent, pc) {
|
|
|
16206
16681
|
const uid = inner.uidSuffix;
|
|
16207
16682
|
lines.push(`${indent}{ const __bic${uid} = ${inner.containerExpr}`);
|
|
16208
16683
|
lines.push(`${indent}if (__bic${uid}) mapArray(() => ${inner.arrayExpr} || [], __bic${uid}, ${inner.keyFn}, (${inner.paramHead}, __bidx${uid}, __existing) => {`);
|
|
16684
|
+
if (inner.indexAlias) {
|
|
16685
|
+
lines.push(`${indent} ${inner.indexAlias}`);
|
|
16686
|
+
}
|
|
16209
16687
|
if (inner.paramUnwrap) {
|
|
16210
16688
|
lines.push(`${indent} ${inner.paramUnwrap}`);
|
|
16211
16689
|
}
|
|
@@ -16288,13 +16766,15 @@ var init_loop_child_arm = __esm({
|
|
|
16288
16766
|
|
|
16289
16767
|
// ../jsx/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts
|
|
16290
16768
|
function stringifyReactiveEffects(lines, plan, opts) {
|
|
16291
|
-
const { indent, elVar, bodyIsMultiRoot } = opts;
|
|
16769
|
+
const { indent, elVar, bodyIsMultiRoot, elementIndexBySlot, textIndexBySlot } = opts;
|
|
16292
16770
|
const lookup = bodyIsMultiRoot ? "qsaItem" : "qsa";
|
|
16293
16771
|
const pc = plan.profileComponentName;
|
|
16294
16772
|
const bindingBfId = (slotId) => profileBindingId(pc, slotId);
|
|
16295
16773
|
for (const slot of plan.attrSlots) {
|
|
16296
16774
|
const varName = `__ra_${varSlotId(slot.slotId)}`;
|
|
16297
|
-
|
|
16775
|
+
const pIdx = elementIndexBySlot?.get(slot.slotId);
|
|
16776
|
+
const lookupExpr = pIdx !== void 0 ? `__p ? __p[${pIdx}] : ${lookup}(${elVar}, '[bf="${slot.slotId}"]')` : `${lookup}(${elVar}, '[bf="${slot.slotId}"]')`;
|
|
16777
|
+
lines.push(`${indent}{ const ${varName} = ${lookupExpr}`);
|
|
16298
16778
|
lines.push(`${indent}if (${varName}) {`);
|
|
16299
16779
|
for (const attr of slot.attrs) {
|
|
16300
16780
|
lines.push(`${indent} createEffect(() => {`);
|
|
@@ -16306,15 +16786,19 @@ function stringifyReactiveEffects(lines, plan, opts) {
|
|
|
16306
16786
|
lines.push(`${indent}} }`);
|
|
16307
16787
|
}
|
|
16308
16788
|
for (const text of plan.outerTexts) {
|
|
16309
|
-
emitOuterText(lines, indent, elVar, text, bindingBfId(text.slotId));
|
|
16789
|
+
emitOuterText(lines, indent, elVar, text, bindingBfId(text.slotId), textIndexBySlot?.get(text.slotId));
|
|
16310
16790
|
}
|
|
16311
16791
|
for (const cond of plan.conditionals) {
|
|
16312
16792
|
emitOuterConditional(lines, indent, elVar, cond, pc);
|
|
16313
16793
|
}
|
|
16314
16794
|
}
|
|
16315
|
-
function emitOuterText(lines, indent, elVar, text, bfId = "") {
|
|
16795
|
+
function emitOuterText(lines, indent, elVar, text, bfId = "", pIdx) {
|
|
16316
16796
|
const varName = `__rt_${varSlotId(text.slotId)}`;
|
|
16317
|
-
|
|
16797
|
+
if (pIdx !== void 0) {
|
|
16798
|
+
lines.push(`${indent}{ const ${varName} = __p ? tAfter(__p[${pIdx}]) : $t(${elVar}, '${text.slotId}')[0]`);
|
|
16799
|
+
} else {
|
|
16800
|
+
lines.push(`${indent}{ const [${varName}] = $t(${elVar}, '${text.slotId}')`);
|
|
16801
|
+
}
|
|
16318
16802
|
lines.push(`${indent}if (${varName}) createEffect(() => { ${varName}.textContent = String(${text.wrappedExpression}) }${bfId}) }`);
|
|
16319
16803
|
}
|
|
16320
16804
|
function emitOuterConditional(lines, indent, elVar, cond, pc) {
|
|
@@ -16354,6 +16838,41 @@ var init_reactive_effects = __esm({
|
|
|
16354
16838
|
}
|
|
16355
16839
|
});
|
|
16356
16840
|
|
|
16841
|
+
// ../jsx/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts
|
|
16842
|
+
function pathExpr(base, path25) {
|
|
16843
|
+
let expr = base;
|
|
16844
|
+
for (const idx of path25) {
|
|
16845
|
+
expr += ".firstChild";
|
|
16846
|
+
if (idx > 0) expr += ".nextSibling".repeat(idx);
|
|
16847
|
+
}
|
|
16848
|
+
return expr;
|
|
16849
|
+
}
|
|
16850
|
+
function buildSkeletonPathPlan(skeletonPaths, elVar, opts) {
|
|
16851
|
+
const arrayElems = [];
|
|
16852
|
+
const elementIndexBySlot = /* @__PURE__ */ new Map();
|
|
16853
|
+
const textIndexBySlot = /* @__PURE__ */ new Map();
|
|
16854
|
+
for (const slotId of opts.elementSlotIds) {
|
|
16855
|
+
if (elementIndexBySlot.has(slotId)) continue;
|
|
16856
|
+
const path25 = skeletonPaths.elementPaths.get(slotId);
|
|
16857
|
+
if (!path25) continue;
|
|
16858
|
+
elementIndexBySlot.set(slotId, arrayElems.length);
|
|
16859
|
+
arrayElems.push(pathExpr(elVar, path25));
|
|
16860
|
+
}
|
|
16861
|
+
for (const slotId of opts.textSlotIds) {
|
|
16862
|
+
if (textIndexBySlot.has(slotId)) continue;
|
|
16863
|
+
const path25 = skeletonPaths.textMarkerPaths.get(slotId);
|
|
16864
|
+
if (!path25) continue;
|
|
16865
|
+
textIndexBySlot.set(slotId, arrayElems.length);
|
|
16866
|
+
arrayElems.push(pathExpr(elVar, path25));
|
|
16867
|
+
}
|
|
16868
|
+
return { arrayElems, elementIndexBySlot, textIndexBySlot };
|
|
16869
|
+
}
|
|
16870
|
+
var init_skeleton_paths = __esm({
|
|
16871
|
+
"../jsx/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts"() {
|
|
16872
|
+
"use strict";
|
|
16873
|
+
}
|
|
16874
|
+
});
|
|
16875
|
+
|
|
16357
16876
|
// ../jsx/src/ir-to-client-js/control-flow/stringify/component-loop.ts
|
|
16358
16877
|
function stringifyComponentLoop(lines, plan) {
|
|
16359
16878
|
const {
|
|
@@ -16416,11 +16935,13 @@ var init_component_loop = __esm({
|
|
|
16416
16935
|
// ../jsx/src/ir-to-client-js/control-flow/stringify/loop.ts
|
|
16417
16936
|
function emitLoopChildRefs(lines, refs, opts) {
|
|
16418
16937
|
if (refs.length === 0) return;
|
|
16419
|
-
const { indent, elVar, bodyIsMultiRoot } = opts;
|
|
16938
|
+
const { indent, elVar, bodyIsMultiRoot, elementIndexBySlot } = opts;
|
|
16420
16939
|
const lookup = bodyIsMultiRoot ? "qsaItem" : "qsa";
|
|
16421
16940
|
for (const ref of refs) {
|
|
16422
16941
|
const varName = `__rf_${varSlotId(ref.childSlotId)}`;
|
|
16423
|
-
|
|
16942
|
+
const pIdx = elementIndexBySlot?.get(ref.childSlotId);
|
|
16943
|
+
const lookupExpr = pIdx !== void 0 ? `__p ? __p[${pIdx}] : ${lookup}(${elVar}, '[bf="${ref.childSlotId}"]')` : `${lookup}(${elVar}, '[bf="${ref.childSlotId}"]')`;
|
|
16944
|
+
lines.push(`${indent}{ const ${varName} = ${lookupExpr}`);
|
|
16424
16945
|
lines.push(`${indent}if (${varName}) ${emitRefCall(ref.callback, varName)} }`);
|
|
16425
16946
|
}
|
|
16426
16947
|
}
|
|
@@ -16500,10 +17021,31 @@ function stringifyPlainLoop(lines, plan, topIndent = " ") {
|
|
|
16500
17021
|
singleRootLayout: "inline"
|
|
16501
17022
|
});
|
|
16502
17023
|
}
|
|
17024
|
+
let pathPlan = null;
|
|
17025
|
+
if (hoistedTpl && plan.skeletonPaths) {
|
|
17026
|
+
const elementSlotIds = [
|
|
17027
|
+
...reactiveEffects?.attrSlots.map((s) => s.slotId) ?? [],
|
|
17028
|
+
...childRefs.map((r2) => r2.childSlotId)
|
|
17029
|
+
];
|
|
17030
|
+
const textSlotIds = reactiveEffects?.outerTexts.map((t) => t.slotId) ?? [];
|
|
17031
|
+
if (elementSlotIds.length > 0 || textSlotIds.length > 0) {
|
|
17032
|
+
const built = buildSkeletonPathPlan(plan.skeletonPaths, "__el", { elementSlotIds, textSlotIds });
|
|
17033
|
+
if (built.arrayElems.length > 0) {
|
|
17034
|
+
pathPlan = built;
|
|
17035
|
+
lines.push(`${bodyIndent}const __p = __existing ? null : [${built.arrayElems.join(", ")}]`);
|
|
17036
|
+
}
|
|
17037
|
+
}
|
|
17038
|
+
}
|
|
16503
17039
|
if (reactiveEffects !== null) {
|
|
16504
|
-
stringifyReactiveEffects(lines, reactiveEffects, {
|
|
17040
|
+
stringifyReactiveEffects(lines, reactiveEffects, {
|
|
17041
|
+
indent: bodyIndent,
|
|
17042
|
+
elVar: "__el",
|
|
17043
|
+
bodyIsMultiRoot,
|
|
17044
|
+
elementIndexBySlot: pathPlan?.elementIndexBySlot,
|
|
17045
|
+
textIndexBySlot: pathPlan?.textIndexBySlot
|
|
17046
|
+
});
|
|
16505
17047
|
}
|
|
16506
|
-
emitLoopChildRefs(lines, childRefs, { indent: bodyIndent, elVar: "__el", bodyIsMultiRoot });
|
|
17048
|
+
emitLoopChildRefs(lines, childRefs, { indent: bodyIndent, elVar: "__el", bodyIsMultiRoot, elementIndexBySlot: pathPlan?.elementIndexBySlot });
|
|
16507
17049
|
lines.push(`${bodyIndent}return __el`);
|
|
16508
17050
|
lines.push(`${topIndent}}, '${markerId}'${loopBfId})`);
|
|
16509
17051
|
}
|
|
@@ -16562,12 +17104,14 @@ function stringifyStaticLoop(lines, plan) {
|
|
|
16562
17104
|
lines.push(` let __iterEl = ${containerVar}.children[${childIndexExpr}]`);
|
|
16563
17105
|
if (csrMaterialize) {
|
|
16564
17106
|
lines.push(` if (!__iterEl) {`);
|
|
17107
|
+
const isSvg = csrMaterialize.bodyIsMultiRoot ? multiRootTemplateNeedsSvgWrap(csrMaterialize.itemTemplate) : templateRootIsSvg(csrMaterialize.itemTemplate);
|
|
17108
|
+
const itemHtml = isSvg ? `<svg>${csrMaterialize.itemTemplate}</svg>` : csrMaterialize.itemTemplate;
|
|
16565
17109
|
if (csrMaterialize.bodyIsMultiRoot) {
|
|
16566
17110
|
lines.push(` const __mtpl = document.createElement('template')`);
|
|
16567
|
-
lines.push(` __mtpl.innerHTML = \`${
|
|
17111
|
+
lines.push(` __mtpl.innerHTML = \`${itemHtml}\``);
|
|
16568
17112
|
lines.push(` const __anchor = ${containerVar}.children[${childIndexExpr}] ?? null`);
|
|
16569
17113
|
lines.push(` let __first = null`);
|
|
16570
|
-
lines.push(` let __sib = __mtpl.content.firstElementChild`);
|
|
17114
|
+
lines.push(` let __sib = __mtpl.content${isSvg ? ".firstElementChild" : ""}.firstElementChild`);
|
|
16571
17115
|
lines.push(` while (__sib) {`);
|
|
16572
17116
|
lines.push(` const __next = __sib.nextElementSibling`);
|
|
16573
17117
|
lines.push(` const __cloned = __sib.cloneNode(true)`);
|
|
@@ -16578,8 +17122,8 @@ function stringifyStaticLoop(lines, plan) {
|
|
|
16578
17122
|
lines.push(` __iterEl = __first`);
|
|
16579
17123
|
} else {
|
|
16580
17124
|
lines.push(` const __tpl = document.createElement('template')`);
|
|
16581
|
-
lines.push(` __tpl.innerHTML = \`${
|
|
16582
|
-
lines.push(` const __cloned = __tpl.content.firstElementChild`);
|
|
17125
|
+
lines.push(` __tpl.innerHTML = \`${itemHtml}\``);
|
|
17126
|
+
lines.push(` const __cloned = __tpl.content${isSvg ? ".firstElementChild" : ""}.firstElementChild`);
|
|
16583
17127
|
lines.push(` if (__cloned) {`);
|
|
16584
17128
|
lines.push(` const __anchor = ${containerVar}.children[${childIndexExpr}] ?? null`);
|
|
16585
17129
|
lines.push(` ${containerVar}.insertBefore(__cloned, __anchor)`);
|
|
@@ -16620,6 +17164,7 @@ var init_loop = __esm({
|
|
|
16620
17164
|
init_emit_reactive();
|
|
16621
17165
|
init_reactive_effects();
|
|
16622
17166
|
init_template_parse();
|
|
17167
|
+
init_skeleton_paths();
|
|
16623
17168
|
init_component_loop();
|
|
16624
17169
|
init_composite_loop();
|
|
16625
17170
|
}
|
|
@@ -16655,7 +17200,10 @@ function emitReactive(lines, inner, indent, pc) {
|
|
|
16655
17200
|
lines.push(`${innerIndent} __innerEl${uid}.__bfExtras = __innerExtras${uid}`);
|
|
16656
17201
|
lines.push(`${indent} }`);
|
|
16657
17202
|
} else {
|
|
16658
|
-
|
|
17203
|
+
const isSvg = templateRootIsSvg(emit.wrappedTemplate);
|
|
17204
|
+
const innerHtml = isSvg ? `<svg>${emit.wrappedTemplate}</svg>` : emit.wrappedTemplate;
|
|
17205
|
+
const childPath = isSvg ? ".firstElementChild.firstElementChild" : ".firstElementChild";
|
|
17206
|
+
lines.push(`${indent} let __innerEl${uid} = __existing ?? (() => { const __t = document.createElement('template'); __t.innerHTML = \`${innerHtml}\`; return __t.content${childPath}.cloneNode(true) })()`);
|
|
16659
17207
|
}
|
|
16660
17208
|
if (emit.wrappedKey) {
|
|
16661
17209
|
lines.push(`${indent} __innerEl${uid}.setAttribute('${keyAttrName(inner.keyDepth)}', String(${emit.wrappedKey}))`);
|
|
@@ -16825,6 +17373,11 @@ function withTurn(call, componentName, childSlotId, eventName) {
|
|
|
16825
17373
|
const id2 = JSON.stringify(`${componentName}#handler:${childSlotId}:${eventName}`);
|
|
16826
17374
|
return `beginTurn(${id2}); try { ${call} } finally { endTurn() }`;
|
|
16827
17375
|
}
|
|
17376
|
+
function indexBindingLine(handler, indexParam, indexExpr) {
|
|
17377
|
+
if (!indexParam || indexParam === indexExpr) return null;
|
|
17378
|
+
if (!extractFreeIdentifiersFromText(handler).has(indexParam)) return null;
|
|
17379
|
+
return `const ${indexParam} = ${indexExpr}`;
|
|
17380
|
+
}
|
|
16828
17381
|
function stringifyEventDelegation(lines, plan) {
|
|
16829
17382
|
const { containerVar, events, itemLookup, profileComponentName } = plan;
|
|
16830
17383
|
const eventsByName = /* @__PURE__ */ new Map();
|
|
@@ -16869,8 +17422,9 @@ function stringifyEventDelegation(lines, plan) {
|
|
|
16869
17422
|
}
|
|
16870
17423
|
}
|
|
16871
17424
|
function emitKeyedLookup(ls, ev, handlerCall, lookup) {
|
|
16872
|
-
const { arrayExpr, param, keyWithItem, mapPreamble, hasBindings } = lookup;
|
|
17425
|
+
const { arrayExpr, param, keyWithItem, mapPreamble, hasBindings, indexParam } = lookup;
|
|
16873
17426
|
if (ev.nestedLoops.length === 0) {
|
|
17427
|
+
const idxLine2 = indexBindingLine(ev.handler, indexParam, `${arrayExpr}.findIndex(item => String(${keyWithItem}) === key)`);
|
|
16874
17428
|
ls.push(` const li = ${varSlotId(ev.childSlotId)}El.closest('[${BF_KEY}]')`);
|
|
16875
17429
|
ls.push(` if (li) {`);
|
|
16876
17430
|
ls.push(` const key = li.getAttribute('${BF_KEY}')`);
|
|
@@ -16879,12 +17433,14 @@ function emitKeyedLookup(ls, ev, handlerCall, lookup) {
|
|
|
16879
17433
|
ls.push(` if (__bfLoopItem) {`);
|
|
16880
17434
|
ls.push(` const ${param} = __bfLoopItem`);
|
|
16881
17435
|
if (mapPreamble) ls.push(` ${mapPreamble}`);
|
|
17436
|
+
if (idxLine2) ls.push(` ${idxLine2}`);
|
|
16882
17437
|
ls.push(` ${handlerCall}`);
|
|
16883
17438
|
ls.push(` }`);
|
|
16884
17439
|
} else {
|
|
16885
17440
|
ls.push(` const ${param} = ${arrayExpr}.find(item => String(${keyWithItem}) === key)`);
|
|
16886
17441
|
if (mapPreamble) ls.push(` ${mapPreamble}`);
|
|
16887
|
-
ls.push(` if (${param}) ${handlerCall}`);
|
|
17442
|
+
if (idxLine2) ls.push(` if (${param}) { ${idxLine2}; ${handlerCall} }`);
|
|
17443
|
+
else ls.push(` if (${param}) ${handlerCall}`);
|
|
16888
17444
|
}
|
|
16889
17445
|
ls.push(` }`);
|
|
16890
17446
|
return;
|
|
@@ -16912,10 +17468,13 @@ function emitKeyedLookup(ls, ev, handlerCall, lookup) {
|
|
|
16912
17468
|
const outerGuard = hasBindings ? "__bfLoopItem" : param;
|
|
16913
17469
|
const allParams = [outerGuard, ...ev.nestedLoops.map((n) => n.param)];
|
|
16914
17470
|
if (mapPreamble) ls.push(` ${mapPreamble}`);
|
|
16915
|
-
|
|
17471
|
+
const idxLine = indexBindingLine(ev.handler, indexParam, `${arrayExpr}.findIndex(item => String(${keyWithItem}) === outerKey)`);
|
|
17472
|
+
if (idxLine) ls.push(` if (${allParams.join(" && ")}) { ${idxLine}; ${handlerCall} }`);
|
|
17473
|
+
else ls.push(` if (${allParams.join(" && ")}) ${handlerCall}`);
|
|
16916
17474
|
}
|
|
16917
17475
|
function emitDynamicIndexLookup(ls, ev, handlerCall, lookup) {
|
|
16918
|
-
const { arrayExpr, param, mapPreamble, hasBindings } = lookup;
|
|
17476
|
+
const { arrayExpr, param, mapPreamble, hasBindings, indexParam } = lookup;
|
|
17477
|
+
const idxLine = indexBindingLine(ev.handler, indexParam, "idx");
|
|
16919
17478
|
ls.push(` const li = ${varSlotId(ev.childSlotId)}El.closest('li, [bf-i]')`);
|
|
16920
17479
|
ls.push(` if (li && li.parentElement) {`);
|
|
16921
17480
|
ls.push(` const idx = Array.from(li.parentElement.children).indexOf(li)`);
|
|
@@ -16924,17 +17483,20 @@ function emitDynamicIndexLookup(ls, ev, handlerCall, lookup) {
|
|
|
16924
17483
|
ls.push(` if (__bfLoopItem) {`);
|
|
16925
17484
|
ls.push(` const ${param} = __bfLoopItem`);
|
|
16926
17485
|
if (mapPreamble) ls.push(` ${mapPreamble}`);
|
|
17486
|
+
if (idxLine) ls.push(` ${idxLine}`);
|
|
16927
17487
|
ls.push(` ${handlerCall}`);
|
|
16928
17488
|
ls.push(` }`);
|
|
16929
17489
|
} else {
|
|
16930
17490
|
ls.push(` const ${param} = ${arrayExpr}[idx]`);
|
|
16931
17491
|
if (mapPreamble) ls.push(` ${mapPreamble}`);
|
|
16932
|
-
ls.push(` if (${param}) ${handlerCall}`);
|
|
17492
|
+
if (idxLine) ls.push(` if (${param}) { ${idxLine}; ${handlerCall} }`);
|
|
17493
|
+
else ls.push(` if (${param}) ${handlerCall}`);
|
|
16933
17494
|
}
|
|
16934
17495
|
ls.push(` }`);
|
|
16935
17496
|
}
|
|
16936
17497
|
function emitStaticIndexLookup(ls, ev, handlerCall, lookup, containerVar) {
|
|
16937
|
-
const { arrayExpr, param, mapPreamble, offset: offset2 } = lookup;
|
|
17498
|
+
const { arrayExpr, param, mapPreamble, offset: offset2, indexParam } = lookup;
|
|
17499
|
+
const idxLine = indexBindingLine(ev.handler, indexParam, "__idx");
|
|
16938
17500
|
ls.push(` let __el = ${varSlotId(ev.childSlotId)}El`);
|
|
16939
17501
|
ls.push(` while (__el.parentElement && __el.parentElement !== ${containerVar}) __el = __el.parentElement`);
|
|
16940
17502
|
ls.push(` if (__el.parentElement === ${containerVar}) {`);
|
|
@@ -16942,7 +17504,8 @@ function emitStaticIndexLookup(ls, ev, handlerCall, lookup, containerVar) {
|
|
|
16942
17504
|
ls.push(` const __idx = Array.from(${containerVar}.children).indexOf(__el)${idxOffset}`);
|
|
16943
17505
|
ls.push(` const ${param} = ${arrayExpr}[__idx]`);
|
|
16944
17506
|
if (mapPreamble) ls.push(` ${mapPreamble}`);
|
|
16945
|
-
ls.push(` if (${param}) ${handlerCall}`);
|
|
17507
|
+
if (idxLine) ls.push(` if (${param}) { ${idxLine}; ${handlerCall} }`);
|
|
17508
|
+
else ls.push(` if (${param}) ${handlerCall}`);
|
|
16946
17509
|
ls.push(` }`);
|
|
16947
17510
|
}
|
|
16948
17511
|
var NON_BUBBLING_EVENTS;
|
|
@@ -16950,6 +17513,7 @@ var init_event_delegation = __esm({
|
|
|
16950
17513
|
"../jsx/src/ir-to-client-js/control-flow/stringify/event-delegation.ts"() {
|
|
16951
17514
|
"use strict";
|
|
16952
17515
|
init_utils();
|
|
17516
|
+
init_csr_substitute();
|
|
16953
17517
|
NON_BUBBLING_EVENTS = /* @__PURE__ */ new Set([
|
|
16954
17518
|
"blur",
|
|
16955
17519
|
"focus",
|
|
@@ -17248,6 +17812,7 @@ function buildPlainLoopPlan(elem, profileComponentName) {
|
|
|
17248
17812
|
mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : "",
|
|
17249
17813
|
template: elem.template,
|
|
17250
17814
|
skeletonTemplate: elem.skeletonTemplate,
|
|
17815
|
+
skeletonPaths: elem.skeletonPaths,
|
|
17251
17816
|
reactiveEffects: hasReactive2 ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
|
|
17252
17817
|
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
17253
17818
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
@@ -20289,6 +20854,7 @@ function isStringTypedOperand(expr, isStringName) {
|
|
|
20289
20854
|
if (expr.kind === "member" && expr.object.kind === "identifier" && expr.object.name === "props") {
|
|
20290
20855
|
return isStringName(expr.property);
|
|
20291
20856
|
}
|
|
20857
|
+
if (expr.kind === "identifier") return isStringName(expr.name);
|
|
20292
20858
|
if (expr.kind === "binary" && expr.op === "+") {
|
|
20293
20859
|
return isStringTypedOperand(expr.left, isStringName) || isStringTypedOperand(expr.right, isStringName);
|
|
20294
20860
|
}
|
|
@@ -20313,7 +20879,7 @@ function emitParsedExpr(expr, emitter) {
|
|
|
20313
20879
|
return emitter.call(expr.callee, expr.args, emit);
|
|
20314
20880
|
}
|
|
20315
20881
|
case "member":
|
|
20316
|
-
return emitter.member(expr.object, expr.property, expr.computed, emit);
|
|
20882
|
+
return emitter.member(expr.object, expr.property, expr.computed, expr.optional, emit);
|
|
20317
20883
|
case "index-access":
|
|
20318
20884
|
return emitter.indexAccess(expr.object, expr.index, emit);
|
|
20319
20885
|
case "binary":
|
|
@@ -20360,6 +20926,213 @@ var init_parsed_expr_emitter = __esm({
|
|
|
20360
20926
|
}
|
|
20361
20927
|
});
|
|
20362
20928
|
|
|
20929
|
+
// ../jsx/src/adapters/loop-bound-names.ts
|
|
20930
|
+
function collectLoopBoundNames(ir) {
|
|
20931
|
+
const names = /* @__PURE__ */ new Set();
|
|
20932
|
+
const visit3 = (node) => {
|
|
20933
|
+
if (!node) return;
|
|
20934
|
+
switch (node.type) {
|
|
20935
|
+
case "element":
|
|
20936
|
+
case "component":
|
|
20937
|
+
case "fragment":
|
|
20938
|
+
case "provider":
|
|
20939
|
+
for (const child of node.children) visit3(child);
|
|
20940
|
+
break;
|
|
20941
|
+
case "async":
|
|
20942
|
+
visit3(node.fallback);
|
|
20943
|
+
for (const child of node.children) visit3(child);
|
|
20944
|
+
break;
|
|
20945
|
+
case "loop":
|
|
20946
|
+
names.add(node.param);
|
|
20947
|
+
if (node.index) names.add(node.index);
|
|
20948
|
+
for (const binding of node.paramBindings ?? []) names.add(binding.name);
|
|
20949
|
+
if (node.filterPredicate) names.add(node.filterPredicate.param);
|
|
20950
|
+
for (const child of node.children) visit3(child);
|
|
20951
|
+
if (node.childComponent) {
|
|
20952
|
+
for (const child of node.childComponent.children) visit3(child);
|
|
20953
|
+
}
|
|
20954
|
+
for (const nested of node.nestedComponents ?? []) {
|
|
20955
|
+
for (const child of nested.children) visit3(child);
|
|
20956
|
+
}
|
|
20957
|
+
for (const frag of node.flatMapCallback?.fragments ?? []) {
|
|
20958
|
+
visit3(frag.ir);
|
|
20959
|
+
}
|
|
20960
|
+
break;
|
|
20961
|
+
case "conditional":
|
|
20962
|
+
visit3(node.whenTrue);
|
|
20963
|
+
visit3(node.whenFalse);
|
|
20964
|
+
break;
|
|
20965
|
+
case "if-statement":
|
|
20966
|
+
visit3(node.consequent);
|
|
20967
|
+
if (node.alternate) visit3(node.alternate);
|
|
20968
|
+
break;
|
|
20969
|
+
case "text":
|
|
20970
|
+
case "expression":
|
|
20971
|
+
case "slot":
|
|
20972
|
+
break;
|
|
20973
|
+
}
|
|
20974
|
+
};
|
|
20975
|
+
visit3(ir.root);
|
|
20976
|
+
return names;
|
|
20977
|
+
}
|
|
20978
|
+
var init_loop_bound_names = __esm({
|
|
20979
|
+
"../jsx/src/adapters/loop-bound-names.ts"() {
|
|
20980
|
+
"use strict";
|
|
20981
|
+
}
|
|
20982
|
+
});
|
|
20983
|
+
|
|
20984
|
+
// ../jsx/src/signal-init-eval.ts
|
|
20985
|
+
function isTransportable(value2, ancestors = /* @__PURE__ */ new Set()) {
|
|
20986
|
+
if (value2 === null) return true;
|
|
20987
|
+
const t = typeof value2;
|
|
20988
|
+
if (t === "boolean" || t === "number" || t === "string") return true;
|
|
20989
|
+
if (t !== "object") return false;
|
|
20990
|
+
if (ancestors.has(value2)) return false;
|
|
20991
|
+
ancestors.add(value2);
|
|
20992
|
+
try {
|
|
20993
|
+
if (Array.isArray(value2)) {
|
|
20994
|
+
if (Object.keys(value2).length !== value2.length) return false;
|
|
20995
|
+
return value2.every((el) => el !== void 0 && isTransportable(el, ancestors));
|
|
20996
|
+
}
|
|
20997
|
+
const proto = Object.getPrototypeOf(value2);
|
|
20998
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
20999
|
+
return Object.values(value2).every((v) => isTransportable(v, ancestors));
|
|
21000
|
+
} finally {
|
|
21001
|
+
ancestors.delete(value2);
|
|
21002
|
+
}
|
|
21003
|
+
}
|
|
21004
|
+
function tryEvaluateSignalInit(expr, props) {
|
|
21005
|
+
const src = expr.trim();
|
|
21006
|
+
if (src === "") return { ok: false };
|
|
21007
|
+
let fn;
|
|
21008
|
+
try {
|
|
21009
|
+
fn = new Function(
|
|
21010
|
+
"props",
|
|
21011
|
+
...BLOCKED_GLOBALS,
|
|
21012
|
+
`'use strict'; return (
|
|
21013
|
+
${src}
|
|
21014
|
+
);`
|
|
21015
|
+
);
|
|
21016
|
+
} catch {
|
|
21017
|
+
return { ok: false };
|
|
21018
|
+
}
|
|
21019
|
+
try {
|
|
21020
|
+
const value2 = fn(props ?? {});
|
|
21021
|
+
if (value2 === void 0) return { ok: true, value: void 0 };
|
|
21022
|
+
return isTransportable(value2) ? { ok: true, value: value2 } : { ok: false };
|
|
21023
|
+
} catch {
|
|
21024
|
+
return { ok: false };
|
|
21025
|
+
}
|
|
21026
|
+
}
|
|
21027
|
+
function evaluateSignalInit(expr, props) {
|
|
21028
|
+
const result2 = tryEvaluateSignalInit(expr, props);
|
|
21029
|
+
return result2.ok && result2.value !== void 0 ? result2.value : null;
|
|
21030
|
+
}
|
|
21031
|
+
var BLOCKED_GLOBALS;
|
|
21032
|
+
var init_signal_init_eval = __esm({
|
|
21033
|
+
"../jsx/src/signal-init-eval.ts"() {
|
|
21034
|
+
"use strict";
|
|
21035
|
+
BLOCKED_GLOBALS = [
|
|
21036
|
+
"globalThis",
|
|
21037
|
+
"window",
|
|
21038
|
+
"document",
|
|
21039
|
+
"Date",
|
|
21040
|
+
"Math",
|
|
21041
|
+
"crypto",
|
|
21042
|
+
"performance",
|
|
21043
|
+
"fetch",
|
|
21044
|
+
"setTimeout",
|
|
21045
|
+
"setInterval",
|
|
21046
|
+
"require",
|
|
21047
|
+
"process",
|
|
21048
|
+
"Function"
|
|
21049
|
+
];
|
|
21050
|
+
}
|
|
21051
|
+
});
|
|
21052
|
+
|
|
21053
|
+
// ../jsx/src/static-literal.ts
|
|
21054
|
+
function evaluateStaticLiteral(expr, bindings) {
|
|
21055
|
+
switch (expr.kind) {
|
|
21056
|
+
case "literal":
|
|
21057
|
+
return { value: expr.value };
|
|
21058
|
+
case "template-literal": {
|
|
21059
|
+
let out = "";
|
|
21060
|
+
for (const part of expr.parts) {
|
|
21061
|
+
if (part.type === "string") {
|
|
21062
|
+
out += part.value;
|
|
21063
|
+
continue;
|
|
21064
|
+
}
|
|
21065
|
+
const resolved = evaluateStaticLiteral(part.expr, bindings);
|
|
21066
|
+
if (!resolved) return null;
|
|
21067
|
+
out += String(resolved.value);
|
|
21068
|
+
}
|
|
21069
|
+
return { value: out };
|
|
21070
|
+
}
|
|
21071
|
+
case "array-literal": {
|
|
21072
|
+
const values2 = [];
|
|
21073
|
+
for (const element of expr.elements) {
|
|
21074
|
+
const resolved = evaluateStaticLiteral(element, bindings);
|
|
21075
|
+
if (!resolved) return null;
|
|
21076
|
+
values2.push(resolved.value);
|
|
21077
|
+
}
|
|
21078
|
+
return { value: values2 };
|
|
21079
|
+
}
|
|
21080
|
+
case "object-literal": {
|
|
21081
|
+
const out = {};
|
|
21082
|
+
for (const prop of expr.properties) {
|
|
21083
|
+
const resolved = evaluateStaticLiteral(prop.value, bindings);
|
|
21084
|
+
if (!resolved) return null;
|
|
21085
|
+
out[prop.key] = resolved.value;
|
|
21086
|
+
}
|
|
21087
|
+
return { value: out };
|
|
21088
|
+
}
|
|
21089
|
+
case "unary": {
|
|
21090
|
+
const resolved = evaluateStaticLiteral(expr.argument, bindings);
|
|
21091
|
+
if (!resolved) return null;
|
|
21092
|
+
if (expr.op === "-") return typeof resolved.value === "number" ? { value: -resolved.value } : null;
|
|
21093
|
+
if (expr.op === "+") return typeof resolved.value === "number" ? { value: +resolved.value } : null;
|
|
21094
|
+
if (expr.op === "!") return { value: !resolved.value };
|
|
21095
|
+
return null;
|
|
21096
|
+
}
|
|
21097
|
+
case "identifier":
|
|
21098
|
+
return bindings?.has(expr.name) ? { value: bindings.get(expr.name) } : null;
|
|
21099
|
+
case "member": {
|
|
21100
|
+
const base = evaluateStaticLiteral(expr.object, bindings);
|
|
21101
|
+
if (!base || base.value === null || typeof base.value !== "object") return null;
|
|
21102
|
+
return { value: base.value[expr.property] };
|
|
21103
|
+
}
|
|
21104
|
+
case "index-access": {
|
|
21105
|
+
const base = evaluateStaticLiteral(expr.object, bindings);
|
|
21106
|
+
const index = evaluateStaticLiteral(expr.index, bindings);
|
|
21107
|
+
if (!base || !index || !Array.isArray(base.value) || typeof index.value !== "number") return null;
|
|
21108
|
+
return { value: base.value[index.value] };
|
|
21109
|
+
}
|
|
21110
|
+
default:
|
|
21111
|
+
return null;
|
|
21112
|
+
}
|
|
21113
|
+
}
|
|
21114
|
+
function isFullyStaticLiteral(expr) {
|
|
21115
|
+
return evaluateStaticLiteral(expr) !== null;
|
|
21116
|
+
}
|
|
21117
|
+
function resolveStaticLoopSource(arrayParsed, localConstants, opts) {
|
|
21118
|
+
if (!arrayParsed) return null;
|
|
21119
|
+
let target2 = arrayParsed;
|
|
21120
|
+
if (arrayParsed.kind === "identifier") {
|
|
21121
|
+
if (opts?.isNameShadowed?.(arrayParsed.name)) return null;
|
|
21122
|
+
const local = localConstants?.find((c) => c.name === arrayParsed.name);
|
|
21123
|
+
if (!local || local.isModule || !local.parsed) return null;
|
|
21124
|
+
target2 = local.parsed;
|
|
21125
|
+
}
|
|
21126
|
+
const resolved = evaluateStaticLiteral(target2);
|
|
21127
|
+
if (!resolved || !Array.isArray(resolved.value)) return null;
|
|
21128
|
+
return resolved.value;
|
|
21129
|
+
}
|
|
21130
|
+
var init_static_literal = __esm({
|
|
21131
|
+
"../jsx/src/static-literal.ts"() {
|
|
21132
|
+
"use strict";
|
|
21133
|
+
}
|
|
21134
|
+
});
|
|
21135
|
+
|
|
20363
21136
|
// ../jsx/src/query-href-lowering.ts
|
|
20364
21137
|
function matchQueryHrefCall(callee, args2, localNames) {
|
|
20365
21138
|
if (callee.kind !== "identifier" || !localNames.has(callee.name)) return null;
|
|
@@ -20522,6 +21295,70 @@ var init_attr_value_emitter = __esm({
|
|
|
20522
21295
|
}
|
|
20523
21296
|
});
|
|
20524
21297
|
|
|
21298
|
+
// ../jsx/src/adapters/dangerous-inner-html.ts
|
|
21299
|
+
function isDangerousInnerHtmlAttr(attr) {
|
|
21300
|
+
return attr.name === DANGEROUS_INNER_HTML_ATTR;
|
|
21301
|
+
}
|
|
21302
|
+
function resolveDangerousInnerHtml(element) {
|
|
21303
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
21304
|
+
if (!attr) return null;
|
|
21305
|
+
if (attr.clientOnly) return null;
|
|
21306
|
+
if (attr.value.kind !== "expression") {
|
|
21307
|
+
return { kind: "dynamic", expr: "", loc: attr.loc };
|
|
21308
|
+
}
|
|
21309
|
+
const parsed = attr.value.parsed ?? parseExpression(attr.value.expr.trim());
|
|
21310
|
+
const html = staticHtmlLiteral(parsed);
|
|
21311
|
+
if (html !== null) return { kind: "static", html };
|
|
21312
|
+
return { kind: "dynamic", expr: attr.value.expr, loc: attr.loc };
|
|
21313
|
+
}
|
|
21314
|
+
function staticHtmlLiteral(parsed) {
|
|
21315
|
+
if (parsed.kind !== "object-literal") return null;
|
|
21316
|
+
if (parsed.properties.length !== 1) return null;
|
|
21317
|
+
const [prop] = parsed.properties;
|
|
21318
|
+
if (prop.shorthand) return null;
|
|
21319
|
+
if (prop.key !== "__html") return null;
|
|
21320
|
+
if (prop.value.kind !== "literal" || prop.value.literalType !== "string") return null;
|
|
21321
|
+
return prop.value.value;
|
|
21322
|
+
}
|
|
21323
|
+
function dangerousInnerHtmlMetacharViolation(html, adapterId) {
|
|
21324
|
+
const pattern = TEMPLATE_METACHAR_PATTERNS[adapterId];
|
|
21325
|
+
if (!pattern) {
|
|
21326
|
+
return `no template-metacharacter guard is defined for adapter '${adapterId}' \u2014 refusing rather than splicing unguarded`;
|
|
21327
|
+
}
|
|
21328
|
+
if (!pattern.test(html)) return null;
|
|
21329
|
+
return `the literal HTML contains a sequence ${adapterId}'s own template compiler would interpret (not inert text once spliced into the template)`;
|
|
21330
|
+
}
|
|
21331
|
+
function dangerousInnerHtmlDiagnostic(expr, loc, reason2) {
|
|
21332
|
+
const detail2 = reason2 ? ` \u2014 ${reason2}.` : "";
|
|
21333
|
+
return {
|
|
21334
|
+
code: "BF101",
|
|
21335
|
+
severity: "error",
|
|
21336
|
+
message: `dangerouslySetInnerHTML requires a compile-time string literal __html value on template adapters (e.g. { __html: '...' })${expr ? `: ${expr.trim()}` : ""}${detail2}`,
|
|
21337
|
+
loc,
|
|
21338
|
+
suggestion: {
|
|
21339
|
+
message: "Dynamic or signal-derived HTML for dangerouslySetInnerHTML is only supported on Hono/CSR today (tracked separately: https://github.com/piconic-ai/barefootjs/issues/2215). Use an inline string literal, or defer it to the client with /* @client */ (e.g. dangerouslySetInnerHTML={/* @client */ { __html: expr }}) so hydration sets it instead of SSR."
|
|
21340
|
+
}
|
|
21341
|
+
};
|
|
21342
|
+
}
|
|
21343
|
+
var DANGEROUS_INNER_HTML_ATTR, TEMPLATE_METACHAR_PATTERNS;
|
|
21344
|
+
var init_dangerous_inner_html = __esm({
|
|
21345
|
+
"../jsx/src/adapters/dangerous-inner-html.ts"() {
|
|
21346
|
+
"use strict";
|
|
21347
|
+
init_expression_parser();
|
|
21348
|
+
DANGEROUS_INNER_HTML_ATTR = "dangerouslySetInnerHTML";
|
|
21349
|
+
TEMPLATE_METACHAR_PATTERNS = {
|
|
21350
|
+
blade: /\{\{|\{!!|<\?|@\w|<\/?\s*x[-:]/,
|
|
21351
|
+
erb: /<%/,
|
|
21352
|
+
"go-template": /\{\{/,
|
|
21353
|
+
jinja: /\{\{|\{%|\{#/,
|
|
21354
|
+
minijinja: /\{\{|\{%|\{#/,
|
|
21355
|
+
mojolicious: /<%|^\s*%/m,
|
|
21356
|
+
twig: /\{\{|\{%|\{#/,
|
|
21357
|
+
xslate: /<:|^\s*:/m
|
|
21358
|
+
};
|
|
21359
|
+
}
|
|
21360
|
+
});
|
|
21361
|
+
|
|
20525
21362
|
// ../jsx/src/combine-client-js.ts
|
|
20526
21363
|
import ts19 from "typescript";
|
|
20527
21364
|
function combineParentChildClientJs(files2) {
|
|
@@ -23338,6 +24175,7 @@ __export(src_exports, {
|
|
|
23338
24175
|
buildStaticBudget: () => buildStaticBudget,
|
|
23339
24176
|
buildWhyUpdate: () => buildWhyUpdate,
|
|
23340
24177
|
collectContextConsumers: () => collectContextConsumers,
|
|
24178
|
+
collectLoopBoundNames: () => collectLoopBoundNames,
|
|
23341
24179
|
collectModuleStringConsts: () => collectModuleStringConsts,
|
|
23342
24180
|
combineParentChildClientJs: () => combineParentChildClientJs,
|
|
23343
24181
|
compileJSX: () => compileJSX,
|
|
@@ -23346,6 +24184,8 @@ __export(src_exports, {
|
|
|
23346
24184
|
createError: () => createError,
|
|
23347
24185
|
createProgramForCorpus: () => createProgramForCorpus,
|
|
23348
24186
|
createProgramForFile: () => createProgramForFile,
|
|
24187
|
+
dangerousInnerHtmlDiagnostic: () => dangerousInnerHtmlDiagnostic,
|
|
24188
|
+
dangerousInnerHtmlMetacharViolation: () => dangerousInnerHtmlMetacharViolation,
|
|
23349
24189
|
describeFallback: () => describeFallback,
|
|
23350
24190
|
diffProfiles: () => diffProfiles,
|
|
23351
24191
|
diffStaticBudget: () => diffStaticBudget,
|
|
@@ -23358,6 +24198,8 @@ __export(src_exports, {
|
|
|
23358
24198
|
envSignalReaderFor: () => envSignalReaderFor,
|
|
23359
24199
|
evalStringArrayJoin: () => evalStringArrayJoin,
|
|
23360
24200
|
evaluateProfileGates: () => evaluateProfileGates,
|
|
24201
|
+
evaluateSignalInit: () => evaluateSignalInit,
|
|
24202
|
+
evaluateStaticLiteral: () => evaluateStaticLiteral,
|
|
23361
24203
|
exprToString: () => exprToString,
|
|
23362
24204
|
extractArrowBodyExpression: () => extractArrowBodyExpression,
|
|
23363
24205
|
extractFunctionParams: () => extractFunctionParams,
|
|
@@ -23398,6 +24240,8 @@ __export(src_exports, {
|
|
|
23398
24240
|
identifierPath: () => identifierPath,
|
|
23399
24241
|
importsSearchParams: () => importsSearchParams,
|
|
23400
24242
|
isBooleanAttr: () => isBooleanAttr,
|
|
24243
|
+
isDangerousInnerHtmlAttr: () => isDangerousInnerHtmlAttr,
|
|
24244
|
+
isFullyStaticLiteral: () => isFullyStaticLiteral,
|
|
23401
24245
|
isLowerableLoopDestructure: () => isLowerableLoopDestructure,
|
|
23402
24246
|
isLowerableObjectRestDestructure: () => isLowerableObjectRestDestructure,
|
|
23403
24247
|
isStringConcatBinary: () => isStringConcatBinary,
|
|
@@ -23432,7 +24276,9 @@ __export(src_exports, {
|
|
|
23432
24276
|
registerLoweringPlugin: () => registerLoweringPlugin,
|
|
23433
24277
|
renderImportMapHtml: () => renderImportMapHtml,
|
|
23434
24278
|
resetCompilerCounters: () => resetCompilerCounters,
|
|
24279
|
+
resolveDangerousInnerHtml: () => resolveDangerousInnerHtml,
|
|
23435
24280
|
resolveSetters: () => resolveSetters,
|
|
24281
|
+
resolveStaticLoopSource: () => resolveStaticLoopSource,
|
|
23436
24282
|
rewriteImportsForTemplate: () => rewriteImportsForTemplate,
|
|
23437
24283
|
searchParamsLocalNames: () => searchParamsLocalNames,
|
|
23438
24284
|
serializeParsedExpr: () => serializeParsedExpr,
|
|
@@ -23440,6 +24286,7 @@ __export(src_exports, {
|
|
|
23440
24286
|
stringifyParsedExpr: () => stringifyParsedExpr,
|
|
23441
24287
|
testAdapter: () => testAdapter,
|
|
23442
24288
|
traceUpdatePath: () => traceUpdatePath,
|
|
24289
|
+
tryEvaluateSignalInit: () => tryEvaluateSignalInit,
|
|
23443
24290
|
tsNodeToParsedExpr: () => tsNodeToParsedExpr
|
|
23444
24291
|
});
|
|
23445
24292
|
var init_src2 = __esm({
|
|
@@ -23457,6 +24304,9 @@ var init_src2 = __esm({
|
|
|
23457
24304
|
init_jsx_adapter();
|
|
23458
24305
|
init_template_imports();
|
|
23459
24306
|
init_parsed_expr_emitter();
|
|
24307
|
+
init_loop_bound_names();
|
|
24308
|
+
init_signal_init_eval();
|
|
24309
|
+
init_static_literal();
|
|
23460
24310
|
init_env_signal();
|
|
23461
24311
|
init_query_href_lowering();
|
|
23462
24312
|
init_lowering_registry();
|
|
@@ -23464,6 +24314,7 @@ var init_src2 = __esm({
|
|
|
23464
24314
|
init_builtin_lowering_plugins();
|
|
23465
24315
|
init_ir_node_emitter();
|
|
23466
24316
|
init_attr_value_emitter();
|
|
24317
|
+
init_dangerous_inner_html();
|
|
23467
24318
|
init_ir_to_client_js();
|
|
23468
24319
|
init_source_map();
|
|
23469
24320
|
init_combine_client_js();
|
|
@@ -24937,7 +25788,7 @@ async function build(config, options2 = {}) {
|
|
|
24937
25788
|
}
|
|
24938
25789
|
}
|
|
24939
25790
|
let runtimeKeepHash = cache2.runtimeKeepHash;
|
|
24940
|
-
if (runtimeMode
|
|
25791
|
+
if (runtimeMode !== "full") {
|
|
24941
25792
|
if (!domDistFile) {
|
|
24942
25793
|
console.warn("Warning: @barefootjs/client dist not found. Skipping barefoot.js generation.");
|
|
24943
25794
|
runtimeKeepHash = void 0;
|
|
@@ -24974,7 +25825,7 @@ async function build(config, options2 = {}) {
|
|
|
24974
25825
|
runtimeKeepHash = void 0;
|
|
24975
25826
|
} else {
|
|
24976
25827
|
const keepNames = /* @__PURE__ */ new Set([
|
|
24977
|
-
...ALWAYS_KEEP_RUNTIME_EXPORTS,
|
|
25828
|
+
...runtimeMode === "treeshake-exact" ? [] : ALWAYS_KEEP_RUNTIME_EXPORTS,
|
|
24978
25829
|
...config.runtimeKeep ?? [],
|
|
24979
25830
|
...merged.names
|
|
24980
25831
|
]);
|
|
@@ -24985,7 +25836,17 @@ async function build(config, options2 = {}) {
|
|
|
24985
25836
|
distHash: hashBytes(distBytes),
|
|
24986
25837
|
keep: [...keepNames].sort()
|
|
24987
25838
|
}));
|
|
24988
|
-
if (
|
|
25839
|
+
if (keepNames.size === 0) {
|
|
25840
|
+
if (nextKeepHash !== cache2.runtimeKeepHash) {
|
|
25841
|
+
try {
|
|
25842
|
+
await unlink(runtimeOutPath);
|
|
25843
|
+
anyOutputChanged = true;
|
|
25844
|
+
console.log(`Skipped: ${runtimeSubdir}/barefoot.js (no runtime exports used)`);
|
|
25845
|
+
} catch {
|
|
25846
|
+
}
|
|
25847
|
+
}
|
|
25848
|
+
runtimeKeepHash = nextKeepHash;
|
|
25849
|
+
} else if (nextKeepHash === cache2.runtimeKeepHash && await fileExists(runtimeOutPath)) {
|
|
24989
25850
|
runtimeKeepHash = nextKeepHash;
|
|
24990
25851
|
} else {
|
|
24991
25852
|
try {
|
|
@@ -27426,8 +28287,8 @@ var bfGoSource, evalGoSource, streamingGoSource, bfdevGoSource;
|
|
|
27426
28287
|
var init_runtimes_generated = __esm({
|
|
27427
28288
|
"src/lib/adapters/runtimes.generated.ts"() {
|
|
27428
28289
|
"use strict";
|
|
27429
|
-
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 "sort"\n "strconv"\n "strings"\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 // Arithmetic\n "bf_add": Add,\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_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_repeat": Repeat,\n "bf_pad_start": PadStart,\n "bf_pad_end": PadEnd,\n "bf_string": String,\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 // 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_to_fixed": ToFixed,\n\n // Array/Slice\n "bf_len": Len,\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_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 // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\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// 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// 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// 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 propsJSON, err := json.Marshal(props)\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// 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`.\nfunc Min(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\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 as Min.\nfunc Max(a, b any) any {\n av, bv := toFloat64(a), toFloat64(b)\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// 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; n<0 would\n// replace all \u2014 that\'s `.replaceAll`, still refused). The replacement\n// is treated literally: unlike JS, special replacement patterns like\n// `$&` / `$1` are NOT interpreted (Go and Perl agree on literal\n// replacement, keeping the two template adapters byte-equal; this\n// diverges from the Hono/CSR JS path only for replacement strings that\n// 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// 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// 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// 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// 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// 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// Slice carves out a sub-range from `items`. Lowers\n// `Array.prototype.slice(start, end?)` (#1448 Tier A). The variadic\n// `end` arg lets Go template\'s call dispatcher pass either 2 or 3\n// arguments; an absent end means "to length".\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\n// - start >= end \u2192 empty slice (no panic)\n//\n// Non-array receivers return an empty `[]any`.\nfunc Slice(items any, start int, end ...int) []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\n // Normalise start (negative = from end).\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 // Normalise end (optional; absent = length).\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\n if start >= stop {\n return []any{}\n }\n\n out := make([]any, 0, stop-start)\n for i := start; i < stop; 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 \u2014 for generated `NewXxxProps` code lowering a conditional\n// inline-object spread condition on an `interface{}` prop (whose runtime\n// value may be a string, number, bool, \u2026). Keeps the spread bag\'s\n// inclusion test faithful to JS rather than string-biased (#1752).\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 value using reflection. For\n// map receivers it falls back to case-variant lookup so JSON-decoded\n// user data (`map[string]any{"price": 30}`) and PascalCase-emitted\n// test data both resolve under a single key name. (#1487)\nfunc getFieldValue(item any, field string) 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.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(field); ok {\n return r\n }\n if cap := capitalize(field); cap != field {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(field); low != field {\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(field); lower != field && lower != decapitalize(field) {\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(field)\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, field) })\n if !fieldVal.IsValid() {\n return nil\n }\n }\n return fieldVal.Interface()\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 pJSON, err := json.Marshal(props)\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// 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// 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 with deduplication.\n// It preserves insertion order for deterministic output.\ntype ScriptCollector struct {\n scripts map[string]bool\n order []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 }\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// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// BfScripts generates script tags for all registered scripts.\n// Returns HTML safe for embedding in templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\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// =============================================================================\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 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';
|
|
27430
|
-
evalGoSource = 'package bf\n\nimport (\n "encoding/json"\n "math"\n "reflect"\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 props, _ := n["properties"].([]any)\n out := make(map[string]any, len(props))\n for _, p := range props {\n pm, _ := p.(map[string]any)\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\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 // Decimal / exponent / "Infinity" numeric strings parse JS-faithfully\n // (ParseFloat handles these, matching the Perl evaluator). The\n // radix-prefixed forms JS Number() also accepts ("0x10" / "0o17" /\n // "0b101") are a documented divergence region: they yield NaN here, as\n // they do in the Perl evaluator (looks_like_number is false for them),\n // so Go==Perl while differing from the JS reference. Template data\n // carries JSON numbers, not radix-string literals, so this never\n // arises in practice.\n f, err := strconv.ParseFloat(t, 64)\n if err != nil {\n return math.NaN()\n }\n return f\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\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';
|
|
28290
|
+
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 "sort"\n "strconv"\n "strings"\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 // 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\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 // 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_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_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 // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\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// 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// 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// 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 propsJSON, err := json.Marshal(props)\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// 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// 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// 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 \u2014 for generated `NewXxxProps` code lowering a conditional\n// inline-object spread condition on an `interface{}` prop (whose runtime\n// value may be a string, number, bool, \u2026). Keeps the spread bag\'s\n// inclusion test faithful to JS rather than string-biased (#1752).\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 value using reflection. For\n// map receivers it falls back to case-variant lookup so JSON-decoded\n// user data (`map[string]any{"price": 30}`) and PascalCase-emitted\n// test data both resolve under a single key name. (#1487)\nfunc getFieldValue(item any, field string) 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.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(field); ok {\n return r\n }\n if cap := capitalize(field); cap != field {\n if r, ok := lookup(cap); ok {\n return r\n }\n }\n if low := decapitalize(field); low != field {\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(field); lower != field && lower != decapitalize(field) {\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(field)\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, field) })\n if !fieldVal.IsValid() {\n return nil\n }\n }\n return fieldVal.Interface()\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 pJSON, err := json.Marshal(props)\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// 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// 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 with deduplication.\n// It preserves insertion order for deterministic output.\ntype ScriptCollector struct {\n scripts map[string]bool\n order []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 }\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// Scripts returns all registered scripts in insertion order.\nfunc (sc *ScriptCollector) Scripts() []string {\n return sc.order\n}\n\n// BfScripts generates script tags for all registered scripts.\n// Returns HTML safe for embedding in templates.\nfunc BfScripts(collector *ScriptCollector) template.HTML {\n if collector == nil {\n return ""\n }\n var result strings.Builder\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// =============================================================================\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 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';
|
|
28291
|
+
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 props, _ := n["properties"].([]any)\n out := make(map[string]any, len(props))\n for _, p := range props {\n pm, _ := p.(map[string]any)\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\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';
|
|
27431
28292
|
streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
|
|
27432
28293
|
//
|
|
27433
28294
|
// Provides StreamRenderer for progressive page rendering using HTTP
|
|
@@ -29081,14 +29942,16 @@ export default createConfig({
|
|
|
29081
29942
|
devDependencies: {
|
|
29082
29943
|
...UNOCSS_DEV_DEPENDENCIES,
|
|
29083
29944
|
"@barefootjs/cli": "latest",
|
|
29084
|
-
// Must
|
|
29085
|
-
//
|
|
29086
|
-
//
|
|
29087
|
-
//
|
|
29088
|
-
//
|
|
29089
|
-
//
|
|
29090
|
-
//
|
|
29091
|
-
|
|
29945
|
+
// Must satisfy wrangler's `peerOptional @cloudflare/workers-types`
|
|
29946
|
+
// (bun tolerates a mismatch; npm does not — CI's smoke-publish gate
|
|
29947
|
+
// catches it). Upstream keeps flip-flopping which major it peers on:
|
|
29948
|
+
// 4.107.1 peers `^4.20260702.1`; 4.108.0 moved to `^5.20260706.1` and
|
|
29949
|
+
// was deprecated same-day; 4.110.0 (which `^4.0.0` resolves to today)
|
|
29950
|
+
// peers `^5.20260708.1`. Rather than chase whichever version last
|
|
29951
|
+
// shipped, accept BOTH majors so npm installs whichever the resolved
|
|
29952
|
+
// wrangler actually peers on — v5 when it wants v5, v4 after a
|
|
29953
|
+
// deprecation falls back to a v4-peering wrangler. No ERESOLVE either way.
|
|
29954
|
+
"@cloudflare/workers-types": "^4.20260702.1 || ^5.20260708.1",
|
|
29092
29955
|
// `@barefootjs/test` powers `renderToTest()` — the canonical
|
|
29093
29956
|
// millisecond IR test the docs (and `bf gen test`) point new users
|
|
29094
29957
|
// at. Without it the scaffold's `test` script is a no-op and any
|