@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,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
|
+
}
|