@barefootjs/go-template 0.16.0 → 0.17.1

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.
Files changed (73) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +23 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +53 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/helper-inline.d.ts +31 -0
  6. package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
  7. package/dist/adapter/expr/url-builder.d.ts +23 -0
  8. package/dist/adapter/expr/url-builder.d.ts.map +1 -0
  9. package/dist/adapter/go-template-adapter.d.ts +302 -1071
  10. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  11. package/dist/adapter/index.js +3116 -2692
  12. package/dist/adapter/lib/compile-state.d.ts +142 -0
  13. package/dist/adapter/lib/compile-state.d.ts.map +1 -0
  14. package/dist/adapter/lib/constants.d.ts +11 -0
  15. package/dist/adapter/lib/constants.d.ts.map +1 -0
  16. package/dist/adapter/lib/go-emit.d.ts +126 -0
  17. package/dist/adapter/lib/go-emit.d.ts.map +1 -0
  18. package/dist/adapter/lib/go-naming.d.ts +39 -0
  19. package/dist/adapter/lib/go-naming.d.ts.map +1 -0
  20. package/dist/adapter/lib/ir-scope.d.ts +13 -0
  21. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  22. package/dist/adapter/lib/types.d.ts +165 -0
  23. package/dist/adapter/lib/types.d.ts.map +1 -0
  24. package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
  25. package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
  26. package/dist/adapter/memo/memo-compute.d.ts +173 -0
  27. package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
  28. package/dist/adapter/memo/memo-type.d.ts +48 -0
  29. package/dist/adapter/memo/memo-type.d.ts.map +1 -0
  30. package/dist/adapter/memo/memo-value.d.ts +64 -0
  31. package/dist/adapter/memo/memo-value.d.ts.map +1 -0
  32. package/dist/adapter/memo/template-interp.d.ts +43 -0
  33. package/dist/adapter/memo/template-interp.d.ts.map +1 -0
  34. package/dist/adapter/props/prop-types.d.ts +31 -0
  35. package/dist/adapter/props/prop-types.d.ts.map +1 -0
  36. package/dist/adapter/spread/spread-codegen.d.ts +40 -0
  37. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  38. package/dist/adapter/type/type-codegen.d.ts +25 -0
  39. package/dist/adapter/type/type-codegen.d.ts.map +1 -0
  40. package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
  41. package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
  42. package/dist/adapter/value/value-lowering.d.ts +46 -0
  43. package/dist/adapter/value/value-lowering.d.ts.map +1 -0
  44. package/dist/build.js +3114 -2690
  45. package/dist/index.js +3116 -2692
  46. package/dist/test-render.d.ts.map +1 -1
  47. package/package.json +3 -3
  48. package/src/__tests__/derived-state-memo.test.ts +155 -84
  49. package/src/__tests__/go-template-adapter.test.ts +398 -54
  50. package/src/__tests__/lowering-plugin.test.ts +109 -0
  51. package/src/__tests__/query-href.test.ts +179 -0
  52. package/src/adapter/analysis/component-tree.ts +174 -0
  53. package/src/adapter/emit-context.ts +59 -0
  54. package/src/adapter/expr/helper-inline.ts +274 -0
  55. package/src/adapter/expr/url-builder.ts +123 -0
  56. package/src/adapter/go-template-adapter.ts +2189 -5320
  57. package/src/adapter/lib/compile-state.ts +181 -0
  58. package/src/adapter/lib/constants.ts +24 -0
  59. package/src/adapter/lib/go-emit.ts +309 -0
  60. package/src/adapter/lib/go-naming.ts +84 -0
  61. package/src/adapter/lib/ir-scope.ts +31 -0
  62. package/src/adapter/lib/types.ts +182 -0
  63. package/src/adapter/memo/ctor-lowering.ts +267 -0
  64. package/src/adapter/memo/memo-compute.ts +661 -0
  65. package/src/adapter/memo/memo-type.ts +93 -0
  66. package/src/adapter/memo/memo-value.ts +197 -0
  67. package/src/adapter/memo/template-interp.ts +246 -0
  68. package/src/adapter/props/prop-types.ts +84 -0
  69. package/src/adapter/spread/spread-codegen.ts +458 -0
  70. package/src/adapter/type/type-codegen.ts +95 -0
  71. package/src/adapter/value/parsed-literal-to-go.ts +94 -0
  72. package/src/adapter/value/value-lowering.ts +162 -0
  73. package/src/test-render.ts +48 -22
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Per-compile mutable state for the Go html/template adapter. The adapter is a
3
+ * reused singleton; everything established (and reset) per `generate()` /
4
+ * `generateTypes()` run lives here. The adapter holds a single `CompileState`
5
+ * and resets its members at the start of each compile.
6
+ *
7
+ * NOT included (deliberately): cross-compile child-shape registries
8
+ * (`childComponentShapes`, `childContextConsumers`, populated before a parent
9
+ * compiles), the render-recursion cursor stacks (`loopParamStack`,
10
+ * `filterExprDepth`, …), and constant config (`options`, `templatePrimitives`).
11
+ */
12
+
13
+ import type {
14
+ CompilerError,
15
+ ContextConsumer,
16
+ EnvSignalReader,
17
+ IRMetadata,
18
+ IRNode,
19
+ LoweringMatcher,
20
+ MemoInfo,
21
+ SsrSeedPlan,
22
+ TypeDefinition,
23
+ TypeInfo,
24
+ } from '@barefootjs/jsx'
25
+
26
+ export class CompileState {
27
+ // --- Reset at the start of `generate()` -----------------------------------
28
+
29
+ componentName: string = ''
30
+ errors: CompilerError[] = []
31
+
32
+ /** Component-scope derived consts referenced by the template during rendering
33
+ * (e.g. `root` → `.Root`). `generateTypes` emits a computed field for each
34
+ * that's resolvable and non-colliding. */
35
+ referencedDerivedConsts: Set<string> = new Set()
36
+
37
+ templateVarCounter: number = 0
38
+
39
+ /**
40
+ * Companion `{{define "<Component>__children_<slot>"}}` blocks queued while
41
+ * rendering the template body. Flushed after the main define in `generate()`.
42
+ */
43
+ pendingChildrenDefines: Array<{ name: string; content: string }> = []
44
+
45
+ propsObjectName: string | null = null
46
+
47
+ /**
48
+ * Component-scoped rest binding identifier (`function({ a, ...rest }: P)`
49
+ * → `'rest'`). Stashed at `generate()` entry so per-attribute emitter
50
+ * callbacks can classify a spread expression against it.
51
+ */
52
+ restPropsName: string | null = null
53
+
54
+ /**
55
+ * Module-scope pure string-literal constants (`const X = 'literal'` at
56
+ * file top-level), keyed by name → resolved literal value. When an identifier
57
+ * resolves to one of these, the adapter inlines the literal value instead of
58
+ * emitting a struct-field reference.
59
+ */
60
+ moduleStringConsts: Map<string, string> = new Map()
61
+
62
+ /**
63
+ * All local constants (module + function-scope) from the IR, retained for
64
+ * the lifetime of `generate()` so the memo-computation path can resolve
65
+ * `Record`-index lookups without re-threading the full `ir` through helpers.
66
+ */
67
+ localConstants: IRMetadata['localConstants'] = []
68
+
69
+ /**
70
+ * Names of component-scope arrow-const helpers (`const sortClass = …`),
71
+ * eligible for call-site inlining.
72
+ */
73
+ localHelperNames: Set<string> = new Set()
74
+
75
+ /** The current IR's memos, stashed like `localConstants` so nested memo
76
+ * resolution can recurse without threading the list through every signature.
77
+ * Full `MemoInfo` so consumers can read the analyzer-attached `parsed` tree. */
78
+ currentMemos: MemoInfo[] = []
79
+
80
+ /** Full type definitions from the current IR, stashed for loop-datum field resolution. */
81
+ currentTypeDefinitions: TypeDefinition[] = []
82
+
83
+ /**
84
+ * `useContext(...)` consumers in the component being generated. Each becomes
85
+ * a struct field defaulted to the `createContext` default.
86
+ */
87
+ contextConsumers: ContextConsumer[] = []
88
+
89
+ /**
90
+ * Local binding names the request-scoped `searchParams()` env signal is
91
+ * imported under (handles `import { searchParams as sp }`).
92
+ */
93
+ searchParamsLocals: Set<string> = new Set()
94
+
95
+ /**
96
+ * Env-signal getter (local binding name, e.g. `searchParams` or an alias) →
97
+ * its registered {@link EnvSignalReader}, keyed by the RECEIVER signal's own
98
+ * `envReader` registration rather than a hardcoded key — so a memo body's
99
+ * `<local>().<method>(...)` resolves to whichever reader that particular
100
+ * local belongs to (open-closed for a future second env signal).
101
+ */
102
+ envSignalReadersByLocal: Map<string, EnvSignalReader> = new Map()
103
+
104
+ /**
105
+ * The backend-neutral SSR seed plan (`computeSsrSeedPlan`, Package G) for
106
+ * the IR currently being compiled — single authority for which signal/memo
107
+ * getters are value-materializable at SSR time (`env-reader` steps are
108
+ * per-request readers; everything else is `derived`/`opaque`). Populated in
109
+ * `primeCompileState`; `envSignalReadersByLocal` and `searchParamsLocals`
110
+ * derive from it there, and the memo-analysis path in `memo-compute.ts`
111
+ * reads its `steps` directly instead of re-deriving from `metadata`.
112
+ */
113
+ ssrSeedPlan: SsrSeedPlan = { baseScope: [], steps: [] }
114
+
115
+ /**
116
+ * Memo name → the hoisted Go local variable name the constructor assigned
117
+ * its SSR value to (populated by the sibling-memo hoisting pre-pass before
118
+ * the main memo-field loop runs). When a later filter-arm memo's free-var
119
+ * resolution finds an entry here, it reuses the local instead of
120
+ * recomputing the sibling's expression a second time.
121
+ */
122
+ hoistedMemoLocals: Map<string, string> = new Map()
123
+
124
+ /**
125
+ * Call-lowering matchers active for this component (#2057), bound to its
126
+ * metadata at init via `prepareLoweringMatchers`. Each maps a recognised call
127
+ * to a backend-neutral `LoweringNode` the adapter renders. Covers both userland
128
+ * plugins and the compiler's built-in plugins (e.g. `queryHref` → `bf_query`,
129
+ * #2042), so there is no separate per-API recognition path.
130
+ */
131
+ loweringMatchers: LoweringMatcher[] = []
132
+
133
+ /**
134
+ * Prop NAMES whose resolved Go struct-field type is exactly `interface{}`
135
+ * — i.e. nillable. Used by the attribute emitter to omit a dynamic attribute
136
+ * whose value is a bare reference to such a prop when it's nil.
137
+ */
138
+ nillablePropNames: Set<string> = new Set()
139
+
140
+ /** Component root scope element(s) — each carries `data-key` for a keyed loop
141
+ * item. */
142
+ rootScopeNodes: Set<IRNode> = new Set()
143
+
144
+ /** Array-memo name → the handler-filled loop slice field its `.map()` feeds
145
+ * (e.g. `visible` → `PostListItems`). Lets `<memo>().length` lower to the
146
+ * slice's length instead of a nil/unset memo field. */
147
+ memoBackedLoopSlice: Map<string, string> = new Map()
148
+
149
+ // --- Reset at the start of `generateTypes()` ------------------------------
150
+
151
+ /** Set during type generation when any emit references
152
+ * `template.HTML(...)`; toggles the `"html/template"` import. */
153
+ usesHtmlTemplate: boolean = false
154
+
155
+ /** Set during type generation when any emit references `fmt.Sprint(...)`;
156
+ * toggles the `"fmt"` import. */
157
+ usesFmt: boolean = false
158
+
159
+ /** Local type names resolved from typeDefinitions (populated during generateTypes). */
160
+ localTypeNames: Set<string> = new Set()
161
+
162
+ /** Local type aliases mapping type name to base type (e.g., Filter → 'string'). */
163
+ localTypeAliases: Map<string, string> = new Map()
164
+
165
+ /**
166
+ * Per-struct field map (type name → source TS key → Go field name), populated
167
+ * during generateTypes. The object-literal baker consults this so a baked
168
+ * struct literal only names fields the generated struct actually declares.
169
+ */
170
+ localStructFields: Map<string, Map<string, string>> = new Map()
171
+
172
+ /**
173
+ * Synthesised array types for untyped object-array signals (signal getter →
174
+ * `[]SynthStruct` TypeInfo), populated during generateTypes.
175
+ */
176
+ synthStructTypes: Map<string, TypeInfo> = new Map()
177
+
178
+ /** Set when a constructor-context lowering emits a `strings.` call, so
179
+ * `strings` is added to the generated types file's import block. */
180
+ needsStringsImport = false
181
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Compile-time constant tables for the Go html/template adapter.
3
+ */
4
+
5
+ import type { PrimitiveSpec } from "./types.ts"
6
+ import { wrapGoArg } from "./go-emit.ts"
7
+
8
+ /**
9
+ * Single source of truth for the Go adapter's template-primitive surface. Each
10
+ * entry pairs the expected arity with the emit function so the two derived maps
11
+ * (`templatePrimitives` and `templatePrimitiveArities`) can't drift out of sync.
12
+ */
13
+ export const GO_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
14
+ 'JSON.stringify': { arity: 1, emit: (args) => `bf_json ${wrapGoArg(args[0])}` },
15
+ 'String': { arity: 1, emit: (args) => `bf_string ${wrapGoArg(args[0])}` },
16
+ 'Number': { arity: 1, emit: (args) => `bf_number ${wrapGoArg(args[0])}` },
17
+ 'Math.floor': { arity: 1, emit: (args) => `bf_floor ${wrapGoArg(args[0])}` },
18
+ 'Math.ceil': { arity: 1, emit: (args) => `bf_ceil ${wrapGoArg(args[0])}` },
19
+ 'Math.round': { arity: 1, emit: (args) => `bf_round ${wrapGoArg(args[0])}` },
20
+ // Two-arg forms only; an N-arg `Math.min(a, b, c)` falls through to the
21
+ // standard BF101 unsupported-call diagnostic via the arity gate.
22
+ 'Math.min': { arity: 2, emit: (args) => `bf_min ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` },
23
+ 'Math.max': { arity: 2, emit: (args) => `bf_max ${wrapGoArg(args[0])} ${wrapGoArg(args[1])}` },
24
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Go html/template emit helpers: string escaping, argument wrapping, `bf_*`
3
+ * runtime-helper call construction, and JSX-literal → Go-literal lowering.
4
+ * Pure free functions — none read adapter instance state.
5
+ */
6
+
7
+ import type { SortComparator, SupportResult, ParsedExpr } from '@barefootjs/jsx'
8
+ import { serializeParsedExpr, freeVarsInBody } from '@barefootjs/jsx'
9
+
10
+ import { capitalize } from "./go-naming.ts"
11
+
12
+ /** Escape a value for embedding in a Go-template double-quoted string. */
13
+ export function escapeGoString(s: string): string {
14
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
15
+ }
16
+
17
+ /**
18
+ * Wrap a rendered Go template fragment in parens when it would otherwise parse
19
+ * as multiple sibling args of an enclosing prefix call. A bare identifier /
20
+ * dotted path / quoted literal stays uncluttered; anything containing
21
+ * whitespace (a call, `len ...`) gets `(...)` so `bf_join (...) bf_trim .Raw`
22
+ * doesn't degrade to four args of `bf_join`.
23
+ */
24
+ export function wrapIfMultiToken(rendered: string): string {
25
+ if (rendered.startsWith('(') && rendered.endsWith(')')) return rendered
26
+ // Quoted literals may contain spaces but parse as one token — leave alone.
27
+ if (rendered.startsWith('"') && rendered.endsWith('"')) return rendered
28
+ if (/\s/.test(rendered)) return `(${rendered})`
29
+ return rendered
30
+ }
31
+
32
+ /**
33
+ * Parenthesize a compound Go template argument (`or .Checked false`) so a
34
+ * primitive call reads it as ONE argument — unwrapped, the parser splits it
35
+ * into three and `bf_string` fails with "want 1 got 3".
36
+ */
37
+ export function wrapGoArg(arg: string): string {
38
+ if (!/\s/.test(arg)) return arg
39
+ if (arg.startsWith('(') && arg.endsWith(')')) return arg
40
+ return `(${arg})`
41
+ }
42
+
43
+ /**
44
+ * Emit the `bf_sort` call:
45
+ *
46
+ * bf_sort <recv> (<keyKind> <keyName> <compareType> <direction>)+
47
+ *
48
+ * keyKind: "self" | "field"
49
+ * keyName: "" when keyKind=self; capitalised field name otherwise
50
+ * compareType: "numeric" | "string" | "auto"
51
+ * direction: "asc" | "desc"
52
+ *
53
+ * The 4-string group repeats once per comparison key: a simple comparator emits
54
+ * one group; a `||`-chained multi-key comparator emits one per operand, applied
55
+ * in order as tie-breakers by the variadic `bf_sort` runtime. Capitalisation
56
+ * mirrors the Go struct-field convention so the runtime's reflect lookup
57
+ * matches without a recapitalise step.
58
+ */
59
+ export function emitBfSort(recv: string, c: SortComparator): string {
60
+ const groups = c.keys.map((k) => {
61
+ const keyName = k.key.kind === 'field' ? capitalize(k.key.field) : ''
62
+ return `"${k.key.kind}" "${keyName}" "${k.type}" "${k.direction}"`
63
+ })
64
+ return `bf_sort ${wrapIfMultiToken(recv)} ${groups.join(' ')}`
65
+ }
66
+
67
+ /**
68
+ * Build the `bf_env` base_env argument for a callback body: one `"name" <value>`
69
+ * pair per captured free variable (a body identifier that isn't a callback
70
+ * param), each lowered to its Go template value via `emit`. No captures →
71
+ * the bare `bf_env` (an empty env).
72
+ */
73
+ function emitEvalEnvArg(body: ParsedExpr, params: string[], emit: (e: ParsedExpr) => string): string {
74
+ const free = freeVarsInBody(body, new Set(params))
75
+ if (free.length === 0) return 'bf_env'
76
+ const pairs = free.map(
77
+ n => `"${escapeGoString(n)}" ${wrapIfMultiToken(emit({ kind: 'identifier', name: n }))}`,
78
+ )
79
+ return `(bf_env ${pairs.join(' ')})`
80
+ }
81
+
82
+ /**
83
+ * Emit a `.sort(cmp)` via the evaluator (#2018): the comparator body travels as
84
+ * serialized-ParsedExpr JSON, evaluated per comparison against `{paramA, paramB,
85
+ * …captured}`. Returns null when the comparator can't be evaluated (e.g. a
86
+ * `localeCompare` body — `serializeParsedExpr` refuses it), so the caller falls
87
+ * back to the structured `bf_sort`. A `||`-chained multi-key comparator needs no
88
+ * special handling — JS `0 || next` is exactly the tie-break semantics.
89
+ */
90
+ export function emitSortEval(
91
+ recv: string,
92
+ body: ParsedExpr,
93
+ params: string[],
94
+ emit: (e: ParsedExpr) => string,
95
+ ): string | null {
96
+ const json = serializeParsedExpr(body)
97
+ if (json === null) return null
98
+ // A comparator needs both params; a wrong-arity arrow would bind the wrong
99
+ // env (or treat a real param as a free var), so fail over to the structured
100
+ // fallback / BF101 instead of inventing default names.
101
+ if (params.length < 2) return null
102
+ const paramA = params[0]
103
+ const paramB = params[1]
104
+ const env = emitEvalEnvArg(body, [paramA, paramB], emit)
105
+ return `bf_sort_eval ${wrapIfMultiToken(recv)} "${escapeGoString(json)}" "${paramA}" "${paramB}" ${env}`
106
+ }
107
+
108
+ /**
109
+ * Emit a `.reduce(fn, init)` via the evaluator (#2018): the reducer body travels
110
+ * as serialized-ParsedExpr JSON, folded over the receiver from `init`. Returns
111
+ * null when the body can't be evaluated, or when `init` isn't a string/number
112
+ * literal (a non-literal seed has no template-time value). A numeric seed is
113
+ * passed through `bf_number` (handles any decimal incl. negative / float); a
114
+ * string seed as a quoted string.
115
+ */
116
+ export function emitReduceEval(
117
+ recv: string,
118
+ body: ParsedExpr,
119
+ params: string[],
120
+ init: ParsedExpr,
121
+ direction: 'left' | 'right',
122
+ emit: (e: ParsedExpr) => string,
123
+ ): string | null {
124
+ const json = serializeParsedExpr(body)
125
+ if (json === null) return null
126
+ // A reducer needs both the accumulator and the element param; a wrong-arity
127
+ // arrow would bind the wrong env, so refuse cleanly (→ BF101) rather than
128
+ // defaulting the names.
129
+ if (params.length < 2) return null
130
+ const paramAcc = params[0]
131
+ const paramItem = params[1]
132
+ // Only a literal seed has a template-time value; anything else (an identifier
133
+ // / call) can't be folded here, so bail to the structured-less fallback (BF101).
134
+ let initGo: string
135
+ if (init.kind === 'literal' && init.literalType === 'string') {
136
+ initGo = `"${escapeGoString(String(init.value))}"`
137
+ } else if (init.kind === 'literal' && init.literalType === 'number') {
138
+ initGo = `(bf_number "${escapeGoString(init.raw ?? String(init.value))}")`
139
+ } else {
140
+ return null
141
+ }
142
+ const env = emitEvalEnvArg(body, [paramAcc, paramItem], emit)
143
+ return `bf_reduce_eval ${wrapIfMultiToken(recv)} "${escapeGoString(json)}" "${paramAcc}" "${paramItem}" ${initGo} "${direction}" ${env}`
144
+ }
145
+
146
+ /**
147
+ * Emit a higher-order predicate call via the evaluator (#2018, P2): the
148
+ * predicate body (already a `ParsedExpr` on the `higher-order` IR node) travels
149
+ * as serialized-ParsedExpr JSON, evaluated per element against `{param,
150
+ * …captured}`. Generalizes the field-equality / truthiness catalogues of
151
+ * `bf_filter` / `bf_find` / `bf_every` / `bf_some` to any pure predicate body.
152
+ * Returns null when the predicate is outside the evaluator surface (e.g. a
153
+ * method-call predicate — `serializeParsedExpr` refuses it), so the caller
154
+ * falls back to the structured helper / template-block path.
155
+ *
156
+ * <func> <recv> "<json>" "<param>" [<extraArgs>…] <env>
157
+ *
158
+ * `extraArgs` are inserted between the param name and the env — used for the
159
+ * find / findIndex `forward` bool (`true` = find / findIndex, `false` =
160
+ * findLast / findLastIndex).
161
+ */
162
+ export function emitPredicateEval(
163
+ funcName: string,
164
+ recv: string,
165
+ predicate: ParsedExpr,
166
+ param: string,
167
+ emit: (e: ParsedExpr) => string,
168
+ extraArgs: string[] = [],
169
+ ): string | null {
170
+ const json = serializeParsedExpr(predicate)
171
+ if (json === null) return null
172
+ const env = emitEvalEnvArg(predicate, [param], emit)
173
+ const extra = extraArgs.length > 0 ? ` ${extraArgs.join(' ')}` : ''
174
+ return `${funcName} ${wrapIfMultiToken(recv)} "${escapeGoString(json)}" "${param}"${extra} ${env}`
175
+ }
176
+
177
+ /**
178
+ * Emit a `.flatMap(proj)` via the evaluator (#2018, P3): the projection body
179
+ * (e.g. `i.tags` / `[i.a, i.b]`) is serialized and evaluated per element by
180
+ * `bf_flat_map_eval`, which flattens the results one level. Returns null when
181
+ * the projection is outside the evaluator surface (→ caller pushes BF101).
182
+ */
183
+ export function emitFlatMapEval(
184
+ recv: string,
185
+ body: ParsedExpr,
186
+ param: string,
187
+ emit: (e: ParsedExpr) => string,
188
+ ): string | null {
189
+ const json = serializeParsedExpr(body)
190
+ if (json === null) return null
191
+ const env = emitEvalEnvArg(body, [param], emit)
192
+ return `bf_flat_map_eval ${wrapIfMultiToken(recv)} "${escapeGoString(json)}" "${param}" ${env}`
193
+ }
194
+
195
+ /**
196
+ * Emit a value-producing `.map(cb)` via the evaluator (#2073): the projection
197
+ * body is serialized and evaluated per element by `bf_map_eval`, one result
198
+ * per element (no flatten — the JS `.map` contract). Composes through the
199
+ * array-method chain (`bf_join (bf_map_eval …) " "`). Returns null when the
200
+ * projection is outside the evaluator surface (→ caller pushes BF101). The
201
+ * JSX-returning `.map` is an IRLoop upstream and never reaches this emit.
202
+ */
203
+ export function emitMapEval(
204
+ recv: string,
205
+ body: ParsedExpr,
206
+ param: string,
207
+ emit: (e: ParsedExpr) => string,
208
+ ): string | null {
209
+ const json = serializeParsedExpr(body)
210
+ if (json === null) return null
211
+ const env = emitEvalEnvArg(body, [param], emit)
212
+ return `bf_map_eval ${wrapIfMultiToken(recv)} "${escapeGoString(json)}" "${param}" ${env}`
213
+ }
214
+
215
+ /**
216
+ * Make an equality comparison string-tolerant when exactly one side is a Go
217
+ * string literal: JS `sorted === 'asc'` is loosely false for `sorted = false`,
218
+ * but Go's template `eq` ERRORS on bool-vs-string (`incompatible types for
219
+ * comparison`). Routing the non-literal side through `bf_string` preserves JS
220
+ * comparison semantics for every concrete type while leaving same-kind
221
+ * comparisons untouched.
222
+ */
223
+ export function stringTolerantEqOperands(l: string, r: string): [string, string] {
224
+ const isStrLit = (x: string) => /^"(?:[^"\\]|\\.)*"$/.test(x)
225
+ if (isStrLit(l) === isStrLit(r)) return [l, r]
226
+ // Keep `wrapGoArg`'s parens: a compound operand must reach `bf_string` as ONE
227
+ // argument — `(bf_string (or .Placement "top"))`, not `(bf_string or …)`.
228
+ const wrap = (x: string) => (isStrLit(x) ? x : `(bf_string ${wrapGoArg(x)})`)
229
+ return [wrap(l), wrap(r)]
230
+ }
231
+
232
+ // Generic remediation appended to BF101 / BF102 diagnostics whose reason
233
+ // doesn't already carry actionable next steps.
234
+ export const GO_REMEDIATION_OPTIONS =
235
+ 'Options:\n1. Use @client directive for client-side evaluation\n2. Pre-compute the value in Go code'
236
+
237
+ // Build the `suggestion.message` for an unsupported expression/condition.
238
+ // A self-contained reason (it already spells out the fix — e.g. the
239
+ // pre-compute / @client hint or the tailored forEach message) is shown
240
+ // as-is; a low-level reason gets the generic options appended; with no
241
+ // reason at all we fall back to the options alone.
242
+ export function buildUnsupportedSuggestion(support: SupportResult): string {
243
+ if (!support.reason) return GO_REMEDIATION_OPTIONS
244
+ if (support.selfContained) return support.reason
245
+ return `${support.reason}\n\n${GO_REMEDIATION_OPTIONS}`
246
+ }
247
+
248
+ /**
249
+ * Translate a JSX param default (e.g. `'default'`, `0`, `false`) into the
250
+ * corresponding Go literal.
251
+ *
252
+ * @returns the Go literal, or `null` when the default is absent or non-trivial
253
+ * (objects, arrow functions, …) — caller then lets Go's zero value win.
254
+ */
255
+ export function goPropDefault(defaultValue: string | undefined): string | null {
256
+ if (!defaultValue) return null
257
+ const trimmed = defaultValue.trim()
258
+ if (trimmed === '') return null
259
+ if (trimmed === 'true' || trimmed === 'false') return trimmed
260
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return trimmed
261
+ if (
262
+ (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
263
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))
264
+ ) {
265
+ const body = trimmed.slice(1, -1)
266
+ return JSON.stringify(body)
267
+ }
268
+ // Anything richer (objects, arrays, expressions) would mis-execute as Go.
269
+ return null
270
+ }
271
+
272
+ /**
273
+ * Wrap an `in.X` reference in a Go expression that substitutes `fallback` when
274
+ * the input is the zero value for its type; the comparison is picked from the
275
+ * fallback literal's shape.
276
+ *
277
+ * Asymmetry on bool/zero defaults is intentional (Go has no
278
+ * unset-vs-explicit-false distinction at the struct-field level):
279
+ * - `true` default → `(in.X || true)`, which is ALWAYS `true`; a caller
280
+ * wanting `false` must set it after `NewXxxProps`, not via the input struct.
281
+ * - `false` / `0` default → matches the Go zero value, so this is a no-op
282
+ * (returns `ref` unchanged).
283
+ * Non-zero numeric defaults substitute, matching JSX `(initial = 5) => …`.
284
+ */
285
+ export function applyGoFallback(ref: string, fallback: string): string {
286
+ if (fallback === 'true' || fallback === 'false') {
287
+ return fallback === 'true' ? `(${ref} || true)` : ref
288
+ }
289
+ if (/^-?\d+(\.\d+)?$/.test(fallback)) {
290
+ if (fallback === '0') return ref
291
+ return `func() int { if ${ref} == 0 { return ${fallback} }; return ${ref} }()`
292
+ }
293
+ // String fallback (quoted).
294
+ return `func() string { if ${ref} == "" { return ${fallback} }; return ${ref} }()`
295
+ }
296
+
297
+ /** Convert a JavaScript literal value to Go literal syntax. */
298
+ export function goLiteral(value: string): string {
299
+ if (value === 'true' || value === 'false') return value
300
+ if (/^-?\d+(\.\d+)?$/.test(value)) return value
301
+ // Single-quoted → Go double quotes; double-quoted kept as-is.
302
+ if (value.startsWith("'") && value.endsWith("'")) {
303
+ return `"${value.slice(1, -1)}"`
304
+ }
305
+ if (value.startsWith('"') && value.endsWith('"')) {
306
+ return value
307
+ }
308
+ return `"${value}"`
309
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Go identifier / field-name conventions: the single source of truth for
3
+ * capitalisation, initialism handling, and slot/loop-key → Go field-path
4
+ * lowering. Pure helpers — none read adapter instance state.
5
+ */
6
+
7
+ /** Matches a bare Go identifier (no dots, no brackets). */
8
+ export const GO_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/
9
+
10
+ /** Go common initialisms that should be fully uppercased (https://go.dev/wiki/CodeReviewComments#initialisms) */
11
+ export const GO_INITIALISMS = new Set([
12
+ 'id', 'url', 'http', 'https', 'api', 'json', 'xml', 'html', 'css', 'sql',
13
+ 'ip', 'tcp', 'udp', 'dns', 'ssh', 'tls', 'ssl', 'uri', 'uid', 'uuid',
14
+ 'ascii', 'utf8', 'eof', 'grpc', 'rpc', 'cpu', 'gpu', 'ram', 'os',
15
+ ])
16
+
17
+ /**
18
+ * Go reserved keywords. When hoisting a local var named after a JSX prop, a
19
+ * collision with one of these is resolved by appending `_` until free.
20
+ */
21
+ export const GO_KEYWORDS = new Set([
22
+ 'break', 'case', 'chan', 'const', 'continue', 'default', 'defer',
23
+ 'else', 'fallthrough', 'for', 'func', 'go', 'goto', 'if', 'import',
24
+ 'interface', 'map', 'package', 'range', 'return', 'select', 'struct',
25
+ 'switch', 'type', 'var',
26
+ ])
27
+
28
+ /**
29
+ * Capitalise a name for use as a Go template field projection. A whole-word
30
+ * Go initialism uppercases entirely (`id` → `ID`, `url` → `URL`) so the
31
+ * `bf_sort` / `bf_reduce` reflect lookup resolves the generated exported
32
+ * field instead of silently folding a zero value.
33
+ */
34
+ export function capitalize(s: string): string {
35
+ if (s.length === 0) return s
36
+ if (GO_INITIALISMS.has(s.toLowerCase())) {
37
+ return s.toUpperCase()
38
+ }
39
+ return s[0].toUpperCase() + s.slice(1)
40
+ }
41
+
42
+ /** Capitalise a JSX prop / field name to its exported Go struct field name. */
43
+ export function capitalizeFieldName(name: string): string {
44
+ if (!name) return name
45
+ // Whole-name initialism (e.g. 'id' → 'ID').
46
+ if (GO_INITIALISMS.has(name.toLowerCase())) {
47
+ return name.toUpperCase()
48
+ }
49
+ return name.charAt(0).toUpperCase() + name.slice(1)
50
+ }
51
+
52
+ /**
53
+ * Convert a slot ID (e.g., 's6') to a Go struct field suffix (e.g., 'Slot6').
54
+ * Keeps field names human-readable regardless of the internal slot ID format.
55
+ */
56
+ export function slotIdToFieldSuffix(slotId: string): string {
57
+ // Strip the parent-owned prefix (^).
58
+ const cleanId = slotId.startsWith('^') ? slotId.slice(1) : slotId
59
+ const match = cleanId.match(/^s(\d+)$/)
60
+ if (match) {
61
+ return `Slot${match[1]}`
62
+ }
63
+ // Fallback for non-standard IDs.
64
+ return cleanId.replace('slot_', 'Slot')
65
+ }
66
+
67
+ /**
68
+ * Lower a keyed-loop `key` expression to the Go field path on the loop's range
69
+ * variable (always `item` in the generated `for i, item := range …`), e.g.
70
+ * `item.label` → `item.Label`.
71
+ *
72
+ * @returns `null` for a non-simple key (computed expression, whole-element key,
73
+ * mismatched param) — caller then skips `data-key` rather than emit
74
+ * something that won't compile.
75
+ */
76
+ export function loopKeyToGoFieldPath(key: string | undefined, param: string | undefined): string | null {
77
+ if (!key || !param) return null
78
+ const segs = key.split('.')
79
+ if (segs[0] !== param) return null
80
+ const rest = segs.slice(1)
81
+ if (rest.length === 0) return null
82
+ if (!rest.every(s => /^[A-Za-z_]\w*$/.test(s))) return null
83
+ return 'item.' + rest.map(capitalize).join('.')
84
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * IR traversal helpers for the Go html/template adapter. Pure functions over
3
+ * the IR tree — no adapter instance state.
4
+ */
5
+
6
+ import type { IRNode, IRIfStatement, IRFragment } from '@barefootjs/jsx'
7
+
8
+ /**
9
+ * Collect the component's root scope element node(s) — the elements that become
10
+ * the rendered root and so carry `data-key` for a keyed loop item. A plain
11
+ * element root is itself; an `if-statement` (early-return) root contributes the
12
+ * top element of each branch, since exactly one renders at runtime.
13
+ */
14
+ export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
15
+ const out = new Set<IRNode>()
16
+ const visit = (n: IRNode | null): void => {
17
+ if (!n) return
18
+ if (n.type === 'element') { out.add(n); return }
19
+ if (n.type === 'if-statement') {
20
+ const s = n as IRIfStatement
21
+ visit(s.consequent)
22
+ visit(s.alternate)
23
+ return
24
+ }
25
+ if (n.type === 'fragment') {
26
+ for (const c of (n as IRFragment).children) visit(c)
27
+ }
28
+ }
29
+ visit(node)
30
+ return out
31
+ }