@barefootjs/jinja 0.1.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 (88) hide show
  1. package/README.md +66 -0
  2. package/dist/adapter/analysis/component-tree.d.ts +26 -0
  3. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  4. package/dist/adapter/boolean-result.d.ts +84 -0
  5. package/dist/adapter/boolean-result.d.ts.map +1 -0
  6. package/dist/adapter/emit-context.d.ts +106 -0
  7. package/dist/adapter/emit-context.d.ts.map +1 -0
  8. package/dist/adapter/expr/array-method.d.ts +75 -0
  9. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  10. package/dist/adapter/expr/emitters.d.ts +143 -0
  11. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  12. package/dist/adapter/expr/operand.d.ts +25 -0
  13. package/dist/adapter/expr/operand.d.ts.map +1 -0
  14. package/dist/adapter/index.d.ts +6 -0
  15. package/dist/adapter/index.d.ts.map +1 -0
  16. package/dist/adapter/index.js +189097 -0
  17. package/dist/adapter/jinja-adapter.d.ts +394 -0
  18. package/dist/adapter/jinja-adapter.d.ts.map +1 -0
  19. package/dist/adapter/lib/constants.d.ts +21 -0
  20. package/dist/adapter/lib/constants.d.ts.map +1 -0
  21. package/dist/adapter/lib/ir-scope.d.ts +50 -0
  22. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  23. package/dist/adapter/lib/jinja-naming.d.ts +77 -0
  24. package/dist/adapter/lib/jinja-naming.d.ts.map +1 -0
  25. package/dist/adapter/lib/types.d.ts +27 -0
  26. package/dist/adapter/lib/types.d.ts.map +1 -0
  27. package/dist/adapter/memo/seed.d.ts +81 -0
  28. package/dist/adapter/memo/seed.d.ts.map +1 -0
  29. package/dist/adapter/props/prop-classes.d.ts +33 -0
  30. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  31. package/dist/adapter/spread/spread-codegen.d.ts +61 -0
  32. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  33. package/dist/adapter/value/parsed-literal.d.ts +28 -0
  34. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  35. package/dist/build.d.ts +28 -0
  36. package/dist/build.d.ts.map +1 -0
  37. package/dist/build.js +189117 -0
  38. package/dist/conformance-pins.d.ts +12 -0
  39. package/dist/conformance-pins.d.ts.map +1 -0
  40. package/dist/index.d.ts +9 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +189118 -0
  43. package/package.json +66 -0
  44. package/python/VERSION +1 -0
  45. package/python/barefootjs/__init__.py +57 -0
  46. package/python/barefootjs/backend_jinja.py +125 -0
  47. package/python/barefootjs/evaluator.py +787 -0
  48. package/python/barefootjs/runtime.py +1334 -0
  49. package/python/barefootjs/search_params.py +89 -0
  50. package/python/pyproject.toml +32 -0
  51. package/python/tests/__init__.py +0 -0
  52. package/python/tests/test_eval_vectors.py +88 -0
  53. package/python/tests/test_evaluator.py +406 -0
  54. package/python/tests/test_helper_vectors.py +288 -0
  55. package/python/tests/test_omit.py +62 -0
  56. package/python/tests/test_props_attr.py +54 -0
  57. package/python/tests/test_query.py +41 -0
  58. package/python/tests/test_render.py +102 -0
  59. package/python/tests/test_render_child.py +96 -0
  60. package/python/tests/test_search_params.py +50 -0
  61. package/python/tests/test_spread_attrs.py +86 -0
  62. package/python/tests/test_template_primitives.py +347 -0
  63. package/python/tests/vector-divergences.json +42 -0
  64. package/src/__tests__/jinja-adapter-unit.test.ts +390 -0
  65. package/src/__tests__/jinja-adapter.test.ts +53 -0
  66. package/src/__tests__/jinja-counter.test.ts +62 -0
  67. package/src/__tests__/jinja-query-href.test.ts +99 -0
  68. package/src/__tests__/jinja-spread-attrs.test.ts +225 -0
  69. package/src/adapter/analysis/component-tree.ts +119 -0
  70. package/src/adapter/boolean-result.ts +176 -0
  71. package/src/adapter/emit-context.ts +118 -0
  72. package/src/adapter/expr/array-method.ts +346 -0
  73. package/src/adapter/expr/emitters.ts +608 -0
  74. package/src/adapter/expr/operand.ts +35 -0
  75. package/src/adapter/index.ts +6 -0
  76. package/src/adapter/jinja-adapter.ts +1747 -0
  77. package/src/adapter/lib/constants.ts +33 -0
  78. package/src/adapter/lib/ir-scope.ts +95 -0
  79. package/src/adapter/lib/jinja-naming.ts +114 -0
  80. package/src/adapter/lib/types.ts +30 -0
  81. package/src/adapter/memo/seed.ts +132 -0
  82. package/src/adapter/props/prop-classes.ts +65 -0
  83. package/src/adapter/spread/spread-codegen.ts +166 -0
  84. package/src/adapter/value/parsed-literal.ts +76 -0
  85. package/src/build.ts +37 -0
  86. package/src/conformance-pins.ts +101 -0
  87. package/src/index.ts +9 -0
  88. package/src/test-render.ts +714 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Array / string method lowering for the Jinja2 template adapter.
3
+ *
4
+ * Ported from `packages/adapter-xslate/src/adapter/expr/array-method.ts`.
5
+ * Pure free functions shared by both the filter-context emitter and the
6
+ * 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 Xslate adapter
10
+ * calls, invoked as `bf.NAME(...)` (bare, no `$` sigil) 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
+ import { escapeJinjaSingleQuoted, jinjaHashKey } from '../lib/jinja-naming.ts'
25
+
26
+ export function renderArrayMethod(
27
+ method: ArrayMethod,
28
+ object: ParsedExpr,
29
+ args: ParsedExpr[],
30
+ emit: (e: ParsedExpr) => string,
31
+ ): string {
32
+ switch (method) {
33
+ case 'join': {
34
+ // Route through the runtime (`bf.join`) rather than a Jinja/Python
35
+ // builtin, so the JS-compat element handling (undef → empty, default
36
+ // separator) is applied consistently.
37
+ const obj = emit(object)
38
+ const sep = args.length >= 1 ? emit(args[0]) : `','`
39
+ return `bf.join(${obj}, ${sep})`
40
+ }
41
+ case 'includes': {
42
+ const obj = emit(object)
43
+ const needle = emit(args[0])
44
+ return `bf.includes(${obj}, ${needle})`
45
+ }
46
+ case 'indexOf':
47
+ case 'lastIndexOf': {
48
+ const fn = method === 'indexOf' ? 'index_of' : 'last_index_of'
49
+ const obj = emit(object)
50
+ const needle = emit(args[0])
51
+ return `bf.${fn}(${obj}, ${needle})`
52
+ }
53
+ case 'at': {
54
+ const obj = emit(object)
55
+ const idx = args.length >= 1 ? emit(args[0]) : '0'
56
+ return `bf.at(${obj}, ${idx})`
57
+ }
58
+ case 'concat': {
59
+ if (args.length === 0) {
60
+ return emit(object)
61
+ }
62
+ const a = emit(object)
63
+ const b = emit(args[0])
64
+ return `bf.concat(${a}, ${b})`
65
+ }
66
+ case 'slice': {
67
+ const recv = emit(object)
68
+ const start = args.length >= 1 ? emit(args[0]) : '0'
69
+ // Jinja's undefined-literal is `none`, not Kolon's `nil` — the runtime
70
+ // `slice` treats it as "to end".
71
+ const end = args.length >= 2 ? emit(args[1]) : 'none'
72
+ return `bf.slice(${recv}, ${start}, ${end})`
73
+ }
74
+ case 'reverse':
75
+ case 'toReversed': {
76
+ const recv = emit(object)
77
+ return `bf.reverse(${recv})`
78
+ }
79
+ case 'toLowerCase': {
80
+ // Route through the runtime (consistent with bf.includes / bf.slice /
81
+ // etc.) rather than a bare `.lower()` filter, so a non-string operand
82
+ // still gets JS-compatible coercion first.
83
+ const recv = emit(object)
84
+ return `bf.lc(${recv})`
85
+ }
86
+ case 'toUpperCase': {
87
+ const recv = emit(object)
88
+ return `bf.uc(${recv})`
89
+ }
90
+ case 'trim': {
91
+ const recv = emit(object)
92
+ return `bf.trim(${recv})`
93
+ }
94
+ case 'toFixed': {
95
+ // `.toFixed(digits?)` — `bf.to_fixed` mirrors JS rounding +
96
+ // zero-padding (default 0 digits). #1897.
97
+ const recv = emit(object)
98
+ const digits = args.length >= 1 ? emit(args[0]) : '0'
99
+ return `bf.to_fixed(${recv}, ${digits})`
100
+ }
101
+ case 'split': {
102
+ const recv = emit(object)
103
+ if (args.length === 0) {
104
+ return `bf.split(${recv})`
105
+ }
106
+ const sep = emit(args[0])
107
+ if (args.length === 1) {
108
+ return `bf.split(${recv}, ${sep})`
109
+ }
110
+ const limit = emit(args[1])
111
+ return `bf.split(${recv}, ${sep}, ${limit})`
112
+ }
113
+ case 'startsWith':
114
+ case 'endsWith': {
115
+ const fn = method === 'startsWith' ? 'starts_with' : 'ends_with'
116
+ const recv = emit(object)
117
+ const arg = emit(args[0])
118
+ if (args.length >= 2) {
119
+ return `bf.${fn}(${recv}, ${arg}, ${emit(args[1])})`
120
+ }
121
+ return `bf.${fn}(${recv}, ${arg})`
122
+ }
123
+ case 'replace': {
124
+ const recv = emit(object)
125
+ const oldS = emit(args[0])
126
+ const newS = emit(args[1])
127
+ return `bf.replace(${recv}, ${oldS}, ${newS})`
128
+ }
129
+ case 'repeat': {
130
+ const recv = emit(object)
131
+ const count = args.length === 0 ? '0' : emit(args[0])
132
+ return `bf.repeat(${recv}, ${count})`
133
+ }
134
+ case 'padStart':
135
+ case 'padEnd': {
136
+ const fn = method === 'padStart' ? 'pad_start' : 'pad_end'
137
+ const recv = emit(object)
138
+ if (args.length === 0) {
139
+ return `bf.${fn}(${recv}, 0)`
140
+ }
141
+ const target = emit(args[0])
142
+ if (args.length === 1) {
143
+ return `bf.${fn}(${recv}, ${target})`
144
+ }
145
+ const pad = emit(args[1])
146
+ return `bf.${fn}(${recv}, ${target}, ${pad})`
147
+ }
148
+ default: {
149
+ // TS-level exhaustiveness guard.
150
+ const _exhaustive: never = method
151
+ throw new Error(
152
+ `renderArrayMethod: unhandled ArrayMethod '${(_exhaustive as string)}'`,
153
+ )
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Build the `base_env` dict argument for an evaluator call: the
160
+ * comparator / reducer body's free variables (body idents minus the
161
+ * callback params), each materialised to its SSR value via `emit`. An
162
+ * empty capture set yields `{}` — the runtime's seed-once env.
163
+ */
164
+ function emitEvalEnvArg(
165
+ body: ParsedExpr,
166
+ params: string[],
167
+ emit: (e: ParsedExpr) => string,
168
+ ): string {
169
+ const free = freeVarsInBody(body, new Set(params))
170
+ if (free.length === 0) return '{}'
171
+ const pairs = free.map(
172
+ n => `${jinjaHashKey(n)}: ${emit({ kind: 'identifier', name: n })}`,
173
+ )
174
+ return `{${pairs.join(', ')}}`
175
+ }
176
+
177
+ /**
178
+ * Emit a `.sort(cmp)` / `.toSorted(cmp)` via the runtime evaluator (#2018):
179
+ * the comparator body travels as serialized-ParsedExpr JSON, evaluated per
180
+ * comparison against `{paramA, paramB, …captured}`. Returns null when the
181
+ * body is outside the evaluator surface (e.g. a `localeCompare` comparator —
182
+ * `serializeParsedExpr` refuses it), so the caller falls back to the
183
+ * structured `bf.sort`. `params` are the comparator arrow's two params
184
+ * (`[paramA, paramB]`).
185
+ */
186
+ export function renderSortEval(
187
+ recv: string,
188
+ body: ParsedExpr,
189
+ params: string[],
190
+ emit: (e: ParsedExpr) => string,
191
+ ): string | null {
192
+ const json = serializeParsedExpr(body)
193
+ if (json === null) return null
194
+ // A comparator needs both params; a wrong-arity arrow would emit an
195
+ // 'undefined' param name, so fail over to the structured fallback / BF101
196
+ // (mirrors the Go / Xslate guards).
197
+ if (params.length < 2) return null
198
+ const [paramA, paramB] = params
199
+ const env = emitEvalEnvArg(body, params, emit)
200
+ return `bf.sort_eval(${recv}, '${escapeJinjaSingleQuoted(json)}', '${paramA}', '${paramB}', ${env})`
201
+ }
202
+
203
+ /**
204
+ * Emit a `.reduce(fn, init)` / `.reduceRight(fn, init)` via the runtime
205
+ * evaluator (#2018): the reducer body travels as serialized-ParsedExpr JSON,
206
+ * folded over the receiver from `init` in `direction` order. `params` are the
207
+ * reducer arrow's params (`[paramAcc, paramItem]`); `init` is the initial-value
208
+ * `ParsedExpr` from the call's trailing argument. Returns null when the body is
209
+ * outside the evaluator surface, or when `init` is not a literal string/number
210
+ * (→ caller refuses with BF101). A numeric seed passes through as a bare
211
+ * Jinja number; a string seed as a single-quoted literal.
212
+ */
213
+ export function renderReduceEval(
214
+ recv: string,
215
+ body: ParsedExpr,
216
+ params: string[],
217
+ init: ParsedExpr,
218
+ direction: 'left' | 'right',
219
+ emit: (e: ParsedExpr) => string,
220
+ ): string | null {
221
+ const json = serializeParsedExpr(body)
222
+ if (json === null) return null
223
+ if (init.kind !== 'literal') return null
224
+ const initOut =
225
+ init.literalType === 'string'
226
+ ? `'${escapeJinjaSingleQuoted(String(init.value))}'`
227
+ : init.literalType === 'number'
228
+ ? String(init.value)
229
+ : null
230
+ if (initOut === null) return null
231
+ // A reducer needs both the accumulator and element param; refuse a
232
+ // wrong-arity arrow cleanly (→ BF101) rather than emitting an 'undefined'
233
+ // param name (mirrors the Go / Xslate guards).
234
+ if (params.length < 2) return null
235
+ const [paramAcc, paramItem] = params
236
+ const env = emitEvalEnvArg(body, params, emit)
237
+ return `bf.reduce_eval(${recv}, '${escapeJinjaSingleQuoted(json)}', '${paramAcc}', '${paramItem}', ${initOut}, '${direction}', ${env})`
238
+ }
239
+
240
+ /**
241
+ * Emit a higher-order predicate call via the runtime evaluator (#2018, P2):
242
+ * `bf.filter_eval` / `bf.every_eval` / `bf.some_eval` / `bf.find_eval` /
243
+ * `bf.find_index_eval`, carrying the serialized predicate body + captured env
244
+ * dict. Generalizes the lambda lowering to the same JS-faithful evaluator
245
+ * the Go/Xslate adapters use. Returns null when the predicate is outside the
246
+ * evaluator surface (e.g. a method-call predicate — `serializeParsedExpr`
247
+ * refuses it), so the caller falls back to the lambda form. `forward`
248
+ * (find / findIndex family only) selects the search direction — `false` =
249
+ * findLast / findLastIndex.
250
+ */
251
+ export function renderPredicateEval(
252
+ funcName: string,
253
+ recv: string,
254
+ predicate: ParsedExpr,
255
+ param: string,
256
+ emit: (e: ParsedExpr) => string,
257
+ forward?: boolean,
258
+ ): string | null {
259
+ const json = serializeParsedExpr(predicate)
260
+ if (json === null) return null
261
+ const env = emitEvalEnvArg(predicate, [param], emit)
262
+ const fwd = forward === undefined ? '' : `, ${forward ? 'true' : 'false'}`
263
+ return `bf.${funcName}(${recv}, '${escapeJinjaSingleQuoted(json)}', '${param}'${fwd}, ${env})`
264
+ }
265
+
266
+ /**
267
+ * Emit a `.flatMap(proj)` via the runtime evaluator (#2018, P3): the projection
268
+ * body serializes to JSON and `bf.flat_map_eval` projects + flattens one
269
+ * level. `param` is the projection arrow's single param. Returns null when the
270
+ * projection is outside the evaluator surface (→ caller refuses with BF101).
271
+ */
272
+ export function renderFlatMapEval(
273
+ recv: string,
274
+ body: ParsedExpr,
275
+ param: string,
276
+ emit: (e: ParsedExpr) => string,
277
+ ): string | null {
278
+ const json = serializeParsedExpr(body)
279
+ if (json === null) return null
280
+ const env = emitEvalEnvArg(body, [param], emit)
281
+ return `bf.flat_map_eval(${recv}, '${escapeJinjaSingleQuoted(json)}', '${param}', ${env})`
282
+ }
283
+
284
+ /**
285
+ * Emit a value-producing `.map(cb)` via the runtime evaluator (#2073): the
286
+ * projection body serializes to JSON and `bf.map_eval` projects each element,
287
+ * one result per element (no flatten — the JS `.map` contract). Composes
288
+ * through the array-method chain (`.map(cb).join(' ')`). Returns null when
289
+ * the projection is outside the evaluator surface (→ caller refuses with
290
+ * BF101). The JSX-returning `.map` is an IRLoop upstream and never reaches
291
+ * this emit.
292
+ */
293
+ export function renderMapEval(
294
+ recv: string,
295
+ body: ParsedExpr,
296
+ param: string,
297
+ emit: (e: ParsedExpr) => string,
298
+ ): string | null {
299
+ const json = serializeParsedExpr(body)
300
+ if (json === null) return null
301
+ const env = emitEvalEnvArg(body, [param], emit)
302
+ return `bf.map_eval(${recv}, '${escapeJinjaSingleQuoted(json)}', '${param}', ${env})`
303
+ }
304
+
305
+ /**
306
+ * Shared Jinja emit for `.sort(cmp)` / `.toSorted(cmp)`. Used by both the
307
+ * filter-context emitter and the top-level emitter, plus the loop-array
308
+ * wrap in `renderLoop`. The runtime `bf.sort` accepts an opts dict and
309
+ * returns a fresh list.
310
+ */
311
+ export function renderSortMethod(recv: string, c: SortComparator): string {
312
+ const keyDicts = c.keys.map((k) => {
313
+ const keyEntry =
314
+ k.key.kind === 'self'
315
+ ? `'key_kind': 'self'`
316
+ : `'key_kind': 'field', 'key': '${k.key.field}'`
317
+ return `{${keyEntry}, 'compare_type': '${k.type}', 'direction': '${k.direction}'}`
318
+ })
319
+ return `bf.sort(${recv}, {'keys': [${keyDicts.join(', ')}]})`
320
+ }
321
+
322
+ // `.flat(depth?)` → `bf.flat(recv, depth)`.
323
+ export function renderFlatMethod(
324
+ recv: string,
325
+ depth: FlatDepth | { expr: ParsedExpr },
326
+ emit: (e: ParsedExpr) => string,
327
+ ): string {
328
+ if (typeof depth === 'object') {
329
+ // Dynamic depth (#2094): routed to a DISTINCT runtime helper
330
+ // (`bf.flat_dynamic`), not `bf.flat`. `bf.flat`'s literal-depth path
331
+ // bakes `-1` in as a compile-time SENTINEL meaning "the source
332
+ // literally said `Infinity`". A genuinely dynamic depth expression that
333
+ // happens to evaluate to `-1` at render time means the OPPOSITE in real
334
+ // JS (`[1,[2]].flat(-1)` never recurses — same as `.flat(0)`). Since
335
+ // both paths would otherwise hand the same literal-looking argument to
336
+ // one shared function, that function can't tell which case it's in —
337
+ // so `bf.flat_dynamic` coerces the raw expression value via JS
338
+ // `ToIntegerOrInfinity` (truncate toward zero; negative → 0; NaN/non-
339
+ // numeric → 0; +Infinity or a huge finite value → flatten fully) before
340
+ // delegating to the same recursive flatten as `bf.flat` (mirrors the
341
+ // Go adapter's `bf_flat_dynamic` / `coerceFlatDepth`).
342
+ return `bf.flat_dynamic(${recv}, ${emit(depth.expr)})`
343
+ }
344
+ const d = depth === 'infinity' ? -1 : depth
345
+ return `bf.flat(${recv}, ${d})`
346
+ }