@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.
- package/README.md +66 -0
- package/dist/adapter/analysis/component-tree.d.ts +26 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +84 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +106 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +143 -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.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189097 -0
- package/dist/adapter/jinja-adapter.d.ts +394 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +21 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +50 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/jinja-naming.d.ts +77 -0
- package/dist/adapter/lib/jinja-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 +81 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +33 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +61 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +28 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189117 -0
- package/dist/conformance-pins.d.ts +12 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189118 -0
- package/package.json +66 -0
- package/python/VERSION +1 -0
- package/python/barefootjs/__init__.py +57 -0
- package/python/barefootjs/backend_jinja.py +125 -0
- package/python/barefootjs/evaluator.py +787 -0
- package/python/barefootjs/runtime.py +1334 -0
- package/python/barefootjs/search_params.py +89 -0
- package/python/pyproject.toml +32 -0
- package/python/tests/__init__.py +0 -0
- package/python/tests/test_eval_vectors.py +88 -0
- package/python/tests/test_evaluator.py +406 -0
- package/python/tests/test_helper_vectors.py +288 -0
- package/python/tests/test_omit.py +62 -0
- package/python/tests/test_props_attr.py +54 -0
- package/python/tests/test_query.py +41 -0
- package/python/tests/test_render.py +102 -0
- package/python/tests/test_render_child.py +96 -0
- package/python/tests/test_search_params.py +50 -0
- package/python/tests/test_spread_attrs.py +86 -0
- package/python/tests/test_template_primitives.py +347 -0
- package/python/tests/vector-divergences.json +42 -0
- package/src/__tests__/jinja-adapter-unit.test.ts +390 -0
- package/src/__tests__/jinja-adapter.test.ts +53 -0
- package/src/__tests__/jinja-counter.test.ts +62 -0
- package/src/__tests__/jinja-query-href.test.ts +99 -0
- package/src/__tests__/jinja-spread-attrs.test.ts +225 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/boolean-result.ts +176 -0
- package/src/adapter/emit-context.ts +118 -0
- package/src/adapter/expr/array-method.ts +346 -0
- package/src/adapter/expr/emitters.ts +608 -0
- package/src/adapter/expr/operand.ts +35 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/jinja-adapter.ts +1747 -0
- package/src/adapter/lib/constants.ts +33 -0
- package/src/adapter/lib/ir-scope.ts +95 -0
- package/src/adapter/lib/jinja-naming.ts +114 -0
- package/src/adapter/lib/types.ts +30 -0
- package/src/adapter/memo/seed.ts +132 -0
- package/src/adapter/props/prop-classes.ts +65 -0
- package/src/adapter/spread/spread-codegen.ts +166 -0
- package/src/adapter/value/parsed-literal.ts +76 -0
- package/src/build.ts +37 -0
- package/src/conformance-pins.ts +101 -0
- package/src/index.ts +9 -0
- package/src/test-render.ts +714 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParsedExpr → Jinja2 emitters for the Jinja template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from `packages/adapter-xslate/src/adapter/expr/emitters.ts`
|
|
5
|
+
* (`XslateFilterEmitter` / `XslateTopLevelEmitter`). Two `ParsedExprEmitter`
|
|
6
|
+
* implementations:
|
|
7
|
+
*
|
|
8
|
+
* - `JinjaFilterEmitter` — filter/predicate context (loop param + local
|
|
9
|
+
* aliases + bare identifier signal fallback); self-contained, reads no
|
|
10
|
+
* adapter state.
|
|
11
|
+
* - `JinjaTopLevelEmitter` — top-level / per-render-var context; depends on
|
|
12
|
+
* the adapter only through the narrow `JinjaEmitContext` seam.
|
|
13
|
+
*
|
|
14
|
+
* Two deliberate divergences from the Kolon port, both documented at their
|
|
15
|
+
* definition site below:
|
|
16
|
+
*
|
|
17
|
+
* 1. **JS-truthy condition wrapping** (`truthyTest`) — Python truthiness
|
|
18
|
+
* diverges from JS on empty containers; Perl doesn't have this problem
|
|
19
|
+
* (a Perl ref is always true), so Kolon never needed this. Every
|
|
20
|
+
* condition-TEST position (`!x`, the left operand of `&&`/`||`, a
|
|
21
|
+
* ternary's test) routes through the shared `bf.truthy(...)` runtime
|
|
22
|
+
* helper unless the operand is structurally already boolean-shaped
|
|
23
|
+
* (`isBooleanResultParsed`). `&&`/`||` still return the ORIGINAL
|
|
24
|
+
* operand VALUE (not a coerced bool) on the taken branch — matching JS
|
|
25
|
+
* `a || b` returning `a` itself, not `true` — only the BRANCH TEST uses
|
|
26
|
+
* `bf.truthy`. Because Jinja's `if/else` ternary has no way to reuse an
|
|
27
|
+
* already-computed test value for both the test and (conditionally) the
|
|
28
|
+
* result, the left operand's rendered text is emitted TWICE (once as
|
|
29
|
+
* the test, once as the value) — safe because every operand reaching
|
|
30
|
+
* this pipeline is a pure, side-effect-free read (a signal/prop lookup
|
|
31
|
+
* or a call into a pure runtime helper), never a stateful expression.
|
|
32
|
+
* 2. **No Jinja lambda for the predicate-callback fallback.** Kolon's
|
|
33
|
+
* `-> $x { … }` lambda is the Xslate top-level emitter's fallback when a
|
|
34
|
+
* `.filter`/`.every`/`.some`/`.find*` predicate can't be serialized to
|
|
35
|
+
* the runtime evaluator's JSON form (e.g. a nested method-call
|
|
36
|
+
* predicate). Jinja has no lambda-expression syntax at all, so there is
|
|
37
|
+
* no equivalent fallback to port. `JinjaTopLevelEmitter` therefore uses
|
|
38
|
+
* ONE mechanism for every higher-order callback: the evaluator-JSON
|
|
39
|
+
* `*_eval` payload (`bf.filter_eval`, `bf.sort_eval`, …). When
|
|
40
|
+
* `serializeParsedExpr` refuses the body, the call surfaces `BF101`
|
|
41
|
+
* instead of silently degrading — `.sort`/`.toSorted` is the one
|
|
42
|
+
* exception, whose non-lambda STRUCTURED fallback (`bf.sort` with a
|
|
43
|
+
* `{keys: […]}` descriptor, no callback body at all) survives the port
|
|
44
|
+
* unchanged since it was never lambda-shaped to begin with.
|
|
45
|
+
* `JinjaFilterEmitter` (the loop `.filter().map()` INLINE predicate,
|
|
46
|
+
* rendered as a plain boolean expression, never a lambda) is otherwise
|
|
47
|
+
* unaffected and still used for that path plus the filter-predicate
|
|
48
|
+
* entry point `_renderJinjaFilterExprPublic`.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
type ParsedExprEmitter,
|
|
53
|
+
type HigherOrderMethod,
|
|
54
|
+
type ArrayMethod,
|
|
55
|
+
type LiteralType,
|
|
56
|
+
type ParsedExpr,
|
|
57
|
+
type ObjectLiteralProperty,
|
|
58
|
+
type FlatDepth,
|
|
59
|
+
type TemplatePart,
|
|
60
|
+
identifierPath,
|
|
61
|
+
matchSearchParamsMethodCall,
|
|
62
|
+
sortComparatorFromArrow,
|
|
63
|
+
} from '@barefootjs/jsx'
|
|
64
|
+
|
|
65
|
+
import type { JinjaEmitContext } from '../emit-context.ts'
|
|
66
|
+
import { JINJA_TEMPLATE_PRIMITIVES } from '../lib/constants.ts'
|
|
67
|
+
import { jinjaIdent, escapeJinjaSingleQuoted } from '../lib/jinja-naming.ts'
|
|
68
|
+
import { isBooleanResultParsed } from '../boolean-result.ts'
|
|
69
|
+
import {
|
|
70
|
+
renderArrayMethod,
|
|
71
|
+
renderSortMethod,
|
|
72
|
+
renderSortEval,
|
|
73
|
+
renderReduceEval,
|
|
74
|
+
renderPredicateEval,
|
|
75
|
+
renderFlatMethod,
|
|
76
|
+
renderFlatMapEval,
|
|
77
|
+
renderMapEval,
|
|
78
|
+
} from './array-method.ts'
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Local shape for the predicate-lowering helper. Mirrors the Kolon port's
|
|
82
|
+
* `PredicateCall`.
|
|
83
|
+
*/
|
|
84
|
+
type PredicateCall = {
|
|
85
|
+
method: HigherOrderMethod
|
|
86
|
+
object: ParsedExpr
|
|
87
|
+
param: string
|
|
88
|
+
predicate: ParsedExpr
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Methods whose callback is a boolean predicate (`<recv>.<m>(x => …)`).
|
|
92
|
+
const PREDICATE_METHODS = new Set<HigherOrderMethod>([
|
|
93
|
+
'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'every', 'some',
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Route a condition-TEST position through `bf.truthy(...)` unless the node
|
|
98
|
+
* is structurally already boolean-shaped. See the file header (divergence
|
|
99
|
+
* 1). Shared by both emitters below and reused by the adapter's top-level
|
|
100
|
+
* `convertConditionToJinja` for IR-level `if` / loop-filter conditions.
|
|
101
|
+
*/
|
|
102
|
+
export function truthyTest(node: ParsedExpr, rendered: string): string {
|
|
103
|
+
return isBooleanResultParsed(node) ? rendered : `bf.truthy(${rendered})`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Lowering for the predicate body of a filter / every / some / find, plus the
|
|
108
|
+
* same shape used by the loop-hoist `.filter().map()` inline condition.
|
|
109
|
+
* Higher-order predicates are emitted using Jinja's own scalar comparison
|
|
110
|
+
* operators.
|
|
111
|
+
*
|
|
112
|
+
* NOTE: Jinja has no `[x for x in … if …]`-as-expression form usable inline
|
|
113
|
+
* here (a comprehension is a value producer, not a boolean test), so a
|
|
114
|
+
* nested higher-order call (`x.tags.filter(...)`, `other.some(...)`) inside
|
|
115
|
+
* a predicate has no faithful scalar lowering here either — same BF101
|
|
116
|
+
* surfacing as Kolon (#2038) instead of silently degrading to the
|
|
117
|
+
* callback's receiver.
|
|
118
|
+
*/
|
|
119
|
+
export class JinjaFilterEmitter implements ParsedExprEmitter {
|
|
120
|
+
constructor(
|
|
121
|
+
private readonly param: string,
|
|
122
|
+
private readonly localVarMap: Map<string, string>,
|
|
123
|
+
private readonly isStringName: (n: string) => boolean = () => false,
|
|
124
|
+
// Records a BF101 for predicate shapes this emitter can only degrade
|
|
125
|
+
// (#2038). Optional so emitter construction stays possible without an
|
|
126
|
+
// adapter; a missing hook keeps the old silent-degrade emit.
|
|
127
|
+
private readonly onUnsupported?: (message: string, reason?: string) => void,
|
|
128
|
+
) {}
|
|
129
|
+
|
|
130
|
+
identifier(name: string): string {
|
|
131
|
+
if (name === this.param) return jinjaIdent(this.param)
|
|
132
|
+
const signal = this.localVarMap.get(name)
|
|
133
|
+
if (signal) return jinjaIdent(signal)
|
|
134
|
+
return jinjaIdent(name)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
138
|
+
if (literalType === 'string') return `'${escapeJinjaSingleQuoted(String(value))}'`
|
|
139
|
+
if (literalType === 'boolean') return value ? 'true' : 'false'
|
|
140
|
+
if (literalType === 'null') return 'none'
|
|
141
|
+
return String(value)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
145
|
+
// `.length` — route through `bf.length` (handles both array element
|
|
146
|
+
// count and string char count, JS-compatibly). Jinja's builtin
|
|
147
|
+
// `|length` filter also faults trying to match JS semantics for every
|
|
148
|
+
// input shape, so route through the runtime uniformly.
|
|
149
|
+
if (property === 'length') {
|
|
150
|
+
return `bf.length(${emit(object)})`
|
|
151
|
+
}
|
|
152
|
+
// Attribute / dict-key access — Jinja `.` resolves both transparently.
|
|
153
|
+
return `${emit(object)}.${property}`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
157
|
+
// Jinja's `[]` postfix is polymorphic (list index or dict key),
|
|
158
|
+
// mirroring JS — no list/dict split is needed. #1897 (data-table's
|
|
159
|
+
// `selected()[index]`).
|
|
160
|
+
return `${emit(object)}[${emit(index)}]`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
164
|
+
// Signal getter calls: filter() → filter
|
|
165
|
+
if (callee.kind === 'identifier' && args.length === 0) {
|
|
166
|
+
return jinjaIdent(callee.name)
|
|
167
|
+
}
|
|
168
|
+
return emit(callee)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
172
|
+
if (op === '!') return `not ${truthyTest(argument, emit(argument))}`
|
|
173
|
+
if (op === '-') return `-${emit(argument)}`
|
|
174
|
+
return emit(argument)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
178
|
+
const l = emit(left)
|
|
179
|
+
const r = emit(right)
|
|
180
|
+
// Jinja's `==` / `!=` are value-equality operators that compare strings
|
|
181
|
+
// and numbers correctly — unlike Perl's numeric `==` (which the Mojo
|
|
182
|
+
// adapter must steer around with `eq`/`ne`). Same reasoning as Kolon.
|
|
183
|
+
const opMap: Record<string, string> = {
|
|
184
|
+
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
185
|
+
'+': '+', '-': '-', '*': '*', '/': '/',
|
|
186
|
+
}
|
|
187
|
+
return `${l} ${opMap[op] ?? op} ${r}`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
191
|
+
const l = emit(left)
|
|
192
|
+
const r = emit(right)
|
|
193
|
+
// See the file header, divergence 1: the branch TEST goes through
|
|
194
|
+
// `bf.truthy`, but the RETURNED value stays the original operand text —
|
|
195
|
+
// `l` appears twice for `&&`/`||` (test + fallback value), safe because
|
|
196
|
+
// every operand here is a pure lookup/call.
|
|
197
|
+
if (op === '&&') return `(${r} if ${truthyTest(left, l)} else ${l})`
|
|
198
|
+
if (op === '||') return `(${l} if ${truthyTest(left, l)} else ${r})`
|
|
199
|
+
// `l is not none` alone is NOT equivalent to JS `??`'s null-or-undefined
|
|
200
|
+
// check: a context var that was never seeded at all (not merely set to
|
|
201
|
+
// `None`) resolves to Jinja's `ChainableUndefined` sentinel under
|
|
202
|
+
// `undefined=ChainableUndefined`, and `Undefined is not none` is TRUE
|
|
203
|
+
// (it's a distinct object, not the `None` singleton) — so the coalesce
|
|
204
|
+
// would silently return the Undefined sentinel itself instead of `r`.
|
|
205
|
+
// Guard with `is defined` too, mirroring the same pair this file's
|
|
206
|
+
// `jinja-adapter.ts` already uses for nullable-optional-prop attribute
|
|
207
|
+
// omission (see that file's header, divergence 5, and the `is defined
|
|
208
|
+
// and is not none` call sites).
|
|
209
|
+
return `(${l} if (${l} is defined and ${l} is not none) else ${r})`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
callbackMethod(
|
|
213
|
+
method: string,
|
|
214
|
+
object: ParsedExpr,
|
|
215
|
+
_arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
|
|
216
|
+
_restArgs: ParsedExpr[],
|
|
217
|
+
emit: (e: ParsedExpr) => string,
|
|
218
|
+
): string {
|
|
219
|
+
// A nested callback method inside a filter predicate has no Jinja scalar
|
|
220
|
+
// form. Surface BF101 instead of silently changing predicate semantics
|
|
221
|
+
// (`!other.some(r => …)` collapsing to `!other`).
|
|
222
|
+
this.onUnsupported?.(
|
|
223
|
+
`Filter predicate contains a nested '.${method}(...)' callback, which has no Jinja scalar form`,
|
|
224
|
+
`Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`,
|
|
225
|
+
)
|
|
226
|
+
return emit(object)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
230
|
+
return `[${elements.map(emit).join(', ')}]`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
arrayMethod(
|
|
234
|
+
method: ArrayMethod,
|
|
235
|
+
object: ParsedExpr,
|
|
236
|
+
args: ParsedExpr[],
|
|
237
|
+
emit: (e: ParsedExpr) => string,
|
|
238
|
+
): string {
|
|
239
|
+
return renderArrayMethod(method, object, args, emit)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
flatMethod(
|
|
243
|
+
object: ParsedExpr,
|
|
244
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
245
|
+
emit: (e: ParsedExpr) => string,
|
|
246
|
+
): string {
|
|
247
|
+
return renderFlatMethod(emit(object), depth, emit)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
|
|
251
|
+
return 'true'
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
templateLiteral(_parts: TemplatePart[]): string {
|
|
255
|
+
return 'true'
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
arrow(_params: string[], _body: ParsedExpr): string {
|
|
259
|
+
return 'true'
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
regex(_raw: string): string {
|
|
263
|
+
return 'true'
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
unsupported(_raw: string, _reason: string): string {
|
|
267
|
+
return 'true'
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
objectLiteral(_properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
|
|
271
|
+
// Filter-predicate context: emit the truthy sentinel exactly as
|
|
272
|
+
// `unsupported` does. Object values lower to Jinja dicts in the
|
|
273
|
+
// conditional/attr paths, not through this dispatcher.
|
|
274
|
+
return 'true'
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Lowering for top-level expressions whose identifiers resolve against the
|
|
280
|
+
* Jinja template's per-render context vars (signals, props, locals
|
|
281
|
+
* introduced by `{% set x = … %}`). Differs from the filter emitter mainly
|
|
282
|
+
* in
|
|
283
|
+
* - `conditional` is supported (filter predicates can't return ternaries),
|
|
284
|
+
* - higher-order methods route through `bf.*` array/evaluator helpers,
|
|
285
|
+
* - no lambda fallback exists (see the file header, divergence 2).
|
|
286
|
+
*/
|
|
287
|
+
export class JinjaTopLevelEmitter implements ParsedExprEmitter {
|
|
288
|
+
constructor(private readonly ctx: JinjaEmitContext) {}
|
|
289
|
+
|
|
290
|
+
identifier(name: string): string {
|
|
291
|
+
// `undefined` / `null` nested inside a larger expression tree — Jinja
|
|
292
|
+
// `none` (#1897).
|
|
293
|
+
if (name === 'undefined' || name === 'null') return 'none'
|
|
294
|
+
// Inline a module-scope pure-string const (`const x = 'literal'`) — it
|
|
295
|
+
// never reaches the per-render context, so a bare reference would
|
|
296
|
+
// resolve to Undefined.
|
|
297
|
+
const inlined = this.ctx._resolveModuleStringConst(name)
|
|
298
|
+
if (inlined !== null) return inlined
|
|
299
|
+
// Same for a literal const of any scope (`const totalPages = 5`,
|
|
300
|
+
// #1897 pagination's `Page {currentPage()} of {totalPages}`).
|
|
301
|
+
const literalConst = this.ctx._resolveLiteralConst(name)
|
|
302
|
+
if (literalConst !== null) return literalConst
|
|
303
|
+
return jinjaIdent(name)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
307
|
+
if (literalType === 'string') return `'${escapeJinjaSingleQuoted(String(value))}'`
|
|
308
|
+
if (literalType === 'boolean') return value ? 'true' : 'false'
|
|
309
|
+
if (literalType === 'null') return 'none'
|
|
310
|
+
return String(value)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
314
|
+
// `props.x` flattens to the bare context var the SSR caller binds each
|
|
315
|
+
// prop to (props arrive as individual top-level context entries, not a
|
|
316
|
+
// nested `props` dict).
|
|
317
|
+
if (object.kind === 'identifier' && object.name === 'props') {
|
|
318
|
+
return jinjaIdent(property)
|
|
319
|
+
}
|
|
320
|
+
// Static property access on a module object-literal const
|
|
321
|
+
// (`variantClasses.ghost`, #1897) resolves at compile time — the
|
|
322
|
+
// generic dot lowering below would reference a context var that
|
|
323
|
+
// doesn't exist server-side and silently resolve to Undefined.
|
|
324
|
+
if (object.kind === 'identifier') {
|
|
325
|
+
const staticValue = this.ctx._resolveStaticRecordLiteral(object.name, property)
|
|
326
|
+
if (staticValue !== null) return staticValue
|
|
327
|
+
}
|
|
328
|
+
const obj = emit(object)
|
|
329
|
+
// `.length` → `bf.length` (array count or string char count, JS-compat).
|
|
330
|
+
if (property === 'length') return `bf.length(${obj})`
|
|
331
|
+
// Jinja `.` access works for both dicts and objects.
|
|
332
|
+
return `${obj}.${property}`
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
336
|
+
// Jinja's `[]` postfix is polymorphic (list index or dict key),
|
|
337
|
+
// mirroring JS. #1897 (data-table's `selected()[index]`).
|
|
338
|
+
return `${emit(object)}[${emit(index)}]`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
342
|
+
// Signal getter: count() → count
|
|
343
|
+
if (callee.kind === 'identifier' && args.length === 0) {
|
|
344
|
+
return jinjaIdent(callee.name)
|
|
345
|
+
}
|
|
346
|
+
// Env-signal method call (#1922): `searchParams().get('sort')` is a real
|
|
347
|
+
// method call on the per-request `searchParams` reader object, not the
|
|
348
|
+
// generic dot deref `member` would emit. Matches the local import
|
|
349
|
+
// binding (incl. an alias).
|
|
350
|
+
if (this.ctx._searchParamsLocals.size > 0) {
|
|
351
|
+
const sp = matchSearchParamsMethodCall(callee, args, this.ctx._searchParamsLocals)
|
|
352
|
+
if (sp) {
|
|
353
|
+
return `searchParams.${sp.method}(${sp.args.map(emit).join(', ')})`
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Identifier-path templatePrimitive: `JSON.stringify(x)` / `Math.floor(x)`
|
|
357
|
+
// → `bf.json(x)` / `bf.floor(x)`. Args render recursively through this
|
|
358
|
+
// same emitter. A wrong-arity call records BF101 and returns `''`.
|
|
359
|
+
const path = identifierPath(callee)
|
|
360
|
+
const spec = path ? JINJA_TEMPLATE_PRIMITIVES[path] : undefined
|
|
361
|
+
if (path && spec) {
|
|
362
|
+
if (args.length === spec.arity) {
|
|
363
|
+
return spec.emit(args.map(emit))
|
|
364
|
+
}
|
|
365
|
+
this.ctx._recordExprBF101(
|
|
366
|
+
`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`,
|
|
367
|
+
`Call '${path}' with exactly ${spec.arity} argument(s).`,
|
|
368
|
+
)
|
|
369
|
+
return "''"
|
|
370
|
+
}
|
|
371
|
+
return emit(callee)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
375
|
+
if (op === '!') return `not ${truthyTest(argument, emit(argument))}`
|
|
376
|
+
if (op === '-') return `-${emit(argument)}`
|
|
377
|
+
return emit(argument)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
381
|
+
const l = emit(left)
|
|
382
|
+
const r = emit(right)
|
|
383
|
+
// Jinja's `==` / `!=` handle both strings and numbers (unlike Perl's
|
|
384
|
+
// numeric `==`), so all equality comparisons stay on `==` / `!=`.
|
|
385
|
+
const opMap: Record<string, string> = {
|
|
386
|
+
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
387
|
+
'+': '+', '-': '-', '*': '*',
|
|
388
|
+
}
|
|
389
|
+
return `${l} ${opMap[op] ?? op} ${r}`
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
393
|
+
const l = emit(left)
|
|
394
|
+
const r = emit(right)
|
|
395
|
+
// See the file header, divergence 1.
|
|
396
|
+
if (op === '&&') return `(${r} if ${truthyTest(left, l)} else ${l})`
|
|
397
|
+
if (op === '||') return `(${l} if ${truthyTest(left, l)} else ${r})`
|
|
398
|
+
// See `JinjaFilterEmitter.logical`'s comment above (same file, same
|
|
399
|
+
// fix): guard with `is defined` too, so a context var that was never
|
|
400
|
+
// seeded (Jinja's `ChainableUndefined`, not Python `None`) still
|
|
401
|
+
// coalesces to `r` instead of leaking the Undefined sentinel through.
|
|
402
|
+
return `(${l} if (${l} is defined and ${l} is not none) else ${r})`
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
callbackMethod(
|
|
406
|
+
method: string,
|
|
407
|
+
object: ParsedExpr,
|
|
408
|
+
arrow: Extract<ParsedExpr, { kind: 'arrow' }>,
|
|
409
|
+
restArgs: ParsedExpr[],
|
|
410
|
+
emit: (e: ParsedExpr) => string,
|
|
411
|
+
): string {
|
|
412
|
+
const recv = emit(object)
|
|
413
|
+
const body = arrow.body
|
|
414
|
+
const params = arrow.params
|
|
415
|
+
|
|
416
|
+
// Predicate family (#2018 P2/P5): `filter` / `find*` / `every` / `some`.
|
|
417
|
+
if (PREDICATE_METHODS.has(method as HigherOrderMethod)) {
|
|
418
|
+
return this._emitPredicateCallback(
|
|
419
|
+
{ method: method as HigherOrderMethod, object, param: params[0], predicate: body },
|
|
420
|
+
recv,
|
|
421
|
+
emit,
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// `.sort(cmp)` / `.toSorted(cmp)` (#2018): serialize the comparator body +
|
|
426
|
+
// emit `bf.sort_eval`; fall back to the structured `bf.sort` when the
|
|
427
|
+
// body is outside the evaluator surface (e.g. `localeCompare`). This
|
|
428
|
+
// structured fallback is data (a `{keys: […]}` descriptor), never a
|
|
429
|
+
// lambda, so it ports unchanged — see the file header, divergence 2.
|
|
430
|
+
if (method === 'sort' || method === 'toSorted') {
|
|
431
|
+
const evalForm = renderSortEval(recv, body, params, emit)
|
|
432
|
+
if (evalForm !== null) return evalForm
|
|
433
|
+
const structured = sortComparatorFromArrow(arrow)
|
|
434
|
+
if (structured !== null) return renderSortMethod(recv, structured)
|
|
435
|
+
this.ctx._recordExprBF101(
|
|
436
|
+
`'.${method}(...)' comparator is outside the Jinja adapter's evaluable / structured surface`,
|
|
437
|
+
`Pre-compute the sorted array, or move this position to a '/* @client */' boundary.`,
|
|
438
|
+
)
|
|
439
|
+
return "''"
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// `.reduce(fn, init)` / `.reduceRight(fn, init)` (#2018): serialize the
|
|
443
|
+
// reducer body + emit `bf.reduce_eval`. The init is the trailing arg.
|
|
444
|
+
if (method === 'reduce' || method === 'reduceRight') {
|
|
445
|
+
const direction = method === 'reduceRight' ? 'right' : 'left'
|
|
446
|
+
const init = restArgs[0]
|
|
447
|
+
const evalForm =
|
|
448
|
+
init !== undefined
|
|
449
|
+
? renderReduceEval(recv, body, params, init, direction, emit)
|
|
450
|
+
: null
|
|
451
|
+
if (evalForm !== null) return evalForm
|
|
452
|
+
this.ctx._recordExprBF101(
|
|
453
|
+
`'.${method}(...)' is outside the Jinja adapter's evaluable surface (needs a literal initial value and an evaluable reducer body)`,
|
|
454
|
+
`Pre-compute the reduced value, or move this position to a '/* @client */' boundary.`,
|
|
455
|
+
)
|
|
456
|
+
return "''"
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// `.flatMap(proj)` (#2018 P3): serialize the projection body + emit
|
|
460
|
+
// `bf.flat_map_eval`.
|
|
461
|
+
if (method === 'flatMap') {
|
|
462
|
+
const evalForm = renderFlatMapEval(recv, body, params[0], emit)
|
|
463
|
+
if (evalForm !== null) return evalForm
|
|
464
|
+
this.ctx._recordExprBF101(
|
|
465
|
+
`'.flatMap(...)' projection is outside the Jinja adapter's evaluable surface`,
|
|
466
|
+
`Pre-compute the projected array, or move this position to a '/* @client */' boundary.`,
|
|
467
|
+
)
|
|
468
|
+
return "''"
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Value-producing `.map(cb)` (#2073): serialize the projection body +
|
|
472
|
+
// emit `bf.map_eval`. (The JSX-returning `.map` is an IRLoop upstream.)
|
|
473
|
+
if (method === 'map') {
|
|
474
|
+
const evalForm = renderMapEval(recv, body, params[0], emit)
|
|
475
|
+
if (evalForm !== null) return evalForm
|
|
476
|
+
this.ctx._recordExprBF101(
|
|
477
|
+
`'.map(...)' projection is outside the Jinja adapter's evaluable surface`,
|
|
478
|
+
`Pre-compute the projected array, or move this position to a '/* @client */' boundary.`,
|
|
479
|
+
)
|
|
480
|
+
return "''"
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Unknown callback method (should not arrive — CALLBACK_METHODS is closed).
|
|
484
|
+
void object
|
|
485
|
+
return recv
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Lower a boolean-predicate callback (`filter` / `find*` / `every` /
|
|
490
|
+
* `some`). See the file header, divergence 2: Jinja has no lambda
|
|
491
|
+
* expression, so — unlike Kolon — there is no non-evaluator fallback here.
|
|
492
|
+
* A predicate the evaluator can't model surfaces `BF101`.
|
|
493
|
+
*/
|
|
494
|
+
private _emitPredicateCallback(
|
|
495
|
+
call: PredicateCall,
|
|
496
|
+
arrayExpr: string,
|
|
497
|
+
emit: (e: ParsedExpr) => string,
|
|
498
|
+
): string {
|
|
499
|
+
const { method, param, predicate } = call
|
|
500
|
+
|
|
501
|
+
const evalFn: Record<string, [string, boolean?]> = {
|
|
502
|
+
filter: ['filter_eval'], every: ['every_eval'], some: ['some_eval'],
|
|
503
|
+
find: ['find_eval', true], findLast: ['find_eval', false],
|
|
504
|
+
findIndex: ['find_index_eval', true], findLastIndex: ['find_index_eval', false],
|
|
505
|
+
}
|
|
506
|
+
// `.filter(Boolean)` (identity predicate `_t => _t`) still needs the
|
|
507
|
+
// evaluator path on Jinja (no lambda fallback) — the identity predicate
|
|
508
|
+
// serializes fine (`{"kind":"identifier","name":"_t"}`), so this isn't a
|
|
509
|
+
// special case here the way it is for Kolon's lambda form.
|
|
510
|
+
const spec = evalFn[method]
|
|
511
|
+
if (spec) {
|
|
512
|
+
const evalForm = renderPredicateEval(spec[0], arrayExpr, predicate, param, emit, spec[1])
|
|
513
|
+
if (evalForm !== null) return evalForm
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
this.ctx._recordExprBF101(
|
|
517
|
+
`'.${method}(...)' predicate is outside the Jinja adapter's evaluable surface — Jinja has no lambda-expression form to fall back to`,
|
|
518
|
+
`Rewrite the predicate as a pure expression the evaluator can serialize (no nested method-call callbacks), or move this position to a '/* @client */' boundary.`,
|
|
519
|
+
)
|
|
520
|
+
return "''"
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
524
|
+
return `[${elements.map(emit).join(', ')}]`
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
arrayMethod(
|
|
528
|
+
method: ArrayMethod,
|
|
529
|
+
object: ParsedExpr,
|
|
530
|
+
args: ParsedExpr[],
|
|
531
|
+
emit: (e: ParsedExpr) => string,
|
|
532
|
+
): string {
|
|
533
|
+
return renderArrayMethod(method, object, args, emit)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
flatMethod(
|
|
537
|
+
object: ParsedExpr,
|
|
538
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
539
|
+
emit: (e: ParsedExpr) => string,
|
|
540
|
+
): string {
|
|
541
|
+
return renderFlatMethod(emit(object), depth, emit)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
conditional(
|
|
545
|
+
test: ParsedExpr,
|
|
546
|
+
consequent: ParsedExpr,
|
|
547
|
+
alternate: ParsedExpr,
|
|
548
|
+
emit: (e: ParsedExpr) => string,
|
|
549
|
+
): string {
|
|
550
|
+
return `(${emit(consequent)} if ${truthyTest(test, emit(test))} else ${emit(alternate)})`
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
templateLiteral(parts: TemplatePart[], emit: (e: ParsedExpr) => string): string {
|
|
554
|
+
// `` `n=${count() + 1}` `` → Jinja string concatenation (`~`):
|
|
555
|
+
// `'n=' ~ bf.string(count + 1)`. Every interpolated (non-string-literal)
|
|
556
|
+
// segment routes through `bf.string(...)` before concatenation — Jinja's
|
|
557
|
+
// `~` stringifies each operand with Python `str()`, which diverges from
|
|
558
|
+
// JS `String(x)` for floats/booleans/None (see `jinja-adapter.ts`'s file
|
|
559
|
+
// header, "Stringification").
|
|
560
|
+
const terms: string[] = []
|
|
561
|
+
for (const part of parts) {
|
|
562
|
+
if (part.type === 'string') {
|
|
563
|
+
if (part.value !== '') {
|
|
564
|
+
terms.push(`'${escapeJinjaSingleQuoted(part.value)}'`)
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
const rendered = emit(part.expr)
|
|
568
|
+
const needsParens =
|
|
569
|
+
part.expr.kind === 'binary' ||
|
|
570
|
+
part.expr.kind === 'logical' ||
|
|
571
|
+
part.expr.kind === 'conditional'
|
|
572
|
+
terms.push(`bf.string(${needsParens ? `(${rendered})` : rendered})`)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (terms.length === 0) return `''`
|
|
576
|
+
return terms.join(' ~ ')
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
arrow(_params: string[], _body: ParsedExpr): string {
|
|
580
|
+
// A bare arrow never stands alone at a render position (it's only
|
|
581
|
+
// meaningful as a callback, handled by `callbackMethod`). Emit the safe
|
|
582
|
+
// empty-string literal so a stray emit can't produce a Jinja syntax
|
|
583
|
+
// error.
|
|
584
|
+
return "''"
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
regex(_raw: string): string {
|
|
588
|
+
// A bare regex literal has no template-render form — mirror `unsupported`.
|
|
589
|
+
return "''"
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
unsupported(_raw: string, _reason: string): string {
|
|
593
|
+
return "''"
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
objectLiteral(properties: ObjectLiteralProperty[], _raw: string, _emit: (e: ParsedExpr) => string): string {
|
|
597
|
+
// The shared `isSupported` gate only ever lets this dispatcher see an
|
|
598
|
+
// object literal as the EMPTY (`?? {}`) fallback operand of `??`
|
|
599
|
+
// (expression-parser.ts, `logical` case) — any other object literal is
|
|
600
|
+
// refused before reaching here. Emit Jinja's real empty dict literal,
|
|
601
|
+
// matching the `'{}'` convention `objectLiteralToJinjaDict` already uses
|
|
602
|
+
// for the zero-property case in the spread path. A populated literal is
|
|
603
|
+
// structurally unreachable given the gate, but still degrades safely to
|
|
604
|
+
// the pre-existing empty-string sentinel rather than silently dropping
|
|
605
|
+
// keys.
|
|
606
|
+
return properties.length === 0 ? '{}' : "''"
|
|
607
|
+
}
|
|
608
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operand-type classification for the Jinja2 template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from `packages/adapter-xslate/src/adapter/expr/operand.ts`. Pure
|
|
5
|
+
* function over `ParsedExpr` taking an `isStringName` predicate rather than
|
|
6
|
+
* reading adapter instance state.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: like Kolon's `==`/`!=`, Jinja/Python's `==`/`!=` are value-equality
|
|
9
|
+
* operators that compare strings and numbers correctly, so the Jinja
|
|
10
|
+
* emitters never need to steer string comparisons onto a different operator
|
|
11
|
+
* family. This helper is therefore not consumed by the Jinja lowering
|
|
12
|
+
* today either — kept only as the parallel of the Xslate/Mojo adapters'
|
|
13
|
+
* `expr/operand.ts` (groundwork for a future shared codegen surface).
|
|
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. Neither the Kolon nor the Jinja emitters consume it —
|
|
22
|
+
* both languages' `==`/`!=` compare strings and numbers correctly, so
|
|
23
|
+
* `===`/`!==` always map to `==`/`!=`. Kept only as the parallel of the
|
|
24
|
+
* Xslate/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
|
+
}
|