@barefootjs/erb 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.
- package/dist/adapter/analysis/component-tree.d.ts +24 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +66 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +107 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/erb-adapter.d.ts +374 -0
- package/dist/adapter/erb-adapter.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +87 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +102 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +34 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189154 -0
- package/dist/adapter/lib/constants.d.ts +25 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +27 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/ruby-naming.d.ts +65 -0
- package/dist/adapter/lib/ruby-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +28 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +35 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +50 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +63 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +21 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189174 -0
- package/dist/conformance-pins.d.ts +13 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189172 -0
- package/lib/barefoot_js/backend/erb.rb +123 -0
- package/lib/barefoot_js/dev_reload.rb +159 -0
- package/lib/barefoot_js/evaluator.rb +714 -0
- package/lib/barefoot_js/search_params.rb +63 -0
- package/lib/barefoot_js.rb +1155 -0
- package/package.json +67 -0
- package/src/__tests__/erb-adapter.test.ts +298 -0
- package/src/adapter/analysis/component-tree.ts +122 -0
- package/src/adapter/boolean-result.ts +157 -0
- package/src/adapter/emit-context.ts +119 -0
- package/src/adapter/erb-adapter.ts +1768 -0
- package/src/adapter/expr/array-method.ts +424 -0
- package/src/adapter/expr/emitters.ts +629 -0
- package/src/adapter/expr/operand.ts +55 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/lib/constants.ts +37 -0
- package/src/adapter/lib/ir-scope.ts +51 -0
- package/src/adapter/lib/ruby-naming.ts +127 -0
- package/src/adapter/lib/types.ts +31 -0
- package/src/adapter/memo/seed.ts +75 -0
- package/src/adapter/props/prop-classes.ts +89 -0
- package/src/adapter/spread/spread-codegen.ts +176 -0
- package/src/adapter/value/parsed-literal.ts +29 -0
- package/src/build.ts +37 -0
- package/src/conformance-pins.ts +100 -0
- package/src/index.ts +9 -0
- package/src/test-render.ts +668 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array / string method lowering for the ERB template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Mojolicious adapter's `expr/array-method.ts` (issue #2018
|
|
5
|
+
* track D lineage). Pure free functions shared by both the filter-context
|
|
6
|
+
* emitter and the top-level emitter — they take an `emit` callback for
|
|
7
|
+
* receiver / argument recursion and read no adapter instance state.
|
|
8
|
+
*
|
|
9
|
+
* Every `bf.*` helper name below is kept 1:1 with the Perl (`BarefootJS.pm`)
|
|
10
|
+
* / Go runtime surface — the coordination contract between this TS emitter
|
|
11
|
+
* and the Ruby runtime (`lib/barefoot_js.rb`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
serializeParsedExpr,
|
|
16
|
+
freeVarsInBody,
|
|
17
|
+
} from '@barefootjs/jsx'
|
|
18
|
+
import type {
|
|
19
|
+
ParsedExpr,
|
|
20
|
+
ArrayMethod,
|
|
21
|
+
SortComparator,
|
|
22
|
+
FlatDepth,
|
|
23
|
+
} from '@barefootjs/jsx'
|
|
24
|
+
import { rubySymbolKey, escapeRubySingleQuoted } from '../lib/ruby-naming.ts'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lower an `arr.<method>(...)` / `str.<method>(...)` value-builtin call to
|
|
28
|
+
* its Ruby form. The IR lifts these into the dedicated `array-method` kind at
|
|
29
|
+
* parse time (see the `arrayMethod` emitter arms), so this is the single
|
|
30
|
+
* place every adapter-supported array/string method is mapped. An unhandled
|
|
31
|
+
* `ArrayMethod` variant throws rather than emitting a silent no-op — the
|
|
32
|
+
* drift defence we already apply to `ParsedExpr.kind` extended to its
|
|
33
|
+
* sub-discriminator.
|
|
34
|
+
*/
|
|
35
|
+
export function renderArrayMethod(
|
|
36
|
+
method: ArrayMethod,
|
|
37
|
+
object: ParsedExpr,
|
|
38
|
+
args: ParsedExpr[],
|
|
39
|
+
emit: (e: ParsedExpr) => string,
|
|
40
|
+
): string {
|
|
41
|
+
switch (method) {
|
|
42
|
+
case 'join': {
|
|
43
|
+
// arr.join(sep) → bf.join(arr, sep). `.join()` defaults the separator
|
|
44
|
+
// to `,` (JS) and ignores any extra argument.
|
|
45
|
+
const obj = emit(object)
|
|
46
|
+
const sep = args.length >= 1 ? emit(args[0]) : `','`
|
|
47
|
+
return `bf.join(${obj}, ${sep})`
|
|
48
|
+
}
|
|
49
|
+
case 'includes': {
|
|
50
|
+
// Both `arr.includes(x)` and `str.includes(sub)` route here — the
|
|
51
|
+
// parser can't disambiguate the receiver type. The Ruby runtime's
|
|
52
|
+
// `bf.includes(recv, elem)` inspects the receiver's class and
|
|
53
|
+
// dispatches: Array scans the list with `==`, String falls back to
|
|
54
|
+
// substring search. Helper lives in lib/barefoot_js.rb.
|
|
55
|
+
const obj = emit(object)
|
|
56
|
+
const needle = emit(args[0])
|
|
57
|
+
return `bf.includes(${obj}, ${needle})`
|
|
58
|
+
}
|
|
59
|
+
case 'indexOf':
|
|
60
|
+
case 'lastIndexOf': {
|
|
61
|
+
// Array `.indexOf(x)` / `.lastIndexOf(x)` value-equality search.
|
|
62
|
+
const fn = method === 'indexOf' ? 'index_of' : 'last_index_of'
|
|
63
|
+
const obj = emit(object)
|
|
64
|
+
const needle = emit(args[0])
|
|
65
|
+
return `bf.${fn}(${obj}, ${needle})`
|
|
66
|
+
}
|
|
67
|
+
case 'at': {
|
|
68
|
+
// `.at(i)` with negative-index support — `.at(-1)` is the last
|
|
69
|
+
// element. `.at()` with no argument is `.at(0)` (the first element);
|
|
70
|
+
// extra arguments are ignored.
|
|
71
|
+
const obj = emit(object)
|
|
72
|
+
const idx = args.length >= 1 ? emit(args[0]) : '0'
|
|
73
|
+
return `bf.at(${obj}, ${idx})`
|
|
74
|
+
}
|
|
75
|
+
case 'concat': {
|
|
76
|
+
// `.concat(other)` merges two arrays. Returns a new Array so the
|
|
77
|
+
// result composes with `.join(...)` / other array-shape methods
|
|
78
|
+
// downstream. `.concat()` with no argument is a shallow copy —
|
|
79
|
+
// indistinguishable from the receiver in an SSR snapshot, so it
|
|
80
|
+
// lowers to the receiver.
|
|
81
|
+
if (args.length === 0) {
|
|
82
|
+
return emit(object)
|
|
83
|
+
}
|
|
84
|
+
const a = emit(object)
|
|
85
|
+
const b = emit(args[0])
|
|
86
|
+
return `bf.concat(${a}, ${b})`
|
|
87
|
+
}
|
|
88
|
+
case 'slice': {
|
|
89
|
+
// `.slice()` / `.slice(start)` / `.slice(start, end)`. The Ruby
|
|
90
|
+
// helper mirrors the Go arithmetic (negative-index normalisation,
|
|
91
|
+
// out-of-bounds clamping, empty result on start >= end). A missing
|
|
92
|
+
// `start` defaults to 0 (full copy); an absent `end` lowers as `nil`,
|
|
93
|
+
// which the helper treats as "to length". JS ignores a third+
|
|
94
|
+
// argument. Returns a new Array so the result composes with
|
|
95
|
+
// `.join(...)` downstream.
|
|
96
|
+
const recv = emit(object)
|
|
97
|
+
const start = args.length >= 1 ? emit(args[0]) : '0'
|
|
98
|
+
const end = args.length >= 2 ? emit(args[1]) : 'nil'
|
|
99
|
+
return `bf.slice(${recv}, ${start}, ${end})`
|
|
100
|
+
}
|
|
101
|
+
case 'reverse':
|
|
102
|
+
case 'toReversed': {
|
|
103
|
+
// Both shapes share a lowering — see the parser arm + Go emit for the
|
|
104
|
+
// SSR-mutation rationale. Returns a new Array so the result composes
|
|
105
|
+
// with `.join(...)` downstream.
|
|
106
|
+
const recv = emit(object)
|
|
107
|
+
return `bf.reverse(${recv})`
|
|
108
|
+
}
|
|
109
|
+
case 'toLowerCase': {
|
|
110
|
+
// Ruby's native `.downcase` is the obvious lowering — no helper
|
|
111
|
+
// method needed. The receiver flows through `emit` so any upstream
|
|
112
|
+
// coercion composes naturally.
|
|
113
|
+
const recv = emit(object)
|
|
114
|
+
return `(${recv}).downcase`
|
|
115
|
+
}
|
|
116
|
+
case 'toUpperCase': {
|
|
117
|
+
// Ruby's native `.upcase` — mirrors `toLowerCase` exactly.
|
|
118
|
+
const recv = emit(object)
|
|
119
|
+
return `(${recv}).upcase`
|
|
120
|
+
}
|
|
121
|
+
case 'trim': {
|
|
122
|
+
// No JS-parity-guaranteed native for leading/trailing whitespace
|
|
123
|
+
// (Ruby `.strip` strips a slightly different whitespace class);
|
|
124
|
+
// route through `bf.trim` so the definition stays in one place.
|
|
125
|
+
const recv = emit(object)
|
|
126
|
+
return `bf.trim(${recv})`
|
|
127
|
+
}
|
|
128
|
+
case 'toFixed': {
|
|
129
|
+
// `.toFixed(digits?)` — Number → fixed-decimal string. `bf.to_fixed`
|
|
130
|
+
// mirrors JS rounding + zero-padding (default 0 digits).
|
|
131
|
+
const recv = emit(object)
|
|
132
|
+
const digits = args.length >= 1 ? emit(args[0]) : '0'
|
|
133
|
+
return `bf.to_fixed(${recv}, ${digits})`
|
|
134
|
+
}
|
|
135
|
+
case 'split': {
|
|
136
|
+
// `.split()` / `.split(sep)` / `.split(sep, limit)` — string → Array
|
|
137
|
+
// via `bf.split`. With no separator the helper returns the whole
|
|
138
|
+
// string as a single element; otherwise it matches the separator
|
|
139
|
+
// literally (not as a regex) and keeps trailing empties, staying
|
|
140
|
+
// byte-equal with Go's `bf_split`. The optional `limit` caps the
|
|
141
|
+
// pieces; JS ignores a third+ argument.
|
|
142
|
+
const recv = emit(object)
|
|
143
|
+
if (args.length === 0) {
|
|
144
|
+
return `bf.split(${recv})`
|
|
145
|
+
}
|
|
146
|
+
const sep = emit(args[0])
|
|
147
|
+
if (args.length === 1) {
|
|
148
|
+
return `bf.split(${recv}, ${sep})`
|
|
149
|
+
}
|
|
150
|
+
const limit = emit(args[1])
|
|
151
|
+
return `bf.split(${recv}, ${sep}, ${limit})`
|
|
152
|
+
}
|
|
153
|
+
case 'startsWith':
|
|
154
|
+
case 'endsWith': {
|
|
155
|
+
// `.startsWith(prefix, position?)` / `.endsWith(suffix, endPosition?)`
|
|
156
|
+
// — string → boolean. The Ruby helpers (`bf.starts_with` /
|
|
157
|
+
// `bf.ends_with`) do an index-anchored comparison so the search
|
|
158
|
+
// string is matched literally (no regex metachar surprises) and a
|
|
159
|
+
// nil receiver stays quiet. The optional second argument re-anchors
|
|
160
|
+
// the test; JS ignores a third+ argument.
|
|
161
|
+
const fn = method === 'startsWith' ? 'starts_with' : 'ends_with'
|
|
162
|
+
const recv = emit(object)
|
|
163
|
+
const arg = emit(args[0])
|
|
164
|
+
if (args.length >= 2) {
|
|
165
|
+
return `bf.${fn}(${recv}, ${arg}, ${emit(args[1])})`
|
|
166
|
+
}
|
|
167
|
+
return `bf.${fn}(${recv}, ${arg})`
|
|
168
|
+
}
|
|
169
|
+
case 'replace': {
|
|
170
|
+
// `.replace(old, new)` — string-pattern form, first occurrence. The
|
|
171
|
+
// `bf.replace` helper splices via index/substring (not a Regexp) so
|
|
172
|
+
// both the pattern and the replacement are literal — no Ruby regex
|
|
173
|
+
// metacharacters and no `\1` / `\&` interpolation in the
|
|
174
|
+
// replacement, keeping it byte-equal with Go's `bf_replace`. The
|
|
175
|
+
// regex-pattern form is refused upstream at the parser.
|
|
176
|
+
const recv = emit(object)
|
|
177
|
+
const oldS = emit(args[0])
|
|
178
|
+
const newS = emit(args[1])
|
|
179
|
+
return `bf.replace(${recv}, ${oldS}, ${newS})`
|
|
180
|
+
}
|
|
181
|
+
case 'repeat': {
|
|
182
|
+
// `.repeat(n)` — string repeated `n` times. The `bf.repeat` helper
|
|
183
|
+
// wraps Ruby's `*` string-repeat operator with the same
|
|
184
|
+
// negative-count → "" clamp and integer truncation Go's `bf_repeat`
|
|
185
|
+
// applies, so the two adapters stay byte-equal. Full JS arity: the
|
|
186
|
+
// no-argument form is `repeat(0)` → ""; a second+ argument is
|
|
187
|
+
// ignored.
|
|
188
|
+
const recv = emit(object)
|
|
189
|
+
const count = args.length === 0 ? '0' : emit(args[0])
|
|
190
|
+
return `bf.repeat(${recv}, ${count})`
|
|
191
|
+
}
|
|
192
|
+
case 'padStart':
|
|
193
|
+
case 'padEnd': {
|
|
194
|
+
// `.padStart(target, pad?)` / `.padEnd(target, pad?)`. The
|
|
195
|
+
// `bf.pad_*` helpers default the pad to a single space when the arg
|
|
196
|
+
// is omitted and measure length in characters, matching Go's
|
|
197
|
+
// rune-based `bf_pad_*`. Full JS arity: the no-argument form is
|
|
198
|
+
// `padStart(0)` → the receiver unchanged; a third+ argument is
|
|
199
|
+
// ignored.
|
|
200
|
+
const fn = method === 'padStart' ? 'pad_start' : 'pad_end'
|
|
201
|
+
const recv = emit(object)
|
|
202
|
+
if (args.length === 0) {
|
|
203
|
+
return `bf.${fn}(${recv}, 0)`
|
|
204
|
+
}
|
|
205
|
+
const target = emit(args[0])
|
|
206
|
+
if (args.length === 1) {
|
|
207
|
+
return `bf.${fn}(${recv}, ${target})`
|
|
208
|
+
}
|
|
209
|
+
const pad = emit(args[1])
|
|
210
|
+
return `bf.${fn}(${recv}, ${target}, ${pad})`
|
|
211
|
+
}
|
|
212
|
+
default: {
|
|
213
|
+
// TS-level exhaustiveness guard. If this throws at runtime, the
|
|
214
|
+
// IR was constructed against a newer `ArrayMethod` variant that
|
|
215
|
+
// this adapter hasn't been updated for — loud failure is better
|
|
216
|
+
// than emitting a silent empty string downstream.
|
|
217
|
+
const _exhaustive: never = method
|
|
218
|
+
throw new Error(
|
|
219
|
+
`renderArrayMethod: unhandled ArrayMethod '${(_exhaustive as string)}'`,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build the `base_env` Hash-literal argument for an evaluator call: the
|
|
227
|
+
* comparator / reducer body's free variables (body idents minus the
|
|
228
|
+
* callback params), each materialised to its SSR value via `emit`, under a
|
|
229
|
+
* SYMBOL key (the Ruby evaluator's env is symbol-keyed — see
|
|
230
|
+
* `Evaluator.evaluate`'s `identifier` arm). An empty capture set yields
|
|
231
|
+
* `{}` — the runtime's seed-once env.
|
|
232
|
+
*/
|
|
233
|
+
function emitEvalEnvArg(
|
|
234
|
+
body: ParsedExpr,
|
|
235
|
+
params: string[],
|
|
236
|
+
emit: (e: ParsedExpr) => string,
|
|
237
|
+
): string {
|
|
238
|
+
const free = freeVarsInBody(body, new Set(params))
|
|
239
|
+
if (free.length === 0) return '{}'
|
|
240
|
+
const pairs = free.map(
|
|
241
|
+
n => `${rubySymbolKey(n)} ${emit({ kind: 'identifier', name: n })}`,
|
|
242
|
+
)
|
|
243
|
+
return `{ ${pairs.join(', ')} }`
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Emit a `.sort(cmp)` / `.toSorted(cmp)` via the runtime evaluator: the
|
|
248
|
+
* comparator body travels as serialized-ParsedExpr JSON, evaluated per
|
|
249
|
+
* comparison against `{paramA, paramB, …captured}`. Returns null when the
|
|
250
|
+
* body can't be evaluated (e.g. a `localeCompare` comparator —
|
|
251
|
+
* `serializeParsedExpr` refuses it), so the caller falls back to the
|
|
252
|
+
* structured `bf.sort`. A `||`-chained multi-key comparator needs no
|
|
253
|
+
* special handling — JS `0 || next` is exactly the tie-break semantics.
|
|
254
|
+
*/
|
|
255
|
+
export function renderSortEval(
|
|
256
|
+
recv: string,
|
|
257
|
+
body: ParsedExpr,
|
|
258
|
+
params: string[],
|
|
259
|
+
emit: (e: ParsedExpr) => string,
|
|
260
|
+
): string | null {
|
|
261
|
+
if (params.length < 2) return null
|
|
262
|
+
const [paramA, paramB] = params
|
|
263
|
+
const json = serializeParsedExpr(body)
|
|
264
|
+
if (json === null) return null
|
|
265
|
+
const env = emitEvalEnvArg(body, [paramA, paramB], emit)
|
|
266
|
+
return `bf.sort_eval(${recv}, '${escapeRubySingleQuoted(json)}', '${paramA}', '${paramB}', ${env})`
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Emit a `.reduce(fn, init)` / `.reduceRight(fn, init)` via the runtime
|
|
271
|
+
* evaluator: the reducer body travels as serialized-ParsedExpr JSON, folded
|
|
272
|
+
* over the receiver from `init` in `direction` order. Returns null when the
|
|
273
|
+
* body can't be evaluated (→ caller falls back to BF101). A numeric seed
|
|
274
|
+
* passes through as a bare Ruby number; a concat seed as a single-quoted
|
|
275
|
+
* string.
|
|
276
|
+
*/
|
|
277
|
+
export function renderReduceEval(
|
|
278
|
+
recv: string,
|
|
279
|
+
body: ParsedExpr,
|
|
280
|
+
params: string[],
|
|
281
|
+
init: ParsedExpr,
|
|
282
|
+
direction: 'left' | 'right',
|
|
283
|
+
emit: (e: ParsedExpr) => string,
|
|
284
|
+
): string | null {
|
|
285
|
+
if (params.length < 2) return null
|
|
286
|
+
const [paramAcc, paramItem] = params
|
|
287
|
+
const json = serializeParsedExpr(body)
|
|
288
|
+
if (json === null) return null
|
|
289
|
+
// Only literal seeds round-trip to a Ruby scalar here: a string literal
|
|
290
|
+
// as a single-quoted Ruby string, a number literal as a bare numeric.
|
|
291
|
+
// Anything else (an expression seed, an omitted seed) can't be
|
|
292
|
+
// materialised → null, and the caller records BF101.
|
|
293
|
+
let initRuby: string
|
|
294
|
+
if (init.kind === 'literal' && init.literalType === 'string') {
|
|
295
|
+
initRuby = `'${escapeRubySingleQuoted(String(init.value))}'`
|
|
296
|
+
} else if (init.kind === 'literal' && init.literalType === 'number') {
|
|
297
|
+
initRuby = String(init.value)
|
|
298
|
+
} else {
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
const env = emitEvalEnvArg(body, [paramAcc, paramItem], emit)
|
|
302
|
+
return `bf.reduce_eval(${recv}, '${escapeRubySingleQuoted(json)}', '${paramAcc}', '${paramItem}', ${initRuby}, '${direction}', ${env})`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Emit a higher-order predicate call via the runtime evaluator:
|
|
307
|
+
* `bf.filter_eval` / `bf.every_eval` / `bf.some_eval` / `bf.find_eval` /
|
|
308
|
+
* `bf.find_index_eval`, carrying the serialized predicate body + captured
|
|
309
|
+
* env Hash. Generalizes the inline predicate lowering to the same
|
|
310
|
+
* JS-faithful evaluator the Go adapter uses (cross-adapter isomorphism).
|
|
311
|
+
* Returns null when the predicate is outside the evaluator surface (e.g. a
|
|
312
|
+
* method-call predicate — `serializeParsedExpr` refuses it), so the caller
|
|
313
|
+
* falls back to the inline form. `forward` (find / findIndex family only)
|
|
314
|
+
* selects the search direction — `false` = findLast / findLastIndex.
|
|
315
|
+
*/
|
|
316
|
+
export function renderPredicateEval(
|
|
317
|
+
funcName: string,
|
|
318
|
+
recv: string,
|
|
319
|
+
predicate: ParsedExpr,
|
|
320
|
+
param: string,
|
|
321
|
+
emit: (e: ParsedExpr) => string,
|
|
322
|
+
forward?: boolean,
|
|
323
|
+
): string | null {
|
|
324
|
+
const json = serializeParsedExpr(predicate)
|
|
325
|
+
if (json === null) return null
|
|
326
|
+
const env = emitEvalEnvArg(predicate, [param], emit)
|
|
327
|
+
const fwd = forward === undefined ? '' : `, ${forward ? 'true' : 'false'}`
|
|
328
|
+
return `bf.${funcName}(${recv}, '${escapeRubySingleQuoted(json)}', '${param}'${fwd}, ${env})`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Emit a `.flatMap(proj)` via the runtime evaluator: the projection `body`
|
|
333
|
+
* serializes to JSON and `bf.flat_map_eval` projects + flattens one level.
|
|
334
|
+
* Returns null when the projection is outside the evaluator surface, and
|
|
335
|
+
* the caller records BF101.
|
|
336
|
+
*/
|
|
337
|
+
export function renderFlatMapEval(
|
|
338
|
+
recv: string,
|
|
339
|
+
body: ParsedExpr,
|
|
340
|
+
param: string,
|
|
341
|
+
emit: (e: ParsedExpr) => string,
|
|
342
|
+
): string | null {
|
|
343
|
+
const json = serializeParsedExpr(body)
|
|
344
|
+
if (json === null) return null
|
|
345
|
+
const env = emitEvalEnvArg(body, [param], emit)
|
|
346
|
+
return `bf.flat_map_eval(${recv}, '${escapeRubySingleQuoted(json)}', '${param}', ${env})`
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Emit a value-producing `.map(cb)` via the runtime evaluator: the
|
|
351
|
+
* projection `body` serializes to JSON and `bf.map_eval` projects each
|
|
352
|
+
* element, one result per element (no flatten — the JS `.map` contract).
|
|
353
|
+
* Composes through the array-method chain (`.map(cb).join(' ')`). Returns
|
|
354
|
+
* null when the projection is outside the evaluator surface, and the caller
|
|
355
|
+
* records BF101. The JSX-returning `.map` is an IRLoop upstream and never
|
|
356
|
+
* reaches this emit.
|
|
357
|
+
*/
|
|
358
|
+
export function renderMapEval(
|
|
359
|
+
recv: string,
|
|
360
|
+
body: ParsedExpr,
|
|
361
|
+
param: string,
|
|
362
|
+
emit: (e: ParsedExpr) => string,
|
|
363
|
+
): string | null {
|
|
364
|
+
const json = serializeParsedExpr(body)
|
|
365
|
+
if (json === null) return null
|
|
366
|
+
const env = emitEvalEnvArg(body, [param], emit)
|
|
367
|
+
return `bf.map_eval(${recv}, '${escapeRubySingleQuoted(json)}', '${param}', ${env})`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Shared ERB emit for `.sort(cmp)` / `.toSorted(cmp)`. Used by both the
|
|
372
|
+
* filter-context emitter and the top-level emitter, plus the loop-hoist path
|
|
373
|
+
* in `renderLoop` — same emit shape across all three so a regression in any
|
|
374
|
+
* one path surfaces consistently.
|
|
375
|
+
*
|
|
376
|
+
* The Ruby helper accepts an opts Hash whose `keys:` entry is an ordered
|
|
377
|
+
* list of per-key Hashes (room for a future `nulls` knob without arity
|
|
378
|
+
* churn), and returns a fresh Array so downstream composition
|
|
379
|
+
* (`bf.sort(...).join(...)`, etc.) stays straightforward.
|
|
380
|
+
*/
|
|
381
|
+
export function renderSortMethod(recv: string, c: SortComparator): string {
|
|
382
|
+
// One Hash per comparison key, in priority order, under `keys:`. A
|
|
383
|
+
// simple comparator yields a one-element list; a `||`-chained multi-key
|
|
384
|
+
// comparator yields one per operand. `bf.sort` walks them in order,
|
|
385
|
+
// falling through to the next on a tie.
|
|
386
|
+
const keyHashes = c.keys.map((k) => {
|
|
387
|
+
const keyEntry =
|
|
388
|
+
k.key.kind === 'self'
|
|
389
|
+
? `key_kind: 'self'`
|
|
390
|
+
: `key_kind: 'field', key: '${k.key.field}'`
|
|
391
|
+
return `{ ${keyEntry}, compare_type: '${k.type}', direction: '${k.direction}' }`
|
|
392
|
+
})
|
|
393
|
+
return `bf.sort(${recv}, { keys: [${keyHashes.join(', ')}] })`
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// `.flat(depth?)` → `bf.flat(recv, depth)`. The `Infinity` form lowers
|
|
397
|
+
// to the `-1` sentinel (flatten fully); a finite depth flattens that many
|
|
398
|
+
// levels (`0` = shallow copy).
|
|
399
|
+
export function renderFlatMethod(
|
|
400
|
+
recv: string,
|
|
401
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
402
|
+
emit: (e: ParsedExpr) => string,
|
|
403
|
+
): string {
|
|
404
|
+
if (typeof depth === 'object') {
|
|
405
|
+
// Dynamic depth (#2094): routed to a SEPARATE runtime helper
|
|
406
|
+
// (`bf.flat_dynamic`), not `bf.flat` — `bf.flat`'s `depth` parameter
|
|
407
|
+
// treats `-1` as a compile-time SENTINEL meaning "the source literally
|
|
408
|
+
// wrote `Infinity`" (the parser's own normalisation of a literal
|
|
409
|
+
// depth). A genuinely dynamic depth value that happens to evaluate to
|
|
410
|
+
// `-1` at render time means the JS-correct OPPOSITE: `.flat(-1)` never
|
|
411
|
+
// recurses (same as `.flat(0)`, a shallow copy). Since both paths would
|
|
412
|
+
// otherwise hand the same literal-looking argument to one shared
|
|
413
|
+
// function, that function couldn't tell which case it's in — so
|
|
414
|
+
// `bf.flat_dynamic` coerces the raw value via JS `ToIntegerOrInfinity`
|
|
415
|
+
// FIRST (truncate toward zero; negative → 0; NaN/non-numeric → 0;
|
|
416
|
+
// +Infinity or a huge finite value → flatten fully) and only then
|
|
417
|
+
// delegates to the same recursion `bf.flat` uses. Mirrors the Go
|
|
418
|
+
// adapter's `bf_flat_dynamic` (go-template-adapter.ts) and runtime
|
|
419
|
+
// `FlatDynamicDepth`/`coerceFlatDepth` (adapter-go-template/runtime/bf.go).
|
|
420
|
+
return `bf.flat_dynamic(${recv}, ${emit(depth.expr)})`
|
|
421
|
+
}
|
|
422
|
+
const d = depth === 'infinity' ? -1 : depth
|
|
423
|
+
return `bf.flat(${recv}, ${d})`
|
|
424
|
+
}
|