@barefootjs/xslate 0.16.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +30 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +94 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/array-method.d.ts +73 -0
  6. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  7. package/dist/adapter/expr/emitters.d.ts +96 -0
  8. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  9. package/dist/adapter/expr/operand.d.ts +25 -0
  10. package/dist/adapter/expr/operand.d.ts.map +1 -0
  11. package/dist/adapter/index.js +1412 -1323
  12. package/dist/adapter/lib/constants.d.ts +22 -0
  13. package/dist/adapter/lib/constants.d.ts.map +1 -0
  14. package/dist/adapter/lib/ir-scope.d.ts +29 -0
  15. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  16. package/dist/adapter/lib/kolon-naming.d.ts +22 -0
  17. package/dist/adapter/lib/kolon-naming.d.ts.map +1 -0
  18. package/dist/adapter/lib/types.d.ts +27 -0
  19. package/dist/adapter/lib/types.d.ts.map +1 -0
  20. package/dist/adapter/memo/seed.d.ts +38 -0
  21. package/dist/adapter/memo/seed.d.ts.map +1 -0
  22. package/dist/adapter/props/prop-classes.d.ts +32 -0
  23. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  24. package/dist/adapter/spread/spread-codegen.d.ts +69 -0
  25. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  26. package/dist/adapter/value/parsed-literal.d.ts +32 -0
  27. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  28. package/dist/adapter/xslate-adapter.d.ts +38 -92
  29. package/dist/adapter/xslate-adapter.d.ts.map +1 -1
  30. package/dist/build.js +1412 -1323
  31. package/dist/index.js +1412 -1323
  32. package/lib/BarefootJS/Backend/Xslate.pm +1 -1
  33. package/package.json +3 -3
  34. package/src/__tests__/query-href.test.ts +96 -0
  35. package/src/__tests__/xslate-adapter.test.ts +200 -8
  36. package/src/adapter/analysis/component-tree.ts +123 -0
  37. package/src/adapter/emit-context.ts +104 -0
  38. package/src/adapter/expr/array-method.ts +332 -0
  39. package/src/adapter/expr/emitters.ts +548 -0
  40. package/src/adapter/expr/operand.ts +35 -0
  41. package/src/adapter/lib/constants.ts +34 -0
  42. package/src/adapter/lib/ir-scope.ts +53 -0
  43. package/src/adapter/lib/kolon-naming.ts +27 -0
  44. package/src/adapter/lib/types.ts +30 -0
  45. package/src/adapter/memo/seed.ts +78 -0
  46. package/src/adapter/props/prop-classes.ts +64 -0
  47. package/src/adapter/spread/spread-codegen.ts +179 -0
  48. package/src/adapter/value/parsed-literal.ts +80 -0
  49. package/src/adapter/xslate-adapter.ts +190 -1236
  50. package/src/test-render.ts +3 -0
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Array / string method lowering for the Text::Xslate (Kolon) template adapter.
3
+ *
4
+ * Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Pure free functions shared by both the filter-context emitter and
6
+ * the top-level emitter — they take an `emit` callback for receiver / argument
7
+ * recursion and read no adapter instance state.
8
+ *
9
+ * The receiver/array helpers are the same runtime methods the Mojo adapter
10
+ * calls, invoked as `$bf.NAME(...)` on the Kolon `$bf` object instead of
11
+ * `bf->NAME`.
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
+
25
+ export function renderArrayMethod(
26
+ method: ArrayMethod,
27
+ object: ParsedExpr,
28
+ args: ParsedExpr[],
29
+ emit: (e: ParsedExpr) => string,
30
+ ): string {
31
+ switch (method) {
32
+ case 'join': {
33
+ // Route through the runtime (`$bf.join`) rather than Kolon's builtin
34
+ // `.join`, so the JS-compat element handling (undef → empty, default
35
+ // separator) is applied consistently — same reasoning as $bf.lc / etc.
36
+ const obj = emit(object)
37
+ const sep = args.length >= 1 ? emit(args[0]) : `','`
38
+ return `$bf.join(${obj}, ${sep})`
39
+ }
40
+ case 'includes': {
41
+ const obj = emit(object)
42
+ const needle = emit(args[0])
43
+ return `$bf.includes(${obj}, ${needle})`
44
+ }
45
+ case 'indexOf':
46
+ case 'lastIndexOf': {
47
+ const fn = method === 'indexOf' ? 'index_of' : 'last_index_of'
48
+ const obj = emit(object)
49
+ const needle = emit(args[0])
50
+ return `$bf.${fn}(${obj}, ${needle})`
51
+ }
52
+ case 'at': {
53
+ const obj = emit(object)
54
+ const idx = args.length >= 1 ? emit(args[0]) : '0'
55
+ return `$bf.at(${obj}, ${idx})`
56
+ }
57
+ case 'concat': {
58
+ if (args.length === 0) {
59
+ return emit(object)
60
+ }
61
+ const a = emit(object)
62
+ const b = emit(args[0])
63
+ return `$bf.concat(${a}, ${b})`
64
+ }
65
+ case 'slice': {
66
+ const recv = emit(object)
67
+ const start = args.length >= 1 ? emit(args[0]) : '0'
68
+ // Kolon's undefined literal is `nil`, not Perl's `undef` — the
69
+ // runtime `slice` treats it as "to end".
70
+ const end = args.length >= 2 ? emit(args[1]) : 'nil'
71
+ return `$bf.slice(${recv}, ${start}, ${end})`
72
+ }
73
+ case 'reverse':
74
+ case 'toReversed': {
75
+ const recv = emit(object)
76
+ return `$bf.reverse(${recv})`
77
+ }
78
+ case 'toLowerCase': {
79
+ // Kolon has no builtin string `lc` / `uc`, so these go through the
80
+ // runtime object (consistent with $bf.includes / $bf.slice / etc.).
81
+ const recv = emit(object)
82
+ return `$bf.lc(${recv})`
83
+ }
84
+ case 'toUpperCase': {
85
+ const recv = emit(object)
86
+ return `$bf.uc(${recv})`
87
+ }
88
+ case 'trim': {
89
+ const recv = emit(object)
90
+ return `$bf.trim(${recv})`
91
+ }
92
+ case 'toFixed': {
93
+ // `.toFixed(digits?)` — `$bf.to_fixed` mirrors JS rounding +
94
+ // zero-padding (default 0 digits). #1897.
95
+ const recv = emit(object)
96
+ const digits = args.length >= 1 ? emit(args[0]) : '0'
97
+ return `$bf.to_fixed(${recv}, ${digits})`
98
+ }
99
+ case 'split': {
100
+ const recv = emit(object)
101
+ if (args.length === 0) {
102
+ return `$bf.split(${recv})`
103
+ }
104
+ const sep = emit(args[0])
105
+ if (args.length === 1) {
106
+ return `$bf.split(${recv}, ${sep})`
107
+ }
108
+ const limit = emit(args[1])
109
+ return `$bf.split(${recv}, ${sep}, ${limit})`
110
+ }
111
+ case 'startsWith':
112
+ case 'endsWith': {
113
+ const fn = method === 'startsWith' ? 'starts_with' : 'ends_with'
114
+ const recv = emit(object)
115
+ const arg = emit(args[0])
116
+ if (args.length >= 2) {
117
+ return `$bf.${fn}(${recv}, ${arg}, ${emit(args[1])})`
118
+ }
119
+ return `$bf.${fn}(${recv}, ${arg})`
120
+ }
121
+ case 'replace': {
122
+ const recv = emit(object)
123
+ const oldS = emit(args[0])
124
+ const newS = emit(args[1])
125
+ return `$bf.replace(${recv}, ${oldS}, ${newS})`
126
+ }
127
+ case 'repeat': {
128
+ const recv = emit(object)
129
+ const count = args.length === 0 ? '0' : emit(args[0])
130
+ return `$bf.repeat(${recv}, ${count})`
131
+ }
132
+ case 'padStart':
133
+ case 'padEnd': {
134
+ const fn = method === 'padStart' ? 'pad_start' : 'pad_end'
135
+ const recv = emit(object)
136
+ if (args.length === 0) {
137
+ return `$bf.${fn}(${recv}, 0)`
138
+ }
139
+ const target = emit(args[0])
140
+ if (args.length === 1) {
141
+ return `$bf.${fn}(${recv}, ${target})`
142
+ }
143
+ const pad = emit(args[1])
144
+ return `$bf.${fn}(${recv}, ${target}, ${pad})`
145
+ }
146
+ default: {
147
+ // TS-level exhaustiveness guard.
148
+ const _exhaustive: never = method
149
+ throw new Error(
150
+ `renderArrayMethod: unhandled ArrayMethod '${(_exhaustive as string)}'`,
151
+ )
152
+ }
153
+ }
154
+ }
155
+
156
+ /** Escape a string for embedding in a Perl single-quoted literal. */
157
+ function escapePerlSingleQuote(s: string): string {
158
+ // Double every backslash and escape apostrophes: Perl single-quote
159
+ // un-escaping then restores the original bytes (a JSON `\\` must reach
160
+ // `JSON::PP->decode` as `\\`, so it travels as `\\\\` in the literal).
161
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
162
+ }
163
+
164
+ /**
165
+ * Build the `base_env` hashref argument for an evaluator call: the
166
+ * comparator / reducer body's free variables (body idents minus the
167
+ * callback params), each materialised to its SSR value via `emit`. An
168
+ * empty capture set yields `{}` — the runtime's seed-once env.
169
+ */
170
+ function emitEvalEnvArg(
171
+ body: ParsedExpr,
172
+ params: string[],
173
+ emit: (e: ParsedExpr) => string,
174
+ ): string {
175
+ const free = freeVarsInBody(body, new Set(params))
176
+ if (free.length === 0) return '{}'
177
+ const pairs = free.map(
178
+ n => `'${escapePerlSingleQuote(n)}' => ${emit({ kind: 'identifier', name: n })}`,
179
+ )
180
+ return `{ ${pairs.join(', ')} }`
181
+ }
182
+
183
+ /**
184
+ * Emit a `.sort(cmp)` / `.toSorted(cmp)` via the runtime evaluator (#2018):
185
+ * the comparator body travels as serialized-ParsedExpr JSON, evaluated per
186
+ * comparison against `{paramA, paramB, …captured}`. Returns null when the
187
+ * body is outside the evaluator surface (e.g. a `localeCompare` comparator —
188
+ * `serializeParsedExpr` refuses it), so the caller falls back to the
189
+ * structured `$bf.sort`. `params` are the comparator arrow's two params
190
+ * (`[paramA, paramB]`).
191
+ */
192
+ export function renderSortEval(
193
+ recv: string,
194
+ body: ParsedExpr,
195
+ params: string[],
196
+ emit: (e: ParsedExpr) => string,
197
+ ): string | null {
198
+ const json = serializeParsedExpr(body)
199
+ if (json === null) return null
200
+ // A comparator needs both params; a wrong-arity arrow would emit an
201
+ // 'undefined' param name, so fail over to the structured fallback / BF101
202
+ // (mirrors the Go / Mojo guards).
203
+ if (params.length < 2) return null
204
+ const [paramA, paramB] = params
205
+ const env = emitEvalEnvArg(body, params, emit)
206
+ return `$bf.sort_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramA}', '${paramB}', ${env})`
207
+ }
208
+
209
+ /**
210
+ * Emit a `.reduce(fn, init)` / `.reduceRight(fn, init)` via the runtime
211
+ * evaluator (#2018): the reducer body travels as serialized-ParsedExpr JSON,
212
+ * folded over the receiver from `init` in `direction` order. `params` are the
213
+ * reducer arrow's params (`[paramAcc, paramItem]`); `init` is the initial-value
214
+ * `ParsedExpr` from the call's trailing argument. Returns null when the body is
215
+ * outside the evaluator surface, or when `init` is not a literal string/number
216
+ * (→ caller refuses with BF101). A numeric seed passes through as a bare Perl
217
+ * number; a string seed as a single-quoted literal.
218
+ */
219
+ export function renderReduceEval(
220
+ recv: string,
221
+ body: ParsedExpr,
222
+ params: string[],
223
+ init: ParsedExpr,
224
+ direction: 'left' | 'right',
225
+ emit: (e: ParsedExpr) => string,
226
+ ): string | null {
227
+ const json = serializeParsedExpr(body)
228
+ if (json === null) return null
229
+ if (init.kind !== 'literal') return null
230
+ const initOut =
231
+ init.literalType === 'string'
232
+ ? `'${escapePerlSingleQuote(String(init.value))}'`
233
+ : init.literalType === 'number'
234
+ ? String(init.value)
235
+ : null
236
+ if (initOut === null) return null
237
+ // A reducer needs both the accumulator and element param; refuse a wrong-arity
238
+ // arrow cleanly (→ BF101) rather than emitting an 'undefined' param name
239
+ // (mirrors the Go / Mojo guards).
240
+ if (params.length < 2) return null
241
+ const [paramAcc, paramItem] = params
242
+ const env = emitEvalEnvArg(body, params, emit)
243
+ return `$bf.reduce_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramAcc}', '${paramItem}', ${initOut}, '${direction}', ${env})`
244
+ }
245
+
246
+ /**
247
+ * Emit a higher-order predicate call via the runtime evaluator (#2018, P2):
248
+ * `$bf.filter_eval` / `$bf.every_eval` / `$bf.some_eval` / `$bf.find_eval` /
249
+ * `$bf.find_index_eval`, carrying the serialized predicate body + captured env
250
+ * hashref. Generalizes the Kolon-lambda `$bf.filter` lowering to the same
251
+ * JS-faithful evaluator the Go adapter uses (cross-adapter isomorphism).
252
+ * Returns null when the predicate is outside the evaluator surface (e.g. a
253
+ * method-call predicate — `serializeParsedExpr` refuses it), so the caller
254
+ * falls back to the lambda form. `forward` (find / findIndex family only)
255
+ * selects the search direction — `false` = findLast / findLastIndex.
256
+ */
257
+ export function renderPredicateEval(
258
+ funcName: string,
259
+ recv: string,
260
+ predicate: ParsedExpr,
261
+ param: string,
262
+ emit: (e: ParsedExpr) => string,
263
+ forward?: boolean,
264
+ ): string | null {
265
+ const json = serializeParsedExpr(predicate)
266
+ if (json === null) return null
267
+ const env = emitEvalEnvArg(predicate, [param], emit)
268
+ const fwd = forward === undefined ? '' : `, ${forward ? 1 : 0}`
269
+ return `$bf.${funcName}(${recv}, '${escapePerlSingleQuote(json)}', '${param}'${fwd}, ${env})`
270
+ }
271
+
272
+ /**
273
+ * Emit a `.flatMap(proj)` via the runtime evaluator (#2018, P3): the projection
274
+ * body serializes to JSON and `$bf.flat_map_eval` projects + flattens one
275
+ * level. `param` is the projection arrow's single param. Returns null when the
276
+ * projection is outside the evaluator surface (→ caller refuses with BF101).
277
+ */
278
+ export function renderFlatMapEval(
279
+ recv: string,
280
+ body: ParsedExpr,
281
+ param: string,
282
+ emit: (e: ParsedExpr) => string,
283
+ ): string | null {
284
+ const json = serializeParsedExpr(body)
285
+ if (json === null) return null
286
+ const env = emitEvalEnvArg(body, [param], emit)
287
+ return `$bf.flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
288
+ }
289
+
290
+ /**
291
+ * Emit a value-producing `.map(cb)` via the runtime evaluator (#2073): the
292
+ * projection body serializes to JSON and `$bf.map_eval` projects each element,
293
+ * one result per element (no flatten — the JS `.map` contract). Composes
294
+ * through the array-method chain (`.map(cb).join(' ')`). Returns null when
295
+ * the projection is outside the evaluator surface (→ caller refuses with
296
+ * BF101). The JSX-returning `.map` is an IRLoop upstream and never reaches
297
+ * this emit.
298
+ */
299
+ export function renderMapEval(
300
+ recv: string,
301
+ body: ParsedExpr,
302
+ param: string,
303
+ emit: (e: ParsedExpr) => string,
304
+ ): string | null {
305
+ const json = serializeParsedExpr(body)
306
+ if (json === null) return null
307
+ const env = emitEvalEnvArg(body, [param], emit)
308
+ return `$bf.map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`
309
+ }
310
+
311
+ /**
312
+ * Shared Kolon emit for `.sort(cmp)` / `.toSorted(cmp)`. Used by both the
313
+ * filter-context emitter and the top-level emitter, plus the loop-array
314
+ * wrap in `renderLoop`. The runtime `$bf.sort` accepts a hashref opts bag and
315
+ * returns a fresh array ref.
316
+ */
317
+ export function renderSortMethod(recv: string, c: SortComparator): string {
318
+ const keyHashes = c.keys.map((k) => {
319
+ const keyEntry =
320
+ k.key.kind === 'self'
321
+ ? `key_kind => 'self'`
322
+ : `key_kind => 'field', key => '${k.key.field}'`
323
+ return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`
324
+ })
325
+ return `$bf.sort(${recv}, { keys => [${keyHashes.join(', ')}] })`
326
+ }
327
+
328
+ // `.flat(depth?)` → `$bf.flat($recv, $depth)`.
329
+ export function renderFlatMethod(recv: string, depth: FlatDepth): string {
330
+ const d = depth === 'infinity' ? -1 : depth
331
+ return `$bf.flat(${recv}, ${d})`
332
+ }