@barefootjs/go-template 0.15.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +291 -1070
  10. package/dist/adapter/go-template-adapter.d.ts.map +1 -1
  11. package/dist/adapter/index.js +2859 -2618
  12. package/dist/adapter/lib/compile-state.d.ts +116 -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 +117 -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 +124 -0
  27. package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
  28. package/dist/adapter/memo/memo-type.d.ts +38 -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 +2857 -2616
  45. package/dist/index.js +2859 -2618
  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 +211 -60
  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 +2100 -5316
  57. package/src/adapter/lib/compile-state.ts +150 -0
  58. package/src/adapter/lib/constants.ts +24 -0
  59. package/src/adapter/lib/go-emit.ts +289 -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 +451 -0
  65. package/src/adapter/memo/memo-type.ts +74 -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 +2 -14
@@ -0,0 +1,150 @@
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
+ IRMetadata,
17
+ IRNode,
18
+ LoweringMatcher,
19
+ MemoInfo,
20
+ TypeDefinition,
21
+ TypeInfo,
22
+ } from '@barefootjs/jsx'
23
+
24
+ export class CompileState {
25
+ // --- Reset at the start of `generate()` -----------------------------------
26
+
27
+ componentName: string = ''
28
+ errors: CompilerError[] = []
29
+
30
+ /** Component-scope derived consts referenced by the template during rendering
31
+ * (e.g. `root` → `.Root`). `generateTypes` emits a computed field for each
32
+ * that's resolvable and non-colliding. */
33
+ referencedDerivedConsts: Set<string> = new Set()
34
+
35
+ templateVarCounter: number = 0
36
+
37
+ /**
38
+ * Companion `{{define "<Component>__children_<slot>"}}` blocks queued while
39
+ * rendering the template body. Flushed after the main define in `generate()`.
40
+ */
41
+ pendingChildrenDefines: Array<{ name: string; content: string }> = []
42
+
43
+ propsObjectName: string | null = null
44
+
45
+ /**
46
+ * Component-scoped rest binding identifier (`function({ a, ...rest }: P)`
47
+ * → `'rest'`). Stashed at `generate()` entry so per-attribute emitter
48
+ * callbacks can classify a spread expression against it.
49
+ */
50
+ restPropsName: string | null = null
51
+
52
+ /**
53
+ * Module-scope pure string-literal constants (`const X = 'literal'` at
54
+ * file top-level), keyed by name → resolved literal value. When an identifier
55
+ * resolves to one of these, the adapter inlines the literal value instead of
56
+ * emitting a struct-field reference.
57
+ */
58
+ moduleStringConsts: Map<string, string> = new Map()
59
+
60
+ /**
61
+ * All local constants (module + function-scope) from the IR, retained for
62
+ * the lifetime of `generate()` so the memo-computation path can resolve
63
+ * `Record`-index lookups without re-threading the full `ir` through helpers.
64
+ */
65
+ localConstants: IRMetadata['localConstants'] = []
66
+
67
+ /**
68
+ * Names of component-scope arrow-const helpers (`const sortClass = …`),
69
+ * eligible for call-site inlining.
70
+ */
71
+ localHelperNames: Set<string> = new Set()
72
+
73
+ /** The current IR's memos, stashed like `localConstants` so nested memo
74
+ * resolution can recurse without threading the list through every signature.
75
+ * Full `MemoInfo` so consumers can read the analyzer-attached `parsed` tree. */
76
+ currentMemos: MemoInfo[] = []
77
+
78
+ /** Full type definitions from the current IR, stashed for loop-datum field resolution. */
79
+ currentTypeDefinitions: TypeDefinition[] = []
80
+
81
+ /**
82
+ * `useContext(...)` consumers in the component being generated. Each becomes
83
+ * a struct field defaulted to the `createContext` default.
84
+ */
85
+ contextConsumers: ContextConsumer[] = []
86
+
87
+ /**
88
+ * Local binding names the request-scoped `searchParams()` env signal is
89
+ * imported under (handles `import { searchParams as sp }`).
90
+ */
91
+ searchParamsLocals: Set<string> = new Set()
92
+
93
+ /**
94
+ * Call-lowering matchers active for this component (#2057), bound to its
95
+ * metadata at init via `prepareLoweringMatchers`. Each maps a recognised call
96
+ * to a backend-neutral `LoweringNode` the adapter renders. Covers both userland
97
+ * plugins and the compiler's built-in plugins (e.g. `queryHref` → `bf_query`,
98
+ * #2042), so there is no separate per-API recognition path.
99
+ */
100
+ loweringMatchers: LoweringMatcher[] = []
101
+
102
+ /**
103
+ * Prop NAMES whose resolved Go struct-field type is exactly `interface{}`
104
+ * — i.e. nillable. Used by the attribute emitter to omit a dynamic attribute
105
+ * whose value is a bare reference to such a prop when it's nil.
106
+ */
107
+ nillablePropNames: Set<string> = new Set()
108
+
109
+ /** Component root scope element(s) — each carries `data-key` for a keyed loop
110
+ * item. */
111
+ rootScopeNodes: Set<IRNode> = new Set()
112
+
113
+ /** Array-memo name → the handler-filled loop slice field its `.map()` feeds
114
+ * (e.g. `visible` → `PostListItems`). Lets `<memo>().length` lower to the
115
+ * slice's length instead of a nil/unset memo field. */
116
+ memoBackedLoopSlice: Map<string, string> = new Map()
117
+
118
+ // --- Reset at the start of `generateTypes()` ------------------------------
119
+
120
+ /** Set during type generation when any emit references
121
+ * `template.HTML(...)`; toggles the `"html/template"` import. */
122
+ usesHtmlTemplate: boolean = false
123
+
124
+ /** Set during type generation when any emit references `fmt.Sprint(...)`;
125
+ * toggles the `"fmt"` import. */
126
+ usesFmt: boolean = false
127
+
128
+ /** Local type names resolved from typeDefinitions (populated during generateTypes). */
129
+ localTypeNames: Set<string> = new Set()
130
+
131
+ /** Local type aliases mapping type name to base type (e.g., Filter → 'string'). */
132
+ localTypeAliases: Map<string, string> = new Map()
133
+
134
+ /**
135
+ * Per-struct field map (type name → source TS key → Go field name), populated
136
+ * during generateTypes. The object-literal baker consults this so a baked
137
+ * struct literal only names fields the generated struct actually declares.
138
+ */
139
+ localStructFields: Map<string, Map<string, string>> = new Map()
140
+
141
+ /**
142
+ * Synthesised array types for untyped object-array signals (signal getter →
143
+ * `[]SynthStruct` TypeInfo), populated during generateTypes.
144
+ */
145
+ synthStructTypes: Map<string, TypeInfo> = new Map()
146
+
147
+ /** Set when a constructor-context lowering emits a `strings.` call, so
148
+ * `strings` is added to the generated types file's import block. */
149
+ needsStringsImport = false
150
+ }
@@ -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,289 @@
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
+ * Make an equality comparison string-tolerant when exactly one side is a Go
197
+ * string literal: JS `sorted === 'asc'` is loosely false for `sorted = false`,
198
+ * but Go's template `eq` ERRORS on bool-vs-string (`incompatible types for
199
+ * comparison`). Routing the non-literal side through `bf_string` preserves JS
200
+ * comparison semantics for every concrete type while leaving same-kind
201
+ * comparisons untouched.
202
+ */
203
+ export function stringTolerantEqOperands(l: string, r: string): [string, string] {
204
+ const isStrLit = (x: string) => /^"(?:[^"\\]|\\.)*"$/.test(x)
205
+ if (isStrLit(l) === isStrLit(r)) return [l, r]
206
+ // Keep `wrapGoArg`'s parens: a compound operand must reach `bf_string` as ONE
207
+ // argument — `(bf_string (or .Placement "top"))`, not `(bf_string or …)`.
208
+ const wrap = (x: string) => (isStrLit(x) ? x : `(bf_string ${wrapGoArg(x)})`)
209
+ return [wrap(l), wrap(r)]
210
+ }
211
+
212
+ // Generic remediation appended to BF101 / BF102 diagnostics whose reason
213
+ // doesn't already carry actionable next steps.
214
+ export const GO_REMEDIATION_OPTIONS =
215
+ 'Options:\n1. Use @client directive for client-side evaluation\n2. Pre-compute the value in Go code'
216
+
217
+ // Build the `suggestion.message` for an unsupported expression/condition.
218
+ // A self-contained reason (it already spells out the fix — e.g. the
219
+ // pre-compute / @client hint or the tailored forEach message) is shown
220
+ // as-is; a low-level reason gets the generic options appended; with no
221
+ // reason at all we fall back to the options alone.
222
+ export function buildUnsupportedSuggestion(support: SupportResult): string {
223
+ if (!support.reason) return GO_REMEDIATION_OPTIONS
224
+ if (support.selfContained) return support.reason
225
+ return `${support.reason}\n\n${GO_REMEDIATION_OPTIONS}`
226
+ }
227
+
228
+ /**
229
+ * Translate a JSX param default (e.g. `'default'`, `0`, `false`) into the
230
+ * corresponding Go literal.
231
+ *
232
+ * @returns the Go literal, or `null` when the default is absent or non-trivial
233
+ * (objects, arrow functions, …) — caller then lets Go's zero value win.
234
+ */
235
+ export function goPropDefault(defaultValue: string | undefined): string | null {
236
+ if (!defaultValue) return null
237
+ const trimmed = defaultValue.trim()
238
+ if (trimmed === '') return null
239
+ if (trimmed === 'true' || trimmed === 'false') return trimmed
240
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return trimmed
241
+ if (
242
+ (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
243
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))
244
+ ) {
245
+ const body = trimmed.slice(1, -1)
246
+ return JSON.stringify(body)
247
+ }
248
+ // Anything richer (objects, arrays, expressions) would mis-execute as Go.
249
+ return null
250
+ }
251
+
252
+ /**
253
+ * Wrap an `in.X` reference in a Go expression that substitutes `fallback` when
254
+ * the input is the zero value for its type; the comparison is picked from the
255
+ * fallback literal's shape.
256
+ *
257
+ * Asymmetry on bool/zero defaults is intentional (Go has no
258
+ * unset-vs-explicit-false distinction at the struct-field level):
259
+ * - `true` default → `(in.X || true)`, which is ALWAYS `true`; a caller
260
+ * wanting `false` must set it after `NewXxxProps`, not via the input struct.
261
+ * - `false` / `0` default → matches the Go zero value, so this is a no-op
262
+ * (returns `ref` unchanged).
263
+ * Non-zero numeric defaults substitute, matching JSX `(initial = 5) => …`.
264
+ */
265
+ export function applyGoFallback(ref: string, fallback: string): string {
266
+ if (fallback === 'true' || fallback === 'false') {
267
+ return fallback === 'true' ? `(${ref} || true)` : ref
268
+ }
269
+ if (/^-?\d+(\.\d+)?$/.test(fallback)) {
270
+ if (fallback === '0') return ref
271
+ return `func() int { if ${ref} == 0 { return ${fallback} }; return ${ref} }()`
272
+ }
273
+ // String fallback (quoted).
274
+ return `func() string { if ${ref} == "" { return ${fallback} }; return ${ref} }()`
275
+ }
276
+
277
+ /** Convert a JavaScript literal value to Go literal syntax. */
278
+ export function goLiteral(value: string): string {
279
+ if (value === 'true' || value === 'false') return value
280
+ if (/^-?\d+(\.\d+)?$/.test(value)) return value
281
+ // Single-quoted → Go double quotes; double-quoted kept as-is.
282
+ if (value.startsWith("'") && value.endsWith("'")) {
283
+ return `"${value.slice(1, -1)}"`
284
+ }
285
+ if (value.startsWith('"') && value.endsWith('"')) {
286
+ return value
287
+ }
288
+ return `"${value}"`
289
+ }
@@ -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
+ }