@barefootjs/cli 0.1.2 → 0.1.3
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/docs/core/{README.md → README.mdx} +5 -11
- package/dist/docs/core/components/{component-authoring.md → component-authoring.mdx} +9 -5
- package/dist/docs/core/components.md +1 -1
- package/dist/docs/core/core-concepts/{how-it-works.md → how-it-works.mdx} +8 -4
- package/dist/docs/core/{introduction.md → introduction.mdx} +6 -5
- package/dist/index.js +396 -54
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Table of Contents
|
|
4
4
|
|
|
5
|
-
### 1. [Introduction](./introduction.
|
|
5
|
+
### 1. [Introduction](./introduction.mdx)
|
|
6
6
|
|
|
7
7
|
- What is BarefootJS?
|
|
8
8
|
- Why BarefootJS?
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
- [MPA-style Development](./core-concepts/mpa-style.md) — Server-rendering by default, JS only where marked
|
|
21
21
|
- [Fine-grained Reactivity](./core-concepts/reactivity.md) — Signals, effects, memos — no virtual DOM needed
|
|
22
22
|
- [AI-native Development](./core-concepts/ai-native.md) — Testable IR, CLI discovery, AI-assisted workflows
|
|
23
|
-
- [How It Works](./core-concepts/how-it-works.
|
|
23
|
+
- [How It Works](./core-concepts/how-it-works.mdx) — Two-phase compilation, hydration markers, clean overrides
|
|
24
24
|
|
|
25
25
|
### 4. [Reactivity](./reactivity.md)
|
|
26
26
|
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
### 6. [Components](./components.md)
|
|
42
42
|
|
|
43
|
-
- [Component Authoring](./components/component-authoring.
|
|
43
|
+
- [Component Authoring](./components/component-authoring.mdx) — Server components, client components, and the compilation model
|
|
44
44
|
- [Props & Type Safety](./components/props-type-safety.md) — Typing props, defaults, and rest spreading
|
|
45
45
|
- [Children & Slots](./components/children-slots.md) — Children prop, the `Slot` component, and the `asChild` pattern
|
|
46
46
|
- [Context API](./components/context-api.md) — Sharing state with `createContext` / `useContext`
|
|
@@ -69,16 +69,10 @@ Code examples use **switchable tabs** for adapter output and package manager com
|
|
|
69
69
|
|
|
70
70
|
**Adapter** — Hono (default) or Go Template:
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
- Hono (default)
|
|
74
|
-
- Go Template
|
|
72
|
+
<Tabs id="adapter" labels="Hono (default),Go Template" />
|
|
75
73
|
|
|
76
74
|
**Package Manager** — npm (default), bun, pnpm, or yarn:
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
- npm (default)
|
|
80
|
-
- bun
|
|
81
|
-
- pnpm
|
|
82
|
-
- yarn
|
|
76
|
+
<Tabs id="pm" labels="npm (default),bun,pnpm,yarn" />
|
|
83
77
|
|
|
84
78
|
> Sections marked with 💡 explain JSX and TypeScript concepts for developers from Go, Python, or other backend languages.
|
|
@@ -40,7 +40,7 @@ export function Counter() {
|
|
|
40
40
|
}
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The compiler produces a **marked template** (server HTML with `bf-*` attributes) and **client JS** (signals, effects, event handlers). See [How It Works](../core-concepts/how-it-works.
|
|
43
|
+
The compiler produces a **marked template** (server HTML with `bf-*` attributes) and **client JS** (signals, effects, event handlers). See [How It Works](../core-concepts/how-it-works.mdx#two-phase-compilation) for details.
|
|
44
44
|
|
|
45
45
|
### When `"use client"` Is Required
|
|
46
46
|
|
|
@@ -92,8 +92,9 @@ export function Toggle() {
|
|
|
92
92
|
|
|
93
93
|
**Marked template:**
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
<Tabs id="adapter" default="Hono">
|
|
96
|
+
<Tab label="Hono" />
|
|
97
|
+
|
|
97
98
|
```tsx
|
|
98
99
|
export function Toggle({ __instanceId, ... }) {
|
|
99
100
|
const __scopeId = __instanceId || `Toggle_${...}`
|
|
@@ -107,7 +108,9 @@ export function Toggle({ __instanceId, ... }) {
|
|
|
107
108
|
)
|
|
108
109
|
}
|
|
109
110
|
```
|
|
110
|
-
|
|
111
|
+
|
|
112
|
+
<Tab label="Go Template" />
|
|
113
|
+
|
|
111
114
|
```go-template
|
|
112
115
|
{{define "Toggle"}}
|
|
113
116
|
<button bf-s="{{bfScopeAttr .}}" bf="s1">
|
|
@@ -116,7 +119,8 @@ export function Toggle({ __instanceId, ... }) {
|
|
|
116
119
|
</button>
|
|
117
120
|
{{end}}
|
|
118
121
|
```
|
|
119
|
-
|
|
122
|
+
|
|
123
|
+
</Tabs>
|
|
120
124
|
|
|
121
125
|
**Client JS:**
|
|
122
126
|
|
|
@@ -11,7 +11,7 @@ For the `"use client"` directive and the server/client boundary, see [Backend Fr
|
|
|
11
11
|
|
|
12
12
|
| Topic | Description |
|
|
13
13
|
|-------|-------------|
|
|
14
|
-
| [Component Authoring](./components/component-authoring.
|
|
14
|
+
| [Component Authoring](./components/component-authoring.mdx) | Server components, client components, and the compilation model |
|
|
15
15
|
| [Props & Type Safety](./components/props-type-safety.md) | Typing props, defaults, and rest spreading |
|
|
16
16
|
| [Children & Slots](./components/children-slots.md) | Children prop, the `Slot` component, and the `asChild` pattern |
|
|
17
17
|
| [Context API](./components/context-api.md) | Sharing state across compound components with `createContext` / `useContext` |
|
|
@@ -51,8 +51,9 @@ The IR records:
|
|
|
51
51
|
|
|
52
52
|
Marked template (Phase 2a):
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
<Tabs id="adapter" default="Hono">
|
|
55
|
+
<Tab label="Hono" />
|
|
56
|
+
|
|
56
57
|
```tsx
|
|
57
58
|
export function Counter({ __instanceId, ... }) {
|
|
58
59
|
const __scopeId = __instanceId || `Counter_${Math.random().toString(36).slice(2, 8)}`
|
|
@@ -65,7 +66,9 @@ export function Counter({ __instanceId, ... }) {
|
|
|
65
66
|
)
|
|
66
67
|
}
|
|
67
68
|
```
|
|
68
|
-
|
|
69
|
+
|
|
70
|
+
<Tab label="Go Template" />
|
|
71
|
+
|
|
69
72
|
```go-template
|
|
70
73
|
{{define "Counter"}}
|
|
71
74
|
<button bf-s="{{bfScopeAttr .}}" bf="s1">
|
|
@@ -73,7 +76,8 @@ export function Counter({ __instanceId, ... }) {
|
|
|
73
76
|
</button>
|
|
74
77
|
{{end}}
|
|
75
78
|
```
|
|
76
|
-
|
|
79
|
+
|
|
80
|
+
</Tabs>
|
|
77
81
|
|
|
78
82
|
Client JS (Phase 2b):
|
|
79
83
|
|
|
@@ -28,8 +28,9 @@ export function Counter() {
|
|
|
28
28
|
|
|
29
29
|
This single file compiles into two outputs:
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
<Tabs id="adapter" default="Hono">
|
|
32
|
+
<Tab label="Hono" />
|
|
33
|
+
|
|
33
34
|
**Marked template** — Renders static HTML with hydration markers:
|
|
34
35
|
|
|
35
36
|
```tsx
|
|
@@ -45,7 +46,8 @@ export function Counter({ __instanceId, ... }) {
|
|
|
45
46
|
}
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
<Tab label="Go Template" />
|
|
50
|
+
|
|
49
51
|
**Marked template** — Go `html/template` with hydration markers:
|
|
50
52
|
|
|
51
53
|
```go-template
|
|
@@ -56,7 +58,7 @@ export function Counter({ __instanceId, ... }) {
|
|
|
56
58
|
{{end}}
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
</Tabs>
|
|
60
62
|
|
|
61
63
|
**Client script** — Wires up only the interactive parts:
|
|
62
64
|
|
|
@@ -84,4 +86,3 @@ hydrate('Counter', {
|
|
|
84
86
|
template: (_p) => `<button bf="s1"> Count: <!--bf:s0-->${(0)}<!--/--></button>`
|
|
85
87
|
})
|
|
86
88
|
```
|
|
87
|
-
|
package/dist/index.js
CHANGED
|
@@ -1127,6 +1127,7 @@ function createAnalyzerContext(sourceFile, filePath) {
|
|
|
1127
1127
|
jsxConstants: /* @__PURE__ */ new Map(),
|
|
1128
1128
|
inlineableJsxConsts: /* @__PURE__ */ new Map(),
|
|
1129
1129
|
jsxFunctions: /* @__PURE__ */ new Map(),
|
|
1130
|
+
jsxMultiReturnFunctions: /* @__PURE__ */ new Map(),
|
|
1130
1131
|
reactiveFactories: /* @__PURE__ */ new Map(),
|
|
1131
1132
|
signalTupleRefs: /* @__PURE__ */ new Map(),
|
|
1132
1133
|
propsType: null,
|
|
@@ -1345,24 +1346,14 @@ var init_errors = __esm({
|
|
|
1345
1346
|
// Directive errors (BF001-BF009)
|
|
1346
1347
|
MISSING_USE_CLIENT: "BF001",
|
|
1347
1348
|
CLIENT_IMPORTING_SERVER: "BF003",
|
|
1348
|
-
// Signal/Memo errors (
|
|
1349
|
-
UNKNOWN_SIGNAL: "BF010",
|
|
1349
|
+
// Signal/Memo errors (BF011-BF019)
|
|
1350
1350
|
SIGNAL_OUTSIDE_COMPONENT: "BF011",
|
|
1351
|
-
|
|
1352
|
-
// JSX errors (BF020-BF029)
|
|
1353
|
-
INVALID_JSX_EXPRESSION: "BF020",
|
|
1351
|
+
// JSX errors (BF021-BF029)
|
|
1354
1352
|
UNSUPPORTED_JSX_PATTERN: "BF021",
|
|
1355
|
-
INVALID_JSX_ATTRIBUTE: "BF022",
|
|
1356
1353
|
MISSING_KEY_IN_LIST: "BF023",
|
|
1357
1354
|
MISSING_KEY_IN_NESTED_LIST: "BF024",
|
|
1358
1355
|
UNSUPPORTED_DESTRUCTURE_REST: "BF025",
|
|
1359
|
-
//
|
|
1360
|
-
TYPE_INFERENCE_FAILED: "BF030",
|
|
1361
|
-
PROPS_TYPE_MISMATCH: "BF031",
|
|
1362
|
-
// Component errors (BF040-BF049)
|
|
1363
|
-
COMPONENT_NOT_FOUND: "BF040",
|
|
1364
|
-
CIRCULAR_DEPENDENCY: "BF041",
|
|
1365
|
-
INVALID_COMPONENT_NAME: "BF042",
|
|
1356
|
+
// Component errors (BF043-BF049)
|
|
1366
1357
|
PROPS_DESTRUCTURING: "BF043",
|
|
1367
1358
|
SIGNAL_GETTER_NOT_CALLED: "BF044",
|
|
1368
1359
|
JSX_IN_LOCAL_FUNCTION: "BF045",
|
|
@@ -1414,12 +1405,8 @@ var init_errors = __esm({
|
|
|
1414
1405
|
errorMessages = {
|
|
1415
1406
|
[ErrorCodes.MISSING_USE_CLIENT]: "'use client' directive required for components with createSignal or event handlers",
|
|
1416
1407
|
[ErrorCodes.CLIENT_IMPORTING_SERVER]: "Client component cannot import server component",
|
|
1417
|
-
[ErrorCodes.UNKNOWN_SIGNAL]: "Unknown signal reference",
|
|
1418
1408
|
[ErrorCodes.SIGNAL_OUTSIDE_COMPONENT]: "Module-level reactive declaration (createSignal / createMemo) is not allowed. The downstream codegen drops the declaration silently and every reference becomes a ReferenceError at SSR and at hydrate. Move the declaration inside a component function so each mount gets its own state.",
|
|
1419
|
-
[ErrorCodes.INVALID_SIGNAL_USAGE]: "Invalid signal usage",
|
|
1420
|
-
[ErrorCodes.INVALID_JSX_EXPRESSION]: "Invalid JSX expression",
|
|
1421
1409
|
[ErrorCodes.UNSUPPORTED_JSX_PATTERN]: "Unsupported JSX pattern",
|
|
1422
|
-
[ErrorCodes.INVALID_JSX_ATTRIBUTE]: "Invalid JSX attribute",
|
|
1423
1410
|
[ErrorCodes.MISSING_KEY_IN_LIST]: "Missing key attribute in list rendering. Add a key prop for efficient updates",
|
|
1424
1411
|
[ErrorCodes.MISSING_KEY_IN_NESTED_LIST]: "Nested .map() loop requires key attribute for event delegation. Add a key prop to elements in the inner loop",
|
|
1425
1412
|
[ErrorCodes.UNSUPPORTED_DESTRUCTURE_REST]: (
|
|
@@ -1431,11 +1418,6 @@ var init_errors = __esm({
|
|
|
1431
1418
|
// stable.
|
|
1432
1419
|
"Computed property key in .map() callback destructure is not supported. Rewrite the callback to destructure explicit bindings (e.g., `({ a, b }) => ...`) so the compiler can rewrite references to per-item signal accessors."
|
|
1433
1420
|
),
|
|
1434
|
-
[ErrorCodes.TYPE_INFERENCE_FAILED]: "Failed to infer type",
|
|
1435
|
-
[ErrorCodes.PROPS_TYPE_MISMATCH]: "Props type mismatch",
|
|
1436
|
-
[ErrorCodes.COMPONENT_NOT_FOUND]: "Component not found",
|
|
1437
|
-
[ErrorCodes.CIRCULAR_DEPENDENCY]: "Circular dependency detected",
|
|
1438
|
-
[ErrorCodes.INVALID_COMPONENT_NAME]: "Component name must start with uppercase letter",
|
|
1439
1421
|
[ErrorCodes.PROPS_DESTRUCTURING]: "Props destructuring in function parameters breaks reactivity. Use props object directly.",
|
|
1440
1422
|
[ErrorCodes.SIGNAL_GETTER_NOT_CALLED]: "Signal/memo getter passed without calling it. Use getter() to read the value.",
|
|
1441
1423
|
[ErrorCodes.JSX_IN_LOCAL_FUNCTION]: "Local function returns JSX but cannot be inlined. Extract it as a top-level PascalCase component or use a single return statement.",
|
|
@@ -1447,7 +1429,7 @@ var init_errors = __esm({
|
|
|
1447
1429
|
[ErrorCodes.STRIPPED_CLIENT_IMPORT_REFERENCED]: "Import was stripped from the client bundle but its binding is still referenced. Client components ('use client' .tsx) are not callable as plain functions from imperative .ts modules \u2014 render them as JSX from a 'use client' parent instead. If the flagged name is a local shadow rather than the stripped import, please file an issue.",
|
|
1448
1430
|
[ErrorCodes.STAGE_REACTIVE_IN_TEMPLATE]: "Reactive binding (signal getter or memo) referenced from template scope. The template lambda runs at module scope without the reactive context, so the value cannot be evaluated at SSR. Wrap the JSX expression in /* @client */ to defer it to hydrate, or restructure so the template uses a prop or static value.",
|
|
1449
1431
|
[ErrorCodes.STAGE_INIT_LOCAL_IN_TEMPLATE]: "Init-scope local referenced from template scope. The template lambda runs at module scope (via render() / renderChild()) and cannot reach init-body locals. Wrap the JSX expression in /* @client */, or lift the value to a prop or module-scope const.",
|
|
1450
|
-
[ErrorCodes.STAGE_AWAIT_IN_TEMPLATE]: "AwaitExpression in template scope. The
|
|
1432
|
+
[ErrorCodes.STAGE_AWAIT_IN_TEMPLATE]: "AwaitExpression in template scope. The generated template and init functions are synchronous \u2014 a bare `await` produces a SyntaxError at parse time. Move the await into the component body (before the return) or into an onMount/effect callback, and pass the resolved value to JSX.",
|
|
1451
1433
|
[ErrorCodes.INLINE_JSX_CALLBACK_CAPTURE]: "Inline JSX-returning arrow function captures a non-module identifier. Extract the callback into a top-level 'use client' component (e.g. `function MyNode(n) { return <div/> }` then `renderNode={MyNode}`) or pass captured values via component props.",
|
|
1452
1434
|
[ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY]: "Tuple destructuring of a non-reactive factory call. The compiler only recognizes createSignal / createMemo calls and same-file helpers that wrap them with a single `return [a, b]` exit."
|
|
1453
1435
|
};
|
|
@@ -2443,6 +2425,126 @@ function extractSingleJsxReturn(body) {
|
|
|
2443
2425
|
if (returnCount !== 1) return null;
|
|
2444
2426
|
return jsxReturn;
|
|
2445
2427
|
}
|
|
2428
|
+
function extractMultiReturnJsxBranches(body) {
|
|
2429
|
+
const branches = [];
|
|
2430
|
+
let fallback = null;
|
|
2431
|
+
const stmts = body.statements;
|
|
2432
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
2433
|
+
const stmt = stmts[i];
|
|
2434
|
+
if (ts6.isIfStatement(stmt)) {
|
|
2435
|
+
let current = stmt;
|
|
2436
|
+
while (ts6.isIfStatement(current)) {
|
|
2437
|
+
const ifStmt = current;
|
|
2438
|
+
if (!isDirectReturnBlock(ifStmt.thenStatement)) return null;
|
|
2439
|
+
const jsxReturn = findJsxReturnInBlock(ifStmt.thenStatement);
|
|
2440
|
+
const nullReturn = findNullReturnInBlock(ifStmt.thenStatement);
|
|
2441
|
+
if (!jsxReturn && !nullReturn) return null;
|
|
2442
|
+
branches.push({ condition: ifStmt.expression, jsxReturn: jsxReturn ?? null });
|
|
2443
|
+
if (ifStmt.elseStatement) {
|
|
2444
|
+
if (ts6.isIfStatement(ifStmt.elseStatement)) {
|
|
2445
|
+
current = ifStmt.elseStatement;
|
|
2446
|
+
continue;
|
|
2447
|
+
}
|
|
2448
|
+
if (!isDirectReturnBlock(ifStmt.elseStatement)) return null;
|
|
2449
|
+
const elseJsx = findJsxReturnInBlock(ifStmt.elseStatement);
|
|
2450
|
+
if (elseJsx) {
|
|
2451
|
+
fallback = elseJsx;
|
|
2452
|
+
} else if (!findNullReturnInBlock(ifStmt.elseStatement)) {
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2455
|
+
if (branches.length === 0) return null;
|
|
2456
|
+
return { branches, fallback };
|
|
2457
|
+
}
|
|
2458
|
+
break;
|
|
2459
|
+
}
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2462
|
+
if (ts6.isSwitchStatement(stmt)) {
|
|
2463
|
+
if (branches.length > 0) return null;
|
|
2464
|
+
if (!ts6.isIdentifier(stmt.expression) && !ts6.isPropertyAccessExpression(stmt.expression)) {
|
|
2465
|
+
return null;
|
|
2466
|
+
}
|
|
2467
|
+
const hasDefault = stmt.caseBlock.clauses.some((c) => ts6.isDefaultClause(c));
|
|
2468
|
+
if (!hasDefault) return null;
|
|
2469
|
+
for (const clause of stmt.caseBlock.clauses) {
|
|
2470
|
+
const jsxReturn = findJsxReturnInCaseClause(clause);
|
|
2471
|
+
const nullReturn = findNullReturnInCaseClause(clause);
|
|
2472
|
+
if (!jsxReturn && !nullReturn) return null;
|
|
2473
|
+
if (ts6.isCaseClause(clause)) {
|
|
2474
|
+
branches.push({
|
|
2475
|
+
condition: clause.expression,
|
|
2476
|
+
jsxReturn: jsxReturn ?? null
|
|
2477
|
+
});
|
|
2478
|
+
} else {
|
|
2479
|
+
fallback = jsxReturn ?? null;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
if (branches.length === 0) return null;
|
|
2483
|
+
return { branches, fallback, switchDiscriminant: stmt.expression };
|
|
2484
|
+
}
|
|
2485
|
+
if (ts6.isReturnStatement(stmt) && stmt.expression) {
|
|
2486
|
+
const expr = unwrapJsxTransparent(stmt.expression);
|
|
2487
|
+
if (ts6.isJsxElement(expr) || ts6.isJsxSelfClosingElement(expr) || ts6.isJsxFragment(expr)) {
|
|
2488
|
+
fallback = expr;
|
|
2489
|
+
} else if (expr.kind === ts6.SyntaxKind.NullKeyword) {
|
|
2490
|
+
} else {
|
|
2491
|
+
return null;
|
|
2492
|
+
}
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
if (ts6.isVariableStatement(stmt)) return null;
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2498
|
+
if (branches.length === 0) return null;
|
|
2499
|
+
return { branches, fallback };
|
|
2500
|
+
}
|
|
2501
|
+
function isDirectReturnBlock(node) {
|
|
2502
|
+
if (ts6.isReturnStatement(node)) return true;
|
|
2503
|
+
if (ts6.isBlock(node)) {
|
|
2504
|
+
let returnCount = 0;
|
|
2505
|
+
for (const stmt of node.statements) {
|
|
2506
|
+
if (ts6.isReturnStatement(stmt)) {
|
|
2507
|
+
returnCount++;
|
|
2508
|
+
} else if (ts6.isIfStatement(stmt) || ts6.isSwitchStatement(stmt) || ts6.isForStatement(stmt) || ts6.isForOfStatement(stmt) || ts6.isForInStatement(stmt) || ts6.isWhileStatement(stmt) || ts6.isDoStatement(stmt) || ts6.isTryStatement(stmt)) {
|
|
2509
|
+
return false;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
return returnCount === 1;
|
|
2513
|
+
}
|
|
2514
|
+
return false;
|
|
2515
|
+
}
|
|
2516
|
+
function findNullReturnInBlock(node) {
|
|
2517
|
+
if (ts6.isBlock(node)) {
|
|
2518
|
+
for (const stmt of node.statements) {
|
|
2519
|
+
if (ts6.isReturnStatement(stmt) && stmt.expression) {
|
|
2520
|
+
const expr = unwrapJsxTransparent(stmt.expression);
|
|
2521
|
+
if (expr.kind === ts6.SyntaxKind.NullKeyword) return true;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
if (ts6.isReturnStatement(node) && node.expression) {
|
|
2526
|
+
const expr = unwrapJsxTransparent(node.expression);
|
|
2527
|
+
if (expr.kind === ts6.SyntaxKind.NullKeyword) return true;
|
|
2528
|
+
}
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
function findJsxReturnInCaseClause(clause) {
|
|
2532
|
+
for (const stmt of clause.statements) {
|
|
2533
|
+
if (ts6.isReturnStatement(stmt) && stmt.expression) {
|
|
2534
|
+
return extractJsxFromExpression(stmt.expression);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
return null;
|
|
2538
|
+
}
|
|
2539
|
+
function findNullReturnInCaseClause(clause) {
|
|
2540
|
+
for (const stmt of clause.statements) {
|
|
2541
|
+
if (ts6.isReturnStatement(stmt) && stmt.expression) {
|
|
2542
|
+
const expr = unwrapJsxTransparent(stmt.expression);
|
|
2543
|
+
if (expr.kind === ts6.SyntaxKind.NullKeyword) return true;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
return false;
|
|
2547
|
+
}
|
|
2446
2548
|
function isMultiReturnJsxFunctionBody(body) {
|
|
2447
2549
|
let returnCount = 0;
|
|
2448
2550
|
let hasJsxReturn = false;
|
|
@@ -2519,6 +2621,15 @@ function collectFunction(node, ctx2, _isModule, isExported = false) {
|
|
|
2519
2621
|
jsxReturn,
|
|
2520
2622
|
params: node.parameters.map((p) => p.name.getText(ctx2.sourceFile))
|
|
2521
2623
|
});
|
|
2624
|
+
} else {
|
|
2625
|
+
const multi = extractMultiReturnJsxBranches(node.body);
|
|
2626
|
+
if (multi) {
|
|
2627
|
+
isJsxFunction = true;
|
|
2628
|
+
ctx2.jsxMultiReturnFunctions.set(name, {
|
|
2629
|
+
...multi,
|
|
2630
|
+
params: node.parameters.map((p) => p.name.getText(ctx2.sourceFile))
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2522
2633
|
}
|
|
2523
2634
|
}
|
|
2524
2635
|
const isAsync = node.modifiers?.some((m) => m.kind === ts6.SyntaxKind.AsyncKeyword) ?? false;
|
|
@@ -2739,6 +2850,15 @@ function collectConstant(node, ctx2, _isModule, declarationKind = "const", isExp
|
|
|
2739
2850
|
jsxReturn,
|
|
2740
2851
|
params: init.parameters.map((p) => p.name.getText(ctx2.sourceFile))
|
|
2741
2852
|
});
|
|
2853
|
+
} else {
|
|
2854
|
+
const multi = extractMultiReturnJsxBranches(arrowBody);
|
|
2855
|
+
if (multi) {
|
|
2856
|
+
isJsxFunction = true;
|
|
2857
|
+
ctx2.jsxMultiReturnFunctions.set(name, {
|
|
2858
|
+
...multi,
|
|
2859
|
+
params: init.parameters.map((p) => p.name.getText(ctx2.sourceFile))
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2742
2862
|
}
|
|
2743
2863
|
} else {
|
|
2744
2864
|
let body = arrowBody;
|
|
@@ -2991,7 +3111,7 @@ function inferTypeFromValue(value) {
|
|
|
2991
3111
|
if (/\.(some|every|includes)\s*\([\s\S]*\)\s*$/.test(trimmed)) {
|
|
2992
3112
|
return { kind: "primitive", raw: "boolean", primitive: "boolean" };
|
|
2993
3113
|
}
|
|
2994
|
-
if (/\.(indexOf|findIndex|lastIndexOf)\s*\([\s\S]*\)\s*$/.test(trimmed)) {
|
|
3114
|
+
if (/\.(indexOf|findIndex|findLastIndex|lastIndexOf)\s*\([\s\S]*\)\s*$/.test(trimmed)) {
|
|
2995
3115
|
return { kind: "primitive", raw: "number", primitive: "number" };
|
|
2996
3116
|
}
|
|
2997
3117
|
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
@@ -3685,7 +3805,7 @@ function convertNode(node, raw) {
|
|
|
3685
3805
|
if (ts7.isCallExpression(node)) {
|
|
3686
3806
|
const callee = convertNode(node.expression, raw);
|
|
3687
3807
|
const args2 = node.arguments.map((arg) => convertNode(arg, raw));
|
|
3688
|
-
if (callee.kind === "member" && ["filter", "every", "some", "find", "findIndex"].includes(callee.property)) {
|
|
3808
|
+
if (callee.kind === "member" && ["filter", "every", "some", "find", "findIndex", "findLast", "findLastIndex"].includes(callee.property)) {
|
|
3689
3809
|
if (args2.length === 1 && args2[0].kind === "arrow-fn") {
|
|
3690
3810
|
const arrowFn = args2[0];
|
|
3691
3811
|
return {
|
|
@@ -4670,19 +4790,17 @@ var init_expression_parser = __esm({
|
|
|
4670
4790
|
"../jsx/src/expression-parser.ts"() {
|
|
4671
4791
|
"use strict";
|
|
4672
4792
|
UNSUPPORTED_METHODS = /* @__PURE__ */ new Set([
|
|
4673
|
-
// Higher-order array methods.
|
|
4674
|
-
// `some`, `find`, `findIndex`) are
|
|
4675
|
-
// IR before reaching this gate;
|
|
4676
|
-
// IRLoop. The rest stay refused — see
|
|
4677
|
-
// design questions.
|
|
4793
|
+
// Higher-order array methods. Seven of these (`filter`, `every`,
|
|
4794
|
+
// `some`, `find`, `findIndex`, `findLast`, `findLastIndex`) are
|
|
4795
|
+
// intercepted as `higher-order` IR before reaching this gate;
|
|
4796
|
+
// `map` is intercepted as an IRLoop. The rest stay refused — see
|
|
4797
|
+
// #1448 Tier C for the design questions.
|
|
4678
4798
|
"filter",
|
|
4679
4799
|
"map",
|
|
4680
4800
|
"reduce",
|
|
4681
4801
|
"reduceRight",
|
|
4682
4802
|
"every",
|
|
4683
4803
|
"some",
|
|
4684
|
-
"findLast",
|
|
4685
|
-
"findLastIndex",
|
|
4686
4804
|
"forEach",
|
|
4687
4805
|
"flatMap",
|
|
4688
4806
|
"flat"
|
|
@@ -5643,6 +5761,23 @@ function transformExpressionInner(expr, ctx2, node, isClientOnly) {
|
|
|
5643
5761
|
}
|
|
5644
5762
|
return ir;
|
|
5645
5763
|
}
|
|
5764
|
+
if (containsAwaitExpression(expr)) {
|
|
5765
|
+
ctx2.analyzer.errors.push(
|
|
5766
|
+
createError(
|
|
5767
|
+
ErrorCodes.STAGE_AWAIT_IN_TEMPLATE,
|
|
5768
|
+
getSourceLocation(expr, ctx2.sourceFile, ctx2.filePath)
|
|
5769
|
+
)
|
|
5770
|
+
);
|
|
5771
|
+
return {
|
|
5772
|
+
type: "expression",
|
|
5773
|
+
expr: "undefined",
|
|
5774
|
+
typeInfo: null,
|
|
5775
|
+
reactive: false,
|
|
5776
|
+
slotId: null,
|
|
5777
|
+
loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath),
|
|
5778
|
+
origin: { phase: "tick", scope: "template", effect: "pure", freeRefs: [] }
|
|
5779
|
+
};
|
|
5780
|
+
}
|
|
5646
5781
|
const exprText = ctx2.getJS(expr);
|
|
5647
5782
|
const freeRefs = resolveFreeRefs(expr, makeBindingEnv(ctx2));
|
|
5648
5783
|
const origin = {
|
|
@@ -5714,6 +5849,105 @@ function transformJsxFunctionCall(callExpr, jsxFunc, ctx2, _isClientOnly) {
|
|
|
5714
5849
|
ctx2.analyzer.getJS = originalAnalyzerGetJS;
|
|
5715
5850
|
}
|
|
5716
5851
|
}
|
|
5852
|
+
function transformMultiReturnJsxFunctionCall(callExpr, info, ctx2) {
|
|
5853
|
+
const substitutions = /* @__PURE__ */ new Map();
|
|
5854
|
+
for (let i = 0; i < info.params.length; i++) {
|
|
5855
|
+
const paramName = info.params[i];
|
|
5856
|
+
const arg = callExpr.arguments[i];
|
|
5857
|
+
if (arg) {
|
|
5858
|
+
substitutions.set(paramName, ctx2.getJS(arg));
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
const baseGetJS = ctx2.analyzer.getJS.bind(ctx2.analyzer);
|
|
5862
|
+
const originalCtxGetJS = ctx2.getJS;
|
|
5863
|
+
const originalAnalyzerGetJS = ctx2.analyzer.getJS;
|
|
5864
|
+
const substitutedGetJS = (node) => {
|
|
5865
|
+
let text = baseGetJS(node);
|
|
5866
|
+
for (const [paramName, argExpr] of substitutions) {
|
|
5867
|
+
text = text.replace(new RegExp(`\\b${paramName}\\b`, "g"), argExpr);
|
|
5868
|
+
}
|
|
5869
|
+
return text;
|
|
5870
|
+
};
|
|
5871
|
+
ctx2.getJS = substitutedGetJS;
|
|
5872
|
+
ctx2.analyzer.getJS = substitutedGetJS;
|
|
5873
|
+
try {
|
|
5874
|
+
const loc = getSourceLocation(callExpr, ctx2.sourceFile, ctx2.filePath);
|
|
5875
|
+
const nullExpr = {
|
|
5876
|
+
type: "expression",
|
|
5877
|
+
expr: "null",
|
|
5878
|
+
typeInfo: { kind: "primitive", raw: "null", primitive: "null" },
|
|
5879
|
+
reactive: false,
|
|
5880
|
+
slotId: null,
|
|
5881
|
+
loc,
|
|
5882
|
+
origin: { phase: "tick", scope: "template", effect: "pure", freeRefs: [] }
|
|
5883
|
+
};
|
|
5884
|
+
let result = info.fallback ? transformNode(info.fallback, ctx2) ?? nullExpr : nullExpr;
|
|
5885
|
+
for (let i = info.branches.length - 1; i >= 0; i--) {
|
|
5886
|
+
const branch = info.branches[i];
|
|
5887
|
+
let conditionText;
|
|
5888
|
+
if (info.switchDiscriminant) {
|
|
5889
|
+
const discText = substitutedGetJS(info.switchDiscriminant);
|
|
5890
|
+
const caseText = substitutedGetJS(branch.condition);
|
|
5891
|
+
conditionText = `${discText} === ${caseText}`;
|
|
5892
|
+
} else {
|
|
5893
|
+
conditionText = substitutedGetJS(branch.condition);
|
|
5894
|
+
}
|
|
5895
|
+
const env = makeBindingEnv(ctx2);
|
|
5896
|
+
const caseFreeRefs = resolveFreeRefs(branch.condition, env);
|
|
5897
|
+
const discFreeRefs = info.switchDiscriminant ? resolveFreeRefs(info.switchDiscriminant, env) : [];
|
|
5898
|
+
const conditionOrigin = {
|
|
5899
|
+
phase: "tick",
|
|
5900
|
+
scope: "template",
|
|
5901
|
+
effect: "pure",
|
|
5902
|
+
freeRefs: [...discFreeRefs, ...caseFreeRefs]
|
|
5903
|
+
};
|
|
5904
|
+
const reactive = isReactiveExpression(conditionText, ctx2, branch.condition) || isReactiveOrigin(conditionOrigin);
|
|
5905
|
+
const loopParamReactive = !reactive && referencesLoopParam(conditionText, ctx2);
|
|
5906
|
+
const callsReactive = exprCallsReactiveGetters(branch.condition, ctx2) || (info.switchDiscriminant ? exprCallsReactiveGetters(info.switchDiscriminant, ctx2) : false);
|
|
5907
|
+
const hasCalls = exprHasFunctionCalls(branch.condition) || (info.switchDiscriminant ? exprHasFunctionCalls(info.switchDiscriminant) : false);
|
|
5908
|
+
const needsSlot = reactive || loopParamReactive || callsReactive || hasCalls;
|
|
5909
|
+
const slotId = needsSlot ? generateSlotId(ctx2) : null;
|
|
5910
|
+
const whenTrue = branch.jsxReturn ? transformNode(branch.jsxReturn, ctx2) ?? nullExpr : nullExpr;
|
|
5911
|
+
let templateCondition;
|
|
5912
|
+
if (info.switchDiscriminant) {
|
|
5913
|
+
const discRewritten = rewriteBarePropRefs2(
|
|
5914
|
+
substitutedGetJS(info.switchDiscriminant),
|
|
5915
|
+
info.switchDiscriminant,
|
|
5916
|
+
ctx2
|
|
5917
|
+
);
|
|
5918
|
+
const caseRewritten = rewriteBarePropRefs2(
|
|
5919
|
+
substitutedGetJS(branch.condition),
|
|
5920
|
+
branch.condition,
|
|
5921
|
+
ctx2
|
|
5922
|
+
);
|
|
5923
|
+
const discPart = discRewritten ?? substitutedGetJS(info.switchDiscriminant);
|
|
5924
|
+
const casePart = caseRewritten ?? substitutedGetJS(branch.condition);
|
|
5925
|
+
templateCondition = `${discPart} === ${casePart}`;
|
|
5926
|
+
} else {
|
|
5927
|
+
templateCondition = rewriteBarePropRefs2(conditionText, branch.condition, ctx2);
|
|
5928
|
+
}
|
|
5929
|
+
const conditional = {
|
|
5930
|
+
type: "conditional",
|
|
5931
|
+
condition: conditionText,
|
|
5932
|
+
templateCondition,
|
|
5933
|
+
conditionType: null,
|
|
5934
|
+
reactive,
|
|
5935
|
+
whenTrue,
|
|
5936
|
+
whenFalse: result,
|
|
5937
|
+
slotId,
|
|
5938
|
+
callsReactiveGetters: callsReactive || void 0,
|
|
5939
|
+
hasFunctionCalls: hasCalls || void 0,
|
|
5940
|
+
loc,
|
|
5941
|
+
origin: conditionOrigin
|
|
5942
|
+
};
|
|
5943
|
+
result = conditional;
|
|
5944
|
+
}
|
|
5945
|
+
return result;
|
|
5946
|
+
} finally {
|
|
5947
|
+
ctx2.getJS = originalCtxGetJS;
|
|
5948
|
+
ctx2.analyzer.getJS = originalAnalyzerGetJS;
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5717
5951
|
function transformConditional(node, ctx2) {
|
|
5718
5952
|
const condition = ctx2.getJS(node.condition);
|
|
5719
5953
|
const conditionOrigin = {
|
|
@@ -5795,6 +6029,11 @@ function containsJsxInExpression(node) {
|
|
|
5795
6029
|
}
|
|
5796
6030
|
return ts10.forEachChild(node, containsJsxInExpression) ?? false;
|
|
5797
6031
|
}
|
|
6032
|
+
function containsAwaitExpression(node) {
|
|
6033
|
+
if (ts10.isAwaitExpression(node)) return true;
|
|
6034
|
+
if (ts10.isFunctionDeclaration(node) || ts10.isFunctionExpression(node) || ts10.isArrowFunction(node)) return false;
|
|
6035
|
+
return ts10.forEachChild(node, containsAwaitExpression) ?? false;
|
|
6036
|
+
}
|
|
5798
6037
|
function transformNullishCoalescing(node, ctx2) {
|
|
5799
6038
|
const leftText = ctx2.getJS(node.left);
|
|
5800
6039
|
const isNullish = node.operatorToken.kind === ts10.SyntaxKind.QuestionQuestionToken;
|
|
@@ -5888,6 +6127,10 @@ function transformJsxExpression(expr, ctx2, isClientOnly = false) {
|
|
|
5888
6127
|
if (jsxFunc) {
|
|
5889
6128
|
return transformJsxFunctionCall(node, jsxFunc, ctx2, isClientOnly);
|
|
5890
6129
|
}
|
|
6130
|
+
const multiJsxFunc = ctx2.analyzer.jsxMultiReturnFunctions.get(callee.text);
|
|
6131
|
+
if (multiJsxFunc) {
|
|
6132
|
+
return transformMultiReturnJsxFunctionCall(node, multiJsxFunc, ctx2);
|
|
6133
|
+
}
|
|
5891
6134
|
}
|
|
5892
6135
|
return null;
|
|
5893
6136
|
}
|
|
@@ -5925,10 +6168,22 @@ function transformJsxExpression(expr, ctx2, isClientOnly = false) {
|
|
|
5925
6168
|
case ts10.SyntaxKind.ArrayLiteralExpression:
|
|
5926
6169
|
return null;
|
|
5927
6170
|
// --- Forbidden in render position ---
|
|
5928
|
-
// Spec A.3.3 / A.3.5 reserve BF050 (`AwaitExpression`) and BF051
|
|
5929
|
-
// (`YieldExpression`) for PR 5 once the dispatcher is the single
|
|
5930
|
-
// entry point; for now preserve today's scalar-fallback behaviour.
|
|
5931
6171
|
case ts10.SyntaxKind.AwaitExpression:
|
|
6172
|
+
ctx2.analyzer.errors.push(
|
|
6173
|
+
createError(
|
|
6174
|
+
ErrorCodes.STAGE_AWAIT_IN_TEMPLATE,
|
|
6175
|
+
getSourceLocation(node, ctx2.sourceFile, ctx2.filePath)
|
|
6176
|
+
)
|
|
6177
|
+
);
|
|
6178
|
+
return {
|
|
6179
|
+
type: "expression",
|
|
6180
|
+
expr: "undefined",
|
|
6181
|
+
typeInfo: null,
|
|
6182
|
+
reactive: false,
|
|
6183
|
+
slotId: null,
|
|
6184
|
+
loc: getSourceLocation(node, ctx2.sourceFile, ctx2.filePath),
|
|
6185
|
+
origin: { phase: "tick", scope: "template", effect: "pure", freeRefs: [] }
|
|
6186
|
+
};
|
|
5932
6187
|
case ts10.SyntaxKind.YieldExpression:
|
|
5933
6188
|
return null;
|
|
5934
6189
|
// --- Unreachable at render position ---
|
|
@@ -6578,7 +6833,8 @@ function transformMapCall(node, ctx2, isClientOnly = false, method = "map") {
|
|
|
6578
6833
|
const bodyIsMultiRoot = loopBodyIsMultiRoot(children);
|
|
6579
6834
|
const callsReactive = exprCallsReactiveGetters(arrayExpr, ctx2);
|
|
6580
6835
|
const hasCalls = exprHasFunctionCalls(arrayExpr);
|
|
6581
|
-
const
|
|
6836
|
+
const isDirectPropArray = method !== "flatMap" && isArrayExprDirectPropRef(arrayExpr, ctx2);
|
|
6837
|
+
const isStaticArray = !isSignalOrMemoArray(array, ctx2) && !isDirectPropArray && !hasCalls;
|
|
6582
6838
|
const nestedComponents = collectNestedComponents(children).filter((c) => c.name !== childComponent?.name);
|
|
6583
6839
|
return {
|
|
6584
6840
|
type: "loop",
|
|
@@ -6601,6 +6857,7 @@ function transformMapCall(node, ctx2, isClientOnly = false, method = "map") {
|
|
|
6601
6857
|
// and ssr-hydration-contract assertions don't shift when loops are added.
|
|
6602
6858
|
markerId: `l${ctx2.loopMarkerCounter++}`,
|
|
6603
6859
|
isStaticArray,
|
|
6860
|
+
isPropDerivedArray: isDirectPropArray || void 0,
|
|
6604
6861
|
callsReactiveGetters: callsReactive || void 0,
|
|
6605
6862
|
hasFunctionCalls: hasCalls || void 0,
|
|
6606
6863
|
bodyIsMultiRoot: bodyIsMultiRoot || void 0,
|
|
@@ -6857,6 +7114,15 @@ function getAttributeValue(attr, ctx2) {
|
|
|
6857
7114
|
expr = branchInit;
|
|
6858
7115
|
}
|
|
6859
7116
|
}
|
|
7117
|
+
if (ts10.isAwaitExpression(expr)) {
|
|
7118
|
+
ctx2.analyzer.errors.push(
|
|
7119
|
+
createError(
|
|
7120
|
+
ErrorCodes.STAGE_AWAIT_IN_TEMPLATE,
|
|
7121
|
+
getSourceLocation(expr, ctx2.sourceFile, ctx2.filePath)
|
|
7122
|
+
)
|
|
7123
|
+
);
|
|
7124
|
+
return AttrValueOf.expression("undefined");
|
|
7125
|
+
}
|
|
6860
7126
|
checkBareSignalOrMemoIdentifier(expr, ctx2);
|
|
6861
7127
|
if (attr.name.getText(ctx2.sourceFile) === "style" && ts10.isObjectLiteralExpression(expr)) {
|
|
6862
7128
|
const cssString = tryStaticStyleObjectToCss(expr);
|
|
@@ -7176,6 +7442,20 @@ function checkBareSignalOrMemoIdentifier(expr, ctx2) {
|
|
|
7176
7442
|
}
|
|
7177
7443
|
}
|
|
7178
7444
|
}
|
|
7445
|
+
function isArrayExprDirectPropRef(arrayExpr, ctx2) {
|
|
7446
|
+
const propNames = new Set(ctx2.patterns.props.map((p) => p.name));
|
|
7447
|
+
const propsObjName = ctx2.analyzer.propsObjectName;
|
|
7448
|
+
if (ts10.isIdentifier(arrayExpr)) {
|
|
7449
|
+
return propNames.has(arrayExpr.text);
|
|
7450
|
+
}
|
|
7451
|
+
if (ts10.isPropertyAccessExpression(arrayExpr) && propsObjName) {
|
|
7452
|
+
const obj = arrayExpr.expression;
|
|
7453
|
+
if (ts10.isIdentifier(obj) && obj.text === propsObjName) {
|
|
7454
|
+
return true;
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
return false;
|
|
7458
|
+
}
|
|
7179
7459
|
function isSignalOrMemoArray(array, ctx2) {
|
|
7180
7460
|
for (const { pattern } of ctx2.patterns.signals) {
|
|
7181
7461
|
if (pattern.test(array)) return true;
|
|
@@ -12730,14 +13010,11 @@ function buildReactiveEmit(inner, level, wrapOuter) {
|
|
|
12730
13010
|
}));
|
|
12731
13011
|
const reactiveAttrs = inner.bindings.reactiveAttrs.map((attr) => {
|
|
12732
13012
|
const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings);
|
|
12733
|
-
const isStyleObject = attr.attrName === "style" && /^\s*\{/.test(attr.expression);
|
|
12734
13013
|
return {
|
|
12735
13014
|
slotId: attr.childSlotId,
|
|
12736
|
-
attrName:
|
|
13015
|
+
attrName: attr.attrName,
|
|
12737
13016
|
wrappedExpression: wrapped,
|
|
12738
|
-
|
|
12739
|
-
isBoolean: isBooleanAttr(attr.attrName),
|
|
12740
|
-
presenceOrUndefined: !!attr.presenceOrUndefined
|
|
13017
|
+
meta: pickAttrMeta(attr)
|
|
12741
13018
|
};
|
|
12742
13019
|
});
|
|
12743
13020
|
const preludeStatements = [];
|
|
@@ -12777,8 +13054,6 @@ var init_build_inner_loop = __esm({
|
|
|
12777
13054
|
init_utils();
|
|
12778
13055
|
init_shared();
|
|
12779
13056
|
init_shared();
|
|
12780
|
-
init_utils();
|
|
12781
|
-
init_html_constants();
|
|
12782
13057
|
}
|
|
12783
13058
|
});
|
|
12784
13059
|
|
|
@@ -13876,16 +14151,11 @@ function emitReactive(lines, inner, indent) {
|
|
|
13876
14151
|
for (const attr of emit.reactiveAttrs) {
|
|
13877
14152
|
const targetVar = `__ta_${attr.slotId.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
13878
14153
|
lines.push(`${indent} { const ${targetVar} = qsa(__innerEl${uid}, '[bf="${attr.slotId}"]')`);
|
|
13879
|
-
if (
|
|
13880
|
-
|
|
13881
|
-
}
|
|
13882
|
-
lines.push(`${indent} if (${targetVar}) createEffect(() => { if (${attr.wrappedExpression}) ${targetVar}.setAttribute('${attr.attrName}', ''); else ${targetVar}.removeAttribute('${attr.attrName}') }) }`);
|
|
13883
|
-
} else if (attr.presenceOrUndefined) {
|
|
13884
|
-
const ariaVal = attr.attrName.startsWith("aria-") ? "'true'" : "''";
|
|
13885
|
-
lines.push(`${indent} if (${targetVar}) createEffect(() => { if (${attr.wrappedExpression}) ${targetVar}.setAttribute('${attr.attrName}', ${ariaVal}); else ${targetVar}.removeAttribute('${attr.attrName}') }) }`);
|
|
13886
|
-
} else {
|
|
13887
|
-
lines.push(`${indent} if (${targetVar}) createEffect(() => { const __v = ${attr.wrappedExpression}; if (__v != null) ${targetVar}.setAttribute('${attr.attrName}', String(__v)); else ${targetVar}.removeAttribute('${attr.attrName}') }) }`);
|
|
14154
|
+
lines.push(`${indent} if (${targetVar}) createEffect(() => {`);
|
|
14155
|
+
for (const stmt of emitAttrUpdate(targetVar, attr.attrName, attr.wrappedExpression, attr.meta)) {
|
|
14156
|
+
lines.push(`${indent} ${stmt}`);
|
|
13888
14157
|
}
|
|
14158
|
+
lines.push(`${indent} }) }`);
|
|
13889
14159
|
}
|
|
13890
14160
|
emitLoopChildRefs(lines, emit.childRefs, {
|
|
13891
14161
|
indent: `${indent} `,
|
|
@@ -13934,6 +14204,7 @@ var init_inner_loop = __esm({
|
|
|
13934
14204
|
"use strict";
|
|
13935
14205
|
init_utils();
|
|
13936
14206
|
init_shared();
|
|
14207
|
+
init_emit_reactive();
|
|
13937
14208
|
init_template_parse();
|
|
13938
14209
|
init_loop();
|
|
13939
14210
|
}
|
|
@@ -19655,13 +19926,56 @@ function parseMdx(source) {
|
|
|
19655
19926
|
const nodes = [];
|
|
19656
19927
|
let buffer = [];
|
|
19657
19928
|
let inFence = false;
|
|
19929
|
+
let inBlock = false;
|
|
19930
|
+
let blockName = "";
|
|
19931
|
+
let blockProps = {};
|
|
19932
|
+
let blockChildren = [];
|
|
19933
|
+
let blockChildProps = null;
|
|
19934
|
+
let blockBuffer = [];
|
|
19935
|
+
let blockFence = false;
|
|
19658
19936
|
const flushBuffer = () => {
|
|
19659
19937
|
if (buffer.length === 0) return;
|
|
19660
19938
|
const text = buffer.join("\n").replace(/^\n+|\n+$/g, "");
|
|
19661
19939
|
if (text.length > 0) nodes.push({ type: "md", text });
|
|
19662
19940
|
buffer = [];
|
|
19663
19941
|
};
|
|
19942
|
+
const flushBlockChild = () => {
|
|
19943
|
+
if (blockChildProps !== null) {
|
|
19944
|
+
const content = blockBuffer.join("\n").replace(/^\n+|\n+$/g, "");
|
|
19945
|
+
blockChildren.push({ props: blockChildProps, content });
|
|
19946
|
+
blockBuffer = [];
|
|
19947
|
+
}
|
|
19948
|
+
};
|
|
19664
19949
|
for (const line of lines) {
|
|
19950
|
+
if (inBlock) {
|
|
19951
|
+
if (FENCE_RE.test(line)) {
|
|
19952
|
+
blockFence = !blockFence;
|
|
19953
|
+
blockBuffer.push(line);
|
|
19954
|
+
continue;
|
|
19955
|
+
}
|
|
19956
|
+
if (!blockFence) {
|
|
19957
|
+
const closeMatch = line.match(CLOSE_TAG_RE);
|
|
19958
|
+
if (closeMatch && closeMatch[1] === blockName) {
|
|
19959
|
+
flushBlockChild();
|
|
19960
|
+
nodes.push({ type: "jsx-block", name: blockName, props: blockProps, children: blockChildren });
|
|
19961
|
+
inBlock = false;
|
|
19962
|
+
blockName = "";
|
|
19963
|
+
blockProps = {};
|
|
19964
|
+
blockChildren = [];
|
|
19965
|
+
blockChildProps = null;
|
|
19966
|
+
blockBuffer = [];
|
|
19967
|
+
continue;
|
|
19968
|
+
}
|
|
19969
|
+
const tabMatch = line.match(TAG_LINE_RE);
|
|
19970
|
+
if (tabMatch) {
|
|
19971
|
+
flushBlockChild();
|
|
19972
|
+
blockChildProps = parseProps(tabMatch[2]);
|
|
19973
|
+
continue;
|
|
19974
|
+
}
|
|
19975
|
+
}
|
|
19976
|
+
blockBuffer.push(line);
|
|
19977
|
+
continue;
|
|
19978
|
+
}
|
|
19665
19979
|
if (FENCE_RE.test(line)) {
|
|
19666
19980
|
inFence = !inFence;
|
|
19667
19981
|
buffer.push(line);
|
|
@@ -19674,6 +19988,18 @@ function parseMdx(source) {
|
|
|
19674
19988
|
nodes.push({ type: "jsx", name: match[1], props: parseProps(match[2]) });
|
|
19675
19989
|
continue;
|
|
19676
19990
|
}
|
|
19991
|
+
const openMatch = line.match(OPEN_TAG_RE);
|
|
19992
|
+
if (openMatch) {
|
|
19993
|
+
flushBuffer();
|
|
19994
|
+
inBlock = true;
|
|
19995
|
+
blockName = openMatch[1];
|
|
19996
|
+
blockProps = parseProps(openMatch[2]);
|
|
19997
|
+
blockChildren = [];
|
|
19998
|
+
blockChildProps = null;
|
|
19999
|
+
blockBuffer = [];
|
|
20000
|
+
blockFence = false;
|
|
20001
|
+
continue;
|
|
20002
|
+
}
|
|
19677
20003
|
}
|
|
19678
20004
|
buffer.push(line);
|
|
19679
20005
|
}
|
|
@@ -19685,6 +20011,10 @@ function projectMdxToMarkdown(source, projectors) {
|
|
|
19685
20011
|
const fm = Object.entries(parsed.frontmatter).map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
19686
20012
|
const body = parsed.nodes.map((node) => {
|
|
19687
20013
|
if (node.type === "md") return node.text;
|
|
20014
|
+
if (node.type === "jsx-block") {
|
|
20015
|
+
const project2 = projectors[node.name];
|
|
20016
|
+
return project2 ? project2(node.props, node.children) : "";
|
|
20017
|
+
}
|
|
19688
20018
|
const project = projectors[node.name];
|
|
19689
20019
|
return project ? project(node.props) : "";
|
|
19690
20020
|
}).filter((chunk) => chunk.length > 0).join("\n\n");
|
|
@@ -19709,11 +20039,13 @@ function renderPackageManagerCommand(pm, command2, mode) {
|
|
|
19709
20039
|
if (pm === "yarn") return `yarn dlx ${command2}`;
|
|
19710
20040
|
return `npx ${command2}`;
|
|
19711
20041
|
}
|
|
19712
|
-
var TAG_LINE_RE, FENCE_RE, ATTR_RE, defaultMdxProjectors;
|
|
20042
|
+
var TAG_LINE_RE, OPEN_TAG_RE, CLOSE_TAG_RE, FENCE_RE, ATTR_RE, defaultMdxProjectors;
|
|
19713
20043
|
var init_mdx = __esm({
|
|
19714
20044
|
"src/lib/mdx.ts"() {
|
|
19715
20045
|
"use strict";
|
|
19716
20046
|
TAG_LINE_RE = /^[\t ]*<([A-Z][A-Za-z0-9]*)\b([^>]*?)\/>[\t ]*$/;
|
|
20047
|
+
OPEN_TAG_RE = /^[\t ]*<([A-Z][A-Za-z0-9]*)\b([^>]*?)>[\t ]*$/;
|
|
20048
|
+
CLOSE_TAG_RE = /^[\t ]*<\/([A-Z][A-Za-z0-9]*)>[\t ]*$/;
|
|
19717
20049
|
FENCE_RE = /^[\t ]*(```|~~~)/;
|
|
19718
20050
|
ATTR_RE = /([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*"([^"]*)"/g;
|
|
19719
20051
|
defaultMdxProjectors = {
|
|
@@ -19721,6 +20053,16 @@ var init_mdx = __esm({
|
|
|
19721
20053
|
const pm = defaultPm || "npm";
|
|
19722
20054
|
const cmd = renderPackageManagerCommand(pm, command2 || "", mode || "dlx");
|
|
19723
20055
|
return "```bash\n" + cmd + "\n```";
|
|
20056
|
+
},
|
|
20057
|
+
Tabs: (props, children) => {
|
|
20058
|
+
if (!children || children.length === 0) {
|
|
20059
|
+
const labels = props.labels;
|
|
20060
|
+
if (labels) return labels.split(",").map((l) => `- ${l.trim()}`).join("\n");
|
|
20061
|
+
return "";
|
|
20062
|
+
}
|
|
20063
|
+
const defaultLabel = props.default;
|
|
20064
|
+
const defaultChild = children.find((c) => c.props.label === defaultLabel) || children[0];
|
|
20065
|
+
return defaultChild?.content || "";
|
|
19724
20066
|
}
|
|
19725
20067
|
};
|
|
19726
20068
|
}
|
|
@@ -21050,7 +21392,7 @@ var bfGoSource, streamingGoSource, barefootPmSource, barefootPluginPmSource, bar
|
|
|
21050
21392
|
var init_runtimes_generated = __esm({
|
|
21051
21393
|
"src/lib/adapters/runtimes.generated.ts"() {
|
|
21052
21394
|
"use strict";
|
|
21053
|
-
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 "os"\n "reflect"\n "sort"\n "strconv"\n "strings"\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\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_string": String,\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\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_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_sort": Sort,\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 // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n }\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// 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// 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// 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// 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: DeepEqual element search\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 reflect.DeepEqual(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// 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// 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// Every returns true if all items have the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.every(item => item.field).\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n return false\n }\n if fieldVal.Kind() == reflect.Bool && !fieldVal.Bool() {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item has the specified field set to true.\n// Mirrors 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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if fieldVal.IsValid() && fieldVal.Kind() == reflect.Bool && fieldVal.Bool() {\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\n capitalizedField := capitalize(field)\n var result []any\n\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n // Compare field value with target value\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n result = append(result, v.Index(i).Interface())\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\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 exactly 5 args):\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"\n// direction: "asc" | "desc"\n//\n// The 4 string operands cover the accepted comparator catalogue:\n// `(a,b) => a.f - b.f`, `(a,b) => a - b`, and\n// `(a,b) => a[.f].localeCompare(b[.f])`, each with a reversed\n// counterpart for `desc`. Anything outside the catalogue refuses at\n// compile time (BF101 from the JSX compiler) and never reaches this\n// helper.\n//\n// A future `nulls => "first" | "last"` knob can land as a 6th arg\n// without rewriting the existing call sites \u2014 the keyKind / keyName\n// split already gives the helper everything it needs to project\n// each item\'s sort key before comparing.\nfunc Sort(items any, keyKind string, keyName string, compareType string, direction 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 desc := direction == "desc"\n sort.SliceStable(result, func(i, j int) bool {\n ki := projectSortKey(result[i], keyKind, keyName)\n kj := projectSortKey(result[j], keyKind, keyName)\n if compareType == "string" {\n // `toString` maps nil / unknown types to "" \u2014 matches\n // the documented `bf->string(undef) === ""` divergence\n // from JS and the Perl `bf->sort` helper\'s `// \'\'`\n // undef-coalesce. Using `fmt.Sprint` here would sort\n // missing fields against the literal string "<nil>".\n si := toString(ki)\n sj := toString(kj)\n if desc {\n return si > sj\n }\n return si < sj\n }\n // numeric\n ni := toFloat64(ki)\n nj := toFloat64(kj)\n if desc {\n return ni > nj\n }\n return ni < nj\n })\n\n return result\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 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 return nil\n }\n return fieldVal.Interface()\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// =============================================================================\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// 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\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // Create script collector and inject into props\n scriptCollector := NewScriptCollector()\n setScriptsField(opts.Props, scriptCollector)\n\n // Create portal collector and inject into props\n portalCollector := NewPortalCollector()\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 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 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 // 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: template.HTML(componentBuf.String()),\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// 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// 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\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// =============================================================================\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';
|
|
21395
|
+
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 "os"\n "reflect"\n "sort"\n "strconv"\n "strings"\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\n // String\n "bf_lower": Lower,\n "bf_upper": Upper,\n "bf_trim": Trim,\n "bf_contains": Contains,\n "bf_join": Join,\n "bf_string": String,\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\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_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\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 // Scope comment for fragment roots\n "bfScopeComment": ScopeComment,\n\n // JSX intrinsic-element spread lowering (#1407)\n "bf_spread_attrs": SpreadAttrs,\n }\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// 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// 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// 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// 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: DeepEqual element search\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 reflect.DeepEqual(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// 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// 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// Every returns true if all items have the specified field set to true.\n// Mirrors JavaScript\'s Array.prototype.every(item => item.field).\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n return false\n }\n if fieldVal.Kind() == reflect.Bool && !fieldVal.Bool() {\n return false\n }\n }\n return true\n}\n\n// Some returns true if at least one item has the specified field set to true.\n// Mirrors 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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if fieldVal.IsValid() && fieldVal.Kind() == reflect.Bool && fieldVal.Bool() {\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\n capitalizedField := capitalize(field)\n var result []any\n\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n // Compare field value with target value\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n result = append(result, v.Index(i).Interface())\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\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\n capitalizedField := capitalize(field)\n for i := 0; i < v.Len(); i++ {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n item = item.Elem()\n }\n if item.Kind() == reflect.Ptr {\n item = item.Elem()\n }\n if item.Kind() != reflect.Struct {\n continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), 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\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\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 continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return v.Index(i).Interface()\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\n capitalizedField := capitalize(field)\n for i := v.Len() - 1; i >= 0; i-- {\n item := v.Index(i)\n if item.Kind() == reflect.Interface {\n if item.IsNil() {\n continue\n }\n item = item.Elem()\n }\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 continue\n }\n\n fieldVal := item.FieldByName(capitalizedField)\n if !fieldVal.IsValid() {\n continue\n }\n\n if reflect.DeepEqual(fieldVal.Interface(), value) {\n return i\n }\n }\n return -1\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 exactly 5 args):\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"\n// direction: "asc" | "desc"\n//\n// The 4 string operands cover the accepted comparator catalogue:\n// `(a,b) => a.f - b.f`, `(a,b) => a - b`, and\n// `(a,b) => a[.f].localeCompare(b[.f])`, each with a reversed\n// counterpart for `desc`. Anything outside the catalogue refuses at\n// compile time (BF101 from the JSX compiler) and never reaches this\n// helper.\n//\n// A future `nulls => "first" | "last"` knob can land as a 6th arg\n// without rewriting the existing call sites \u2014 the keyKind / keyName\n// split already gives the helper everything it needs to project\n// each item\'s sort key before comparing.\nfunc Sort(items any, keyKind string, keyName string, compareType string, direction 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 desc := direction == "desc"\n sort.SliceStable(result, func(i, j int) bool {\n ki := projectSortKey(result[i], keyKind, keyName)\n kj := projectSortKey(result[j], keyKind, keyName)\n if compareType == "string" {\n // `toString` maps nil / unknown types to "" \u2014 matches\n // the documented `bf->string(undef) === ""` divergence\n // from JS and the Perl `bf->sort` helper\'s `// \'\'`\n // undef-coalesce. Using `fmt.Sprint` here would sort\n // missing fields against the literal string "<nil>".\n si := toString(ki)\n sj := toString(kj)\n if desc {\n return si > sj\n }\n return si < sj\n }\n // numeric\n ni := toFloat64(ki)\n nj := toFloat64(kj)\n if desc {\n return ni > nj\n }\n return ni < nj\n })\n\n return result\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 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 return nil\n }\n return fieldVal.Interface()\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// =============================================================================\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// 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\nfunc (r *Renderer) Render(opts RenderOptions) string {\n // Create script collector and inject into props\n scriptCollector := NewScriptCollector()\n setScriptsField(opts.Props, scriptCollector)\n\n // Create portal collector and inject into props\n portalCollector := NewPortalCollector()\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 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 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 // 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: template.HTML(componentBuf.String()),\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// 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// 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\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// =============================================================================\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';
|
|
21054
21396
|
streamingGoSource = `// Package bf \u2014 Out-of-Order Streaming SSR helpers
|
|
21055
21397
|
//
|
|
21056
21398
|
// Provides StreamRenderer for progressive page rendering using HTTP
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI for agent-driven UI component discovery and scaffolding",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"typescript": "^5.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@barefootjs/jsx": "0.1.
|
|
34
|
+
"@barefootjs/jsx": "0.1.3",
|
|
35
35
|
"@types/node": "^22.0.0"
|
|
36
36
|
}
|
|
37
37
|
}
|