@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.
- package/dist/adapter/analysis/component-tree.d.ts +23 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +53 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/helper-inline.d.ts +31 -0
- package/dist/adapter/expr/helper-inline.d.ts.map +1 -0
- package/dist/adapter/expr/url-builder.d.ts +23 -0
- package/dist/adapter/expr/url-builder.d.ts.map +1 -0
- package/dist/adapter/go-template-adapter.d.ts +302 -1071
- package/dist/adapter/go-template-adapter.d.ts.map +1 -1
- package/dist/adapter/index.js +3116 -2692
- package/dist/adapter/lib/compile-state.d.ts +142 -0
- package/dist/adapter/lib/compile-state.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +11 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/go-emit.d.ts +126 -0
- package/dist/adapter/lib/go-emit.d.ts.map +1 -0
- package/dist/adapter/lib/go-naming.d.ts +39 -0
- package/dist/adapter/lib/go-naming.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +13 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +165 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/ctor-lowering.d.ts +39 -0
- package/dist/adapter/memo/ctor-lowering.d.ts.map +1 -0
- package/dist/adapter/memo/memo-compute.d.ts +173 -0
- package/dist/adapter/memo/memo-compute.d.ts.map +1 -0
- package/dist/adapter/memo/memo-type.d.ts +48 -0
- package/dist/adapter/memo/memo-type.d.ts.map +1 -0
- package/dist/adapter/memo/memo-value.d.ts +64 -0
- package/dist/adapter/memo/memo-value.d.ts.map +1 -0
- package/dist/adapter/memo/template-interp.d.ts +43 -0
- package/dist/adapter/memo/template-interp.d.ts.map +1 -0
- package/dist/adapter/props/prop-types.d.ts +31 -0
- package/dist/adapter/props/prop-types.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +40 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/type/type-codegen.d.ts +25 -0
- package/dist/adapter/type/type-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts +17 -0
- package/dist/adapter/value/parsed-literal-to-go.d.ts.map +1 -0
- package/dist/adapter/value/value-lowering.d.ts +46 -0
- package/dist/adapter/value/value-lowering.d.ts.map +1 -0
- package/dist/build.js +3114 -2690
- package/dist/index.js +3116 -2692
- package/dist/test-render.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/derived-state-memo.test.ts +155 -84
- package/src/__tests__/go-template-adapter.test.ts +398 -54
- package/src/__tests__/lowering-plugin.test.ts +109 -0
- package/src/__tests__/query-href.test.ts +179 -0
- package/src/adapter/analysis/component-tree.ts +174 -0
- package/src/adapter/emit-context.ts +59 -0
- package/src/adapter/expr/helper-inline.ts +274 -0
- package/src/adapter/expr/url-builder.ts +123 -0
- package/src/adapter/go-template-adapter.ts +2189 -5320
- package/src/adapter/lib/compile-state.ts +181 -0
- package/src/adapter/lib/constants.ts +24 -0
- package/src/adapter/lib/go-emit.ts +309 -0
- package/src/adapter/lib/go-naming.ts +84 -0
- package/src/adapter/lib/ir-scope.ts +31 -0
- package/src/adapter/lib/types.ts +182 -0
- package/src/adapter/memo/ctor-lowering.ts +267 -0
- package/src/adapter/memo/memo-compute.ts +661 -0
- package/src/adapter/memo/memo-type.ts +93 -0
- package/src/adapter/memo/memo-value.ts +197 -0
- package/src/adapter/memo/template-interp.ts +246 -0
- package/src/adapter/props/prop-types.ts +84 -0
- package/src/adapter/spread/spread-codegen.ts +458 -0
- package/src/adapter/type/type-codegen.ts +95 -0
- package/src/adapter/value/parsed-literal-to-go.ts +94 -0
- package/src/adapter/value/value-lowering.ts +162 -0
- 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
|
+
}
|