@barefootjs/xslate 0.16.0 → 0.17.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/dist/adapter/analysis/component-tree.d.ts +30 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +94 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +63 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +97 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +25 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.js +1428 -1326
- package/dist/adapter/lib/constants.d.ts +22 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +34 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/kolon-naming.d.ts +22 -0
- package/dist/adapter/lib/kolon-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +27 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +38 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +32 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +69 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +32 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/adapter/xslate-adapter.d.ts +38 -92
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +1428 -1326
- package/dist/index.js +1428 -1326
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/query-href.test.ts +96 -0
- package/src/__tests__/xslate-adapter.test.ts +55 -0
- package/src/adapter/analysis/component-tree.ts +123 -0
- package/src/adapter/emit-context.ts +104 -0
- package/src/adapter/expr/array-method.ts +311 -0
- package/src/adapter/expr/emitters.ts +530 -0
- package/src/adapter/expr/operand.ts +35 -0
- package/src/adapter/lib/constants.ts +34 -0
- package/src/adapter/lib/ir-scope.ts +64 -0
- package/src/adapter/lib/kolon-naming.ts +27 -0
- package/src/adapter/lib/types.ts +30 -0
- package/src/adapter/memo/seed.ts +125 -0
- package/src/adapter/props/prop-classes.ts +64 -0
- package/src/adapter/spread/spread-codegen.ts +179 -0
- package/src/adapter/value/parsed-literal.ts +80 -0
- package/src/adapter/xslate-adapter.ts +169 -1233
- package/src/test-render.ts +3 -0
|
@@ -46,26 +46,21 @@ import {
|
|
|
46
46
|
type AdapterOutput,
|
|
47
47
|
type AdapterGenerateOptions,
|
|
48
48
|
type TemplateSections,
|
|
49
|
-
type ParsedExprEmitter,
|
|
50
|
-
type HigherOrderMethod,
|
|
51
|
-
type ArrayMethod,
|
|
52
|
-
type LiteralType,
|
|
53
49
|
type IRNodeEmitter,
|
|
54
50
|
type EmitIRNode,
|
|
55
51
|
type AttrValueEmitter,
|
|
56
52
|
isBooleanAttr,
|
|
57
53
|
parseExpression,
|
|
54
|
+
stringifyParsedExpr,
|
|
58
55
|
exprToString,
|
|
59
56
|
parseProviderObjectLiteral,
|
|
60
57
|
parseStyleObjectEntries,
|
|
61
58
|
isSupported,
|
|
62
|
-
identifierPath,
|
|
63
59
|
emitParsedExpr,
|
|
64
60
|
emitIRNode,
|
|
65
61
|
emitAttrValue,
|
|
66
62
|
augmentInheritedPropAccesses,
|
|
67
63
|
parseRecordIndexAccess,
|
|
68
|
-
evalStringArrayJoin,
|
|
69
64
|
collectModuleStringConsts,
|
|
70
65
|
extractArrowBodyExpression,
|
|
71
66
|
collectContextConsumers,
|
|
@@ -73,127 +68,45 @@ import {
|
|
|
73
68
|
type ContextConsumer,
|
|
74
69
|
lookupStaticRecordLiteral,
|
|
75
70
|
searchParamsLocalNames,
|
|
76
|
-
|
|
71
|
+
prepareLoweringMatchers,
|
|
72
|
+
queryHrefArgs,
|
|
73
|
+
sortComparatorFromArrow,
|
|
77
74
|
} from '@barefootjs/jsx'
|
|
78
75
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
79
76
|
import ts from 'typescript'
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Xslate adapter's IRNode render context. Like the Mojo adapter, Kolon's
|
|
83
|
-
* lowering doesn't consume any render-position flags, so the Ctx is empty.
|
|
84
|
-
* Kept as a named alias so future flags can extend it without changing the
|
|
85
|
-
* `IRNodeEmitter` interface.
|
|
86
|
-
*/
|
|
87
|
-
type XslateRenderCtx = Record<string, never>
|
|
88
|
-
import type { ParsedExpr, ParsedStatement, SortComparator, ReduceOp, FlatDepth, FlatMapOp, TemplatePart } from '@barefootjs/jsx'
|
|
77
|
+
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
89
78
|
import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
|
|
90
79
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Object.fromEntries(
|
|
119
|
-
Object.entries(XSLATE_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit])
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Find the `children` prop's `jsx-children` payload. Narrowed via the
|
|
124
|
-
* AttrValue `kind` discriminator so adapter code stays type-safe if the IR
|
|
125
|
-
* shape evolves.
|
|
126
|
-
*/
|
|
127
|
-
/**
|
|
128
|
-
* Escape a string for a Kolon/Perl single-quoted literal: backslash first
|
|
129
|
-
* (so it doesn't double-escape the quote we add next), then the quote. Used
|
|
130
|
-
* by every `'…'` hashref key/value emitter below.
|
|
131
|
-
*/
|
|
132
|
-
function escapeKolonSingleQuoted(s: string): string {
|
|
133
|
-
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Quote a hashref KEY for Kolon when it isn't a bare-identifier-safe name.
|
|
138
|
-
* Kolon parses `data-slot` as `data - slot` (subtraction) and faults on the
|
|
139
|
-
* undefined `data` symbol, so a hyphenated key (`data-slot`, `aria-label`)
|
|
140
|
-
* must be single-quoted: `'data-slot'`. Bare identifiers pass through unquoted.
|
|
141
|
-
*/
|
|
142
|
-
function kolonHashKey(name: string): string {
|
|
143
|
-
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${escapeKolonSingleQuoted(name)}'`
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function resolveJsxChildrenProp(props: readonly IRProp[]): IRNode[] {
|
|
147
|
-
const prop = props.find(p => p.name === 'children')
|
|
148
|
-
if (!prop) return []
|
|
149
|
-
if (prop.value.kind !== 'jsx-children') return []
|
|
150
|
-
return prop.value.children
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Collect the component's root scope element node(s) — the elements that
|
|
155
|
-
* become the rendered root and so carry `data-key` for a keyed loop item. A
|
|
156
|
-
* plain element root is itself; an `if-statement` (early-return) root
|
|
157
|
-
* contributes the top element of each branch, since exactly one renders at
|
|
158
|
-
* runtime. (#1297)
|
|
159
|
-
*/
|
|
160
|
-
function collectRootScopeNodes(node: IRNode): Set<IRNode> {
|
|
161
|
-
const out = new Set<IRNode>()
|
|
162
|
-
const visit = (n: IRNode | null): void => {
|
|
163
|
-
if (!n) return
|
|
164
|
-
if (n.type === 'element') { out.add(n); return }
|
|
165
|
-
if (n.type === 'if-statement') {
|
|
166
|
-
const s = n as IRIfStatement
|
|
167
|
-
visit(s.consequent)
|
|
168
|
-
visit(s.alternate)
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
if (n.type === 'fragment') {
|
|
172
|
-
for (const c of (n as IRFragment).children) visit(c)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
visit(node)
|
|
176
|
-
return out
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* True when every `$var` the lowered Kolon expression references is already in
|
|
181
|
-
* scope — guards in-template memo seeding against an out-of-scope binding. (#1297)
|
|
182
|
-
*/
|
|
183
|
-
function referencedVarsAreAvailable(expr: string, available: ReadonlySet<string>): boolean {
|
|
184
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
185
|
-
if (!available.has(m[1])) return false
|
|
186
|
-
}
|
|
187
|
-
return true
|
|
188
|
-
}
|
|
80
|
+
import type { XslateRenderCtx } from './lib/types.ts'
|
|
81
|
+
import { XSLATE_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
|
|
82
|
+
import { kolonHashKey } from './lib/kolon-naming.ts'
|
|
83
|
+
import {
|
|
84
|
+
resolveJsxChildrenProp,
|
|
85
|
+
collectRootScopeNodes,
|
|
86
|
+
} from './lib/ir-scope.ts'
|
|
87
|
+
import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
|
|
88
|
+
import { XslateFilterEmitter, XslateTopLevelEmitter } from './expr/emitters.ts'
|
|
89
|
+
import type { XslateEmitContext, XslateSpreadContext, XslateMemoContext } from './emit-context.ts'
|
|
90
|
+
import {
|
|
91
|
+
hasClientInteractivity,
|
|
92
|
+
collectImportedLoopChildComponentErrors,
|
|
93
|
+
} from './analysis/component-tree.ts'
|
|
94
|
+
import {
|
|
95
|
+
conditionalSpreadToKolon,
|
|
96
|
+
objectLiteralExprToKolonHashref,
|
|
97
|
+
} from './spread/spread-codegen.ts'
|
|
98
|
+
import {
|
|
99
|
+
generateContextConsumerSeed,
|
|
100
|
+
generateDerivedMemoSeed,
|
|
101
|
+
} from './memo/seed.ts'
|
|
102
|
+
import {
|
|
103
|
+
collectBooleanTypedProps,
|
|
104
|
+
collectNullableOptionalProps,
|
|
105
|
+
collectStringValueNames,
|
|
106
|
+
} from './props/prop-classes.ts'
|
|
189
107
|
|
|
190
|
-
export
|
|
191
|
-
|
|
192
|
-
clientJsBasePath?: string
|
|
193
|
-
|
|
194
|
-
/** Path to barefoot.js runtime (default: '/static/components/barefoot.js') */
|
|
195
|
-
barefootJsPath?: string
|
|
196
|
-
}
|
|
108
|
+
export type { XslateAdapterOptions } from './lib/types.ts'
|
|
109
|
+
import type { XslateAdapterOptions } from './lib/types.ts'
|
|
197
110
|
|
|
198
111
|
export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRenderCtx> {
|
|
199
112
|
name = 'xslate'
|
|
@@ -230,10 +143,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
230
143
|
private propsParams: { name: string }[] = []
|
|
231
144
|
private booleanTypedProps: Set<string> = new Set()
|
|
232
145
|
/**
|
|
233
|
-
* Names (signal getters + props) whose value is a string
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
146
|
+
* Names (signal getters + props) whose value is a string. In the Mojo
|
|
147
|
+
* adapter this drives choosing Perl `eq`/`ne` over numeric `==`/`!=` for a
|
|
148
|
+
* string `===`/`!==`. The Kolon emitters do NOT consume this: Kolon's
|
|
149
|
+
* `==`/`!=` are value-equality operators that compare strings and numbers
|
|
150
|
+
* correctly, so `===`/`!==` always map to `==`/`!=`. The set is populated
|
|
151
|
+
* and threaded for parity with the Mojo adapter (and as groundwork for a
|
|
152
|
+
* shared Perl-family codegen surface), not because Kolon needs it today.
|
|
237
153
|
*/
|
|
238
154
|
private stringValueNames: Set<string> = new Set()
|
|
239
155
|
|
|
@@ -255,7 +171,15 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
255
171
|
* the generic dot deref. Set at `generate()` entry from `ir.metadata.imports`;
|
|
256
172
|
* read by the top-level ParsedExpr emitter.
|
|
257
173
|
*/
|
|
258
|
-
_searchParamsLocals: Set<string> = new Set()
|
|
174
|
+
private _searchParamsLocals: Set<string> = new Set()
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Call-lowering matchers active for this component (#2057). Bound at
|
|
178
|
+
* `generate()` entry via `prepareLoweringMatchers` and read by the top-level
|
|
179
|
+
* emitter. Covers both userland plugins and the compiler's built-in plugins
|
|
180
|
+
* (e.g. `queryHref` → `$bf.query`, #2042) — one uniform path, no per-API branch.
|
|
181
|
+
*/
|
|
182
|
+
private _loweringMatchers: LoweringMatcher[] = []
|
|
259
183
|
|
|
260
184
|
/**
|
|
261
185
|
* Local + module constants from the IR, used by the conditional-spread and
|
|
@@ -294,40 +218,14 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
294
218
|
// (`data-active={props.isActive}`) must stringify as JS
|
|
295
219
|
// `String(boolean)` ("true"/"false"), not Perl's native `1`/`''`
|
|
296
220
|
// (#1897, pagination's data-active).
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
.filter(prop => prop.type?.primitive === 'boolean' || prop.type?.raw === 'boolean')
|
|
300
|
-
.map(prop => prop.name),
|
|
301
|
-
)
|
|
221
|
+
// Per-compile prop classifications (see `props/prop-classes.ts`).
|
|
222
|
+
this.booleanTypedProps = collectBooleanTypedProps(ir)
|
|
302
223
|
this.localConstants = ir.metadata.localConstants ?? []
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// `emitExpression`. See the `nullableOptionalProps` field docstring.
|
|
306
|
-
this.nullableOptionalProps = new Set(
|
|
307
|
-
ir.metadata.propsParams
|
|
308
|
-
.filter(
|
|
309
|
-
p =>
|
|
310
|
-
p.defaultValue === undefined &&
|
|
311
|
-
!p.isRest &&
|
|
312
|
-
p.type?.kind !== 'primitive',
|
|
313
|
-
)
|
|
314
|
-
.map(p => p.name),
|
|
315
|
-
)
|
|
316
|
-
// Record string-typed signals and props so equality comparisons against
|
|
317
|
-
// them lower to `eq`/`ne`. A signal is string-typed when its inferred
|
|
318
|
-
// type is `string` (or, defensively, when its initial value is a bare
|
|
319
|
-
// string literal); a prop when its annotated type is `string`.
|
|
320
|
-
this.stringValueNames = new Set<string>()
|
|
321
|
-
for (const s of ir.metadata.signals) {
|
|
322
|
-
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
323
|
-
this.stringValueNames.add(s.getter)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
for (const p of ir.metadata.propsParams) {
|
|
327
|
-
if (isStringTypeInfo(p.type)) this.stringValueNames.add(p.name)
|
|
328
|
-
}
|
|
224
|
+
this.nullableOptionalProps = collectNullableOptionalProps(ir)
|
|
225
|
+
this.stringValueNames = collectStringValueNames(ir)
|
|
329
226
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants)
|
|
330
227
|
this._searchParamsLocals = searchParamsLocalNames(ir.metadata)
|
|
228
|
+
this._loweringMatchers = prepareLoweringMatchers(ir.metadata)
|
|
331
229
|
this.errors = []
|
|
332
230
|
this.childrenCaptureCounter = 0
|
|
333
231
|
|
|
@@ -338,7 +236,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
338
236
|
// Surface it loudly here. Suppressed when the caller guarantees that all
|
|
339
237
|
// sibling templates are registered on the same instance at render time.
|
|
340
238
|
if (!options?.siblingTemplatesRegistered) {
|
|
341
|
-
this.
|
|
239
|
+
this.errors.push(...collectImportedLoopChildComponentErrors(ir, this.componentName))
|
|
342
240
|
}
|
|
343
241
|
|
|
344
242
|
this.rootScopeNodes = collectRootScopeNodes(ir.root)
|
|
@@ -354,13 +252,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
354
252
|
// SSR context consumers (`const x = useContext(Ctx)`): seed each local
|
|
355
253
|
// from the active provider value (or the `createContext` default). The
|
|
356
254
|
// provider side pushes the value via `emitProvider`. (#1297)
|
|
357
|
-
const ctxSeed =
|
|
255
|
+
const ctxSeed = generateContextConsumerSeed(ir)
|
|
358
256
|
|
|
359
257
|
// Prop/signal-derived memos with a `null` static SSR default (e.g.
|
|
360
258
|
// `createMemo(() => props.value * 10)`) are computed in-template from the
|
|
361
259
|
// already-seeded prop/signal vars — mirroring Go's generated child
|
|
362
260
|
// constructor. (#1297)
|
|
363
|
-
const memoSeed = this.
|
|
261
|
+
const memoSeed = generateDerivedMemoSeed(this.memoCtx, ir)
|
|
364
262
|
|
|
365
263
|
const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}\n`
|
|
366
264
|
|
|
@@ -392,7 +290,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
392
290
|
// ===========================================================================
|
|
393
291
|
|
|
394
292
|
private generateScriptRegistrations(ir: ComponentIR, scriptBaseName?: string): string {
|
|
395
|
-
const hasInteractivity =
|
|
293
|
+
const hasInteractivity = hasClientInteractivity(ir)
|
|
396
294
|
if (!hasInteractivity) return ''
|
|
397
295
|
|
|
398
296
|
const name = scriptBaseName ?? ir.metadata.componentName
|
|
@@ -412,15 +310,6 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
412
310
|
return lines.join('\n')
|
|
413
311
|
}
|
|
414
312
|
|
|
415
|
-
private hasClientInteractivity(ir: ComponentIR): boolean {
|
|
416
|
-
return (
|
|
417
|
-
ir.metadata.signals.length > 0 ||
|
|
418
|
-
ir.metadata.effects.length > 0 ||
|
|
419
|
-
ir.metadata.onMounts.length > 0 ||
|
|
420
|
-
(ir.metadata.clientAnalysis?.needsInit ?? false)
|
|
421
|
-
)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
313
|
// ===========================================================================
|
|
425
314
|
// Node Rendering
|
|
426
315
|
// ===========================================================================
|
|
@@ -541,105 +430,6 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
541
430
|
return `{ ${entries.join(', ')} }`
|
|
542
431
|
}
|
|
543
432
|
|
|
544
|
-
/** Kolon literal for a context-consumer's `createContext` default. */
|
|
545
|
-
private contextDefaultKolon(c: ContextConsumer): string {
|
|
546
|
-
const d = c.defaultValue
|
|
547
|
-
if (d === null || d === undefined) return 'nil'
|
|
548
|
-
if (typeof d === 'string') return `'${d.replace(/[\\']/g, m => `\\${m}`)}'`
|
|
549
|
-
if (typeof d === 'boolean') return d ? '1' : '0'
|
|
550
|
-
return String(d)
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Emit one `: my $<local> = $bf.use_context(...)` line-statement per
|
|
555
|
-
* context consumer so the body's bare `$<local>` resolves to the active
|
|
556
|
-
* provider value (or the `createContext` default). (#1297)
|
|
557
|
-
*/
|
|
558
|
-
private generateContextConsumerSeed(ir: ComponentIR): string {
|
|
559
|
-
const consumers = collectContextConsumers(ir.metadata)
|
|
560
|
-
if (consumers.length === 0) return ''
|
|
561
|
-
return (
|
|
562
|
-
consumers
|
|
563
|
-
.map(
|
|
564
|
-
c =>
|
|
565
|
-
`: my $${c.localName} = $bf.use_context('${c.contextName}', ${this.contextDefaultKolon(c)});`,
|
|
566
|
-
)
|
|
567
|
-
.join('\n') + '\n'
|
|
568
|
-
)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Seed memos whose SSR default is `null` (not statically evaluable) by
|
|
573
|
-
* computing them in-template from the already-seeded prop / signal vars
|
|
574
|
-
* (`createMemo(() => props.value * 10)` → `: my $x = $value * 10;`). Without
|
|
575
|
-
* this the memo's `$x` renders empty — the reason
|
|
576
|
-
* `props-reactivity-comparison` was skipped. Only emitted when every var the
|
|
577
|
-
* lowering references is already in scope. (#1297)
|
|
578
|
-
*/
|
|
579
|
-
private generateDerivedMemoSeed(ir: ComponentIR): string {
|
|
580
|
-
const memos = ir.metadata.memos ?? []
|
|
581
|
-
const signals = ir.metadata.signals ?? []
|
|
582
|
-
if (memos.length === 0 && signals.length === 0) return ''
|
|
583
|
-
// Props seed first; each signal/memo adds its own name as it lands.
|
|
584
|
-
const available = new Set<string>(ir.metadata.propsParams.map(p => p.name))
|
|
585
|
-
const lines: string[] = []
|
|
586
|
-
|
|
587
|
-
// Prop/signal-derived signals (`createSignal(props.defaultOn ?? false)`):
|
|
588
|
-
// a loop-child render gets no stash seed, so its `$on` would render nil;
|
|
589
|
-
// and the static default can't capture the per-call prop. Seed it
|
|
590
|
-
// in-template when the init lowers cleanly AND references an in-scope var.
|
|
591
|
-
// Object/array/constant inits keep the existing ssr-defaults seeding.
|
|
592
|
-
for (const signal of signals) {
|
|
593
|
-
const kolon = this.tryLowerToKolon(signal.initialValue, available)
|
|
594
|
-
// Kolon can't express `: my $x = … $x …` — declaring `my $x` makes the
|
|
595
|
-
// RHS `$x` an undefined lexical rather than the render var. A same-name
|
|
596
|
-
// signal (`createSignal(props.x ?? d)`, getter == prop) is just the prop
|
|
597
|
-
// with a default, which the harness already seeds correctly from the
|
|
598
|
-
// passed prop — skip the in-template seed for it. (Different-name
|
|
599
|
-
// prop-derived signals like toggle's `on` from `defaultOn` are unaffected.)
|
|
600
|
-
const refsSelf = kolon !== null && new RegExp(`\\$${signal.getter}\\b`).test(kolon)
|
|
601
|
-
if (kolon !== null && !refsSelf) lines.push(`: my $${signal.getter} = ${kolon};`)
|
|
602
|
-
available.add(signal.getter)
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
for (const memo of memos) {
|
|
606
|
-
// Seed every memo whose body lowers cleanly — not just the ones whose
|
|
607
|
-
// static SSR default is null. A statically-foldable prop-derived memo
|
|
608
|
-
// (`createMemo(() => props.disabled ?? false)` → default `false`)
|
|
609
|
-
// still depends on the per-call prop: the static stash seed bakes in
|
|
610
|
-
// the absent-prop fold, so a caller passing `disabled => 1` would
|
|
611
|
-
// render the default branch (#1897, select's disabled item). The
|
|
612
|
-
// in-template recomputation reads the prop lexical already in scope;
|
|
613
|
-
// block-bodied arrows / out-of-scope references fall back to the
|
|
614
|
-
// static ssr-defaults seed. Same self-reference guard as the signal
|
|
615
|
-
// loop above — Kolon's `my` shadows the render var on the RHS.
|
|
616
|
-
const body = extractArrowBodyExpression(memo.computation)
|
|
617
|
-
if (body !== null) {
|
|
618
|
-
const kolon = this.tryLowerToKolon(body, available)
|
|
619
|
-
const refsSelf = kolon !== null && new RegExp(`\\$${memo.name}\\b`).test(kolon)
|
|
620
|
-
if (kolon !== null && !refsSelf) lines.push(`: my $${memo.name} = ${kolon};`)
|
|
621
|
-
}
|
|
622
|
-
available.add(memo.name)
|
|
623
|
-
}
|
|
624
|
-
return lines.length > 0 ? lines.join('\n') + '\n' : ''
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Lower a signal init / memo body to Kolon for an in-template SSR seed, or
|
|
629
|
-
* `null` when it shouldn't be seeded this way: not a supported shape
|
|
630
|
-
* (`isSupported` pre-check, so object/array literals don't fail the build),
|
|
631
|
-
* references no in-scope var (a constant — keep ssr-defaults seeding), or
|
|
632
|
-
* references an out-of-scope binding. (#1297)
|
|
633
|
-
*/
|
|
634
|
-
private tryLowerToKolon(expr: string, available: ReadonlySet<string>): string | null {
|
|
635
|
-
const trimmed = expr.trim()
|
|
636
|
-
if (!trimmed) return null
|
|
637
|
-
if (!isSupported(parseExpression(trimmed)).supported) return null
|
|
638
|
-
const kolon = this.convertExpressionToKolon(trimmed)
|
|
639
|
-
if (kolon === '' || !/\$[A-Za-z_]\w*/.test(kolon)) return null
|
|
640
|
-
return referencedVarsAreAvailable(kolon, available) ? kolon : null
|
|
641
|
-
}
|
|
642
|
-
|
|
643
433
|
emitAsync(node: IRAsync, _ctx: XslateRenderCtx, _emit: EmitIRNode<XslateRenderCtx>): string {
|
|
644
434
|
return this.renderAsync(node)
|
|
645
435
|
}
|
|
@@ -773,85 +563,6 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
773
563
|
return `<: $bf.comment("cond-start:${condId}") | mark_raw :>${content}<: $bf.comment("cond-end:${condId}") | mark_raw :>`
|
|
774
564
|
}
|
|
775
565
|
|
|
776
|
-
// ===========================================================================
|
|
777
|
-
// Imported-component-in-loop check (BF103)
|
|
778
|
-
// ===========================================================================
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Push a `BF103` diagnostic for every component reference inside a loop body
|
|
782
|
-
* whose name is imported from a relative-path module. Mirror of the Mojo
|
|
783
|
-
* adapter's check — the Xslate adapter has the same cross-template-
|
|
784
|
-
* registration constraint at request time.
|
|
785
|
-
*/
|
|
786
|
-
private checkImportedLoopChildComponents(ir: ComponentIR): void {
|
|
787
|
-
const relativeImports = new Set<string>()
|
|
788
|
-
for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
|
|
789
|
-
if (!imp.source.startsWith('./') && !imp.source.startsWith('../')) continue
|
|
790
|
-
if (imp.isTypeOnly) continue
|
|
791
|
-
for (const spec of imp.specifiers) {
|
|
792
|
-
relativeImports.add(spec.alias ?? spec.name)
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
if (relativeImports.size === 0) return
|
|
796
|
-
|
|
797
|
-
const loc = { file: this.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } }
|
|
798
|
-
const visit = (node: IRNode, inLoop: boolean): void => {
|
|
799
|
-
switch (node.type) {
|
|
800
|
-
case 'component': {
|
|
801
|
-
const comp = node as IRComponent
|
|
802
|
-
if (inLoop && relativeImports.has(comp.name)) {
|
|
803
|
-
this.errors.push({
|
|
804
|
-
code: 'BF103',
|
|
805
|
-
severity: 'error',
|
|
806
|
-
message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Xslate adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
|
|
807
|
-
loc: comp.loc ?? loc,
|
|
808
|
-
suggestion: {
|
|
809
|
-
message:
|
|
810
|
-
`Options:\n` +
|
|
811
|
-
` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Xslate template alongside the parent at render time.\n` +
|
|
812
|
-
` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.\n` +
|
|
813
|
-
` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`,
|
|
814
|
-
},
|
|
815
|
-
})
|
|
816
|
-
}
|
|
817
|
-
for (const child of comp.children) visit(child, inLoop)
|
|
818
|
-
break
|
|
819
|
-
}
|
|
820
|
-
case 'element':
|
|
821
|
-
for (const child of (node as IRElement).children) visit(child, inLoop)
|
|
822
|
-
break
|
|
823
|
-
case 'fragment':
|
|
824
|
-
for (const child of (node as IRFragment).children) visit(child, inLoop)
|
|
825
|
-
break
|
|
826
|
-
case 'conditional': {
|
|
827
|
-
const cond = node as IRConditional
|
|
828
|
-
visit(cond.whenTrue, inLoop)
|
|
829
|
-
if (cond.whenFalse) visit(cond.whenFalse, inLoop)
|
|
830
|
-
break
|
|
831
|
-
}
|
|
832
|
-
case 'loop':
|
|
833
|
-
for (const child of (node as IRLoop).children) visit(child, true)
|
|
834
|
-
break
|
|
835
|
-
case 'if-statement': {
|
|
836
|
-
const stmt = node as IRIfStatement
|
|
837
|
-
visit(stmt.consequent, inLoop)
|
|
838
|
-
if (stmt.alternate) visit(stmt.alternate, inLoop)
|
|
839
|
-
break
|
|
840
|
-
}
|
|
841
|
-
case 'provider':
|
|
842
|
-
for (const child of (node as IRProvider).children) visit(child, inLoop)
|
|
843
|
-
break
|
|
844
|
-
case 'async': {
|
|
845
|
-
const a = node as IRAsync
|
|
846
|
-
visit(a.fallback, inLoop)
|
|
847
|
-
for (const child of a.children) visit(child, inLoop)
|
|
848
|
-
break
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
visit(ir.root, false)
|
|
853
|
-
}
|
|
854
|
-
|
|
855
566
|
// ===========================================================================
|
|
856
567
|
// Loop Rendering
|
|
857
568
|
// ===========================================================================
|
|
@@ -893,7 +604,20 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
893
604
|
// helper runs once.
|
|
894
605
|
let array = rawArray
|
|
895
606
|
if (loop.sortComparator) {
|
|
896
|
-
|
|
607
|
+
// Evaluator-first (#2018 P3): serialize the comparator arrow body + emit
|
|
608
|
+
// `$bf.sort_eval`; fall back to the structured `$bf.sort` for a
|
|
609
|
+
// comparator the evaluator can't model (e.g. `localeCompare`). The
|
|
610
|
+
// comparator now arrives as an `IRLoopSort` carrying the generic
|
|
611
|
+
// `arrow` + its params.
|
|
612
|
+
const sort = loop.sortComparator
|
|
613
|
+
const sortEmit = (e: ParsedExpr) => this.convertExpressionToKolon('', e)
|
|
614
|
+
const arrow = sort.arrow
|
|
615
|
+
const params =
|
|
616
|
+
arrow.kind === 'arrow' ? arrow.params : [sort.paramA, sort.paramB]
|
|
617
|
+
const structured = sortComparatorFromArrow(arrow)
|
|
618
|
+
array =
|
|
619
|
+
renderSortEval(rawArray, arrow.kind === 'arrow' ? arrow.body : arrow, params, sortEmit) ??
|
|
620
|
+
(structured !== null ? renderSortMethod(rawArray, structured) : rawArray)
|
|
897
621
|
}
|
|
898
622
|
const param = loop.param
|
|
899
623
|
// Kolon binds the item directly via `for LIST -> $item`. The index, when
|
|
@@ -957,12 +681,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
957
681
|
// Handle filter().map() pattern by wrapping children in if-condition
|
|
958
682
|
if (loop.filterPredicate) {
|
|
959
683
|
let filterCond: string
|
|
960
|
-
if (loop.filterPredicate.
|
|
961
|
-
filterCond = this.renderBlockBodyCondition(
|
|
962
|
-
loop.filterPredicate.blockBody,
|
|
963
|
-
loop.filterPredicate.param
|
|
964
|
-
)
|
|
965
|
-
} else if (loop.filterPredicate.predicate) {
|
|
684
|
+
if (loop.filterPredicate.predicate) {
|
|
966
685
|
filterCond = this.renderKolonFilterExpr(
|
|
967
686
|
loop.filterPredicate.predicate,
|
|
968
687
|
loop.filterPredicate.param
|
|
@@ -1009,6 +728,17 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1009
728
|
if (value.parts) {
|
|
1010
729
|
return `${kolonHashKey(name)} => ${this.convertTemplateLiteralPartsToKolon(value.parts)}`
|
|
1011
730
|
}
|
|
731
|
+
// Inline object-literal child prop (carousel's `opts={{ align: 'start' }}`):
|
|
732
|
+
// lower to a Kolon hashref so the child can serialize it (`data-opts`),
|
|
733
|
+
// instead of refusing the bare object with BF101. (#1971 Perl) Read the
|
|
734
|
+
// IR-carried structured `ParsedExpr` tree (#2018) instead of re-parsing
|
|
735
|
+
// `value.expr` with `ts.createSourceFile`; the lowering returns null for
|
|
736
|
+
// any non-object-literal shape, so the common non-object case falls
|
|
737
|
+
// straight through to the bare-expression path below.
|
|
738
|
+
if (value.parsed) {
|
|
739
|
+
const hashref = objectLiteralExprToKolonHashref(this.spreadCtx, value.parsed)
|
|
740
|
+
if (hashref !== null) return `${kolonHashKey(name)} => ${hashref}`
|
|
741
|
+
}
|
|
1012
742
|
return `${kolonHashKey(name)} => ${this.convertExpressionToKolon(value.expr)}`
|
|
1013
743
|
},
|
|
1014
744
|
emitSpread: (value) => {
|
|
@@ -1284,7 +1014,9 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1284
1014
|
// Emit a Kolon inline ternary of hashrefs — Perl truthiness handles the
|
|
1285
1015
|
// condition for free, and the falsy `{}` branch OMITS the key
|
|
1286
1016
|
// (`spread_attrs` does NOT emit empty hashref entries).
|
|
1287
|
-
|
|
1017
|
+
// Read the spread's IR-carried `ParsedExpr` tree (#2018) instead of
|
|
1018
|
+
// re-parsing `trimmed` with `ts.createSourceFile`.
|
|
1019
|
+
const ternaryHashref = conditionalSpreadToKolon(this.spreadCtx, value.parsed)
|
|
1288
1020
|
if (ternaryHashref !== null) {
|
|
1289
1021
|
return `<: $bf.spread_attrs(${ternaryHashref}) | mark_raw :>`
|
|
1290
1022
|
}
|
|
@@ -1301,7 +1033,15 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1301
1033
|
if (localConst?.value !== undefined) {
|
|
1302
1034
|
const initTrimmed = localConst.value.trim()
|
|
1303
1035
|
if (!/^[A-Za-z_$][\w$]*$/.test(initTrimmed)) {
|
|
1304
|
-
const
|
|
1036
|
+
// The local const's initializer text isn't carried as a structured
|
|
1037
|
+
// tree on the spread attr, so parse it once via the shared
|
|
1038
|
+
// `parseExpression` (the analyzer's own entry) — not
|
|
1039
|
+
// `ts.createSourceFile` — mirroring go-template's same local-const
|
|
1040
|
+
// resolution path.
|
|
1041
|
+
const resolved = conditionalSpreadToKolon(
|
|
1042
|
+
this.spreadCtx,
|
|
1043
|
+
parseExpression(initTrimmed),
|
|
1044
|
+
)
|
|
1305
1045
|
if (resolved !== null) {
|
|
1306
1046
|
return `<: $bf.spread_attrs(${resolved}) | mark_raw :>`
|
|
1307
1047
|
}
|
|
@@ -1410,99 +1150,6 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1410
1150
|
return emitParsedExpr(expr, new XslateFilterEmitter(param, localVarMap, n => this._isStringValueName(n)))
|
|
1411
1151
|
}
|
|
1412
1152
|
|
|
1413
|
-
/**
|
|
1414
|
-
* Render a complex block body filter into a Kolon condition.
|
|
1415
|
-
* Handles patterns like: filter(t => { const f = filter(); if (...) return ...; })
|
|
1416
|
-
*/
|
|
1417
|
-
private renderBlockBodyCondition(
|
|
1418
|
-
statements: ParsedStatement[],
|
|
1419
|
-
param: string
|
|
1420
|
-
): string {
|
|
1421
|
-
const localVarMap = new Map<string, string>()
|
|
1422
|
-
const paths = this.collectReturnPaths(statements, [], localVarMap, param)
|
|
1423
|
-
|
|
1424
|
-
if (paths.length === 0) return '1'
|
|
1425
|
-
if (paths.length === 1) return this.buildSinglePathCondition(paths[0], param, localVarMap)
|
|
1426
|
-
|
|
1427
|
-
// Multiple paths: build OR condition
|
|
1428
|
-
const parts: string[] = []
|
|
1429
|
-
for (const path of paths) {
|
|
1430
|
-
if (path.result.kind === 'literal' && path.result.literalType === 'boolean' && path.result.value === false) continue
|
|
1431
|
-
const cond = this.buildSinglePathCondition(path, param, localVarMap)
|
|
1432
|
-
if (cond !== '0') parts.push(cond)
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
if (parts.length === 0) return '0'
|
|
1436
|
-
if (parts.length === 1) return parts[0]
|
|
1437
|
-
return `(${parts.join(' || ')})`
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
private collectReturnPaths(
|
|
1441
|
-
statements: ParsedStatement[],
|
|
1442
|
-
currentConditions: ParsedExpr[],
|
|
1443
|
-
localVarMap: Map<string, string>,
|
|
1444
|
-
param: string
|
|
1445
|
-
): Array<{ conditions: ParsedExpr[]; result: ParsedExpr }> {
|
|
1446
|
-
const paths: Array<{ conditions: ParsedExpr[]; result: ParsedExpr }> = []
|
|
1447
|
-
|
|
1448
|
-
for (const stmt of statements) {
|
|
1449
|
-
if (stmt.kind === 'var-decl') {
|
|
1450
|
-
if (stmt.init.kind === 'call' && stmt.init.callee.kind === 'identifier') {
|
|
1451
|
-
localVarMap.set(stmt.name, stmt.init.callee.name)
|
|
1452
|
-
}
|
|
1453
|
-
} else if (stmt.kind === 'return') {
|
|
1454
|
-
paths.push({ conditions: [...currentConditions], result: stmt.value })
|
|
1455
|
-
break
|
|
1456
|
-
} else if (stmt.kind === 'if') {
|
|
1457
|
-
const thenPaths = this.collectReturnPaths(stmt.consequent, [...currentConditions, stmt.condition], localVarMap, param)
|
|
1458
|
-
paths.push(...thenPaths)
|
|
1459
|
-
|
|
1460
|
-
if (stmt.alternate) {
|
|
1461
|
-
const negated: ParsedExpr = { kind: 'unary', op: '!', argument: stmt.condition }
|
|
1462
|
-
const elsePaths = this.collectReturnPaths(stmt.alternate, [...currentConditions, negated], localVarMap, param)
|
|
1463
|
-
paths.push(...elsePaths)
|
|
1464
|
-
} else {
|
|
1465
|
-
currentConditions.push({ kind: 'unary', op: '!', argument: stmt.condition })
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
return paths
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
private buildSinglePathCondition(
|
|
1474
|
-
path: { conditions: ParsedExpr[]; result: ParsedExpr },
|
|
1475
|
-
param: string,
|
|
1476
|
-
localVarMap: Map<string, string>
|
|
1477
|
-
): string {
|
|
1478
|
-
if (path.result.kind === 'literal' && path.result.literalType === 'boolean') {
|
|
1479
|
-
if (path.result.value === true) {
|
|
1480
|
-
if (path.conditions.length === 0) return '1'
|
|
1481
|
-
return this.renderConditionsAnd(path.conditions, param, localVarMap)
|
|
1482
|
-
}
|
|
1483
|
-
return '0'
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
if (path.conditions.length === 0) {
|
|
1487
|
-
return this.renderKolonFilterExpr(path.result, param, localVarMap)
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
const condPart = this.renderConditionsAnd(path.conditions, param, localVarMap)
|
|
1491
|
-
const resultPart = this.renderKolonFilterExpr(path.result, param, localVarMap)
|
|
1492
|
-
return `(${condPart} && ${resultPart})`
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
private renderConditionsAnd(
|
|
1496
|
-
conditions: ParsedExpr[],
|
|
1497
|
-
param: string,
|
|
1498
|
-
localVarMap: Map<string, string>
|
|
1499
|
-
): string {
|
|
1500
|
-
if (conditions.length === 0) return '1'
|
|
1501
|
-
if (conditions.length === 1) return this.renderKolonFilterExpr(conditions[0], param, localVarMap)
|
|
1502
|
-
const parts = conditions.map(c => this.renderKolonFilterExpr(c, param, localVarMap))
|
|
1503
|
-
return `(${parts.join(' && ')})`
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
1153
|
// ===========================================================================
|
|
1507
1154
|
// Expression Conversion: JS → Kolon
|
|
1508
1155
|
// ===========================================================================
|
|
@@ -1585,137 +1232,92 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1585
1232
|
}
|
|
1586
1233
|
|
|
1587
1234
|
/**
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
* `
|
|
1594
|
-
* falls back to its normal lowering. Mirror of `conditionalSpreadToPerl`.
|
|
1235
|
+
* Build the EmitContext seam the top-level `ParsedExpr` emitter depends on.
|
|
1236
|
+
* Built as a private object (the adapter does NOT `implements XslateEmitContext`)
|
|
1237
|
+
* so the wrapped bookkeeping — `_searchParamsLocals`, the const/record
|
|
1238
|
+
* resolvers, BF101 recording, the filter-predicate entry — stays private and
|
|
1239
|
+
* off the exported adapter's public type, matching the Go adapter's
|
|
1240
|
+
* `emitCtx` and the `spreadCtx` / `memoCtx` seams below.
|
|
1595
1241
|
*/
|
|
1596
|
-
private
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
const unwrap = (e: ts.Expression): ts.Expression => {
|
|
1605
|
-
let n = e
|
|
1606
|
-
while (ts.isParenthesizedExpression(n)) n = n.expression
|
|
1607
|
-
return n
|
|
1608
|
-
}
|
|
1609
|
-
const whenTrue = unwrap(node.whenTrue)
|
|
1610
|
-
const whenFalse = unwrap(node.whenFalse)
|
|
1611
|
-
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
1612
|
-
return null
|
|
1242
|
+
private get emitCtx(): XslateEmitContext {
|
|
1243
|
+
return {
|
|
1244
|
+
_searchParamsLocals: this._searchParamsLocals,
|
|
1245
|
+
_resolveModuleStringConst: (name) => this._resolveModuleStringConst(name),
|
|
1246
|
+
_resolveLiteralConst: (name) => this._resolveLiteralConst(name),
|
|
1247
|
+
_resolveStaticRecordLiteral: (o, k) => this._resolveStaticRecordLiteral(o, k),
|
|
1248
|
+
_recordExprBF101: (message, reason) => this._recordExprBF101(message, reason),
|
|
1249
|
+
_renderKolonFilterExprPublic: (e, p) => this._renderKolonFilterExprPublic(e, p),
|
|
1613
1250
|
}
|
|
1614
|
-
const condPerl = this.convertExpressionToKolon(node.condition.getText(sf))
|
|
1615
|
-
const truePerl = this.objectLiteralToKolonHashref(whenTrue, sf)
|
|
1616
|
-
const falsePerl = this.objectLiteralToKolonHashref(whenFalse, sf)
|
|
1617
|
-
if (truePerl === null || falsePerl === null) return null
|
|
1618
|
-
return `${condPerl} ? ${truePerl} : ${falsePerl}`
|
|
1619
1251
|
}
|
|
1620
1252
|
|
|
1621
1253
|
/**
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
*
|
|
1625
|
-
*
|
|
1626
|
-
* → `{}`. Mirror of `objectLiteralToPerlHashref`.
|
|
1254
|
+
* Build the narrow context the extracted spread lowering depends on. Passing
|
|
1255
|
+
* a purpose-built object (rather than `this`) keeps the adapter's bookkeeping
|
|
1256
|
+
* members private — they stay internal implementation detail, not part of the
|
|
1257
|
+
* exported class's public surface.
|
|
1627
1258
|
*/
|
|
1628
|
-
private
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
let key: string
|
|
1636
|
-
if (ts.isIdentifier(prop.name)) {
|
|
1637
|
-
key = prop.name.text
|
|
1638
|
-
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
1639
|
-
key = prop.name.text
|
|
1640
|
-
} else {
|
|
1641
|
-
return null
|
|
1642
|
-
}
|
|
1643
|
-
const initNode = (() => {
|
|
1644
|
-
let n: ts.Expression = prop.initializer
|
|
1645
|
-
while (ts.isParenthesizedExpression(n)) n = n.expression
|
|
1646
|
-
return n
|
|
1647
|
-
})()
|
|
1648
|
-
const indexed = this.recordIndexAccessToKolon(initNode)
|
|
1649
|
-
if (
|
|
1650
|
-
indexed === null &&
|
|
1651
|
-
ts.isElementAccessExpression(initNode) &&
|
|
1652
|
-
initNode.argumentExpression &&
|
|
1653
|
-
!ts.isNumericLiteral(initNode.argumentExpression) &&
|
|
1654
|
-
!ts.isStringLiteral(initNode.argumentExpression)
|
|
1655
|
-
) {
|
|
1656
|
-
// Variable-index record access (`sizeMap[size]`) the static-inline
|
|
1657
|
-
// path couldn't resolve (non-scalar value / non-const receiver).
|
|
1658
|
-
// Since #1897 made variable indices parseable (`index-access`),
|
|
1659
|
-
// the generic value lowering would emit `$sizeMap[$size]` against
|
|
1660
|
-
// an UNBOUND module const instead of refusing — record BF101 and
|
|
1661
|
-
// bail so the spread surfaces the out-of-shape diagnostic,
|
|
1662
|
-
// matching pre-#1897 behaviour. (Mirrors the Mojo adapter.)
|
|
1663
|
-
this.errors.push({
|
|
1664
|
-
code: 'BF101',
|
|
1665
|
-
severity: 'error',
|
|
1666
|
-
message: `Spread object value '${initNode.getText(sf)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Kolon hashref.`,
|
|
1667
|
-
loc: { file: this.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1668
|
-
suggestion: {
|
|
1669
|
-
message: 'Index a record whose values are number/string literals, or move the spread into a `\'use client\'` component so hydration computes it.',
|
|
1670
|
-
},
|
|
1671
|
-
})
|
|
1672
|
-
return null
|
|
1673
|
-
}
|
|
1674
|
-
const valPerl =
|
|
1675
|
-
indexed !== null
|
|
1676
|
-
? indexed
|
|
1677
|
-
: this.convertExpressionToKolon(prop.initializer.getText(sf))
|
|
1678
|
-
entries.push(`'${escapeKolonSingleQuoted(key)}' => ${valPerl}`)
|
|
1259
|
+
private get spreadCtx(): XslateSpreadContext {
|
|
1260
|
+
return {
|
|
1261
|
+
componentName: this.componentName,
|
|
1262
|
+
errors: this.errors,
|
|
1263
|
+
localConstants: this.localConstants,
|
|
1264
|
+
propsParams: this.propsParams,
|
|
1265
|
+
convertExpressionToKolon: (e, preParsed) => this.convertExpressionToKolon(e, preParsed),
|
|
1679
1266
|
}
|
|
1680
|
-
return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`
|
|
1681
1267
|
}
|
|
1682
1268
|
|
|
1683
|
-
/**
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
* `{ 'sm' => 16, 'md' => 20, ... }[$size]`.
|
|
1687
|
-
* Reuses the shared structural parse (`parseRecordIndexAccess`); this wrapper
|
|
1688
|
-
* only does the single-quote escaping + Kolon index emit. NB: Kolon indexes a
|
|
1689
|
-
* hashref literal with bracket syntax `{…}[$key]`, NOT Perl's arrow-deref
|
|
1690
|
-
* `{…}->{$key}` (which Kolon's parser rejects) — this is the one divergence
|
|
1691
|
-
* from the Mojo `recordIndexAccessToPerl` emit.
|
|
1692
|
-
*/
|
|
1693
|
-
private recordIndexAccessToKolon(val: ts.Expression): string | null {
|
|
1694
|
-
const parsed = parseRecordIndexAccess(val, this.localConstants ?? [], this.propsParams)
|
|
1695
|
-
if (!parsed) return null
|
|
1696
|
-
const entries = parsed.entries.map(e => {
|
|
1697
|
-
const mapVal =
|
|
1698
|
-
e.value.kind === 'number' ? e.value.text : `'${escapeKolonSingleQuoted(e.value.text)}'`
|
|
1699
|
-
return `'${escapeKolonSingleQuoted(e.key)}' => ${mapVal}`
|
|
1700
|
-
})
|
|
1701
|
-
return `{ ${entries.join(', ')} }[$${parsed.indexPropName}]`
|
|
1269
|
+
/** Build the narrow context the extracted memo seeding depends on. */
|
|
1270
|
+
private get memoCtx(): XslateMemoContext {
|
|
1271
|
+
return { convertExpressionToKolon: (e, preParsed) => this.convertExpressionToKolon(e, preParsed) }
|
|
1702
1272
|
}
|
|
1703
1273
|
|
|
1704
|
-
private convertExpressionToKolon(expr: string): string {
|
|
1274
|
+
private convertExpressionToKolon(expr: string, preParsed?: ParsedExpr): string {
|
|
1705
1275
|
// Parse-first lowering — parity with the Mojo adapter's
|
|
1706
1276
|
// `convertExpressionToPerl`. Parse the JS expression once, gate it on the
|
|
1707
1277
|
// shared `isSupported`, and render every supported shape through the AST
|
|
1708
1278
|
// emitter. Unsupported shapes surface as BF101.
|
|
1709
|
-
|
|
1710
|
-
|
|
1279
|
+
//
|
|
1280
|
+
// `preParsed` is the IR-carried `ParsedExpr` tree (cf. go-template's
|
|
1281
|
+
// `convertExpressionToGo(jsExpr, out?, preParsed?)`); when present it is
|
|
1282
|
+
// used directly instead of re-parsing `expr`, so spread condition/value
|
|
1283
|
+
// lowering threads the carried tree through without a stringify→re-parse
|
|
1284
|
+
// round-trip. The diagnostic text is then derived from the tree
|
|
1285
|
+
// (`stringifyParsedExpr`) so callers can pass `''` for `expr`.
|
|
1286
|
+
let parsed: ParsedExpr
|
|
1287
|
+
if (preParsed) {
|
|
1288
|
+
parsed = preParsed
|
|
1289
|
+
} else {
|
|
1290
|
+
const trimmed = expr.trim()
|
|
1291
|
+
if (trimmed === '') return "''"
|
|
1292
|
+
parsed = parseExpression(trimmed)
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Registered call lowerings (#2057) — including the built-in `queryHref`
|
|
1296
|
+
// plugin (#2042), which lowers `queryHref(base, { … })` to a neutral
|
|
1297
|
+
// `guard-list` on the `query` helper → `$bf.query(base, <triples>)`.
|
|
1298
|
+
// Recognised before the support gate because the object-literal arg is
|
|
1299
|
+
// otherwise `unsupported` (BF101). The `query` helper includes a pair iff its
|
|
1300
|
+
// guard is truthy AND its value is a non-empty string (the client's
|
|
1301
|
+
// `if (value)`): a plain `key: v` passes guard `1`, a conditional
|
|
1302
|
+
// `key: cond ? v : undefined` passes the lowered cond. Only the `query`
|
|
1303
|
+
// helper renders to `$bf.query`; another guard-list helper must not be
|
|
1304
|
+
// silently mis-rendered as a query.
|
|
1305
|
+
if (parsed.kind === 'call') {
|
|
1306
|
+
for (const matcher of this._loweringMatchers) {
|
|
1307
|
+
const node = matcher(parsed.callee, parsed.args)
|
|
1308
|
+
if (node?.kind === 'guard-list' && node.helper === 'query') {
|
|
1309
|
+
const qArgs = queryHrefArgs(node, n => this.renderParsedExprToKolon(n))
|
|
1310
|
+
return `$bf.query(${qArgs.join(', ')})`
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1711
1314
|
|
|
1712
|
-
const parsed = parseExpression(trimmed)
|
|
1713
1315
|
const support = isSupported(parsed)
|
|
1714
1316
|
if (!support.supported) {
|
|
1715
1317
|
this.errors.push({
|
|
1716
1318
|
code: 'BF101',
|
|
1717
1319
|
severity: 'error',
|
|
1718
|
-
message: `Expression not supported: ${
|
|
1320
|
+
message: `Expression not supported: ${preParsed ? stringifyParsedExpr(parsed) : expr.trim()}`,
|
|
1719
1321
|
loc: { file: this.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1720
1322
|
suggestion: {
|
|
1721
1323
|
message: support.reason
|
|
@@ -1736,12 +1338,12 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1736
1338
|
* expressions where identifiers are signals / template vars.
|
|
1737
1339
|
*/
|
|
1738
1340
|
private renderParsedExprToKolon(expr: ParsedExpr): string {
|
|
1739
|
-
return emitParsedExpr(expr, new XslateTopLevelEmitter(this))
|
|
1341
|
+
return emitParsedExpr(expr, new XslateTopLevelEmitter(this.emitCtx))
|
|
1740
1342
|
}
|
|
1741
1343
|
|
|
1742
1344
|
/** Whether `name` (a signal getter or prop) holds a string value, so an
|
|
1743
1345
|
* equality comparison against it should use Perl `eq`/`ne`. */
|
|
1744
|
-
_isStringValueName(name: string): boolean {
|
|
1346
|
+
private _isStringValueName(name: string): boolean {
|
|
1745
1347
|
return this.stringValueNames.has(name)
|
|
1746
1348
|
}
|
|
1747
1349
|
|
|
@@ -1803,7 +1405,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1803
1405
|
* pagination) — function-scope consts never reach the per-render
|
|
1804
1406
|
* stash, so a bare `$totalPages` renders empty.
|
|
1805
1407
|
*/
|
|
1806
|
-
_resolveLiteralConst(name: string): string | null {
|
|
1408
|
+
private _resolveLiteralConst(name: string): string | null {
|
|
1807
1409
|
const c = (this.localConstants ?? []).find(lc => lc.name === name)
|
|
1808
1410
|
if (c?.value === undefined) return null
|
|
1809
1411
|
const v = c.value.trim()
|
|
@@ -1813,7 +1415,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1813
1415
|
return null
|
|
1814
1416
|
}
|
|
1815
1417
|
|
|
1816
|
-
_resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
1418
|
+
private _resolveStaticRecordLiteral(objectName: string, key: string): string | null {
|
|
1817
1419
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants)
|
|
1818
1420
|
if (!hit) return null
|
|
1819
1421
|
return hit.kind === 'number'
|
|
@@ -1821,7 +1423,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1821
1423
|
: `'${hit.text.replace(/[\\']/g, m => `\\${m}`)}'`
|
|
1822
1424
|
}
|
|
1823
1425
|
|
|
1824
|
-
_resolveModuleStringConst(name: string): string | null {
|
|
1426
|
+
private _resolveModuleStringConst(name: string): string | null {
|
|
1825
1427
|
// A loop body may bind `my $<param>` that shadows a module const of the
|
|
1826
1428
|
// same name; never inline inside one (conservative — drop to `$name`).
|
|
1827
1429
|
if (this.inLoop) return null
|
|
@@ -1830,7 +1432,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1830
1432
|
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
1831
1433
|
}
|
|
1832
1434
|
|
|
1833
|
-
_recordExprBF101(message: string, reason?: string): void {
|
|
1435
|
+
private _recordExprBF101(message: string, reason?: string): void {
|
|
1834
1436
|
this.errors.push({
|
|
1835
1437
|
code: 'BF101',
|
|
1836
1438
|
severity: 'error',
|
|
@@ -1845,675 +1447,9 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1845
1447
|
}
|
|
1846
1448
|
|
|
1847
1449
|
/** Internal hook for higher-order: predicate body re-uses the filter emitter. */
|
|
1848
|
-
_renderKolonFilterExprPublic(expr: ParsedExpr, param: string): string {
|
|
1450
|
+
private _renderKolonFilterExprPublic(expr: ParsedExpr, param: string): string {
|
|
1849
1451
|
return this.renderKolonFilterExpr(expr, param)
|
|
1850
1452
|
}
|
|
1851
1453
|
}
|
|
1852
1454
|
|
|
1853
|
-
// ===========================================================================
|
|
1854
|
-
// ParsedExpr emitters
|
|
1855
|
-
// ===========================================================================
|
|
1856
|
-
|
|
1857
|
-
/**
|
|
1858
|
-
* Lowering for `array-method` IR nodes — shared between the filter and
|
|
1859
|
-
* top-level emitters so the emitted Kolon form stays consistent regardless of
|
|
1860
|
-
* which context the chain lands in. The receiver/array helpers are the same
|
|
1861
|
-
* runtime methods the Mojo adapter calls, invoked as `$bf.NAME(...)` on the
|
|
1862
|
-
* Kolon `$bf` object instead of `bf->NAME`.
|
|
1863
|
-
*
|
|
1864
|
-
* Perl-native string ops the Mojo adapter inlines (`lc`, `uc`) have no Kolon
|
|
1865
|
-
* builtin, so they route through dedicated runtime helpers — but those
|
|
1866
|
-
* helpers aren't part of the validated v1 surface, so they're emitted as
|
|
1867
|
-
* `$bf.NAME(...)` calls consistent with the rest. Array methods whose Mojo
|
|
1868
|
-
* form relied on Perl `@{...}` deref (`join`) route through `$bf` helpers.
|
|
1869
|
-
*/
|
|
1870
|
-
function renderArrayMethod(
|
|
1871
|
-
method: ArrayMethod,
|
|
1872
|
-
object: ParsedExpr,
|
|
1873
|
-
args: ParsedExpr[],
|
|
1874
|
-
emit: (e: ParsedExpr) => string,
|
|
1875
|
-
): string {
|
|
1876
|
-
switch (method) {
|
|
1877
|
-
case 'join': {
|
|
1878
|
-
// Route through the runtime (`$bf.join`) rather than Kolon's builtin
|
|
1879
|
-
// `.join`, so the JS-compat element handling (undef → empty, default
|
|
1880
|
-
// separator) is applied consistently — same reasoning as $bf.lc / etc.
|
|
1881
|
-
const obj = emit(object)
|
|
1882
|
-
const sep = args.length >= 1 ? emit(args[0]) : `','`
|
|
1883
|
-
return `$bf.join(${obj}, ${sep})`
|
|
1884
|
-
}
|
|
1885
|
-
case 'includes': {
|
|
1886
|
-
const obj = emit(object)
|
|
1887
|
-
const needle = emit(args[0])
|
|
1888
|
-
return `$bf.includes(${obj}, ${needle})`
|
|
1889
|
-
}
|
|
1890
|
-
case 'indexOf':
|
|
1891
|
-
case 'lastIndexOf': {
|
|
1892
|
-
const fn = method === 'indexOf' ? 'index_of' : 'last_index_of'
|
|
1893
|
-
const obj = emit(object)
|
|
1894
|
-
const needle = emit(args[0])
|
|
1895
|
-
return `$bf.${fn}(${obj}, ${needle})`
|
|
1896
|
-
}
|
|
1897
|
-
case 'at': {
|
|
1898
|
-
const obj = emit(object)
|
|
1899
|
-
const idx = args.length >= 1 ? emit(args[0]) : '0'
|
|
1900
|
-
return `$bf.at(${obj}, ${idx})`
|
|
1901
|
-
}
|
|
1902
|
-
case 'concat': {
|
|
1903
|
-
if (args.length === 0) {
|
|
1904
|
-
return emit(object)
|
|
1905
|
-
}
|
|
1906
|
-
const a = emit(object)
|
|
1907
|
-
const b = emit(args[0])
|
|
1908
|
-
return `$bf.concat(${a}, ${b})`
|
|
1909
|
-
}
|
|
1910
|
-
case 'slice': {
|
|
1911
|
-
const recv = emit(object)
|
|
1912
|
-
const start = args.length >= 1 ? emit(args[0]) : '0'
|
|
1913
|
-
// Kolon's undefined literal is `nil`, not Perl's `undef` — the
|
|
1914
|
-
// runtime `slice` treats it as "to end".
|
|
1915
|
-
const end = args.length >= 2 ? emit(args[1]) : 'nil'
|
|
1916
|
-
return `$bf.slice(${recv}, ${start}, ${end})`
|
|
1917
|
-
}
|
|
1918
|
-
case 'reverse':
|
|
1919
|
-
case 'toReversed': {
|
|
1920
|
-
const recv = emit(object)
|
|
1921
|
-
return `$bf.reverse(${recv})`
|
|
1922
|
-
}
|
|
1923
|
-
case 'toLowerCase': {
|
|
1924
|
-
// Kolon has no builtin string `lc` / `uc`, so these go through the
|
|
1925
|
-
// runtime object (consistent with $bf.includes / $bf.slice / etc.).
|
|
1926
|
-
const recv = emit(object)
|
|
1927
|
-
return `$bf.lc(${recv})`
|
|
1928
|
-
}
|
|
1929
|
-
case 'toUpperCase': {
|
|
1930
|
-
const recv = emit(object)
|
|
1931
|
-
return `$bf.uc(${recv})`
|
|
1932
|
-
}
|
|
1933
|
-
case 'trim': {
|
|
1934
|
-
const recv = emit(object)
|
|
1935
|
-
return `$bf.trim(${recv})`
|
|
1936
|
-
}
|
|
1937
|
-
case 'toFixed': {
|
|
1938
|
-
// `.toFixed(digits?)` — `$bf.to_fixed` mirrors JS rounding +
|
|
1939
|
-
// zero-padding (default 0 digits). #1897.
|
|
1940
|
-
const recv = emit(object)
|
|
1941
|
-
const digits = args.length >= 1 ? emit(args[0]) : '0'
|
|
1942
|
-
return `$bf.to_fixed(${recv}, ${digits})`
|
|
1943
|
-
}
|
|
1944
|
-
case 'split': {
|
|
1945
|
-
const recv = emit(object)
|
|
1946
|
-
if (args.length === 0) {
|
|
1947
|
-
return `$bf.split(${recv})`
|
|
1948
|
-
}
|
|
1949
|
-
const sep = emit(args[0])
|
|
1950
|
-
if (args.length === 1) {
|
|
1951
|
-
return `$bf.split(${recv}, ${sep})`
|
|
1952
|
-
}
|
|
1953
|
-
const limit = emit(args[1])
|
|
1954
|
-
return `$bf.split(${recv}, ${sep}, ${limit})`
|
|
1955
|
-
}
|
|
1956
|
-
case 'startsWith':
|
|
1957
|
-
case 'endsWith': {
|
|
1958
|
-
const fn = method === 'startsWith' ? 'starts_with' : 'ends_with'
|
|
1959
|
-
const recv = emit(object)
|
|
1960
|
-
const arg = emit(args[0])
|
|
1961
|
-
if (args.length >= 2) {
|
|
1962
|
-
return `$bf.${fn}(${recv}, ${arg}, ${emit(args[1])})`
|
|
1963
|
-
}
|
|
1964
|
-
return `$bf.${fn}(${recv}, ${arg})`
|
|
1965
|
-
}
|
|
1966
|
-
case 'replace': {
|
|
1967
|
-
const recv = emit(object)
|
|
1968
|
-
const oldS = emit(args[0])
|
|
1969
|
-
const newS = emit(args[1])
|
|
1970
|
-
return `$bf.replace(${recv}, ${oldS}, ${newS})`
|
|
1971
|
-
}
|
|
1972
|
-
case 'repeat': {
|
|
1973
|
-
const recv = emit(object)
|
|
1974
|
-
const count = args.length === 0 ? '0' : emit(args[0])
|
|
1975
|
-
return `$bf.repeat(${recv}, ${count})`
|
|
1976
|
-
}
|
|
1977
|
-
case 'padStart':
|
|
1978
|
-
case 'padEnd': {
|
|
1979
|
-
const fn = method === 'padStart' ? 'pad_start' : 'pad_end'
|
|
1980
|
-
const recv = emit(object)
|
|
1981
|
-
if (args.length === 0) {
|
|
1982
|
-
return `$bf.${fn}(${recv}, 0)`
|
|
1983
|
-
}
|
|
1984
|
-
const target = emit(args[0])
|
|
1985
|
-
if (args.length === 1) {
|
|
1986
|
-
return `$bf.${fn}(${recv}, ${target})`
|
|
1987
|
-
}
|
|
1988
|
-
const pad = emit(args[1])
|
|
1989
|
-
return `$bf.${fn}(${recv}, ${target}, ${pad})`
|
|
1990
|
-
}
|
|
1991
|
-
default: {
|
|
1992
|
-
// TS-level exhaustiveness guard.
|
|
1993
|
-
const _exhaustive: never = method
|
|
1994
|
-
throw new Error(
|
|
1995
|
-
`renderArrayMethod: unhandled ArrayMethod '${(_exhaustive as string)}'`,
|
|
1996
|
-
)
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
/**
|
|
2002
|
-
* Shared Kolon emit for `.sort(cmp)` / `.toSorted(cmp)`. Used by both the
|
|
2003
|
-
* filter-context emitter and the top-level emitter, plus the loop-array
|
|
2004
|
-
* wrap in `renderLoop`. The runtime `$bf.sort` accepts a hashref opts bag and
|
|
2005
|
-
* returns a fresh array ref.
|
|
2006
|
-
*/
|
|
2007
|
-
function renderSortMethod(recv: string, c: SortComparator): string {
|
|
2008
|
-
const keyHashes = c.keys.map((k) => {
|
|
2009
|
-
const keyEntry =
|
|
2010
|
-
k.key.kind === 'self'
|
|
2011
|
-
? `key_kind => 'self'`
|
|
2012
|
-
: `key_kind => 'field', key => '${k.key.field}'`
|
|
2013
|
-
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`
|
|
2014
|
-
})
|
|
2015
|
-
return `$bf.sort(${recv}, { keys => [${keyHashes.join(', ')}] })`
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
/**
|
|
2019
|
-
* Render a `.reduce(fn, init)` arithmetic fold as a `$bf.reduce(...)` call.
|
|
2020
|
-
*/
|
|
2021
|
-
function renderReduceMethod(recv: string, op: ReduceOp, direction: 'left' | 'right'): string {
|
|
2022
|
-
const keyEntry =
|
|
2023
|
-
op.key.kind === 'self'
|
|
2024
|
-
? `key_kind => 'self'`
|
|
2025
|
-
: `key_kind => 'field', key => '${op.key.field}'`
|
|
2026
|
-
const init =
|
|
2027
|
-
op.type === 'string'
|
|
2028
|
-
? `'${op.init.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
2029
|
-
: op.init
|
|
2030
|
-
return `$bf.reduce(${recv}, { op => '${op.op}', ${keyEntry}, type => '${op.type}', init => ${init}, direction => '${direction}' })`
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
// `.flat(depth?)` → `$bf.flat($recv, $depth)`.
|
|
2034
|
-
function renderFlatMethod(recv: string, depth: FlatDepth): string {
|
|
2035
|
-
const d = depth === 'infinity' ? -1 : depth
|
|
2036
|
-
return `$bf.flat(${recv}, ${d})`
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
// `.flatMap(...)` → `$bf.flat_map(...)` / `$bf.flat_map_tuple(...)`.
|
|
2040
|
-
function renderFlatMapMethod(recv: string, op: FlatMapOp): string {
|
|
2041
|
-
const proj = op.projection
|
|
2042
|
-
if (proj.kind === 'tuple') {
|
|
2043
|
-
const specs = proj.elements
|
|
2044
|
-
.map(l => (l.kind === 'self' ? `['self', '']` : `['field', '${l.field}']`))
|
|
2045
|
-
.join(', ')
|
|
2046
|
-
return `$bf.flat_map_tuple(${recv}, ${specs})`
|
|
2047
|
-
}
|
|
2048
|
-
if (proj.kind === 'self') return `$bf.flat_map(${recv}, 'self', '')`
|
|
2049
|
-
return `$bf.flat_map(${recv}, 'field', '${proj.field}')`
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
/**
|
|
2053
|
-
* Parse a const initializer's source text. Returns the unescaped string value
|
|
2054
|
-
* when the whole initializer is a single pure string literal — single/double
|
|
2055
|
-
* quoted, or a no-substitution backtick template (no `${}`) — else `null`.
|
|
2056
|
-
* Only such a value can be inlined byte-for-byte; template literals with
|
|
2057
|
-
* interpolation, numbers, objects, and `Record<T,string>` maps are excluded.
|
|
2058
|
-
*/
|
|
2059
|
-
function parsePureStringLiteral(source: string): string | null {
|
|
2060
|
-
let s = source.trim()
|
|
2061
|
-
// Peel a single layer of wrapping parens.
|
|
2062
|
-
while (s.startsWith('(') && s.endsWith(')')) s = s.slice(1, -1).trim()
|
|
2063
|
-
const quote = s[0]
|
|
2064
|
-
if ((quote === "'" || quote === '"') && s[s.length - 1] === quote) {
|
|
2065
|
-
const body = s.slice(1, -1)
|
|
2066
|
-
// Reject if an unescaped matching quote appears inside (not a single
|
|
2067
|
-
// literal then).
|
|
2068
|
-
if (containsUnescaped(body, quote)) return null
|
|
2069
|
-
return unescapeStringLiteralBody(body)
|
|
2070
|
-
}
|
|
2071
|
-
if (quote === '`' && s[s.length - 1] === '`') {
|
|
2072
|
-
const body = s.slice(1, -1)
|
|
2073
|
-
if (body.includes('${')) return null
|
|
2074
|
-
if (containsUnescaped(body, '`')) return null
|
|
2075
|
-
return unescapeStringLiteralBody(body)
|
|
2076
|
-
}
|
|
2077
|
-
// `[<literals>].join(' ')` module consts (e.g. Switch's `trackStateClasses`)
|
|
2078
|
-
// → inline the flattened string byte-for-byte. See `evalStringArrayJoin`.
|
|
2079
|
-
return evalStringArrayJoin(source)
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
/** Whether `s` contains an unescaped occurrence of `ch`. */
|
|
2083
|
-
function containsUnescaped(s: string, ch: string): boolean {
|
|
2084
|
-
for (let i = 0; i < s.length; i++) {
|
|
2085
|
-
if (s[i] === '\\') { i++; continue }
|
|
2086
|
-
if (s[i] === ch) return true
|
|
2087
|
-
}
|
|
2088
|
-
return false
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
/** Unescape a JS string-literal body's common escape sequences. */
|
|
2092
|
-
function unescapeStringLiteralBody(s: string): string {
|
|
2093
|
-
return s.replace(/\\(.)/g, (_, c) => {
|
|
2094
|
-
switch (c) {
|
|
2095
|
-
case 'n': return '\n'
|
|
2096
|
-
case 'r': return '\r'
|
|
2097
|
-
case 't': return '\t'
|
|
2098
|
-
case '0': return '\0'
|
|
2099
|
-
default: return c
|
|
2100
|
-
}
|
|
2101
|
-
})
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
/** True when `type` is the `string` primitive. */
|
|
2106
|
-
function isStringTypeInfo(type: TypeInfo | undefined): boolean {
|
|
2107
|
-
return type?.kind === 'primitive' && type.primitive === 'string'
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
/** True when `initialValue` is a bare string-literal expression. */
|
|
2111
|
-
function isBareStringLiteral(initialValue: string | undefined): boolean {
|
|
2112
|
-
if (!initialValue) return false
|
|
2113
|
-
const v = initialValue.trim()
|
|
2114
|
-
return (v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
/**
|
|
2118
|
-
* Whether a comparison operand is string-typed, so JS `===`/`!==` against it
|
|
2119
|
-
* must lower to Perl `eq`/`ne` instead of numeric `==`/`!=`.
|
|
2120
|
-
*/
|
|
2121
|
-
function isStringTypedOperand(expr: ParsedExpr, isStringName: (n: string) => boolean): boolean {
|
|
2122
|
-
if (expr.kind === 'literal' && expr.literalType === 'string') return true
|
|
2123
|
-
if (expr.kind === 'call' && expr.callee.kind === 'identifier' && expr.args.length === 0) {
|
|
2124
|
-
return isStringName(expr.callee.name)
|
|
2125
|
-
}
|
|
2126
|
-
if (expr.kind === 'member' && expr.object.kind === 'identifier' && expr.object.name === 'props') {
|
|
2127
|
-
return isStringName(expr.property)
|
|
2128
|
-
}
|
|
2129
|
-
return false
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
/**
|
|
2133
|
-
* Lowering for the predicate body of a filter / every / some / find, plus the
|
|
2134
|
-
* same shape used by `renderBlockBodyCondition` for complex block-body
|
|
2135
|
-
* filters. Higher-order predicates are emitted using Kolon's own scalar
|
|
2136
|
-
* comparison operators (which delegate to Perl semantics).
|
|
2137
|
-
*
|
|
2138
|
-
* NOTE: Kolon has no `grep { } @{...}` form, so nested higher-order chains
|
|
2139
|
-
* (`x.tags.filter(...).length`) inside a predicate route through the
|
|
2140
|
-
* top-level emitter's `$bf`-helper higher-order lowering. This emitter keeps
|
|
2141
|
-
* the scalar-comparison surface the predicates the adapter accepts actually
|
|
2142
|
-
* use; richer nested shapes fall back to the helper or surface as BF101 via
|
|
2143
|
-
* the top-level emitter.
|
|
2144
|
-
*/
|
|
2145
|
-
class XslateFilterEmitter implements ParsedExprEmitter {
|
|
2146
|
-
constructor(
|
|
2147
|
-
private readonly param: string,
|
|
2148
|
-
private readonly localVarMap: Map<string, string>,
|
|
2149
|
-
private readonly isStringName: (n: string) => boolean = () => false,
|
|
2150
|
-
) {}
|
|
2151
|
-
|
|
2152
|
-
identifier(name: string): string {
|
|
2153
|
-
if (name === this.param) return `$${this.param}`
|
|
2154
|
-
const signal = this.localVarMap.get(name)
|
|
2155
|
-
if (signal) return `$${signal}`
|
|
2156
|
-
return `$${name}`
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
2160
|
-
if (literalType === 'string') return `'${value}'`
|
|
2161
|
-
if (literalType === 'boolean') return value ? '1' : '0'
|
|
2162
|
-
if (literalType === 'null') return 'nil'
|
|
2163
|
-
return String(value)
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
2167
|
-
// `.length` — route through `$bf.length` (handles both array element
|
|
2168
|
-
// count and string char count, JS-compatibly). Kolon's builtin `.size()`
|
|
2169
|
-
// is array-only and faults on a string.
|
|
2170
|
-
if (property === 'length') {
|
|
2171
|
-
return `$bf.length(${emit(object)})`
|
|
2172
|
-
}
|
|
2173
|
-
// Hash field access — Kolon dot works on hash refs.
|
|
2174
|
-
return `${emit(object)}.${property}`
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2178
|
-
// Kolon's `[]` postfix is polymorphic (array index or hash key),
|
|
2179
|
-
// mirroring JS — no array/hash split is needed (unlike Perl's
|
|
2180
|
-
// `->[]` vs `->{}`). #1897 (data-table's `selected()[index]`).
|
|
2181
|
-
return `${emit(object)}[${emit(index)}]`
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
2185
|
-
// Signal getter calls: filter() → $filter
|
|
2186
|
-
if (callee.kind === 'identifier' && args.length === 0) {
|
|
2187
|
-
return `$${callee.name}`
|
|
2188
|
-
}
|
|
2189
|
-
return emit(callee)
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2193
|
-
const arg = emit(argument)
|
|
2194
|
-
if (op === '!') {
|
|
2195
|
-
const needsParens = argument.kind === 'binary' || argument.kind === 'logical'
|
|
2196
|
-
return needsParens ? `!(${arg})` : `!${arg}`
|
|
2197
|
-
}
|
|
2198
|
-
if (op === '-') return `-${arg}`
|
|
2199
|
-
return arg
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2203
|
-
const l = emit(left)
|
|
2204
|
-
const r = emit(right)
|
|
2205
|
-
// Kolon's `==` / `!=` are value-equality operators that compare strings
|
|
2206
|
-
// and numbers correctly — unlike Perl's numeric `==` (which the Mojo
|
|
2207
|
-
// adapter must steer around with `eq`/`ne`). Kolon has no `eq`/`ne`
|
|
2208
|
-
// operator at all, so string comparisons stay on `==` / `!=` here.
|
|
2209
|
-
const opMap: Record<string, string> = {
|
|
2210
|
-
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
2211
|
-
'+': '+', '-': '-', '*': '*', '/': '/',
|
|
2212
|
-
}
|
|
2213
|
-
return `${l} ${opMap[op] ?? op} ${r}`
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2217
|
-
const l = emit(left)
|
|
2218
|
-
const r = emit(right)
|
|
2219
|
-
if (op === '&&') return `(${l} && ${r})`
|
|
2220
|
-
if (op === '||') return `(${l} || ${r})`
|
|
2221
|
-
return `(${l} // ${r})`
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
higherOrder(
|
|
2225
|
-
method: HigherOrderMethod,
|
|
2226
|
-
object: ParsedExpr,
|
|
2227
|
-
param: string,
|
|
2228
|
-
predicate: ParsedExpr,
|
|
2229
|
-
emit: (e: ParsedExpr) => string,
|
|
2230
|
-
): string {
|
|
2231
|
-
// Nested higher-order inside a filter predicate has no Kolon scalar form;
|
|
2232
|
-
// defer to the receiver so the predicate at least references a real value
|
|
2233
|
-
// (a richer chain would surface its own diagnostic at the top level).
|
|
2234
|
-
void method
|
|
2235
|
-
void param
|
|
2236
|
-
void predicate
|
|
2237
|
-
return emit(object)
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
2241
|
-
return `[${elements.map(emit).join(', ')}]`
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
arrayMethod(
|
|
2245
|
-
method: ArrayMethod,
|
|
2246
|
-
object: ParsedExpr,
|
|
2247
|
-
args: ParsedExpr[],
|
|
2248
|
-
emit: (e: ParsedExpr) => string,
|
|
2249
|
-
): string {
|
|
2250
|
-
return renderArrayMethod(method, object, args, emit)
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
sortMethod(
|
|
2254
|
-
_method: 'sort' | 'toSorted',
|
|
2255
|
-
object: ParsedExpr,
|
|
2256
|
-
comparator: SortComparator,
|
|
2257
|
-
emit: (e: ParsedExpr) => string,
|
|
2258
|
-
): string {
|
|
2259
|
-
return renderSortMethod(emit(object), comparator)
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
reduceMethod(method: 'reduce' | 'reduceRight', object: ParsedExpr, reduceOp: ReduceOp, emit: (e: ParsedExpr) => string): string {
|
|
2263
|
-
return renderReduceMethod(emit(object), reduceOp, method === 'reduceRight' ? 'right' : 'left')
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
|
|
2267
|
-
return renderFlatMethod(emit(object), depth)
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
flatMapMethod(object: ParsedExpr, op: FlatMapOp, emit: (e: ParsedExpr) => string): string {
|
|
2271
|
-
return renderFlatMapMethod(emit(object), op)
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
|
|
2275
|
-
return '1'
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
templateLiteral(_parts: TemplatePart[]): string {
|
|
2279
|
-
return '1'
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
arrowFn(_param: string, _body: ParsedExpr): string {
|
|
2283
|
-
return '1'
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
unsupported(_raw: string, _reason: string): string {
|
|
2287
|
-
return '1'
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
/**
|
|
2292
|
-
* Lowering for top-level expressions whose identifiers resolve against the
|
|
2293
|
-
* Kolon template's per-render vars (signals, props, locals introduced by `:
|
|
2294
|
-
* my $x = ...` lines). Differs from the filter emitter mainly in
|
|
2295
|
-
* - `.length` → `.size()` (Kolon array length),
|
|
2296
|
-
* - `conditional` is supported (filter predicates can't return ternaries),
|
|
2297
|
-
* - higher-order methods route through `$bf` array helpers.
|
|
2298
|
-
*/
|
|
2299
|
-
class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
2300
|
-
constructor(private readonly adapter: XslateAdapter) {}
|
|
2301
|
-
|
|
2302
|
-
identifier(name: string): string {
|
|
2303
|
-
// `undefined` / `null` nested inside a larger expression tree —
|
|
2304
|
-
// Kolon `nil` (#1897).
|
|
2305
|
-
if (name === 'undefined' || name === 'null') return 'nil'
|
|
2306
|
-
// Inline a module-scope pure-string const (`const x = 'literal'`) — it
|
|
2307
|
-
// never reaches the per-render stash, so a bare `$x` would render empty.
|
|
2308
|
-
const inlined = this.adapter._resolveModuleStringConst(name)
|
|
2309
|
-
if (inlined !== null) return inlined
|
|
2310
|
-
// Same for a literal const of any scope (`const totalPages = 5`,
|
|
2311
|
-
// #1897 pagination's `Page {currentPage()} of {totalPages}`).
|
|
2312
|
-
const literalConst = this.adapter._resolveLiteralConst(name)
|
|
2313
|
-
if (literalConst !== null) return literalConst
|
|
2314
|
-
return `$${name}`
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
literal(value: string | number | boolean | null, literalType: LiteralType): string {
|
|
2318
|
-
if (literalType === 'string') return `'${value}'`
|
|
2319
|
-
if (literalType === 'boolean') return value ? '1' : '0'
|
|
2320
|
-
if (literalType === 'null') return 'nil'
|
|
2321
|
-
return String(value)
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
2325
|
-
// `props.x` flattens to the bare `$x` the SSR caller binds each prop to
|
|
2326
|
-
// (props arrive as individual top-level vars, not a `$props` hashref).
|
|
2327
|
-
if (object.kind === 'identifier' && object.name === 'props') {
|
|
2328
|
-
return `$${property}`
|
|
2329
|
-
}
|
|
2330
|
-
// Static property access on a module object-literal const
|
|
2331
|
-
// (`variantClasses.ghost`, #1897) resolves at compile time — the
|
|
2332
|
-
// generic dot lowering below would reference a Kolon var that
|
|
2333
|
-
// doesn't exist server-side and silently render ''.
|
|
2334
|
-
if (object.kind === 'identifier') {
|
|
2335
|
-
const staticValue = this.adapter._resolveStaticRecordLiteral(object.name, property)
|
|
2336
|
-
if (staticValue !== null) return staticValue
|
|
2337
|
-
}
|
|
2338
|
-
const obj = emit(object)
|
|
2339
|
-
// `.length` → `$bf.length` (array count or string char count, JS-compat);
|
|
2340
|
-
// Kolon's builtin `.size()` is array-only and faults on a string.
|
|
2341
|
-
if (property === 'length') return `$bf.length(${obj})`
|
|
2342
|
-
// Kolon dot access works for hash refs.
|
|
2343
|
-
return `${obj}.${property}`
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2347
|
-
// Kolon's `[]` postfix is polymorphic (array index or hash key),
|
|
2348
|
-
// mirroring JS. #1897 (data-table's `selected()[index]`).
|
|
2349
|
-
return `${emit(object)}[${emit(index)}]`
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
call(callee: ParsedExpr, args: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
2353
|
-
// Signal getter: count() → $count
|
|
2354
|
-
if (callee.kind === 'identifier' && args.length === 0) {
|
|
2355
|
-
return `$${callee.name}`
|
|
2356
|
-
}
|
|
2357
|
-
// Env-signal method call (#1922): `searchParams().get('sort')` is a real
|
|
2358
|
-
// method call on the per-request `$searchParams` reader object, not the
|
|
2359
|
-
// generic dot deref `member` would emit (`$searchParams.get`, which drops
|
|
2360
|
-
// the arg). Matches the local import binding (incl. an alias).
|
|
2361
|
-
if (this.adapter._searchParamsLocals.size > 0) {
|
|
2362
|
-
const sp = matchSearchParamsMethodCall(callee, args, this.adapter._searchParamsLocals)
|
|
2363
|
-
if (sp) {
|
|
2364
|
-
return `$searchParams.${sp.method}(${sp.args.map(emit).join(', ')})`
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
// Identifier-path templatePrimitive: `JSON.stringify(x)` / `Math.floor(x)`
|
|
2368
|
-
// → `$bf.json($x)` / `$bf.floor($x)`. Args render recursively through this
|
|
2369
|
-
// same emitter. A wrong-arity call records BF101 and returns `''`.
|
|
2370
|
-
const path = identifierPath(callee)
|
|
2371
|
-
const spec = path ? XSLATE_TEMPLATE_PRIMITIVES[path] : undefined
|
|
2372
|
-
if (path && spec) {
|
|
2373
|
-
if (args.length === spec.arity) {
|
|
2374
|
-
return spec.emit(args.map(emit))
|
|
2375
|
-
}
|
|
2376
|
-
this.adapter._recordExprBF101(
|
|
2377
|
-
`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`,
|
|
2378
|
-
`Call '${path}' with exactly ${spec.arity} argument(s).`,
|
|
2379
|
-
)
|
|
2380
|
-
return "''"
|
|
2381
|
-
}
|
|
2382
|
-
return emit(callee)
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
unary(op: string, argument: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2386
|
-
const arg = emit(argument)
|
|
2387
|
-
if (op === '!') return `!${arg}`
|
|
2388
|
-
if (op === '-') return `-${arg}`
|
|
2389
|
-
return arg
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2393
|
-
const l = emit(left)
|
|
2394
|
-
const r = emit(right)
|
|
2395
|
-
// Kolon's `==` / `!=` are value-equality operators handling both strings
|
|
2396
|
-
// and numbers (unlike Perl's numeric `==`, which the Mojo adapter must
|
|
2397
|
-
// route around with `eq`/`ne`). Kolon has no `eq`/`ne` operator, so all
|
|
2398
|
-
// equality comparisons — string or numeric — stay on `==` / `!=`.
|
|
2399
|
-
const opMap: Record<string, string> = {
|
|
2400
|
-
'===': '==', '!==': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=',
|
|
2401
|
-
'+': '+', '-': '-', '*': '*',
|
|
2402
|
-
}
|
|
2403
|
-
return `${l} ${opMap[op] ?? op} ${r}`
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
logical(op: '&&' | '||' | '??', left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
2407
|
-
const l = emit(left)
|
|
2408
|
-
const r = emit(right)
|
|
2409
|
-
if (op === '&&') return `(${l} && ${r})`
|
|
2410
|
-
if (op === '||') return `(${l} || ${r})`
|
|
2411
|
-
return `(${l} // ${r})`
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
higherOrder(
|
|
2415
|
-
method: HigherOrderMethod,
|
|
2416
|
-
object: ParsedExpr,
|
|
2417
|
-
param: string,
|
|
2418
|
-
predicate: ParsedExpr,
|
|
2419
|
-
emit: (e: ParsedExpr) => string,
|
|
2420
|
-
): string {
|
|
2421
|
-
// Higher-order array methods all take a JS arrow predicate, lowered to a
|
|
2422
|
-
// Kolon lambda `-> $param { PRED }` (callable from Perl as a code ref), and
|
|
2423
|
-
// go through the runtime object — consistent with the other array helpers
|
|
2424
|
-
// ($bf.includes / $bf.slice / ...). `.find*` map to snake_case runtime
|
|
2425
|
-
// methods (like index_of / last_index_of). The `.filter(...).map(...)`
|
|
2426
|
-
// *loop* form is handled separately by renderLoop's inline predicate.
|
|
2427
|
-
const arrayExpr = emit(object)
|
|
2428
|
-
const predBody = this.adapter._renderKolonFilterExprPublic(predicate, param)
|
|
2429
|
-
const lambda = `-> $${param} { ${predBody} }`
|
|
2430
|
-
const fn: Record<string, string> = {
|
|
2431
|
-
filter: 'filter',
|
|
2432
|
-
every: 'every',
|
|
2433
|
-
some: 'some',
|
|
2434
|
-
find: 'find',
|
|
2435
|
-
findIndex: 'find_index',
|
|
2436
|
-
findLast: 'find_last',
|
|
2437
|
-
findLastIndex: 'find_last_index',
|
|
2438
|
-
}
|
|
2439
|
-
if (fn[method]) return `$bf.${fn[method]}(${arrayExpr}, ${lambda})`
|
|
2440
|
-
void predicate
|
|
2441
|
-
void param
|
|
2442
|
-
return emit(object)
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
arrayLiteral(elements: ParsedExpr[], emit: (e: ParsedExpr) => string): string {
|
|
2446
|
-
return `[${elements.map(emit).join(', ')}]`
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
arrayMethod(
|
|
2450
|
-
method: ArrayMethod,
|
|
2451
|
-
object: ParsedExpr,
|
|
2452
|
-
args: ParsedExpr[],
|
|
2453
|
-
emit: (e: ParsedExpr) => string,
|
|
2454
|
-
): string {
|
|
2455
|
-
return renderArrayMethod(method, object, args, emit)
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
sortMethod(
|
|
2459
|
-
_method: 'sort' | 'toSorted',
|
|
2460
|
-
object: ParsedExpr,
|
|
2461
|
-
comparator: SortComparator,
|
|
2462
|
-
emit: (e: ParsedExpr) => string,
|
|
2463
|
-
): string {
|
|
2464
|
-
return renderSortMethod(emit(object), comparator)
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
reduceMethod(method: 'reduce' | 'reduceRight', object: ParsedExpr, reduceOp: ReduceOp, emit: (e: ParsedExpr) => string): string {
|
|
2468
|
-
return renderReduceMethod(emit(object), reduceOp, method === 'reduceRight' ? 'right' : 'left')
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
|
|
2472
|
-
return renderFlatMethod(emit(object), depth)
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
flatMapMethod(object: ParsedExpr, op: FlatMapOp, emit: (e: ParsedExpr) => string): string {
|
|
2476
|
-
return renderFlatMapMethod(emit(object), op)
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
conditional(
|
|
2480
|
-
test: ParsedExpr,
|
|
2481
|
-
consequent: ParsedExpr,
|
|
2482
|
-
alternate: ParsedExpr,
|
|
2483
|
-
emit: (e: ParsedExpr) => string,
|
|
2484
|
-
): string {
|
|
2485
|
-
return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
templateLiteral(parts: TemplatePart[], emit: (e: ParsedExpr) => string): string {
|
|
2489
|
-
// `` `n=${count() + 1}` `` → Kolon string concatenation (`~`):
|
|
2490
|
-
// `'n=' ~ ($count + 1)`. Kolon's `~` is the explicit concat operator.
|
|
2491
|
-
const terms: string[] = []
|
|
2492
|
-
for (const part of parts) {
|
|
2493
|
-
if (part.type === 'string') {
|
|
2494
|
-
if (part.value !== '') {
|
|
2495
|
-
terms.push(`'${part.value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`)
|
|
2496
|
-
}
|
|
2497
|
-
} else {
|
|
2498
|
-
const rendered = emit(part.expr)
|
|
2499
|
-
const needsParens =
|
|
2500
|
-
part.expr.kind === 'binary' ||
|
|
2501
|
-
part.expr.kind === 'logical' ||
|
|
2502
|
-
part.expr.kind === 'conditional'
|
|
2503
|
-
terms.push(needsParens ? `(${rendered})` : rendered)
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
if (terms.length === 0) return `''`
|
|
2507
|
-
return terms.join(' ~ ')
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
|
-
arrowFn(_param: string, _body: ParsedExpr): string {
|
|
2511
|
-
return "''"
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
unsupported(_raw: string, _reason: string): string {
|
|
2515
|
-
return "''"
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
1455
|
export const xslateAdapter = new XslateAdapter()
|