@barefootjs/xslate 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 (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 +63 -0
  6. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  7. package/dist/adapter/expr/emitters.d.ts +97 -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 +1428 -1324
  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 +34 -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 +1428 -1324
  31. package/dist/index.js +1428 -1324
  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 +94 -2
  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 +311 -0
  39. package/src/adapter/expr/emitters.ts +530 -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 +64 -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 +125 -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 +176 -1233
  50. package/src/test-render.ts +3 -0
@@ -0,0 +1,530 @@
1
+ /**
2
+ * ParsedExpr → Kolon emitters for the Text::Xslate template adapter.
3
+ *
4
+ * Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Two `ParsedExprEmitter` implementations:
6
+ *
7
+ * - `XslateFilterEmitter` — filter/predicate context (loop param + local
8
+ * aliases + bare `$name` signal fallback); self-contained, reads no
9
+ * adapter state.
10
+ * - `XslateTopLevelEmitter` — top-level / per-render-var context; depends on
11
+ * the adapter only through the narrow `XslateEmitContext` seam.
12
+ */
13
+
14
+ import {
15
+ type ParsedExprEmitter,
16
+ type HigherOrderMethod,
17
+ type ArrayMethod,
18
+ type LiteralType,
19
+ type ParsedExpr,
20
+ type ObjectLiteralProperty,
21
+ type FlatDepth,
22
+ type TemplatePart,
23
+ identifierPath,
24
+ matchSearchParamsMethodCall,
25
+ sortComparatorFromArrow,
26
+ } from '@barefootjs/jsx'
27
+
28
+ import type { XslateEmitContext } from '../emit-context.ts'
29
+ import { XSLATE_TEMPLATE_PRIMITIVES } from '../lib/constants.ts'
30
+ import {
31
+ renderArrayMethod,
32
+ renderSortMethod,
33
+ renderSortEval,
34
+ renderReduceEval,
35
+ renderPredicateEval,
36
+ renderFlatMethod,
37
+ renderFlatMapEval,
38
+ } from './array-method.ts'
39
+
40
+ /**
41
+ * Local shape for the predicate-lowering helpers (`buildPredicateEval` and the
42
+ * Kolon-lambda fallback). Previously these read a `higher-order` ParsedExpr
43
+ * node directly; after the #2018 P5 collapse the callback arrives as a generic
44
+ * `call` and is destructured by `asCallbackMethodCall` / `callbackMethod`, so
45
+ * the helpers take this narrow record instead.
46
+ */
47
+ type PredicateCall = {
48
+ method: HigherOrderMethod
49
+ object: ParsedExpr
50
+ param: string
51
+ predicate: ParsedExpr
52
+ }
53
+
54
+ // Methods whose callback is a boolean predicate (`<recv>.<m>(x => …)`).
55
+ const PREDICATE_METHODS = new Set<HigherOrderMethod>([
56
+ 'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'every', 'some',
57
+ ])
58
+
59
+ /**
60
+ * Lowering for the predicate body of a filter / every / some / find, plus the
61
+ * same shape used by `renderBlockBodyCondition` for complex block-body
62
+ * filters. Higher-order predicates are emitted using Kolon's own scalar
63
+ * comparison operators (which delegate to Perl semantics).
64
+ *
65
+ * NOTE: Kolon has no `grep { } @{...}` form, so nested higher-order chains
66
+ * (`x.tags.filter(...).length`) inside a predicate route through the
67
+ * top-level emitter's `$bf`-helper higher-order lowering. This emitter keeps
68
+ * the scalar-comparison surface the predicates the adapter accepts actually
69
+ * use; richer nested shapes fall back to the helper or surface as BF101 via
70
+ * the top-level emitter.
71
+ */
72
+ export class XslateFilterEmitter implements ParsedExprEmitter {
73
+ constructor(
74
+ private readonly param: string,
75
+ private readonly localVarMap: Map<string, string>,
76
+ private readonly isStringName: (n: string) => boolean = () => false,
77
+ ) {}
78
+
79
+ identifier(name: string): string {
80
+ if (name === this.param) return `$${this.param}`
81
+ const signal = this.localVarMap.get(name)
82
+ if (signal) return `$${signal}`
83
+ return `$${name}`
84
+ }
85
+
86
+ literal(value: string | number | boolean | null, literalType: LiteralType): string {
87
+ if (literalType === 'string') return `'${value}'`
88
+ if (literalType === 'boolean') return value ? '1' : '0'
89
+ if (literalType === 'null') return 'nil'
90
+ return String(value)
91
+ }
92
+
93
+ member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
94
+ // `.length` — route through `$bf.length` (handles both array element
95
+ // count and string char count, JS-compatibly). Kolon's builtin `.size()`
96
+ // is array-only and faults on a string.
97
+ if (property === 'length') {
98
+ return `$bf.length(${emit(object)})`
99
+ }
100
+ // Hash field access — Kolon dot works on hash refs.
101
+ return `${emit(object)}.${property}`
102
+ }
103
+
104
+ indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
105
+ // Kolon's `[]` postfix is polymorphic (array index or hash key),
106
+ // mirroring JS — no array/hash split is needed (unlike Perl's
107
+ // `->[]` vs `->{}`). #1897 (data-table's `selected()[index]`).
108
+ return `${emit(object)}[${emit(index)}]`
109
+ }
110
+
111
+ call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
112
+ // Signal getter calls: filter() → $filter
113
+ if (callee.kind === 'identifier' && args.length === 0) {
114
+ return `$${callee.name}`
115
+ }
116
+ return emit(callee)
117
+ }
118
+
119
+ unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
120
+ const arg = emit(argument)
121
+ if (op === '!') {
122
+ const needsParens = argument.kind === 'binary' || argument.kind === 'logical'
123
+ return needsParens ? `!(${arg})` : `!${arg}`
124
+ }
125
+ if (op === '-') return `-${arg}`
126
+ return arg
127
+ }
128
+
129
+ binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
130
+ const l = emit(left)
131
+ const r = emit(right)
132
+ // Kolon's `==` / `!=` are value-equality operators that compare strings
133
+ // and numbers correctly — unlike Perl's numeric `==` (which the Mojo
134
+ // adapter must steer around with `eq`/`ne`). Kolon has no `eq`/`ne`
135
+ // operator at all, so string comparisons stay on `==` / `!=` here.
136
+ const opMap: Record<string, string> = {
137
+ '===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
138
+ '+': '+', '-': '-', '*': '*', '/': '/',
139
+ }
140
+ return `${l} ${opMap[op] ?? op} ${r}`
141
+ }
142
+
143
+ logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
144
+ const l = emit(left)
145
+ const r = emit(right)
146
+ if (op === '&&') return `(${l} && ${r})`
147
+ if (op === '||') return `(${l} || ${r})`
148
+ return `(${l} // ${r})`
149
+ }
150
+
151
+ callbackMethod(
152
+ method: string,
153
+ object: ParsedExpr,
154
+ _arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
155
+ _restArgs: ParsedExpr[],
156
+ emit: (e: ParsedExpr) => string,
157
+ ): string {
158
+ // Nested callback method inside a filter predicate has no Kolon scalar
159
+ // form; defer to the receiver so the predicate at least references a real
160
+ // value (a richer chain would surface its own diagnostic at the top level).
161
+ // Matches the pre-#2018-P5 `higherOrder` arm, which returned `emit(object)`
162
+ // for every method.
163
+ void method
164
+ return emit(object)
165
+ }
166
+
167
+ arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
168
+ return `[${elements.map(emit).join(', ')}]`
169
+ }
170
+
171
+ arrayMethod(
172
+ method: ArrayMethod,
173
+ object: ParsedExpr,
174
+ args: ParsedExpr[],
175
+ emit: (e: ParsedExpr) => string,
176
+ ): string {
177
+ return renderArrayMethod(method, object, args, emit)
178
+ }
179
+
180
+ flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
181
+ return renderFlatMethod(emit(object), depth)
182
+ }
183
+
184
+ conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
185
+ return '1'
186
+ }
187
+
188
+ templateLiteral(_parts: TemplatePart[]): string {
189
+ return '1'
190
+ }
191
+
192
+ arrow(_params: string[], _body: ParsedExpr): string {
193
+ return '1'
194
+ }
195
+
196
+ regex(_raw: string): string {
197
+ return '1'
198
+ }
199
+
200
+ unsupported(_raw: string, _reason: string): string {
201
+ return '1'
202
+ }
203
+
204
+ objectLiteral(_properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
205
+ // Filter-predicate context: emit the truthy sentinel exactly as
206
+ // `unsupported` does, byte-identical with the pre-`object-literal`
207
+ // fallback (Roadmap A-1). Object values lower to Kolon hashrefs in the
208
+ // conditional/attr paths, not through this dispatcher.
209
+ return '1'
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Lowering for top-level expressions whose identifiers resolve against the
215
+ * Kolon template's per-render vars (signals, props, locals introduced by `:
216
+ * my $x = ...` lines). Differs from the filter emitter mainly in
217
+ * - `.length` → `.size()` (Kolon array length),
218
+ * - `conditional` is supported (filter predicates can't return ternaries),
219
+ * - higher-order methods route through `$bf` array helpers.
220
+ */
221
+ export class XslateTopLevelEmitter implements ParsedExprEmitter {
222
+ constructor(private readonly ctx: XslateEmitContext) {}
223
+
224
+ identifier(name: string): string {
225
+ // `undefined` / `null` nested inside a larger expression tree —
226
+ // Kolon `nil` (#1897).
227
+ if (name === 'undefined' || name === 'null') return 'nil'
228
+ // Inline a module-scope pure-string const (`const x = 'literal'`) — it
229
+ // never reaches the per-render stash, so a bare `$x` would render empty.
230
+ const inlined = this.ctx._resolveModuleStringConst(name)
231
+ if (inlined !== null) return inlined
232
+ // Same for a literal const of any scope (`const totalPages = 5`,
233
+ // #1897 pagination's `Page {currentPage()} of {totalPages}`).
234
+ const literalConst = this.ctx._resolveLiteralConst(name)
235
+ if (literalConst !== null) return literalConst
236
+ return `$${name}`
237
+ }
238
+
239
+ literal(value: string | number | boolean | null, literalType: LiteralType): string {
240
+ if (literalType === 'string') return `'${value}'`
241
+ if (literalType === 'boolean') return value ? '1' : '0'
242
+ if (literalType === 'null') return 'nil'
243
+ return String(value)
244
+ }
245
+
246
+ member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
247
+ // `props.x` flattens to the bare `$x` the SSR caller binds each prop to
248
+ // (props arrive as individual top-level vars, not a `$props` hashref).
249
+ if (object.kind === 'identifier' && object.name === 'props') {
250
+ return `$${property}`
251
+ }
252
+ // Static property access on a module object-literal const
253
+ // (`variantClasses.ghost`, #1897) resolves at compile time — the
254
+ // generic dot lowering below would reference a Kolon var that
255
+ // doesn't exist server-side and silently render ''.
256
+ if (object.kind === 'identifier') {
257
+ const staticValue = this.ctx._resolveStaticRecordLiteral(object.name, property)
258
+ if (staticValue !== null) return staticValue
259
+ }
260
+ const obj = emit(object)
261
+ // `.length` → `$bf.length` (array count or string char count, JS-compat);
262
+ // Kolon's builtin `.size()` is array-only and faults on a string.
263
+ if (property === 'length') return `$bf.length(${obj})`
264
+ // Kolon dot access works for hash refs.
265
+ return `${obj}.${property}`
266
+ }
267
+
268
+ indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
269
+ // Kolon's `[]` postfix is polymorphic (array index or hash key),
270
+ // mirroring JS. #1897 (data-table's `selected()[index]`).
271
+ return `${emit(object)}[${emit(index)}]`
272
+ }
273
+
274
+ call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
275
+ // Signal getter: count() → $count
276
+ if (callee.kind === 'identifier' && args.length === 0) {
277
+ return `$${callee.name}`
278
+ }
279
+ // Env-signal method call (#1922): `searchParams().get('sort')` is a real
280
+ // method call on the per-request `$searchParams` reader object, not the
281
+ // generic dot deref `member` would emit (`$searchParams.get`, which drops
282
+ // the arg). Matches the local import binding (incl. an alias).
283
+ if (this.ctx._searchParamsLocals.size > 0) {
284
+ const sp = matchSearchParamsMethodCall(callee, args, this.ctx._searchParamsLocals)
285
+ if (sp) {
286
+ return `$searchParams.${sp.method}(${sp.args.map(emit).join(', ')})`
287
+ }
288
+ }
289
+ // Identifier-path templatePrimitive: `JSON.stringify(x)` / `Math.floor(x)`
290
+ // → `$bf.json($x)` / `$bf.floor($x)`. Args render recursively through this
291
+ // same emitter. A wrong-arity call records BF101 and returns `''`.
292
+ const path = identifierPath(callee)
293
+ const spec = path ? XSLATE_TEMPLATE_PRIMITIVES[path] : undefined
294
+ if (path && spec) {
295
+ if (args.length === spec.arity) {
296
+ return spec.emit(args.map(emit))
297
+ }
298
+ this.ctx._recordExprBF101(
299
+ `templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`,
300
+ `Call '${path}' with exactly ${spec.arity} argument(s).`,
301
+ )
302
+ return "''"
303
+ }
304
+ return emit(callee)
305
+ }
306
+
307
+ unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
308
+ const arg = emit(argument)
309
+ if (op === '!') return `!${arg}`
310
+ if (op === '-') return `-${arg}`
311
+ return arg
312
+ }
313
+
314
+ binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
315
+ const l = emit(left)
316
+ const r = emit(right)
317
+ // Kolon's `==` / `!=` are value-equality operators handling both strings
318
+ // and numbers (unlike Perl's numeric `==`, which the Mojo adapter must
319
+ // route around with `eq`/`ne`). Kolon has no `eq`/`ne` operator, so all
320
+ // equality comparisons — string or numeric — stay on `==` / `!=`.
321
+ const opMap: Record<string, string> = {
322
+ '===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
323
+ '+': '+', '-': '-', '*': '*',
324
+ }
325
+ return `${l} ${opMap[op] ?? op} ${r}`
326
+ }
327
+
328
+ logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
329
+ const l = emit(left)
330
+ const r = emit(right)
331
+ if (op === '&&') return `(${l} && ${r})`
332
+ if (op === '||') return `(${l} || ${r})`
333
+ return `(${l} // ${r})`
334
+ }
335
+
336
+ callbackMethod(
337
+ method: string,
338
+ object: ParsedExpr,
339
+ arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
340
+ restArgs: ParsedExpr[],
341
+ emit: (e: ParsedExpr) => string,
342
+ ): string {
343
+ const recv = emit(object)
344
+ const body = arrow.body
345
+ const params = arrow.params
346
+
347
+ // Predicate family (#2018 P2/P5): `filter` / `find*` / `every` / `some`.
348
+ if (PREDICATE_METHODS.has(method as HigherOrderMethod)) {
349
+ return this._emitPredicateCallback(
350
+ { method: method as HigherOrderMethod, object, param: params[0], predicate: body },
351
+ recv,
352
+ emit,
353
+ )
354
+ }
355
+
356
+ // `.sort(cmp)` / `.toSorted(cmp)` (#2018): serialize the comparator body +
357
+ // emit `$bf.sort_eval`; fall back to the structured `$bf.sort` when the
358
+ // body is outside the evaluator surface (e.g. `localeCompare`). A
359
+ // comparator that neither serializes nor classifies → BF101.
360
+ if (method === 'sort' || method === 'toSorted') {
361
+ const evalForm = renderSortEval(recv, body, params, emit)
362
+ if (evalForm !== null) return evalForm
363
+ const structured = sortComparatorFromArrow(arrow)
364
+ if (structured !== null) return renderSortMethod(recv, structured)
365
+ this.ctx._recordExprBF101(
366
+ `'.${method}(...)' comparator is outside the Xslate adapter's evaluable / structured surface`,
367
+ `Pre-compute the sorted array, or move this position to a '/* @client */' boundary.`,
368
+ )
369
+ return "''"
370
+ }
371
+
372
+ // `.reduce(fn, init)` / `.reduceRight(fn, init)` (#2018): serialize the
373
+ // reducer body + emit `$bf.reduce_eval`. The init is the trailing arg.
374
+ if (method === 'reduce' || method === 'reduceRight') {
375
+ const direction = method === 'reduceRight' ? 'right' : 'left'
376
+ const init = restArgs[0]
377
+ const evalForm =
378
+ init !== undefined
379
+ ? renderReduceEval(recv, body, params, init, direction, emit)
380
+ : null
381
+ if (evalForm !== null) return evalForm
382
+ this.ctx._recordExprBF101(
383
+ `'.${method}(...)' is outside the Xslate adapter's evaluable surface (needs a literal initial value and an evaluable reducer body)`,
384
+ `Pre-compute the reduced value, or move this position to a '/* @client */' boundary.`,
385
+ )
386
+ return "''"
387
+ }
388
+
389
+ // `.flatMap(proj)` (#2018 P3): serialize the projection body + emit
390
+ // `$bf.flat_map_eval`.
391
+ if (method === 'flatMap') {
392
+ const evalForm = renderFlatMapEval(recv, body, params[0], emit)
393
+ if (evalForm !== null) return evalForm
394
+ this.ctx._recordExprBF101(
395
+ `'.flatMap(...)' projection is outside the Xslate adapter's evaluable surface`,
396
+ `Pre-compute the projected array, or move this position to a '/* @client */' boundary.`,
397
+ )
398
+ return "''"
399
+ }
400
+
401
+ // Unknown callback method (should not arrive — CALLBACK_METHODS is closed).
402
+ void object
403
+ return recv
404
+ }
405
+
406
+ /**
407
+ * Lower a boolean-predicate callback (`filter` / `find*` / `every` / `some`),
408
+ * extracted from the pre-#2018-P5 `higherOrder` arm. Higher-order array
409
+ * methods all take a JS arrow predicate, lowered to a Kolon lambda
410
+ * `-> $param { PRED }` (callable from Perl as a code ref), and go through the
411
+ * runtime object — consistent with the other array helpers ($bf.includes /
412
+ * $bf.slice / ...). `.find*` map to snake_case runtime methods. The
413
+ * `.filter(...).map(...)` *loop* form is handled separately by renderLoop's
414
+ * inline predicate.
415
+ */
416
+ private _emitPredicateCallback(
417
+ call: PredicateCall,
418
+ arrayExpr: string,
419
+ emit: (e: ParsedExpr) => string,
420
+ ): string {
421
+ const { method, object, param, predicate } = call
422
+
423
+ // Evaluator path (#2018 P2): serialize the predicate body + emit the
424
+ // matching `$bf.*_eval` helper (isomorphic with the Go adapter). Falls
425
+ // back to the Kolon-lambda runtime call below for a predicate the
426
+ // evaluator can't model (e.g. a method-call predicate).
427
+ const evalFn: Record<string, [string, boolean?]> = {
428
+ filter: ['filter_eval'], every: ['every_eval'], some: ['some_eval'],
429
+ find: ['find_eval', true], findLast: ['find_eval', false],
430
+ findIndex: ['find_index_eval', true], findLastIndex: ['find_index_eval', false],
431
+ }
432
+ // `.filter(Boolean)` (identity predicate `_t => _t`) keeps the lambda
433
+ // form — it composes through the array-method chain and renders
434
+ // identically to a truthiness filter.
435
+ const isIdentity =
436
+ method === 'filter' && predicate.kind === 'identifier' && predicate.name === param
437
+ const spec = evalFn[method]
438
+ if (spec && !isIdentity) {
439
+ const evalForm = renderPredicateEval(spec[0], arrayExpr, predicate, param, emit, spec[1])
440
+ if (evalForm !== null) return evalForm
441
+ }
442
+
443
+ const predBody = this.ctx._renderKolonFilterExprPublic(predicate, param)
444
+ const lambda = `-> $${param} { ${predBody} }`
445
+ const fn: Record<string, string> = {
446
+ filter: 'filter',
447
+ every: 'every',
448
+ some: 'some',
449
+ find: 'find',
450
+ findIndex: 'find_index',
451
+ findLast: 'find_last',
452
+ findLastIndex: 'find_last_index',
453
+ }
454
+ if (fn[method]) return `$bf.${fn[method]}(${arrayExpr}, ${lambda})`
455
+ return emit(object)
456
+ }
457
+
458
+ arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
459
+ return `[${elements.map(emit).join(', ')}]`
460
+ }
461
+
462
+ arrayMethod(
463
+ method: ArrayMethod,
464
+ object: ParsedExpr,
465
+ args: ParsedExpr[],
466
+ emit: (e: ParsedExpr) => string,
467
+ ): string {
468
+ return renderArrayMethod(method, object, args, emit)
469
+ }
470
+
471
+ flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
472
+ return renderFlatMethod(emit(object), depth)
473
+ }
474
+
475
+ conditional(
476
+ test: ParsedExpr,
477
+ consequent: ParsedExpr,
478
+ alternate: ParsedExpr,
479
+ emit: (e: ParsedExpr) => string,
480
+ ): string {
481
+ return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`
482
+ }
483
+
484
+ templateLiteral(parts: TemplatePart[], emit: (e: ParsedExpr) => string): string {
485
+ // `` `n=${count() + 1}` `` → Kolon string concatenation (`~`):
486
+ // `'n=' ~ ($count + 1)`. Kolon's `~` is the explicit concat operator.
487
+ const terms: string[] = []
488
+ for (const part of parts) {
489
+ if (part.type === 'string') {
490
+ if (part.value !== '') {
491
+ terms.push(`'${part.value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`)
492
+ }
493
+ } else {
494
+ const rendered = emit(part.expr)
495
+ const needsParens =
496
+ part.expr.kind === 'binary' ||
497
+ part.expr.kind === 'logical' ||
498
+ part.expr.kind === 'conditional'
499
+ terms.push(needsParens ? `(${rendered})` : rendered)
500
+ }
501
+ }
502
+ if (terms.length === 0) return `''`
503
+ return terms.join(' ~ ')
504
+ }
505
+
506
+ arrow(_params: string[], _body: ParsedExpr): string {
507
+ // A bare arrow never stands alone at a render position (it's only
508
+ // meaningful as a callback, handled by `callbackMethod`). Emit the safe
509
+ // empty-string literal so a stray emit can't produce a Kolon syntax error.
510
+ return "''"
511
+ }
512
+
513
+ regex(_raw: string): string {
514
+ // A bare regex literal has no template-render form — mirror `unsupported`.
515
+ return "''"
516
+ }
517
+
518
+ unsupported(_raw: string, _reason: string): string {
519
+ return "''"
520
+ }
521
+
522
+ objectLiteral(_properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
523
+ // Mirror `unsupported`: a bare object literal reaching the dispatcher
524
+ // lowers to the safe empty-string literal, exactly as before the
525
+ // `object-literal` kind existed (byte-identical; Roadmap A-1). Object
526
+ // values that round-trip to a Kolon hashref go through the dedicated
527
+ // `objectLiteralToKolonHashref` lowering in the conditional/attr paths.
528
+ return "''"
529
+ }
530
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Operand-type classification for the Text::Xslate (Kolon) template adapter.
3
+ *
4
+ * Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Pure function over `ParsedExpr` taking an `isStringName`
6
+ * predicate rather than reading adapter instance state.
7
+ *
8
+ * NOTE: unlike the Mojo adapter, Kolon's `==`/`!=` are value-equality
9
+ * operators that compare strings and numbers correctly, so the Kolon
10
+ * emitters never need to steer string comparisons onto `eq`/`ne`. This
11
+ * helper is therefore not consumed by the Kolon lowering today — it is kept
12
+ * as the parallel of the Mojo adapter's `expr/operand.ts` (groundwork for a
13
+ * future shared Perl-evaluator surface, issue #2018 track D).
14
+ */
15
+
16
+ import type { ParsedExpr } from '@barefootjs/jsx'
17
+
18
+ /**
19
+ * Whether a comparison operand is string-typed. In the Mojo adapter this
20
+ * selects Perl `eq`/`ne` over numeric `==`/`!=` for a `===`/`!==` against a
21
+ * string operand. The Kolon emitters do NOT consume it — Kolon's `==`/`!=`
22
+ * are value-equality operators that compare strings and numbers correctly, so
23
+ * `===`/`!==` always map to `==`/`!=` here (see the file header). Kept only as
24
+ * the parallel of the Mojo helper.
25
+ */
26
+ export function isStringTypedOperand(expr: ParsedExpr, isStringName: (n: string) => boolean): boolean {
27
+ if (expr.kind === 'literal' && expr.literalType === 'string') return true
28
+ if (expr.kind === 'call' && expr.callee.kind === 'identifier' && expr.args.length === 0) {
29
+ return isStringName(expr.callee.name)
30
+ }
31
+ if (expr.kind === 'member' && expr.object.kind === 'identifier' && expr.object.name === 'props') {
32
+ return isStringName(expr.property)
33
+ }
34
+ return false
35
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Compile-time constant tables for the Text::Xslate (Kolon) template adapter.
3
+ *
4
+ * Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D).
6
+ */
7
+
8
+ import type { PrimitiveSpec } from './types.ts'
9
+
10
+ /**
11
+ * Single source of truth for the Xslate adapter's template-primitive
12
+ * surface. Each entry pairs the expected arity with the emit function.
13
+ *
14
+ * The emit fn returns a Kolon expression (no surrounding `<: :>`) suitable
15
+ * for embedding inside an interpolation — `$bf.json($val)`,
16
+ * `$bf.floor($val)`, etc. The same primitive names as the Mojo adapter, but
17
+ * invoked as `$bf.NAME(args)` on the runtime instance instead of `bf->NAME`.
18
+ */
19
+ export const XSLATE_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
20
+ 'JSON.stringify': { arity: 1, emit: (args) => `$bf.json(${args[0]})` },
21
+ 'String': { arity: 1, emit: (args) => `$bf.string(${args[0]})` },
22
+ 'Number': { arity: 1, emit: (args) => `$bf.number(${args[0]})` },
23
+ 'Math.floor': { arity: 1, emit: (args) => `$bf.floor(${args[0]})` },
24
+ 'Math.ceil': { arity: 1, emit: (args) => `$bf.ceil(${args[0]})` },
25
+ 'Math.round': { arity: 1, emit: (args) => `$bf.round(${args[0]})` },
26
+ }
27
+
28
+ /**
29
+ * Module-scope `templatePrimitives` map derived once from the spec record.
30
+ */
31
+ export const XSLATE_PRIMITIVE_EMIT_MAP: Record<string, (args: string[]) => string> =
32
+ Object.fromEntries(
33
+ Object.entries(XSLATE_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit])
34
+ )
@@ -0,0 +1,64 @@
1
+ /**
2
+ * IR traversal helpers for the Text::Xslate (Kolon) template adapter.
3
+ *
4
+ * Extracted from `xslate-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Pure functions over the IR tree — no adapter instance state.
6
+ *
7
+ * SHARED CANDIDATE: the bodies here are byte-identical to the Mojo
8
+ * adapter's `lib/ir-scope.ts`. They are adapter-agnostic (no Perl/Kolon
9
+ * specifics), so they are the obvious first extraction into a shared
10
+ * Perl-family codegen module once one exists — the groundwork for the
11
+ * future Perl evaluator integration (issue #2018 track D). Kept per-adapter
12
+ * for now, matching the repo convention (the Go adapter keeps its own copy).
13
+ */
14
+
15
+ import type { IRNode, IRProp, IRIfStatement, IRFragment } from '@barefootjs/jsx'
16
+
17
+ /**
18
+ * Find the `children` prop's `jsx-children` payload. Narrowed via the
19
+ * AttrValue `kind` discriminator so adapter code stays type-safe if the IR
20
+ * shape evolves.
21
+ */
22
+ export function resolveJsxChildrenProp(props: readonly IRProp[]): IRNode[] {
23
+ const prop = props.find(p => p.name === 'children')
24
+ if (!prop) return []
25
+ if (prop.value.kind !== 'jsx-children') return []
26
+ return prop.value.children
27
+ }
28
+
29
+ /**
30
+ * Collect the component's root scope element node(s) — the elements that
31
+ * become the rendered root and so carry `data-key` for a keyed loop item. A
32
+ * plain element root is itself; an `if-statement` (early-return) root
33
+ * contributes the top element of each branch, since exactly one renders at
34
+ * runtime. (#1297)
35
+ */
36
+ export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
37
+ const out = new Set<IRNode>()
38
+ const visit = (n: IRNode | null): void => {
39
+ if (!n) return
40
+ if (n.type === 'element') { out.add(n); return }
41
+ if (n.type === 'if-statement') {
42
+ const s = n as IRIfStatement
43
+ visit(s.consequent)
44
+ visit(s.alternate)
45
+ return
46
+ }
47
+ if (n.type === 'fragment') {
48
+ for (const c of (n as IRFragment).children) visit(c)
49
+ }
50
+ }
51
+ visit(node)
52
+ return out
53
+ }
54
+
55
+ /**
56
+ * True when every `$var` the lowered Kolon expression references is already in
57
+ * scope — guards in-template memo seeding against an out-of-scope binding. (#1297)
58
+ */
59
+ export function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
60
+ for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
61
+ if (!available.has(m[1])) return false
62
+ }
63
+ return true
64
+ }