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