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