@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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural classifier for JS expressions whose result is a boolean value
|
|
3
|
+
* (or unambiguously stringifies to "true"/"false" in JS).
|
|
4
|
+
*
|
|
5
|
+
* Ported from `packages/adapter-jinja/src/adapter/boolean-result.ts`
|
|
6
|
+
* (itself ported from the Xslate/Mojo adapters' `bf->bool_str` classifier).
|
|
7
|
+
* Used by the Blade adapter for TWO purposes:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Attribute/text stringification**: route a boolean-shaped reactive
|
|
10
|
+
* binding through the runtime `bf.bool_str` helper so the serialised
|
|
11
|
+
* value matches JS `String(boolean)` ("true"/"false"). PHP has a real
|
|
12
|
+
* `bool` type, but PHP's own `(string) true` == `"1"` (not `"true"`) and
|
|
13
|
+
* `(string) false` == `""` — still wrong for HTML output — so the same
|
|
14
|
+
* explicit routing is required.
|
|
15
|
+
* 2. **Condition-position truthy wrapping**: PHP truthiness diverges from JS
|
|
16
|
+
* specifically on the string `'0'` (falsy in PHP, truthy in JS) and empty
|
|
17
|
+
* arrays (`[]` — JS objects/arrays are unconditionally truthy; a PHP
|
|
18
|
+
* empty array is falsy). The Blade adapter's condition-emission call sites
|
|
19
|
+
* (see `blade-adapter.ts`'s `convertConditionToBlade`) reuse this SAME
|
|
20
|
+
* structural classifier: a condition that is already unambiguously
|
|
21
|
+
* boolean-shaped emits directly; everything else is wrapped in
|
|
22
|
+
* `bf.truthy(...)` (a JS-faithful `ToBoolean`) before being used as an
|
|
23
|
+
* `{% if %}` / ternary test.
|
|
24
|
+
*
|
|
25
|
+
* The classifier walks a `ParsedExpr` produced by
|
|
26
|
+
* `@barefootjs/jsx::parseExpression` — same AST the filter / loop lowerings
|
|
27
|
+
* already use — so detection is structural rather than regex-text-matching.
|
|
28
|
+
* Wrapped expression text is left to the caller's existing
|
|
29
|
+
* `convertExpressionToBlade` pipeline; this module only decides whether to
|
|
30
|
+
* wrap.
|
|
31
|
+
*
|
|
32
|
+
* Detected shapes:
|
|
33
|
+
* - `binary` with a comparison operator (`<`, `>`, `<=`, `>=`, `==`, `===`,
|
|
34
|
+
* `!=`, `!==`)
|
|
35
|
+
* - `unary` with logical `!`
|
|
36
|
+
* - `literal` with `literalType: 'boolean'`
|
|
37
|
+
* - `logical` (`&&` / `||` / `??`) when both sides are themselves
|
|
38
|
+
* boolean-result (catches `x > 0 && y < 10`; intentionally does NOT
|
|
39
|
+
* catch `x() || 'fallback'` whose right side stringifies as a regular
|
|
40
|
+
* value)
|
|
41
|
+
* - `conditional` (`?:`) when both branches are themselves boolean-result
|
|
42
|
+
*
|
|
43
|
+
* Anything else returns `false` — including bare identifiers (`accepted`)
|
|
44
|
+
* and call expressions (`accepted()`) whose return type the adapter has no
|
|
45
|
+
* way to infer from source text alone.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { parseExpression, type ParsedExpr } from '@barefootjs/jsx'
|
|
49
|
+
|
|
50
|
+
const COMPARISON_OPS = new Set([
|
|
51
|
+
'<',
|
|
52
|
+
'>',
|
|
53
|
+
'<=',
|
|
54
|
+
'>=',
|
|
55
|
+
'==',
|
|
56
|
+
'===',
|
|
57
|
+
'!=',
|
|
58
|
+
'!==',
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Structural boolean-result check over an already-parsed `ParsedExpr` tree.
|
|
63
|
+
* Exported so the condition-position truthy-wrapping call sites can reuse it
|
|
64
|
+
* without a stringify → re-parse round-trip.
|
|
65
|
+
*/
|
|
66
|
+
export function isBooleanResultParsed(node: ParsedExpr): boolean {
|
|
67
|
+
switch (node.kind) {
|
|
68
|
+
case 'literal':
|
|
69
|
+
return node.literalType === 'boolean'
|
|
70
|
+
case 'binary':
|
|
71
|
+
return COMPARISON_OPS.has(node.op)
|
|
72
|
+
case 'unary':
|
|
73
|
+
return node.op === '!'
|
|
74
|
+
case 'logical':
|
|
75
|
+
// `x > 0 && y < 10` is boolean; `x() || 'fallback'` is not.
|
|
76
|
+
// Only both-sides-boolean qualifies.
|
|
77
|
+
return (
|
|
78
|
+
isBooleanResultParsed(node.left) && isBooleanResultParsed(node.right)
|
|
79
|
+
)
|
|
80
|
+
case 'conditional':
|
|
81
|
+
// `cond ? bool : bool` is boolean; `cond ? 'a' : 'b'` is not.
|
|
82
|
+
return (
|
|
83
|
+
isBooleanResultParsed(node.consequent) &&
|
|
84
|
+
isBooleanResultParsed(node.alternate)
|
|
85
|
+
)
|
|
86
|
+
default:
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isBooleanResultExpr(expr: string): boolean {
|
|
92
|
+
const parsed = parseExpression(expr.trim())
|
|
93
|
+
if (!parsed) return false
|
|
94
|
+
return isBooleanResultParsed(parsed)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* True when `expr`'s top-level shape is an explicit JS `String(x)` call
|
|
99
|
+
* (the `EVAL_BUILTIN_IDENTS` builtin the compiler recognizes structurally —
|
|
100
|
+
* `packages/jsx/src/expression-parser.ts`'s `EVAL_BUILTIN_IDENTS`; lowered
|
|
101
|
+
* by this adapter's `String` template primitive to `bf.string(x)`, see
|
|
102
|
+
* `lib/constants.ts`).
|
|
103
|
+
*
|
|
104
|
+
* Guards the `isAriaBooleanAttr`-driven `bf.bool_str(...)` override in
|
|
105
|
+
* `blade-adapter.ts`'s `elementAttrEmitter`: `bf.string` and `bf.bool_str`
|
|
106
|
+
* produce IDENTICAL text for a real PHP `bool` (both are `"true"` /
|
|
107
|
+
* `"false"`), so applying `bf.bool_str` to `String(x)`'s ALREADY-STRINGIFIED
|
|
108
|
+
* result is not a no-op — it is a PHP-truthiness test over that STRING
|
|
109
|
+
* ("false" is a non-empty PHP string, hence truthy, so
|
|
110
|
+
* `bf.bool_str(bf.string(false))` would wrongly render `"true"`). An author
|
|
111
|
+
* who explicitly writes `String(...)` has already opted into JS `String()`
|
|
112
|
+
* semantics — `bf.string(x)` alone (which DOES special-case booleans, see
|
|
113
|
+
* the PHP runtime's `string()` helper) is the complete, correct lowering; no
|
|
114
|
+
* attribute-name-driven override should run again on top of it.
|
|
115
|
+
*/
|
|
116
|
+
export function isExplicitStringCall(expr: string): boolean {
|
|
117
|
+
const parsed = parseExpression(expr.trim())
|
|
118
|
+
return (
|
|
119
|
+
!!parsed &&
|
|
120
|
+
parsed.kind === 'call' &&
|
|
121
|
+
parsed.callee.kind === 'identifier' &&
|
|
122
|
+
parsed.callee.name === 'String' &&
|
|
123
|
+
parsed.args.length === 1
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* ARIA attributes whose spec values are `"true"`, `"false"`, and (for
|
|
129
|
+
* tri-state members) `"mixed"`. When a fixture binds one of these to an
|
|
130
|
+
* arbitrary JS expression (`aria-checked={accepted()}`), the expression's
|
|
131
|
+
* actual type isn't recoverable from source text — but the attribute name
|
|
132
|
+
* itself witnesses that the binding is boolean-shaped. Routing these through
|
|
133
|
+
* `bf.bool_str` produces the spec-canonical `"true"` / `"false"` even when
|
|
134
|
+
* the expression is opaque.
|
|
135
|
+
*
|
|
136
|
+
* Deliberately conservative — only includes ARIA attributes whose spec value
|
|
137
|
+
* set is exactly `true | false` or `true | false | mixed`. Tokenised ARIA
|
|
138
|
+
* attributes (`aria-current` is `page | step | …`, `aria-sort` is
|
|
139
|
+
* `ascending | descending | …`) are intentionally excluded so a
|
|
140
|
+
* string-valued binding doesn't get coerced to `"true"` / `"false"`.
|
|
141
|
+
*/
|
|
142
|
+
const ARIA_BOOLEAN_ATTRS = new Set([
|
|
143
|
+
// Strict boolean state (true | false; some allow `undefined` = attribute
|
|
144
|
+
// absent, which the runtime emits as no-attr regardless).
|
|
145
|
+
'aria-atomic',
|
|
146
|
+
'aria-busy',
|
|
147
|
+
'aria-disabled',
|
|
148
|
+
'aria-hidden',
|
|
149
|
+
'aria-modal',
|
|
150
|
+
'aria-multiline',
|
|
151
|
+
'aria-multiselectable',
|
|
152
|
+
'aria-readonly',
|
|
153
|
+
'aria-required',
|
|
154
|
+
// true | false | undefined (absent) — selection / disclosure state.
|
|
155
|
+
'aria-selected',
|
|
156
|
+
'aria-expanded',
|
|
157
|
+
// Tri-state (true | false | mixed). The `bool_str` helper only maps
|
|
158
|
+
// truthy / falsy to true / false — a fixture that wants the literal
|
|
159
|
+
// "mixed" would bind a string-valued JSX attr (`aria-checked="mixed"`),
|
|
160
|
+
// which lowers through the `literal` emit path and never touches this
|
|
161
|
+
// code.
|
|
162
|
+
'aria-checked',
|
|
163
|
+
'aria-pressed',
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
export function isAriaBooleanAttr(name: string): boolean {
|
|
167
|
+
return ARIA_BOOLEAN_ATTRS.has(name)
|
|
168
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract the extracted expression-emitter modules depend on instead of
|
|
3
|
+
* the concrete `BladeAdapter`.
|
|
4
|
+
*
|
|
5
|
+
* Ported from `packages/adapter-jinja/src/adapter/emit-context.ts`. The Blade
|
|
6
|
+
* adapter's top-level expression lowering is mutually recursive with the
|
|
7
|
+
* adapter's own const/record resolution and its filter-predicate emitter, so
|
|
8
|
+
* the extracted `BladeTopLevelEmitter` still needs to call back into shared
|
|
9
|
+
* per-compile state and recursive entry points. `BladeEmitContext` is that
|
|
10
|
+
* seam: the emitter takes a `BladeEmitContext` built by the adapter's private
|
|
11
|
+
* `emitCtx` getter (the adapter does NOT `implements` this interface, so the
|
|
12
|
+
* wrapped members stay private and off its exported public type). The
|
|
13
|
+
* emitter depends on this narrow interface rather than the full class, so
|
|
14
|
+
* the coupling is explicit and it's unit-testable against a stub.
|
|
15
|
+
*
|
|
16
|
+
* Keep this surface minimal: add a member only when an extracted module
|
|
17
|
+
* genuinely needs it, so the seam documents the real cross-module coupling
|
|
18
|
+
* rather than re-exposing the whole adapter.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ParsedExpr, CompilerError, IRMetadata } from '@barefootjs/jsx'
|
|
22
|
+
|
|
23
|
+
export interface BladeEmitContext {
|
|
24
|
+
/**
|
|
25
|
+
* (#1922) Local binding names the request-scoped `searchParams()` env signal
|
|
26
|
+
* is imported under. Non-empty enables the env-signal method-call lowering.
|
|
27
|
+
*/
|
|
28
|
+
readonly _searchParamsLocals: Set<string>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Inline a module-scope pure string-literal const by name as the resolved
|
|
32
|
+
* literal value, or null when the name is not such a const.
|
|
33
|
+
*/
|
|
34
|
+
_resolveModuleStringConst(name: string): string | null
|
|
35
|
+
|
|
36
|
+
/** Resolve a literal const (`const totalPages = 5`) to its Blade value, or null. */
|
|
37
|
+
_resolveLiteralConst(name: string): string | null
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a static property access on a module object-literal const
|
|
41
|
+
* (`variantClasses.ghost`) to its Blade value at compile time, or null.
|
|
42
|
+
*/
|
|
43
|
+
_resolveStaticRecordLiteral(objectName: string, key: string): string | null
|
|
44
|
+
|
|
45
|
+
/** Record a BF101 unsupported-expression diagnostic. */
|
|
46
|
+
_recordExprBF101(message: string, reason?: string): void
|
|
47
|
+
|
|
48
|
+
/** Lower a filter/predicate body to its Blade form, bound to `param`. */
|
|
49
|
+
_renderBladeFilterExprPublic(expr: ParsedExpr, param: string): string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The contract the extracted object-literal / conditional-spread lowering
|
|
54
|
+
* (`spread/spread-codegen.ts`) depends on. Declared separately from
|
|
55
|
+
* `BladeEmitContext` so each extracted module's real coupling is documented
|
|
56
|
+
* precisely. Mirror of the Jinja adapter's `JinjaSpreadContext`.
|
|
57
|
+
*/
|
|
58
|
+
export interface BladeSpreadContext {
|
|
59
|
+
/** Component name, for diagnostic source locations. */
|
|
60
|
+
readonly componentName: string
|
|
61
|
+
|
|
62
|
+
/** Per-compile diagnostic list the spread lowering appends to. */
|
|
63
|
+
readonly errors: CompilerError[]
|
|
64
|
+
|
|
65
|
+
/** Local-constant metadata, for resolving `Record[key]` spread values. */
|
|
66
|
+
readonly localConstants: IRMetadata['localConstants']
|
|
67
|
+
|
|
68
|
+
/** Prop params, for classifying a bare-identifier index as a prop. */
|
|
69
|
+
readonly propsParams: { name: string }[]
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Lower a JS expression to its Blade form (the core recursive entry).
|
|
73
|
+
*
|
|
74
|
+
* When the IR already carries a structured `ParsedExpr` tree, pass it as
|
|
75
|
+
* `preParsed` so the converter threads it straight through instead of
|
|
76
|
+
* re-parsing `expr`. With `preParsed` set, `expr` is unused for parsing
|
|
77
|
+
* (the converter derives any diagnostic text from the tree), so callers
|
|
78
|
+
* may pass `''`.
|
|
79
|
+
*/
|
|
80
|
+
convertExpressionToBlade(expr: string, preParsed?: ParsedExpr): string
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Lower a JS expression to a Blade CONDITION (routes through `bf.truthy`
|
|
84
|
+
* unless the expression is structurally already boolean-shaped — see
|
|
85
|
+
* `boolean-result.ts`). Used for the conditional-spread ternary's test,
|
|
86
|
+
* which is a condition position, not a value position. Same `preParsed`
|
|
87
|
+
* contract as `convertExpressionToBlade`.
|
|
88
|
+
*/
|
|
89
|
+
convertConditionToBlade(expr: string, preParsed?: ParsedExpr): string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The contract the extracted in-template memo / context seeding
|
|
94
|
+
* (`memo/seed.ts`) depends on. The seed lowering recurses into the core
|
|
95
|
+
* expression lowering to compute a derived signal/memo value or a context
|
|
96
|
+
* default; that recursive entry is its only adapter coupling.
|
|
97
|
+
*/
|
|
98
|
+
export interface BladeMemoContext {
|
|
99
|
+
/**
|
|
100
|
+
* Lower a JS expression to its Blade form (the core recursive entry). See
|
|
101
|
+
* `BladeSpreadContext.convertExpressionToBlade` for the `preParsed` contract.
|
|
102
|
+
*/
|
|
103
|
+
convertExpressionToBlade(expr: string, preParsed?: ParsedExpr): string
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Per-compile diagnostic list `convertExpressionToBlade` appends to on an
|
|
107
|
+
* unsupported shape (`_recordExprBF101`). `memo/seed.ts`'s
|
|
108
|
+
* `generateDerivedMemoSeed` is a SPECULATIVE "try this in-template
|
|
109
|
+
* recomputation, else fall back to the static ssrDefault seed" attempt per
|
|
110
|
+
* plan step — unlike every other `convertExpressionToBlade` call site, a
|
|
111
|
+
* failure here must NOT become a hard compile error, so it snapshots this
|
|
112
|
+
* array's length before calling in and truncates back to it on failure
|
|
113
|
+
* (discarding whatever `_recordExprBF101` appended) rather than letting
|
|
114
|
+
* the error escape.
|
|
115
|
+
*/
|
|
116
|
+
readonly errors: CompilerError[]
|
|
117
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array / string method lowering for the Blade template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from `packages/adapter-jinja/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 Jinja adapter
|
|
10
|
+
* calls, invoked as `$bf->NAME(...)`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
serializeParsedExpr,
|
|
15
|
+
freeVarsInBody,
|
|
16
|
+
} from '@barefootjs/jsx'
|
|
17
|
+
import type {
|
|
18
|
+
ParsedExpr,
|
|
19
|
+
ArrayMethod,
|
|
20
|
+
SortComparator,
|
|
21
|
+
FlatDepth,
|
|
22
|
+
} from '@barefootjs/jsx'
|
|
23
|
+
import { escapeBladeSingleQuoted, bladeHashKey } from '../lib/blade-naming.ts'
|
|
24
|
+
|
|
25
|
+
export function renderArrayMethod(
|
|
26
|
+
method: ArrayMethod,
|
|
27
|
+
object: ParsedExpr,
|
|
28
|
+
args: ParsedExpr[],
|
|
29
|
+
emit: (e: ParsedExpr) => string,
|
|
30
|
+
): string {
|
|
31
|
+
switch (method) {
|
|
32
|
+
case 'join': {
|
|
33
|
+
// Route through the runtime (`$bf->join`) rather than a Blade builtin, so
|
|
34
|
+
// the JS-compat element handling (undef → empty, default separator) is
|
|
35
|
+
// applied consistently.
|
|
36
|
+
const obj = emit(object)
|
|
37
|
+
const sep = args.length >= 1 ? emit(args[0]) : `','`
|
|
38
|
+
return `$bf->join(${obj}, ${sep})`
|
|
39
|
+
}
|
|
40
|
+
case 'includes': {
|
|
41
|
+
const obj = emit(object)
|
|
42
|
+
const needle = emit(args[0])
|
|
43
|
+
return `$bf->includes(${obj}, ${needle})`
|
|
44
|
+
}
|
|
45
|
+
case 'indexOf':
|
|
46
|
+
case 'lastIndexOf': {
|
|
47
|
+
const fn = method === 'indexOf' ? 'index_of' : 'last_index_of'
|
|
48
|
+
const obj = emit(object)
|
|
49
|
+
const needle = emit(args[0])
|
|
50
|
+
return `$bf->${fn}(${obj}, ${needle})`
|
|
51
|
+
}
|
|
52
|
+
case 'at': {
|
|
53
|
+
const obj = emit(object)
|
|
54
|
+
const idx = args.length >= 1 ? emit(args[0]) : '0'
|
|
55
|
+
return `$bf->at(${obj}, ${idx})`
|
|
56
|
+
}
|
|
57
|
+
case 'concat': {
|
|
58
|
+
if (args.length === 0) {
|
|
59
|
+
return emit(object)
|
|
60
|
+
}
|
|
61
|
+
const a = emit(object)
|
|
62
|
+
const b = emit(args[0])
|
|
63
|
+
return `$bf->concat(${a}, ${b})`
|
|
64
|
+
}
|
|
65
|
+
case 'slice': {
|
|
66
|
+
const recv = emit(object)
|
|
67
|
+
const start = args.length >= 1 ? emit(args[0]) : '0'
|
|
68
|
+
// Blade's undefined-literal is `null`, not Kolon's `nil` or Jinja's
|
|
69
|
+
// `none` — the runtime `slice` treats it as "to end".
|
|
70
|
+
const end = args.length >= 2 ? emit(args[1]) : 'null'
|
|
71
|
+
return `$bf->slice(${recv}, ${start}, ${end})`
|
|
72
|
+
}
|
|
73
|
+
case 'reverse':
|
|
74
|
+
case 'toReversed': {
|
|
75
|
+
const recv = emit(object)
|
|
76
|
+
return `$bf->reverse(${recv})`
|
|
77
|
+
}
|
|
78
|
+
case 'toLowerCase': {
|
|
79
|
+
// Route through the runtime (consistent with bf.includes / bf.slice /
|
|
80
|
+
// etc.) rather than a bare `|lower` filter, so a non-string operand
|
|
81
|
+
// still gets JS-compatible coercion first.
|
|
82
|
+
const recv = emit(object)
|
|
83
|
+
return `$bf->lc(${recv})`
|
|
84
|
+
}
|
|
85
|
+
case 'toUpperCase': {
|
|
86
|
+
const recv = emit(object)
|
|
87
|
+
return `$bf->uc(${recv})`
|
|
88
|
+
}
|
|
89
|
+
case 'trim': {
|
|
90
|
+
const recv = emit(object)
|
|
91
|
+
return `$bf->trim(${recv})`
|
|
92
|
+
}
|
|
93
|
+
case 'toFixed': {
|
|
94
|
+
// `.toFixed(digits?)` — `$bf->to_fixed` mirrors JS rounding +
|
|
95
|
+
// zero-padding (default 0 digits). #1897.
|
|
96
|
+
const recv = emit(object)
|
|
97
|
+
const digits = args.length >= 1 ? emit(args[0]) : '0'
|
|
98
|
+
return `$bf->to_fixed(${recv}, ${digits})`
|
|
99
|
+
}
|
|
100
|
+
case 'split': {
|
|
101
|
+
const recv = emit(object)
|
|
102
|
+
if (args.length === 0) {
|
|
103
|
+
return `$bf->split(${recv})`
|
|
104
|
+
}
|
|
105
|
+
const sep = emit(args[0])
|
|
106
|
+
if (args.length === 1) {
|
|
107
|
+
return `$bf->split(${recv}, ${sep})`
|
|
108
|
+
}
|
|
109
|
+
const limit = emit(args[1])
|
|
110
|
+
return `$bf->split(${recv}, ${sep}, ${limit})`
|
|
111
|
+
}
|
|
112
|
+
case 'startsWith':
|
|
113
|
+
case 'endsWith': {
|
|
114
|
+
const fn = method === 'startsWith' ? 'starts_with' : 'ends_with'
|
|
115
|
+
const recv = emit(object)
|
|
116
|
+
const arg = emit(args[0])
|
|
117
|
+
if (args.length >= 2) {
|
|
118
|
+
return `$bf->${fn}(${recv}, ${arg}, ${emit(args[1])})`
|
|
119
|
+
}
|
|
120
|
+
return `$bf->${fn}(${recv}, ${arg})`
|
|
121
|
+
}
|
|
122
|
+
case 'replace': {
|
|
123
|
+
const recv = emit(object)
|
|
124
|
+
const oldS = emit(args[0])
|
|
125
|
+
const newS = emit(args[1])
|
|
126
|
+
return `$bf->replace(${recv}, ${oldS}, ${newS})`
|
|
127
|
+
}
|
|
128
|
+
case 'repeat': {
|
|
129
|
+
const recv = emit(object)
|
|
130
|
+
const count = args.length === 0 ? '0' : emit(args[0])
|
|
131
|
+
return `$bf->repeat(${recv}, ${count})`
|
|
132
|
+
}
|
|
133
|
+
case 'padStart':
|
|
134
|
+
case 'padEnd': {
|
|
135
|
+
const fn = method === 'padStart' ? 'pad_start' : 'pad_end'
|
|
136
|
+
const recv = emit(object)
|
|
137
|
+
if (args.length === 0) {
|
|
138
|
+
return `$bf->${fn}(${recv}, 0)`
|
|
139
|
+
}
|
|
140
|
+
const target = emit(args[0])
|
|
141
|
+
if (args.length === 1) {
|
|
142
|
+
return `$bf->${fn}(${recv}, ${target})`
|
|
143
|
+
}
|
|
144
|
+
const pad = emit(args[1])
|
|
145
|
+
return `$bf->${fn}(${recv}, ${target}, ${pad})`
|
|
146
|
+
}
|
|
147
|
+
default: {
|
|
148
|
+
// TS-level exhaustiveness guard.
|
|
149
|
+
const _exhaustive: never = method
|
|
150
|
+
throw new Error(
|
|
151
|
+
`renderArrayMethod: unhandled ArrayMethod '${(_exhaustive as string)}'`,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the `base_env` PHP-array argument for an evaluator call: the
|
|
159
|
+
* comparator / reducer body's free variables (body idents minus the
|
|
160
|
+
* callback params), each materialised to its SSR value via `emit`. An
|
|
161
|
+
* empty capture set yields `[]` — the runtime's seed-once env.
|
|
162
|
+
*/
|
|
163
|
+
function emitEvalEnvArg(
|
|
164
|
+
body: ParsedExpr,
|
|
165
|
+
params: string[],
|
|
166
|
+
emit: (e: ParsedExpr) => string,
|
|
167
|
+
): string {
|
|
168
|
+
const free = freeVarsInBody(body, new Set(params))
|
|
169
|
+
if (free.length === 0) return '[]'
|
|
170
|
+
const pairs = free.map(
|
|
171
|
+
n => `${bladeHashKey(n)} => ${emit({ kind: 'identifier', name: n })}`,
|
|
172
|
+
)
|
|
173
|
+
return `[${pairs.join(', ')}]`
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Emit a `.sort(cmp)` / `.toSorted(cmp)` via the runtime evaluator (#2018):
|
|
178
|
+
* the comparator body travels as serialized-ParsedExpr JSON, evaluated per
|
|
179
|
+
* comparison against `{paramA, paramB, …captured}`. Returns null when the
|
|
180
|
+
* body is outside the evaluator surface (e.g. a `localeCompare` comparator —
|
|
181
|
+
* `serializeParsedExpr` refuses it), so the caller falls back to the
|
|
182
|
+
* structured `$bf->sort`. `params` are the comparator arrow's two params
|
|
183
|
+
* (`[paramA, paramB]`).
|
|
184
|
+
*/
|
|
185
|
+
export function renderSortEval(
|
|
186
|
+
recv: string,
|
|
187
|
+
body: ParsedExpr,
|
|
188
|
+
params: string[],
|
|
189
|
+
emit: (e: ParsedExpr) => string,
|
|
190
|
+
): string | null {
|
|
191
|
+
const json = serializeParsedExpr(body)
|
|
192
|
+
if (json === null) return null
|
|
193
|
+
// A comparator needs both params; a wrong-arity arrow would emit an
|
|
194
|
+
// 'undefined' param name, so fail over to the structured fallback / BF101
|
|
195
|
+
// (mirrors the Go / Jinja guards).
|
|
196
|
+
if (params.length < 2) return null
|
|
197
|
+
const [paramA, paramB] = params
|
|
198
|
+
const env = emitEvalEnvArg(body, params, emit)
|
|
199
|
+
return `$bf->sort_eval(${recv}, '${escapeBladeSingleQuoted(json)}', '${paramA}', '${paramB}', ${env})`
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Emit a `.reduce(fn, init)` / `.reduceRight(fn, init)` via the runtime
|
|
204
|
+
* evaluator (#2018): the reducer body travels as serialized-ParsedExpr JSON,
|
|
205
|
+
* folded over the receiver from `init` in `direction` order. `params` are the
|
|
206
|
+
* reducer arrow's params (`[paramAcc, paramItem]`); `init` is the initial-value
|
|
207
|
+
* `ParsedExpr` from the call's trailing argument. Returns null when the body is
|
|
208
|
+
* outside the evaluator surface, or when `init` is not a literal string/number
|
|
209
|
+
* (→ caller refuses with BF101). A numeric seed passes through as a bare
|
|
210
|
+
* Blade number; a string seed as a single-quoted literal.
|
|
211
|
+
*/
|
|
212
|
+
export function renderReduceEval(
|
|
213
|
+
recv: string,
|
|
214
|
+
body: ParsedExpr,
|
|
215
|
+
params: string[],
|
|
216
|
+
init: ParsedExpr,
|
|
217
|
+
direction: 'left' | 'right',
|
|
218
|
+
emit: (e: ParsedExpr) => string,
|
|
219
|
+
): string | null {
|
|
220
|
+
const json = serializeParsedExpr(body)
|
|
221
|
+
if (json === null) return null
|
|
222
|
+
if (init.kind !== 'literal') return null
|
|
223
|
+
const initOut =
|
|
224
|
+
init.literalType === 'string'
|
|
225
|
+
? `'${escapeBladeSingleQuoted(String(init.value))}'`
|
|
226
|
+
: init.literalType === 'number'
|
|
227
|
+
? String(init.value)
|
|
228
|
+
: null
|
|
229
|
+
if (initOut === null) return null
|
|
230
|
+
// A reducer needs both the accumulator and element param; refuse a
|
|
231
|
+
// wrong-arity arrow cleanly (→ BF101) rather than emitting an 'undefined'
|
|
232
|
+
// param name (mirrors the Go / Jinja guards).
|
|
233
|
+
if (params.length < 2) return null
|
|
234
|
+
const [paramAcc, paramItem] = params
|
|
235
|
+
const env = emitEvalEnvArg(body, params, emit)
|
|
236
|
+
return `$bf->reduce_eval(${recv}, '${escapeBladeSingleQuoted(json)}', '${paramAcc}', '${paramItem}', ${initOut}, '${direction}', ${env})`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Emit a higher-order predicate call via the runtime evaluator (#2018, P2):
|
|
241
|
+
* `$bf->filter_eval` / `$bf->every_eval` / `$bf->some_eval` / `$bf->find_eval` /
|
|
242
|
+
* `$bf->find_index_eval`, carrying the serialized predicate body + captured env
|
|
243
|
+
* dict. Generalizes the lambda lowering to the same JS-faithful evaluator
|
|
244
|
+
* the Go/Jinja adapters use. Returns null when the predicate is outside the
|
|
245
|
+
* evaluator surface (e.g. a method-call predicate — `serializeParsedExpr`
|
|
246
|
+
* refuses it), so the caller falls back to the lambda form. `forward`
|
|
247
|
+
* (find / findIndex family only) selects the search direction — `false` =
|
|
248
|
+
* findLast / findLastIndex.
|
|
249
|
+
*/
|
|
250
|
+
export function renderPredicateEval(
|
|
251
|
+
funcName: string,
|
|
252
|
+
recv: string,
|
|
253
|
+
predicate: ParsedExpr,
|
|
254
|
+
param: string,
|
|
255
|
+
emit: (e: ParsedExpr) => string,
|
|
256
|
+
forward?: boolean,
|
|
257
|
+
): string | null {
|
|
258
|
+
const json = serializeParsedExpr(predicate)
|
|
259
|
+
if (json === null) return null
|
|
260
|
+
const env = emitEvalEnvArg(predicate, [param], emit)
|
|
261
|
+
const fwd = forward === undefined ? '' : `, ${forward ? 'true' : 'false'}`
|
|
262
|
+
return `$bf->${funcName}(${recv}, '${escapeBladeSingleQuoted(json)}', '${param}'${fwd}, ${env})`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Emit a `.flatMap(proj)` via the runtime evaluator (#2018, P3): the projection
|
|
267
|
+
* body serializes to JSON and `$bf->flat_map_eval` projects + flattens one
|
|
268
|
+
* level. `param` is the projection arrow's single param. Returns null when the
|
|
269
|
+
* projection is outside the evaluator surface (→ caller refuses with BF101).
|
|
270
|
+
*/
|
|
271
|
+
export function renderFlatMapEval(
|
|
272
|
+
recv: string,
|
|
273
|
+
body: ParsedExpr,
|
|
274
|
+
param: string,
|
|
275
|
+
emit: (e: ParsedExpr) => string,
|
|
276
|
+
): string | null {
|
|
277
|
+
const json = serializeParsedExpr(body)
|
|
278
|
+
if (json === null) return null
|
|
279
|
+
const env = emitEvalEnvArg(body, [param], emit)
|
|
280
|
+
return `$bf->flat_map_eval(${recv}, '${escapeBladeSingleQuoted(json)}', '${param}', ${env})`
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Emit a value-producing `.map(cb)` via the runtime evaluator (#2073): the
|
|
285
|
+
* projection body serializes to JSON and `$bf->map_eval` projects each element,
|
|
286
|
+
* one result per element (no flatten — the JS `.map` contract). Composes
|
|
287
|
+
* through the array-method chain (`.map(cb).join(' ')`). Returns null when
|
|
288
|
+
* the projection is outside the evaluator surface (→ caller refuses with
|
|
289
|
+
* BF101). The JSX-returning `.map` is an IRLoop upstream and never reaches
|
|
290
|
+
* this emit.
|
|
291
|
+
*/
|
|
292
|
+
export function renderMapEval(
|
|
293
|
+
recv: string,
|
|
294
|
+
body: ParsedExpr,
|
|
295
|
+
param: string,
|
|
296
|
+
emit: (e: ParsedExpr) => string,
|
|
297
|
+
): string | null {
|
|
298
|
+
const json = serializeParsedExpr(body)
|
|
299
|
+
if (json === null) return null
|
|
300
|
+
const env = emitEvalEnvArg(body, [param], emit)
|
|
301
|
+
return `$bf->map_eval(${recv}, '${escapeBladeSingleQuoted(json)}', '${param}', ${env})`
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Shared Blade emit for `.sort(cmp)` / `.toSorted(cmp)`. Used by both the
|
|
306
|
+
* filter-context emitter and the top-level emitter, plus the loop-array
|
|
307
|
+
* wrap in `renderLoop`. The runtime `$bf->sort` accepts an opts dict and
|
|
308
|
+
* returns a fresh list.
|
|
309
|
+
*/
|
|
310
|
+
export function renderSortMethod(recv: string, c: SortComparator): string {
|
|
311
|
+
const keyDicts = c.keys.map((k) => {
|
|
312
|
+
const keyEntry =
|
|
313
|
+
k.key.kind === 'self'
|
|
314
|
+
? `'key_kind' => 'self'`
|
|
315
|
+
: `'key_kind' => 'field', 'key' => '${k.key.field}'`
|
|
316
|
+
return `[${keyEntry}, 'compare_type' => '${k.type}', 'direction' => '${k.direction}']`
|
|
317
|
+
})
|
|
318
|
+
return `$bf->sort(${recv}, ['keys' => [${keyDicts.join(', ')}]])`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// `.flat(depth?)` → `$bf->flat(recv, depth)`.
|
|
322
|
+
export function renderFlatMethod(
|
|
323
|
+
recv: string,
|
|
324
|
+
depth: FlatDepth | { expr: ParsedExpr },
|
|
325
|
+
emit: (e: ParsedExpr) => string,
|
|
326
|
+
): string {
|
|
327
|
+
if (typeof depth === 'object') {
|
|
328
|
+
// Dynamic depth (#2094): routed to a distinct runtime helper,
|
|
329
|
+
// `$bf->flat_dynamic`, rather than reusing `$bf->flat`. The literal-depth
|
|
330
|
+
// path (`$bf->flat`) bakes `-1` in as a compile-time SENTINEL meaning "the
|
|
331
|
+
// source literally said `Infinity`". A genuinely dynamic depth value
|
|
332
|
+
// that happens to evaluate to `-1` at render time means the OPPOSITE in
|
|
333
|
+
// real JS (`[1,[2]].flat(-1)` never recurses — same as `.flat(0)`).
|
|
334
|
+
// Since both paths would otherwise hand the same literal-looking `-1`
|
|
335
|
+
// argument to one shared function, that function cannot tell which case
|
|
336
|
+
// it's in — so it must be two different runtime entry points.
|
|
337
|
+
// `$bf->flat_dynamic` performs the JS `ToIntegerOrInfinity` coercion on its
|
|
338
|
+
// second argument at render time (truncate toward zero; negative → 0;
|
|
339
|
+
// NaN/non-numeric → 0; +Infinity or a huge finite value → flatten
|
|
340
|
+
// fully) before delegating to the same recursive flattening as `flat`.
|
|
341
|
+
return `$bf->flat_dynamic(${recv}, ${emit(depth.expr)})`
|
|
342
|
+
}
|
|
343
|
+
const d = depth === 'infinity' ? -1 : depth
|
|
344
|
+
return `$bf->flat(${recv}, ${d})`
|
|
345
|
+
}
|