@barefootjs/mojolicious 0.16.0 → 0.17.1
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 +96 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +85 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +78 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +34 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.js +1482 -1339
- package/dist/adapter/lib/constants.d.ts +27 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +33 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/perl-naming.d.ts +29 -0
- package/dist/adapter/lib/perl-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +28 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +34 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/mojo-adapter.d.ts +46 -104
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/adapter/props/prop-classes.d.ts +48 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +64 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +26 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.js +1482 -1339
- package/dist/index.js +1482 -1339
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
- package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +286 -69
- package/src/__tests__/query-href.test.ts +94 -0
- package/src/adapter/analysis/component-tree.ts +128 -0
- package/src/adapter/emit-context.ts +107 -0
- package/src/adapter/expr/array-method.ts +429 -0
- package/src/adapter/expr/emitters.ts +639 -0
- package/src/adapter/expr/operand.ts +55 -0
- package/src/adapter/lib/constants.ts +39 -0
- package/src/adapter/lib/ir-scope.ts +57 -0
- package/src/adapter/lib/perl-naming.ts +36 -0
- package/src/adapter/lib/types.ts +31 -0
- package/src/adapter/memo/seed.ts +73 -0
- package/src/adapter/mojo-adapter.ts +248 -1485
- package/src/adapter/props/prop-classes.ts +87 -0
- package/src/adapter/spread/spread-codegen.ts +181 -0
- package/src/adapter/value/parsed-literal.ts +34 -0
- package/src/test-render.ts +2 -0
package/dist/adapter/index.js
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
// src/adapter/mojo-adapter.ts
|
|
2
|
-
import ts from "typescript";
|
|
3
2
|
import {
|
|
4
3
|
BaseAdapter,
|
|
5
4
|
isBooleanAttr,
|
|
6
5
|
parseExpression as parseExpression2,
|
|
6
|
+
stringifyParsedExpr as stringifyParsedExpr2,
|
|
7
7
|
parseStyleObjectEntries,
|
|
8
8
|
isSupported,
|
|
9
9
|
exprToString,
|
|
10
10
|
parseProviderObjectLiteral,
|
|
11
|
-
|
|
12
|
-
emitParsedExpr,
|
|
11
|
+
emitParsedExpr as emitParsedExpr2,
|
|
13
12
|
emitIRNode,
|
|
14
13
|
emitAttrValue,
|
|
15
14
|
augmentInheritedPropAccesses,
|
|
16
|
-
parseRecordIndexAccess,
|
|
17
|
-
evalStringArrayJoin,
|
|
18
|
-
extractArrowBodyExpression,
|
|
19
|
-
collectContextConsumers,
|
|
20
15
|
isLowerableObjectRestDestructure,
|
|
21
16
|
collectModuleStringConsts,
|
|
22
17
|
lookupStaticRecordLiteral,
|
|
23
18
|
searchParamsLocalNames,
|
|
24
|
-
|
|
19
|
+
prepareLoweringMatchers,
|
|
20
|
+
queryHrefArgs,
|
|
21
|
+
sortComparatorFromArrow as sortComparatorFromArrow2
|
|
25
22
|
} from "@barefootjs/jsx";
|
|
26
23
|
|
|
27
24
|
// src/adapter/boolean-result.ts
|
|
@@ -79,6 +76,8 @@ function isAriaBooleanAttr(name) {
|
|
|
79
76
|
|
|
80
77
|
// src/adapter/mojo-adapter.ts
|
|
81
78
|
import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
|
|
79
|
+
|
|
80
|
+
// src/adapter/lib/constants.ts
|
|
82
81
|
var MOJO_TEMPLATE_PRIMITIVES = {
|
|
83
82
|
"JSON.stringify": { arity: 1, emit: (args) => `bf->json(${args[0]})` },
|
|
84
83
|
String: { arity: 1, emit: (args) => `bf->string(${args[0]})` },
|
|
@@ -88,6 +87,16 @@ var MOJO_TEMPLATE_PRIMITIVES = {
|
|
|
88
87
|
"Math.round": { arity: 1, emit: (args) => `bf->round(${args[0]})` }
|
|
89
88
|
};
|
|
90
89
|
var MOJO_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(MOJO_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
|
|
90
|
+
|
|
91
|
+
// src/adapter/lib/perl-naming.ts
|
|
92
|
+
function perlHashKey(name) {
|
|
93
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
|
|
94
|
+
}
|
|
95
|
+
function perlIdentifierFromMarkerId(markerId) {
|
|
96
|
+
return markerId.replace(/[^a-zA-Z0-9]/g, (ch) => ch === "_" ? "__" : `_x${ch.charCodeAt(0).toString(16)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/adapter/lib/ir-scope.ts
|
|
91
100
|
function resolveJsxChildrenProp(props) {
|
|
92
101
|
const prop = props.find((p) => p.name === "children");
|
|
93
102
|
if (!prop)
|
|
@@ -96,9 +105,6 @@ function resolveJsxChildrenProp(props) {
|
|
|
96
105
|
return [];
|
|
97
106
|
return prop.value.children;
|
|
98
107
|
}
|
|
99
|
-
function perlHashKey(name) {
|
|
100
|
-
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
|
|
101
|
-
}
|
|
102
108
|
function collectRootScopeNodes(node) {
|
|
103
109
|
const out = new Set;
|
|
104
110
|
const visit = (n) => {
|
|
@@ -122,1482 +128,1619 @@ function collectRootScopeNodes(node) {
|
|
|
122
128
|
visit(node);
|
|
123
129
|
return out;
|
|
124
130
|
}
|
|
125
|
-
function referencedVarsAreAvailable(expr, available) {
|
|
126
|
-
for (const m of expr.matchAll(/\$([A-Za-z_]\w*)/g)) {
|
|
127
|
-
if (!available.has(m[1]))
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.booleanTypedProps = new Set(ir.metadata.propsParams.filter((prop) => prop.type?.primitive === "boolean" || prop.type?.raw === "boolean").map((prop) => prop.name));
|
|
166
|
-
this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
|
|
167
|
-
this.stringValueNames = new Set;
|
|
168
|
-
for (const s of ir.metadata.signals) {
|
|
169
|
-
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
170
|
-
this.stringValueNames.add(s.getter);
|
|
132
|
+
// src/adapter/expr/array-method.ts
|
|
133
|
+
import {
|
|
134
|
+
serializeParsedExpr,
|
|
135
|
+
freeVarsInBody
|
|
136
|
+
} from "@barefootjs/jsx";
|
|
137
|
+
function renderArrayMethod(method, object, args, emit) {
|
|
138
|
+
switch (method) {
|
|
139
|
+
case "join": {
|
|
140
|
+
const obj = emit(object);
|
|
141
|
+
const sep = args.length >= 1 ? emit(args[0]) : `','`;
|
|
142
|
+
return `join(${sep}, @{${obj}})`;
|
|
143
|
+
}
|
|
144
|
+
case "includes": {
|
|
145
|
+
const obj = emit(object);
|
|
146
|
+
const needle = emit(args[0]);
|
|
147
|
+
return `bf->includes(${obj}, ${needle})`;
|
|
148
|
+
}
|
|
149
|
+
case "indexOf":
|
|
150
|
+
case "lastIndexOf": {
|
|
151
|
+
const fn = method === "indexOf" ? "index_of" : "last_index_of";
|
|
152
|
+
const obj = emit(object);
|
|
153
|
+
const needle = emit(args[0]);
|
|
154
|
+
return `bf->${fn}(${obj}, ${needle})`;
|
|
155
|
+
}
|
|
156
|
+
case "at": {
|
|
157
|
+
const obj = emit(object);
|
|
158
|
+
const idx = args.length >= 1 ? emit(args[0]) : "0";
|
|
159
|
+
return `bf->at(${obj}, ${idx})`;
|
|
160
|
+
}
|
|
161
|
+
case "concat": {
|
|
162
|
+
if (args.length === 0) {
|
|
163
|
+
return emit(object);
|
|
171
164
|
}
|
|
165
|
+
const a = emit(object);
|
|
166
|
+
const b = emit(args[0]);
|
|
167
|
+
return `bf->concat(${a}, ${b})`;
|
|
172
168
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
case "slice": {
|
|
170
|
+
const recv = emit(object);
|
|
171
|
+
const start = args.length >= 1 ? emit(args[0]) : "0";
|
|
172
|
+
const end = args.length >= 2 ? emit(args[1]) : "undef";
|
|
173
|
+
return `bf->slice(${recv}, ${start}, ${end})`;
|
|
176
174
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
this.errors = [];
|
|
182
|
-
this.childrenCaptureCounter = 0;
|
|
183
|
-
if (!options?.siblingTemplatesRegistered) {
|
|
184
|
-
this.checkImportedLoopChildComponents(ir);
|
|
175
|
+
case "reverse":
|
|
176
|
+
case "toReversed": {
|
|
177
|
+
const recv = emit(object);
|
|
178
|
+
return `bf->reverse(${recv})`;
|
|
185
179
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const ctxSeed = this.generateContextConsumerSeed(ir);
|
|
190
|
-
const memoSeed = this.generateDerivedMemoSeed(ir);
|
|
191
|
-
const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
|
|
192
|
-
`;
|
|
193
|
-
if (this.errors.length > 0) {
|
|
194
|
-
ir.errors.push(...this.errors);
|
|
180
|
+
case "toLowerCase": {
|
|
181
|
+
const recv = emit(object);
|
|
182
|
+
return `lc(${recv})`;
|
|
195
183
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
184
|
+
case "toUpperCase": {
|
|
185
|
+
const recv = emit(object);
|
|
186
|
+
return `uc(${recv})`;
|
|
187
|
+
}
|
|
188
|
+
case "trim": {
|
|
189
|
+
const recv = emit(object);
|
|
190
|
+
return `bf->trim(${recv})`;
|
|
191
|
+
}
|
|
192
|
+
case "toFixed": {
|
|
193
|
+
const recv = emit(object);
|
|
194
|
+
const digits = args.length >= 1 ? emit(args[0]) : "0";
|
|
195
|
+
return `bf->to_fixed(${recv}, ${digits})`;
|
|
196
|
+
}
|
|
197
|
+
case "split": {
|
|
198
|
+
const recv = emit(object);
|
|
199
|
+
if (args.length === 0) {
|
|
200
|
+
return `bf->split(${recv})`;
|
|
201
|
+
}
|
|
202
|
+
const sep = emit(args[0]);
|
|
203
|
+
if (args.length === 1) {
|
|
204
|
+
return `bf->split(${recv}, ${sep})`;
|
|
205
|
+
}
|
|
206
|
+
const limit = emit(args[1]);
|
|
207
|
+
return `bf->split(${recv}, ${sep}, ${limit})`;
|
|
208
|
+
}
|
|
209
|
+
case "startsWith":
|
|
210
|
+
case "endsWith": {
|
|
211
|
+
const fn = method === "startsWith" ? "starts_with" : "ends_with";
|
|
212
|
+
const recv = emit(object);
|
|
213
|
+
const arg = emit(args[0]);
|
|
214
|
+
if (args.length >= 2) {
|
|
215
|
+
return `bf->${fn}(${recv}, ${arg}, ${emit(args[1])})`;
|
|
216
|
+
}
|
|
217
|
+
return `bf->${fn}(${recv}, ${arg})`;
|
|
218
|
+
}
|
|
219
|
+
case "replace": {
|
|
220
|
+
const recv = emit(object);
|
|
221
|
+
const oldS = emit(args[0]);
|
|
222
|
+
const newS = emit(args[1]);
|
|
223
|
+
return `bf->replace(${recv}, ${oldS}, ${newS})`;
|
|
224
|
+
}
|
|
225
|
+
case "repeat": {
|
|
226
|
+
const recv = emit(object);
|
|
227
|
+
const count = args.length === 0 ? "0" : emit(args[0]);
|
|
228
|
+
return `bf->repeat(${recv}, ${count})`;
|
|
229
|
+
}
|
|
230
|
+
case "padStart":
|
|
231
|
+
case "padEnd": {
|
|
232
|
+
const fn = method === "padStart" ? "pad_start" : "pad_end";
|
|
233
|
+
const recv = emit(object);
|
|
234
|
+
if (args.length === 0) {
|
|
235
|
+
return `bf->${fn}(${recv}, 0)`;
|
|
236
|
+
}
|
|
237
|
+
const target = emit(args[0]);
|
|
238
|
+
if (args.length === 1) {
|
|
239
|
+
return `bf->${fn}(${recv}, ${target})`;
|
|
240
|
+
}
|
|
241
|
+
const pad = emit(args[1]);
|
|
242
|
+
return `bf->${fn}(${recv}, ${target}, ${pad})`;
|
|
243
|
+
}
|
|
244
|
+
default: {
|
|
245
|
+
const _exhaustive = method;
|
|
246
|
+
throw new Error(`renderArrayMethod: unhandled ArrayMethod '${_exhaustive}'`);
|
|
212
247
|
}
|
|
213
|
-
if (!/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
214
|
-
return false;
|
|
215
|
-
return this.booleanTypedProps.has(bare);
|
|
216
248
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
249
|
+
}
|
|
250
|
+
function escapePerlSingleQuote(s) {
|
|
251
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
252
|
+
}
|
|
253
|
+
function emitEvalEnvArg(body, params, emit) {
|
|
254
|
+
const free = freeVarsInBody(body, new Set(params));
|
|
255
|
+
if (free.length === 0)
|
|
256
|
+
return "{}";
|
|
257
|
+
const pairs = free.map((n) => `'${escapePerlSingleQuote(n)}' => ${emit({ kind: "identifier", name: n })}`);
|
|
258
|
+
return `{ ${pairs.join(", ")} }`;
|
|
259
|
+
}
|
|
260
|
+
function renderSortEval(recv, body, params, emit) {
|
|
261
|
+
if (params.length < 2)
|
|
262
|
+
return null;
|
|
263
|
+
const [paramA, paramB] = params;
|
|
264
|
+
const json = serializeParsedExpr(body);
|
|
265
|
+
if (json === null)
|
|
266
|
+
return null;
|
|
267
|
+
const env = emitEvalEnvArg(body, [paramA, paramB], emit);
|
|
268
|
+
return `bf->sort_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramA}', '${paramB}', ${env})`;
|
|
269
|
+
}
|
|
270
|
+
function renderReduceEval(recv, body, params, init, direction, emit) {
|
|
271
|
+
if (params.length < 2)
|
|
272
|
+
return null;
|
|
273
|
+
const [paramAcc, paramItem] = params;
|
|
274
|
+
const json = serializeParsedExpr(body);
|
|
275
|
+
if (json === null)
|
|
276
|
+
return null;
|
|
277
|
+
let initPerl;
|
|
278
|
+
if (init.kind === "literal" && init.literalType === "string") {
|
|
279
|
+
initPerl = `'${escapePerlSingleQuote(String(init.value))}'`;
|
|
280
|
+
} else if (init.kind === "literal" && init.literalType === "number") {
|
|
281
|
+
initPerl = String(init.value);
|
|
282
|
+
} else {
|
|
283
|
+
return null;
|
|
229
284
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
285
|
+
const env = emitEvalEnvArg(body, [paramAcc, paramItem], emit);
|
|
286
|
+
return `bf->reduce_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramAcc}', '${paramItem}', ${initPerl}, '${direction}', ${env})`;
|
|
287
|
+
}
|
|
288
|
+
function renderPredicateEval(funcName, recv, predicate, param, emit, forward) {
|
|
289
|
+
const json = serializeParsedExpr(predicate);
|
|
290
|
+
if (json === null)
|
|
291
|
+
return null;
|
|
292
|
+
const env = emitEvalEnvArg(predicate, [param], emit);
|
|
293
|
+
const fwd = forward === undefined ? "" : `, ${forward ? 1 : 0}`;
|
|
294
|
+
return `bf->${funcName}(${recv}, '${escapePerlSingleQuote(json)}', '${param}'${fwd}, ${env})`;
|
|
295
|
+
}
|
|
296
|
+
function renderFlatMapEval(recv, body, param, emit) {
|
|
297
|
+
const json = serializeParsedExpr(body);
|
|
298
|
+
if (json === null)
|
|
299
|
+
return null;
|
|
300
|
+
const env = emitEvalEnvArg(body, [param], emit);
|
|
301
|
+
return `bf->flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
302
|
+
}
|
|
303
|
+
function renderMapEval(recv, body, param, emit) {
|
|
304
|
+
const json = serializeParsedExpr(body);
|
|
305
|
+
if (json === null)
|
|
242
306
|
return null;
|
|
307
|
+
const env = emitEvalEnvArg(body, [param], emit);
|
|
308
|
+
return `bf->map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
|
|
309
|
+
}
|
|
310
|
+
function renderSortMethod(recv, c) {
|
|
311
|
+
const keyHashes = c.keys.map((k) => {
|
|
312
|
+
const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
|
|
313
|
+
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
|
|
314
|
+
});
|
|
315
|
+
return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
|
|
316
|
+
}
|
|
317
|
+
function renderFlatMethod(recv, depth) {
|
|
318
|
+
const d = depth === "infinity" ? -1 : depth;
|
|
319
|
+
return `bf->flat(${recv}, ${d})`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/adapter/expr/emitters.ts
|
|
323
|
+
import {
|
|
324
|
+
emitParsedExpr,
|
|
325
|
+
identifierPath,
|
|
326
|
+
matchSearchParamsMethodCall,
|
|
327
|
+
sortComparatorFromArrow,
|
|
328
|
+
asCallbackMethodCall
|
|
329
|
+
} from "@barefootjs/jsx";
|
|
330
|
+
|
|
331
|
+
// src/adapter/expr/operand.ts
|
|
332
|
+
function isStringTypedOperand(expr, isStringName) {
|
|
333
|
+
if (expr.kind === "literal" && expr.literalType === "string")
|
|
334
|
+
return true;
|
|
335
|
+
if (expr.kind === "call" && expr.callee.kind === "identifier" && expr.args.length === 0) {
|
|
336
|
+
return isStringName(expr.callee.name);
|
|
243
337
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return null;
|
|
247
|
-
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
248
|
-
if (!hit)
|
|
249
|
-
return null;
|
|
250
|
-
return hit.kind === "number" ? hit.text : `'${hit.text.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
338
|
+
if (expr.kind === "member" && expr.object.kind === "identifier" && expr.object.name === "props") {
|
|
339
|
+
return isStringName(expr.property);
|
|
251
340
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
function emitIndexAccessPerl(object, index, emit, isStringName) {
|
|
344
|
+
const i = emit(index);
|
|
345
|
+
return isStringTypedOperand(index, isStringName) ? `${emit(object)}->{${i}}` : `${emit(object)}->[${i}]`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/adapter/expr/emitters.ts
|
|
349
|
+
var PREDICATE_METHODS = new Set([
|
|
350
|
+
"filter",
|
|
351
|
+
"find",
|
|
352
|
+
"findIndex",
|
|
353
|
+
"findLast",
|
|
354
|
+
"findLastIndex",
|
|
355
|
+
"every",
|
|
356
|
+
"some"
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
class MojoFilterEmitter {
|
|
360
|
+
param;
|
|
361
|
+
localVarMap;
|
|
362
|
+
isStringName;
|
|
363
|
+
onUnsupported;
|
|
364
|
+
constructor(param, localVarMap, isStringName = () => false, onUnsupported) {
|
|
365
|
+
this.param = param;
|
|
366
|
+
this.localVarMap = localVarMap;
|
|
367
|
+
this.isStringName = isStringName;
|
|
368
|
+
this.onUnsupported = onUnsupported;
|
|
259
369
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const lines = [];
|
|
268
|
-
lines.push(`% bf->register_script('${runtimePath}');`);
|
|
269
|
-
lines.push(`% bf->register_script('${clientJsPath}');`);
|
|
270
|
-
lines.push("");
|
|
271
|
-
return lines.join(`
|
|
272
|
-
`);
|
|
370
|
+
identifier(name) {
|
|
371
|
+
if (name === this.param)
|
|
372
|
+
return `$${this.param}`;
|
|
373
|
+
const signal = this.localVarMap.get(name);
|
|
374
|
+
if (signal)
|
|
375
|
+
return `$${signal}`;
|
|
376
|
+
return `$${name}`;
|
|
273
377
|
}
|
|
274
|
-
|
|
275
|
-
|
|
378
|
+
literal(value, literalType) {
|
|
379
|
+
if (literalType === "string")
|
|
380
|
+
return `'${value}'`;
|
|
381
|
+
if (literalType === "boolean")
|
|
382
|
+
return value ? "1" : "0";
|
|
383
|
+
if (literalType === "null")
|
|
384
|
+
return "undef";
|
|
385
|
+
return String(value);
|
|
276
386
|
}
|
|
277
|
-
|
|
278
|
-
|
|
387
|
+
member(object, property, _computed, emit) {
|
|
388
|
+
if (property === "length" && (asCallbackMethodCall(object) !== null || object.kind === "array-literal")) {
|
|
389
|
+
return `scalar(@{${emit(object)}})`;
|
|
390
|
+
}
|
|
391
|
+
return `${emit(object)}->{${property}}`;
|
|
279
392
|
}
|
|
280
|
-
|
|
281
|
-
return this.
|
|
393
|
+
indexAccess(object, index, emit) {
|
|
394
|
+
return emitIndexAccessPerl(object, index, emit, this.isStringName);
|
|
282
395
|
}
|
|
283
|
-
|
|
284
|
-
|
|
396
|
+
call(callee, args, emit) {
|
|
397
|
+
if (callee.kind === "identifier" && args.length === 0) {
|
|
398
|
+
return `$${callee.name}`;
|
|
399
|
+
}
|
|
400
|
+
return emit(callee);
|
|
285
401
|
}
|
|
286
|
-
|
|
287
|
-
|
|
402
|
+
unary(op, argument, emit) {
|
|
403
|
+
const arg = emit(argument);
|
|
404
|
+
if (op === "!") {
|
|
405
|
+
const needsParens = argument.kind === "binary" || argument.kind === "logical";
|
|
406
|
+
return needsParens ? `!(${arg})` : `!${arg}`;
|
|
407
|
+
}
|
|
408
|
+
if (op === "-")
|
|
409
|
+
return `-${arg}`;
|
|
410
|
+
return arg;
|
|
288
411
|
}
|
|
289
|
-
|
|
290
|
-
|
|
412
|
+
binary(op, left, right, emit) {
|
|
413
|
+
const l = emit(left);
|
|
414
|
+
const r = emit(right);
|
|
415
|
+
const isStr = (e) => isStringTypedOperand(e, this.isStringName);
|
|
416
|
+
const stringCmp = isStr(left) || isStr(right);
|
|
417
|
+
if ((op === "===" || op === "==") && stringCmp) {
|
|
418
|
+
return `${l} eq ${r}`;
|
|
419
|
+
}
|
|
420
|
+
if ((op === "!==" || op === "!=") && stringCmp) {
|
|
421
|
+
return `${l} ne ${r}`;
|
|
422
|
+
}
|
|
423
|
+
const opMap = {
|
|
424
|
+
"===": "==",
|
|
425
|
+
"!==": "!=",
|
|
426
|
+
">": ">",
|
|
427
|
+
"<": "<",
|
|
428
|
+
">=": ">=",
|
|
429
|
+
"<=": "<=",
|
|
430
|
+
"+": "+",
|
|
431
|
+
"-": "-",
|
|
432
|
+
"*": "*",
|
|
433
|
+
"/": "/"
|
|
434
|
+
};
|
|
435
|
+
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
291
436
|
}
|
|
292
|
-
|
|
293
|
-
|
|
437
|
+
logical(op, left, right, emit) {
|
|
438
|
+
const l = emit(left);
|
|
439
|
+
const r = emit(right);
|
|
440
|
+
if (op === "&&")
|
|
441
|
+
return `(${l} && ${r})`;
|
|
442
|
+
if (op === "||")
|
|
443
|
+
return `(${l} || ${r})`;
|
|
444
|
+
return `(${l} // ${r})`;
|
|
294
445
|
}
|
|
295
|
-
|
|
296
|
-
|
|
446
|
+
callbackMethod(method, object, arrow, _restArgs, emit) {
|
|
447
|
+
if (!PREDICATE_METHODS.has(method)) {
|
|
448
|
+
this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
|
|
449
|
+
return "1";
|
|
450
|
+
}
|
|
451
|
+
const param = arrow.params[0];
|
|
452
|
+
const predicate = arrow.body;
|
|
453
|
+
const arrayExpr = emit(object);
|
|
454
|
+
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName, this.onUnsupported));
|
|
455
|
+
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
|
|
456
|
+
if (method === "filter")
|
|
457
|
+
return `[grep { ${grepBody} } @{${arrayExpr}}]`;
|
|
458
|
+
if (method === "every")
|
|
459
|
+
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
460
|
+
if (method === "some")
|
|
461
|
+
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
462
|
+
this.onUnsupported?.(`Filter predicate contains a nested '.${method}(...)' callback, which has no Perl scalar form`, `Rewrite the predicate without a nested callback method, or add /* @client */ for client-only evaluation (no SSR).`);
|
|
463
|
+
return arrayExpr;
|
|
297
464
|
}
|
|
298
|
-
|
|
299
|
-
return
|
|
465
|
+
arrayLiteral(elements, emit) {
|
|
466
|
+
return `[${elements.map(emit).join(", ")}]`;
|
|
300
467
|
}
|
|
301
|
-
|
|
302
|
-
return
|
|
468
|
+
arrayMethod(method, object, args, emit) {
|
|
469
|
+
return renderArrayMethod(method, object, args, emit);
|
|
303
470
|
}
|
|
304
|
-
|
|
305
|
-
return
|
|
471
|
+
flatMethod(object, depth, emit) {
|
|
472
|
+
return renderFlatMethod(emit(object), depth);
|
|
306
473
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const children = this.renderChildren(node.children);
|
|
310
|
-
const name = node.contextName;
|
|
311
|
-
return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
|
|
474
|
+
conditional(_test, _consequent, _alternate) {
|
|
475
|
+
return "1";
|
|
312
476
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (v.kind === "literal") {
|
|
316
|
-
return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
|
|
317
|
-
}
|
|
318
|
-
if (v.kind === "expression") {
|
|
319
|
-
const hashref = this.providerObjectLiteralPerl(v.expr);
|
|
320
|
-
if (hashref !== null)
|
|
321
|
-
return hashref;
|
|
322
|
-
return this.convertExpressionToPerl(v.expr);
|
|
323
|
-
}
|
|
324
|
-
if (v.kind === "template")
|
|
325
|
-
return this.convertTemplateLiteralPartsToPerl(v.parts);
|
|
326
|
-
return "undef";
|
|
477
|
+
templateLiteral(_parts) {
|
|
478
|
+
return "1";
|
|
327
479
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (members === null)
|
|
331
|
-
return null;
|
|
332
|
-
const entries = members.map((m) => {
|
|
333
|
-
const key = `'${m.name.replace(/[\\']/g, (c) => `\\${c}`)}'`;
|
|
334
|
-
if (m.kind === "function" || /^on[A-Z]/.test(m.name))
|
|
335
|
-
return `${key} => undef`;
|
|
336
|
-
const src = m.kind === "getter" ? m.body : m.expr;
|
|
337
|
-
return `${key} => ${this.convertExpressionToPerl(src)}`;
|
|
338
|
-
});
|
|
339
|
-
return `{ ${entries.join(", ")} }`;
|
|
480
|
+
arrow(_params, _body, _emit) {
|
|
481
|
+
return "1";
|
|
340
482
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (d === null || d === undefined)
|
|
344
|
-
return "undef";
|
|
345
|
-
if (typeof d === "string")
|
|
346
|
-
return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
347
|
-
if (typeof d === "boolean")
|
|
348
|
-
return d ? "1" : "0";
|
|
349
|
-
return String(d);
|
|
350
|
-
}
|
|
351
|
-
generateContextConsumerSeed(ir) {
|
|
352
|
-
const consumers = collectContextConsumers(ir.metadata);
|
|
353
|
-
if (consumers.length === 0)
|
|
354
|
-
return "";
|
|
355
|
-
return consumers.map((c) => `% my $${c.localName} = bf->use_context('${c.contextName}', ${this.contextDefaultPerl(c)});`).join(`
|
|
356
|
-
`) + `
|
|
357
|
-
`;
|
|
483
|
+
regex(_raw) {
|
|
484
|
+
return "1";
|
|
358
485
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const signals = ir.metadata.signals ?? [];
|
|
362
|
-
if (memos.length === 0 && signals.length === 0)
|
|
363
|
-
return "";
|
|
364
|
-
const available = new Set(ir.metadata.propsParams.map((p) => p.name));
|
|
365
|
-
const lines = [];
|
|
366
|
-
for (const signal of signals) {
|
|
367
|
-
const perl = this.tryLowerToPerl(signal.initialValue, available);
|
|
368
|
-
if (perl !== null)
|
|
369
|
-
lines.push(`% my $${signal.getter} = ${perl};`);
|
|
370
|
-
available.add(signal.getter);
|
|
371
|
-
}
|
|
372
|
-
for (const memo of memos) {
|
|
373
|
-
const body = extractArrowBodyExpression(memo.computation);
|
|
374
|
-
if (body !== null) {
|
|
375
|
-
const perl = this.tryLowerToPerl(body, available);
|
|
376
|
-
if (perl !== null)
|
|
377
|
-
lines.push(`% my $${memo.name} = ${perl};`);
|
|
378
|
-
}
|
|
379
|
-
available.add(memo.name);
|
|
380
|
-
}
|
|
381
|
-
return lines.length > 0 ? lines.join(`
|
|
382
|
-
`) + `
|
|
383
|
-
` : "";
|
|
486
|
+
unsupported(_raw, _reason) {
|
|
487
|
+
return "1";
|
|
384
488
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (!trimmed)
|
|
388
|
-
return null;
|
|
389
|
-
if (!isSupported(parseExpression2(trimmed)).supported)
|
|
390
|
-
return null;
|
|
391
|
-
const perl = this.convertExpressionToPerl(trimmed);
|
|
392
|
-
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
393
|
-
return null;
|
|
394
|
-
return referencedVarsAreAvailable(perl, available) ? perl : null;
|
|
489
|
+
objectLiteral(_properties, _raw, _emit) {
|
|
490
|
+
return "1";
|
|
395
491
|
}
|
|
396
|
-
|
|
397
|
-
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
class MojoTopLevelEmitter {
|
|
495
|
+
ctx;
|
|
496
|
+
constructor(ctx) {
|
|
497
|
+
this.ctx = ctx;
|
|
398
498
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
499
|
+
identifier(name) {
|
|
500
|
+
if (name === "undefined" || name === "null")
|
|
501
|
+
return "undef";
|
|
502
|
+
const inlined = this.ctx.resolveModuleStringConst(name);
|
|
503
|
+
if (inlined !== null)
|
|
504
|
+
return inlined;
|
|
505
|
+
const literalConst = this.ctx.resolveLiteralConst(name);
|
|
506
|
+
if (literalConst !== null)
|
|
507
|
+
return literalConst;
|
|
508
|
+
return `$${name}`;
|
|
509
|
+
}
|
|
510
|
+
literal(value, literalType) {
|
|
511
|
+
if (literalType === "string")
|
|
512
|
+
return `'${value}'`;
|
|
513
|
+
if (literalType === "boolean")
|
|
514
|
+
return value ? "1" : "0";
|
|
515
|
+
if (literalType === "null")
|
|
516
|
+
return "undef";
|
|
517
|
+
return String(value);
|
|
518
|
+
}
|
|
519
|
+
member(object, property, _computed, emit) {
|
|
520
|
+
if (object.kind === "identifier" && object.name === "props") {
|
|
521
|
+
return `$${property}`;
|
|
406
522
|
}
|
|
407
|
-
if (
|
|
408
|
-
|
|
523
|
+
if (object.kind === "identifier") {
|
|
524
|
+
const staticValue = this.ctx.resolveStaticRecordLiteral(object.name, property);
|
|
525
|
+
if (staticValue !== null)
|
|
526
|
+
return staticValue;
|
|
409
527
|
}
|
|
410
|
-
|
|
411
|
-
|
|
528
|
+
const obj = emit(object);
|
|
529
|
+
if (property === "length")
|
|
530
|
+
return `scalar(@{${obj}})`;
|
|
531
|
+
return `${obj}->{${property}}`;
|
|
532
|
+
}
|
|
533
|
+
indexAccess(object, index, emit) {
|
|
534
|
+
return emitIndexAccessPerl(object, index, emit, (n) => this.ctx._isStringValueName(n));
|
|
535
|
+
}
|
|
536
|
+
call(callee, args, emit) {
|
|
537
|
+
if (callee.kind === "identifier" && args.length === 0) {
|
|
538
|
+
return `$${callee.name}`;
|
|
412
539
|
}
|
|
413
|
-
if (
|
|
414
|
-
|
|
540
|
+
if (this.ctx._searchParamsLocals.size > 0) {
|
|
541
|
+
const sp = matchSearchParamsMethodCall(callee, args, this.ctx._searchParamsLocals);
|
|
542
|
+
if (sp) {
|
|
543
|
+
return `$searchParams->${sp.method}(${sp.args.map(emit).join(", ")})`;
|
|
544
|
+
}
|
|
415
545
|
}
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
"
|
|
424
|
-
"input",
|
|
425
|
-
"link",
|
|
426
|
-
"meta",
|
|
427
|
-
"param",
|
|
428
|
-
"source",
|
|
429
|
-
"track",
|
|
430
|
-
"wbr"
|
|
431
|
-
];
|
|
432
|
-
if (voidElements.includes(tag.toLowerCase())) {
|
|
433
|
-
return `<${tag}${attrs}${hydrationAttrs}>`;
|
|
546
|
+
const path = identifierPath(callee);
|
|
547
|
+
const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
|
|
548
|
+
if (path && spec) {
|
|
549
|
+
if (args.length === spec.arity) {
|
|
550
|
+
return spec.emit(args.map(emit));
|
|
551
|
+
}
|
|
552
|
+
this.ctx._recordExprBF101(`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`, `Call '${path}' with exactly ${spec.arity} argument(s).`);
|
|
553
|
+
return "''";
|
|
434
554
|
}
|
|
435
|
-
return
|
|
555
|
+
return emit(callee);
|
|
436
556
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return
|
|
557
|
+
unary(op, argument, emit) {
|
|
558
|
+
const arg = emit(argument);
|
|
559
|
+
if (op === "!")
|
|
560
|
+
return `!${arg}`;
|
|
561
|
+
if (op === "-")
|
|
562
|
+
return `-${arg}`;
|
|
563
|
+
return arg;
|
|
564
|
+
}
|
|
565
|
+
binary(op, left, right, emit) {
|
|
566
|
+
const l = emit(left);
|
|
567
|
+
const r = emit(right);
|
|
568
|
+
const isStr = (e) => isStringTypedOperand(e, (n) => this.ctx._isStringValueName(n));
|
|
569
|
+
const stringCmp = isStr(left) || isStr(right);
|
|
570
|
+
if ((op === "===" || op === "==") && stringCmp) {
|
|
571
|
+
return `${l} eq ${r}`;
|
|
443
572
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
return `<%== bf->text_start("${expr.slotId}") %><%= ${perlExpr} %><%== bf->text_end %>`;
|
|
573
|
+
if ((op === "!==" || op === "!=") && stringCmp) {
|
|
574
|
+
return `${l} ne ${r}`;
|
|
447
575
|
}
|
|
448
|
-
|
|
576
|
+
const opMap = {
|
|
577
|
+
"===": "==",
|
|
578
|
+
"!==": "!=",
|
|
579
|
+
">": ">",
|
|
580
|
+
"<": "<",
|
|
581
|
+
">=": ">=",
|
|
582
|
+
"<=": "<=",
|
|
583
|
+
"+": "+",
|
|
584
|
+
"-": "-",
|
|
585
|
+
"*": "*"
|
|
586
|
+
};
|
|
587
|
+
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
449
588
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
589
|
+
logical(op, left, right, emit) {
|
|
590
|
+
const l = emit(left);
|
|
591
|
+
const r = emit(right);
|
|
592
|
+
if (op === "&&")
|
|
593
|
+
return `(${l} && ${r})`;
|
|
594
|
+
if (op === "||")
|
|
595
|
+
return `(${l} || ${r})`;
|
|
596
|
+
return `(${l} // ${r})`;
|
|
597
|
+
}
|
|
598
|
+
callbackMethod(method, object, arrow, restArgs, emit) {
|
|
599
|
+
const recv = emit(object);
|
|
600
|
+
const body = arrow.body;
|
|
601
|
+
const params = arrow.params;
|
|
602
|
+
if (method === "sort" || method === "toSorted") {
|
|
603
|
+
const evalForm = renderSortEval(recv, body, params, emit);
|
|
604
|
+
if (evalForm !== null)
|
|
605
|
+
return evalForm;
|
|
606
|
+
const c = sortComparatorFromArrow(arrow);
|
|
607
|
+
if (c !== null)
|
|
608
|
+
return renderSortMethod(recv, c);
|
|
609
|
+
this.ctx._recordExprBF101(`.${method}(...) comparator is not lowerable to a template sort`, `Pre-sort the array in the route handler, or mark the loop @client-only.`);
|
|
610
|
+
return "''";
|
|
453
611
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
markedTrue = this.addCondMarkerToFirstElement(whenTrue, cond.slotId);
|
|
463
|
-
markedFalse = whenFalse ? this.addCondMarkerToFirstElement(whenFalse, cond.slotId) : whenFalse;
|
|
612
|
+
if (method === "reduce" || method === "reduceRight") {
|
|
613
|
+
const direction = method === "reduceRight" ? "right" : "left";
|
|
614
|
+
const init = restArgs[0];
|
|
615
|
+
const evalForm = init !== undefined ? renderReduceEval(recv, body, params, init, direction, emit) : null;
|
|
616
|
+
if (evalForm !== null)
|
|
617
|
+
return evalForm;
|
|
618
|
+
this.ctx._recordExprBF101(`.${method}(...) is not lowerable to a template fold`, `Pre-compute the fold in the route handler, or mark the loop @client-only.`);
|
|
619
|
+
return "''";
|
|
464
620
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
${whenFalse}
|
|
472
|
-
% }
|
|
473
|
-
` : `
|
|
474
|
-
% if (${condition}) {
|
|
475
|
-
${whenTrue}
|
|
476
|
-
% }
|
|
477
|
-
`;
|
|
478
|
-
result = `<%== bf->comment("cond-start:${cond.slotId}") %>${inner}<%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
479
|
-
} else if (markedFalse) {
|
|
480
|
-
result = `
|
|
481
|
-
% if (${condition}) {
|
|
482
|
-
${markedTrue}
|
|
483
|
-
% } else {
|
|
484
|
-
${markedFalse}
|
|
485
|
-
% }
|
|
486
|
-
`;
|
|
487
|
-
} else if (cond.slotId) {
|
|
488
|
-
result = `<%== bf->comment("cond-start:${cond.slotId}") %>
|
|
489
|
-
% if (${condition}) {
|
|
490
|
-
${whenTrue}
|
|
491
|
-
% }
|
|
492
|
-
<%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
493
|
-
} else {
|
|
494
|
-
result = `
|
|
495
|
-
% if (${condition}) {
|
|
496
|
-
${whenTrue}
|
|
497
|
-
% }
|
|
498
|
-
`;
|
|
621
|
+
if (method === "flatMap") {
|
|
622
|
+
const evalForm = renderFlatMapEval(recv, body, params[0], emit);
|
|
623
|
+
if (evalForm !== null)
|
|
624
|
+
return evalForm;
|
|
625
|
+
this.ctx._recordExprBF101(`.flatMap(...) projection is not lowerable to a template flat-map`, `Pre-compute the projection in the route handler, or mark the loop @client-only.`);
|
|
626
|
+
return "''";
|
|
499
627
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
628
|
+
if (method === "map") {
|
|
629
|
+
const evalForm = renderMapEval(recv, body, params[0], emit);
|
|
630
|
+
if (evalForm !== null)
|
|
631
|
+
return evalForm;
|
|
632
|
+
this.ctx._recordExprBF101(`.map(...) projection is not lowerable to a template map`, `Pre-compute the projection in the route handler, or mark the position @client-only.`);
|
|
633
|
+
return "''";
|
|
505
634
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
635
|
+
const cb = {
|
|
636
|
+
method,
|
|
637
|
+
object,
|
|
638
|
+
param: params[0],
|
|
639
|
+
predicate: body
|
|
640
|
+
};
|
|
641
|
+
return this.renderPredicate(cb, recv, emit);
|
|
642
|
+
}
|
|
643
|
+
renderPredicate(cb, arrayExpr, emit) {
|
|
644
|
+
const { method, param, predicate } = cb;
|
|
645
|
+
const evalFn = {
|
|
646
|
+
filter: ["filter_eval"],
|
|
647
|
+
every: ["every_eval"],
|
|
648
|
+
some: ["some_eval"],
|
|
649
|
+
find: ["find_eval", true],
|
|
650
|
+
findLast: ["find_eval", false],
|
|
651
|
+
findIndex: ["find_index_eval", true],
|
|
652
|
+
findLastIndex: ["find_index_eval", false]
|
|
653
|
+
};
|
|
654
|
+
const isIdentity = method === "filter" && predicate.kind === "identifier" && predicate.name === param;
|
|
655
|
+
const spec = evalFn[method];
|
|
656
|
+
if (spec && !isIdentity) {
|
|
657
|
+
const evalForm = renderPredicateEval(spec[0], arrayExpr, predicate, param, emit, spec[1]);
|
|
658
|
+
if (evalForm !== null)
|
|
659
|
+
return evalForm;
|
|
660
|
+
}
|
|
661
|
+
const predBody = this.ctx._renderPerlFilterExprPublic(predicate, param);
|
|
662
|
+
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
|
|
663
|
+
if (method === "filter")
|
|
664
|
+
return `[grep { ${grepBody} } @{${arrayExpr}}]`;
|
|
665
|
+
if (method === "every")
|
|
666
|
+
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
667
|
+
if (method === "some")
|
|
668
|
+
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
669
|
+
const findHelper = {
|
|
670
|
+
find: "find",
|
|
671
|
+
findIndex: "find_index",
|
|
672
|
+
findLast: "find_last",
|
|
673
|
+
findLastIndex: "find_last_index"
|
|
674
|
+
};
|
|
675
|
+
if (findHelper[method]) {
|
|
676
|
+
return `bf->${findHelper[method]}(${arrayExpr}, sub { my $${param} = $_[0]; ${predBody} })`;
|
|
512
677
|
}
|
|
513
|
-
return
|
|
678
|
+
return arrayExpr;
|
|
514
679
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
680
|
+
arrayLiteral(elements, emit) {
|
|
681
|
+
return `[${elements.map(emit).join(", ")}]`;
|
|
682
|
+
}
|
|
683
|
+
arrayMethod(method, object, args, emit) {
|
|
684
|
+
return renderArrayMethod(method, object, args, emit);
|
|
685
|
+
}
|
|
686
|
+
flatMethod(object, depth, emit) {
|
|
687
|
+
return renderFlatMethod(emit(object), depth);
|
|
688
|
+
}
|
|
689
|
+
conditional(test, consequent, alternate, emit) {
|
|
690
|
+
return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
|
|
691
|
+
}
|
|
692
|
+
templateLiteral(parts, emit) {
|
|
693
|
+
const terms = [];
|
|
694
|
+
for (const part of parts) {
|
|
695
|
+
if (part.type === "string") {
|
|
696
|
+
if (part.value !== "") {
|
|
697
|
+
terms.push(`"${part.value.replace(/[\\"$@]/g, (m) => `\\${m}`)}"`);
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
const rendered = emit(part.expr);
|
|
701
|
+
const needsParens = part.expr.kind === "binary" || part.expr.kind === "logical" || part.expr.kind === "conditional";
|
|
702
|
+
terms.push(needsParens ? `(${rendered})` : rendered);
|
|
524
703
|
}
|
|
525
704
|
}
|
|
526
|
-
if (
|
|
527
|
-
return;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
705
|
+
if (terms.length === 0)
|
|
706
|
+
return '""';
|
|
707
|
+
return terms.join(" . ");
|
|
708
|
+
}
|
|
709
|
+
arrow(_params, _body, _emit) {
|
|
710
|
+
return "''";
|
|
711
|
+
}
|
|
712
|
+
regex(_raw) {
|
|
713
|
+
return "''";
|
|
714
|
+
}
|
|
715
|
+
unsupported(_raw, _reason) {
|
|
716
|
+
return "''";
|
|
717
|
+
}
|
|
718
|
+
objectLiteral(_properties, _raw, _emit) {
|
|
719
|
+
return "''";
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/adapter/analysis/component-tree.ts
|
|
724
|
+
function hasClientInteractivity(ir) {
|
|
725
|
+
return ir.metadata.signals.length > 0 || ir.metadata.effects.length > 0 || ir.metadata.onMounts.length > 0 || (ir.metadata.clientAnalysis?.needsInit ?? false);
|
|
726
|
+
}
|
|
727
|
+
function collectImportedLoopChildComponentErrors(ir, componentName) {
|
|
728
|
+
const errors = [];
|
|
729
|
+
const relativeImports = new Set;
|
|
730
|
+
for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
|
|
731
|
+
if (!imp.source.startsWith("./") && !imp.source.startsWith("../"))
|
|
732
|
+
continue;
|
|
733
|
+
if (imp.isTypeOnly)
|
|
734
|
+
continue;
|
|
735
|
+
for (const spec of imp.specifiers) {
|
|
736
|
+
relativeImports.add(spec.alias ?? spec.name);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (relativeImports.size === 0)
|
|
740
|
+
return errors;
|
|
741
|
+
const loc = { file: componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
|
|
742
|
+
const visit = (node, inLoop) => {
|
|
743
|
+
switch (node.type) {
|
|
744
|
+
case "component": {
|
|
745
|
+
const comp = node;
|
|
746
|
+
if (inLoop && relativeImports.has(comp.name)) {
|
|
747
|
+
errors.push({
|
|
748
|
+
code: "BF103",
|
|
749
|
+
severity: "error",
|
|
750
|
+
message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Mojo adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
|
|
751
|
+
loc: comp.loc ?? loc,
|
|
752
|
+
suggestion: {
|
|
753
|
+
message: `Options:
|
|
541
754
|
` + ` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Mojo template alongside the parent at render time.
|
|
542
755
|
` + ` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.
|
|
543
756
|
` + ` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
for (const child of comp.children)
|
|
548
|
-
visit(child, inLoop);
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
case "element":
|
|
552
|
-
for (const child of node.children)
|
|
553
|
-
visit(child, inLoop);
|
|
554
|
-
break;
|
|
555
|
-
case "fragment":
|
|
556
|
-
for (const child of node.children)
|
|
557
|
-
visit(child, inLoop);
|
|
558
|
-
break;
|
|
559
|
-
case "conditional": {
|
|
560
|
-
const cond = node;
|
|
561
|
-
visit(cond.whenTrue, inLoop);
|
|
562
|
-
if (cond.whenFalse)
|
|
563
|
-
visit(cond.whenFalse, inLoop);
|
|
564
|
-
break;
|
|
565
|
-
}
|
|
566
|
-
case "loop":
|
|
567
|
-
for (const child of node.children)
|
|
568
|
-
visit(child, true);
|
|
569
|
-
break;
|
|
570
|
-
case "if-statement": {
|
|
571
|
-
const stmt = node;
|
|
572
|
-
visit(stmt.consequent, inLoop);
|
|
573
|
-
if (stmt.alternate)
|
|
574
|
-
visit(stmt.alternate, inLoop);
|
|
575
|
-
break;
|
|
576
|
-
}
|
|
577
|
-
case "provider":
|
|
578
|
-
for (const child of node.children)
|
|
579
|
-
visit(child, inLoop);
|
|
580
|
-
break;
|
|
581
|
-
case "async": {
|
|
582
|
-
const a = node;
|
|
583
|
-
visit(a.fallback, inLoop);
|
|
584
|
-
for (const child of a.children)
|
|
585
|
-
visit(child, inLoop);
|
|
586
|
-
break;
|
|
757
|
+
}
|
|
758
|
+
});
|
|
587
759
|
}
|
|
760
|
+
for (const child of comp.children)
|
|
761
|
+
visit(child, inLoop);
|
|
762
|
+
break;
|
|
588
763
|
}
|
|
589
|
-
|
|
590
|
-
|
|
764
|
+
case "element":
|
|
765
|
+
for (const child of node.children)
|
|
766
|
+
visit(child, inLoop);
|
|
767
|
+
break;
|
|
768
|
+
case "fragment":
|
|
769
|
+
for (const child of node.children)
|
|
770
|
+
visit(child, inLoop);
|
|
771
|
+
break;
|
|
772
|
+
case "conditional": {
|
|
773
|
+
const cond = node;
|
|
774
|
+
visit(cond.whenTrue, inLoop);
|
|
775
|
+
if (cond.whenFalse)
|
|
776
|
+
visit(cond.whenFalse, inLoop);
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
case "loop":
|
|
780
|
+
for (const child of node.children)
|
|
781
|
+
visit(child, true);
|
|
782
|
+
break;
|
|
783
|
+
case "if-statement": {
|
|
784
|
+
const stmt = node;
|
|
785
|
+
visit(stmt.consequent, inLoop);
|
|
786
|
+
if (stmt.alternate)
|
|
787
|
+
visit(stmt.alternate, inLoop);
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case "provider":
|
|
791
|
+
for (const child of node.children)
|
|
792
|
+
visit(child, inLoop);
|
|
793
|
+
break;
|
|
794
|
+
case "async": {
|
|
795
|
+
const a = node;
|
|
796
|
+
visit(a.fallback, inLoop);
|
|
797
|
+
for (const child of a.children)
|
|
798
|
+
visit(child, inLoop);
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
visit(ir.root, false);
|
|
804
|
+
return errors;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/adapter/spread/spread-codegen.ts
|
|
808
|
+
import ts from "typescript";
|
|
809
|
+
import { parseRecordIndexAccess, stringifyParsedExpr } from "@barefootjs/jsx";
|
|
810
|
+
function conditionalSpreadToPerl(ctx, expr) {
|
|
811
|
+
if (!expr || expr.kind !== "conditional")
|
|
812
|
+
return null;
|
|
813
|
+
const whenTrue = expr.consequent;
|
|
814
|
+
const whenFalse = expr.alternate;
|
|
815
|
+
if (whenTrue.kind !== "object-literal" || whenFalse.kind !== "object-literal") {
|
|
816
|
+
return null;
|
|
591
817
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
818
|
+
const condPerl = ctx.convertExpressionToPerl("", expr.test);
|
|
819
|
+
const truePerl = objectLiteralToPerlHashref(ctx, whenTrue);
|
|
820
|
+
const falsePerl = objectLiteralToPerlHashref(ctx, whenFalse);
|
|
821
|
+
if (truePerl === null || falsePerl === null)
|
|
822
|
+
return null;
|
|
823
|
+
return `${condPerl} ? ${truePerl} : ${falsePerl}`;
|
|
824
|
+
}
|
|
825
|
+
function objectLiteralExprToPerlHashref(ctx, expr) {
|
|
826
|
+
if (!expr || expr.kind !== "object-literal")
|
|
827
|
+
return null;
|
|
828
|
+
return objectLiteralToPerlHashref(ctx, expr);
|
|
829
|
+
}
|
|
830
|
+
function objectLiteralToPerlHashref(ctx, obj) {
|
|
831
|
+
const entries = [];
|
|
832
|
+
for (const prop of obj.properties) {
|
|
833
|
+
if (prop.shorthand)
|
|
834
|
+
return null;
|
|
835
|
+
if (prop.keyKind === "numeric")
|
|
836
|
+
return null;
|
|
837
|
+
const key = prop.key;
|
|
838
|
+
const val = prop.value;
|
|
839
|
+
const indexed = recordIndexAccessToPerl(ctx, val);
|
|
840
|
+
if (indexed === null && val.kind === "index-access" && !isLiteralIndex(val.index)) {
|
|
841
|
+
ctx.errors.push({
|
|
842
|
+
code: "BF101",
|
|
600
843
|
severity: "error",
|
|
601
|
-
message: `
|
|
602
|
-
loc:
|
|
844
|
+
message: `Spread object value '${stringifyParsedExpr(val)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Perl hashref.`,
|
|
845
|
+
loc: { file: ctx.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
603
846
|
suggestion: {
|
|
604
|
-
message: `
|
|
605
|
-
` + ` 1. Rename the parameter to a single name and access tuple elements with index syntax in the body (e.g. \`entry => entry->[0]\` instead of \`([k, v]) => ...\`).
|
|
606
|
-
` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
|
|
607
|
-
` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
|
|
847
|
+
message: "Index a record whose values are number/string literals, or move the spread into a `'use client'` component so hydration computes it."
|
|
608
848
|
}
|
|
609
849
|
});
|
|
850
|
+
return null;
|
|
610
851
|
}
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
let array = rawArray;
|
|
614
|
-
if (loop.sortComparator) {
|
|
615
|
-
sortedHoist = `bf_iter_${perlIdentifierFromMarkerId(loop.markerId)}`;
|
|
616
|
-
array = `$${sortedHoist}`;
|
|
617
|
-
}
|
|
618
|
-
const param = loop.param;
|
|
619
|
-
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
620
|
-
const loopBound = loop.iterationShape === "keys" ? [param] : supportableDestructure ? ["__bf_item", ...(loop.paramBindings ?? []).map((b) => b.name), loop.index ?? "_i"] : [param, loop.index ?? "_i"];
|
|
621
|
-
for (const n of loopBound) {
|
|
622
|
-
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
623
|
-
}
|
|
624
|
-
const prevInLoop = this.inLoop;
|
|
625
|
-
this.inLoop = true;
|
|
626
|
-
const renderedChildren = this.renderChildren(loop.children);
|
|
627
|
-
this.inLoop = prevInLoop;
|
|
628
|
-
const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
|
|
629
|
-
${renderedChildren}` : renderedChildren;
|
|
630
|
-
const lines = [];
|
|
631
|
-
lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
|
|
632
|
-
if (sortedHoist && loop.sortComparator) {
|
|
633
|
-
lines.push(`% my $${sortedHoist} = ${renderSortMethod(rawArray, loop.sortComparator)};`);
|
|
634
|
-
}
|
|
635
|
-
lines.push(`% for my ${indexVar} (0..$#{${array}}) {`);
|
|
636
|
-
if (loop.iterationShape !== "keys") {
|
|
637
|
-
if (supportableDestructure) {
|
|
638
|
-
lines.push(`% my $__bf_item = ${array}->[${indexVar}];`);
|
|
639
|
-
for (const b of loop.paramBindings ?? []) {
|
|
640
|
-
lines.push(b.rest ? `% my $${b.name} = $__bf_item;` : `% my $${b.name} = $__bf_item->{${b.path.slice(1)}};`);
|
|
641
|
-
}
|
|
642
|
-
} else {
|
|
643
|
-
lines.push(`% my $${param} = ${array}->[${indexVar}];`);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
if (loop.filterPredicate) {
|
|
647
|
-
let filterCond;
|
|
648
|
-
if (loop.filterPredicate.blockBody) {
|
|
649
|
-
filterCond = this.renderBlockBodyCondition(loop.filterPredicate.blockBody, loop.filterPredicate.param);
|
|
650
|
-
} else if (loop.filterPredicate.predicate) {
|
|
651
|
-
filterCond = this.renderPerlFilterExpr(loop.filterPredicate.predicate, loop.filterPredicate.param);
|
|
652
|
-
} else {
|
|
653
|
-
filterCond = "1";
|
|
654
|
-
}
|
|
655
|
-
if (loop.filterPredicate.param !== param) {
|
|
656
|
-
filterCond = filterCond.replace(new RegExp(`\\$${loop.filterPredicate.param}\\b`, "g"), `$${param}`);
|
|
657
|
-
}
|
|
658
|
-
lines.push(`% if (${filterCond}) {`);
|
|
659
|
-
lines.push(children);
|
|
660
|
-
lines.push(`% }`);
|
|
661
|
-
} else {
|
|
662
|
-
lines.push(children);
|
|
663
|
-
}
|
|
664
|
-
for (const n of loopBound) {
|
|
665
|
-
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
666
|
-
if (c <= 0)
|
|
667
|
-
this.loopBoundNames.delete(n);
|
|
668
|
-
else
|
|
669
|
-
this.loopBoundNames.set(n, c);
|
|
670
|
-
}
|
|
671
|
-
lines.push(`% }`);
|
|
672
|
-
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
673
|
-
return lines.join(`
|
|
674
|
-
`);
|
|
852
|
+
const valPerl = indexed !== null ? indexed : ctx.convertExpressionToPerl("", val);
|
|
853
|
+
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
|
|
675
854
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
855
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
856
|
+
}
|
|
857
|
+
function isLiteralIndex(index) {
|
|
858
|
+
return index.kind === "literal" && (index.literalType === "number" || index.literalType === "string");
|
|
859
|
+
}
|
|
860
|
+
function recordIndexAccessToPerl(ctx, val) {
|
|
861
|
+
if (val.kind !== "index-access" || val.object.kind !== "identifier" || val.index.kind !== "identifier") {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
const tsVal = ts.factory.createElementAccessExpression(ts.factory.createIdentifier(val.object.name), ts.factory.createIdentifier(val.index.name));
|
|
865
|
+
const parsed = parseRecordIndexAccess(tsVal, ctx.localConstants, ctx.propsParams);
|
|
866
|
+
if (!parsed)
|
|
867
|
+
return null;
|
|
868
|
+
const entries = parsed.entries.map((e) => {
|
|
869
|
+
const mapVal = e.value.kind === "number" ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`;
|
|
870
|
+
return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`;
|
|
871
|
+
});
|
|
872
|
+
return `{ ${entries.join(", ")} }->{$${parsed.indexPropName}}`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/adapter/memo/seed.ts
|
|
876
|
+
import {
|
|
877
|
+
collectContextConsumers,
|
|
878
|
+
computeSsrSeedPlan
|
|
879
|
+
} from "@barefootjs/jsx";
|
|
880
|
+
function contextDefaultPerl(c) {
|
|
881
|
+
const d = c.defaultValue;
|
|
882
|
+
if (d === null || d === undefined)
|
|
883
|
+
return "undef";
|
|
884
|
+
if (typeof d === "string")
|
|
885
|
+
return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
886
|
+
if (typeof d === "boolean")
|
|
887
|
+
return d ? "1" : "0";
|
|
888
|
+
return String(d);
|
|
889
|
+
}
|
|
890
|
+
function generateContextConsumerSeed(ir) {
|
|
891
|
+
const consumers = collectContextConsumers(ir.metadata);
|
|
892
|
+
if (consumers.length === 0)
|
|
893
|
+
return "";
|
|
894
|
+
return consumers.map((c) => `% my $${c.localName} = bf->use_context('${c.contextName}', ${contextDefaultPerl(c)});`).join(`
|
|
895
|
+
`) + `
|
|
896
|
+
`;
|
|
897
|
+
}
|
|
898
|
+
function generateDerivedMemoSeed(ctx, ir) {
|
|
899
|
+
const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata);
|
|
900
|
+
const lines = [];
|
|
901
|
+
for (const step of plan.steps) {
|
|
902
|
+
if (step.kind !== "derived")
|
|
903
|
+
continue;
|
|
904
|
+
const perl = ctx.convertExpressionToPerl(step.expr, step.parsed);
|
|
905
|
+
if (perl === "" || !/\$[A-Za-z_]\w*/.test(perl))
|
|
906
|
+
continue;
|
|
907
|
+
lines.push(`% my $${step.name} = ${perl};`);
|
|
908
|
+
}
|
|
909
|
+
return lines.length > 0 ? lines.join(`
|
|
910
|
+
`) + `
|
|
911
|
+
` : "";
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// src/adapter/value/parsed-literal.ts
|
|
915
|
+
function isStringTypeInfo(type) {
|
|
916
|
+
return type?.kind === "primitive" && type.primitive === "string";
|
|
917
|
+
}
|
|
918
|
+
function isBareStringLiteral(initialValue) {
|
|
919
|
+
if (!initialValue)
|
|
920
|
+
return false;
|
|
921
|
+
const v = initialValue.trim();
|
|
922
|
+
return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/adapter/props/prop-classes.ts
|
|
926
|
+
function collectProviderDataNames(ir) {
|
|
927
|
+
return new Set([
|
|
928
|
+
...ir.metadata.propsParams.map((p) => p.name),
|
|
929
|
+
...(ir.metadata.signals ?? []).map((s) => s.getter),
|
|
930
|
+
...(ir.metadata.memos ?? []).map((m) => m.name)
|
|
931
|
+
]);
|
|
932
|
+
}
|
|
933
|
+
function collectBooleanTypedProps(ir) {
|
|
934
|
+
return new Set(ir.metadata.propsParams.filter((prop) => prop.type?.primitive === "boolean" || prop.type?.raw === "boolean").map((prop) => prop.name));
|
|
935
|
+
}
|
|
936
|
+
function collectNullableOptionalProps(ir) {
|
|
937
|
+
return new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
|
|
938
|
+
}
|
|
939
|
+
function collectStringValueNames(ir) {
|
|
940
|
+
const names = new Set;
|
|
941
|
+
for (const s of ir.metadata.signals) {
|
|
942
|
+
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
943
|
+
names.add(s.getter);
|
|
715
944
|
}
|
|
716
|
-
return `<%== bf->render_child('${tplName}'${propsStr}) %>`;
|
|
717
945
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
946
|
+
for (const p of ir.metadata.propsParams) {
|
|
947
|
+
if (isStringTypeInfo(p.type))
|
|
948
|
+
names.add(p.name);
|
|
722
949
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
950
|
+
return names;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/adapter/mojo-adapter.ts
|
|
954
|
+
class MojoAdapter extends BaseAdapter {
|
|
955
|
+
name = "mojolicious";
|
|
956
|
+
extension = ".html.ep";
|
|
957
|
+
templatesPerComponent = true;
|
|
958
|
+
importMapInjection = "html-snippet";
|
|
959
|
+
templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
|
|
960
|
+
componentName = "";
|
|
961
|
+
rootScopeNodes = new Set;
|
|
962
|
+
options;
|
|
963
|
+
errors = [];
|
|
964
|
+
inLoop = false;
|
|
965
|
+
propsObjectName = null;
|
|
966
|
+
propsParams = [];
|
|
967
|
+
booleanTypedProps = new Set;
|
|
968
|
+
providerDataNames = new Set;
|
|
969
|
+
stringValueNames = new Set;
|
|
970
|
+
_searchParamsLocals = new Set;
|
|
971
|
+
_loweringMatchers = [];
|
|
972
|
+
moduleStringConsts = new Map;
|
|
973
|
+
localConstants = [];
|
|
974
|
+
loopBoundNames = new Map;
|
|
975
|
+
nullableOptionalProps = new Set;
|
|
976
|
+
constructor(options = {}) {
|
|
977
|
+
super();
|
|
978
|
+
this.options = {
|
|
979
|
+
clientJsBasePath: options.clientJsBasePath ?? "/static/components/",
|
|
980
|
+
barefootJsPath: options.barefootJsPath ?? "/static/components/barefoot.js"
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
generate(ir, options) {
|
|
984
|
+
this.componentName = ir.metadata.componentName;
|
|
985
|
+
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
986
|
+
augmentInheritedPropAccesses(ir);
|
|
987
|
+
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
988
|
+
this.providerDataNames = collectProviderDataNames(ir);
|
|
989
|
+
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
990
|
+
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
991
|
+
this.stringValueNames = collectStringValueNames(ir);
|
|
992
|
+
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
993
|
+
this._searchParamsLocals = searchParamsLocalNames(ir.metadata);
|
|
994
|
+
this._loweringMatchers = prepareLoweringMatchers(ir.metadata);
|
|
995
|
+
this.localConstants = ir.metadata.localConstants ?? [];
|
|
996
|
+
this.loopBoundNames.clear();
|
|
997
|
+
this.errors = [];
|
|
998
|
+
this.childrenCaptureCounter = 0;
|
|
999
|
+
if (!options?.siblingTemplatesRegistered) {
|
|
1000
|
+
this.errors.push(...collectImportedLoopChildComponentErrors(ir, this.componentName));
|
|
1001
|
+
}
|
|
1002
|
+
this.rootScopeNodes = collectRootScopeNodes(ir.root);
|
|
1003
|
+
const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
|
|
1004
|
+
const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
|
|
1005
|
+
const ctxSeed = generateContextConsumerSeed(ir);
|
|
1006
|
+
const memoSeed = generateDerivedMemoSeed(this.memoCtx, ir);
|
|
1007
|
+
const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
|
|
737
1008
|
`;
|
|
738
|
-
|
|
1009
|
+
if (this.errors.length > 0) {
|
|
1010
|
+
ir.errors.push(...this.errors);
|
|
739
1011
|
}
|
|
740
|
-
|
|
741
|
-
|
|
1012
|
+
const sections = {
|
|
1013
|
+
imports: "",
|
|
1014
|
+
types: "",
|
|
1015
|
+
component: template,
|
|
1016
|
+
defaultExport: ""
|
|
1017
|
+
};
|
|
1018
|
+
return {
|
|
1019
|
+
template,
|
|
1020
|
+
sections,
|
|
1021
|
+
extension: this.extension
|
|
1022
|
+
};
|
|
742
1023
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
|
|
1024
|
+
isBooleanTypedPropRef(expr) {
|
|
1025
|
+
let bare = expr.trim();
|
|
1026
|
+
if (this.propsObjectName && bare.startsWith(`${this.propsObjectName}.`)) {
|
|
1027
|
+
bare = bare.slice(this.propsObjectName.length + 1);
|
|
747
1028
|
}
|
|
748
|
-
|
|
1029
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(bare))
|
|
1030
|
+
return false;
|
|
1031
|
+
return this.booleanTypedProps.has(bare);
|
|
749
1032
|
}
|
|
750
|
-
|
|
751
|
-
|
|
1033
|
+
parseUndefinedAlternateTernary(expr) {
|
|
1034
|
+
const parsed = parseExpression2(expr.trim());
|
|
1035
|
+
if (parsed?.kind !== "conditional")
|
|
1036
|
+
return null;
|
|
1037
|
+
const alt = parsed.alternate;
|
|
1038
|
+
const isUndef = alt.kind === "identifier" && (alt.name === "undefined" || alt.name === "null") || alt.kind === "literal" && (alt.value === null || alt.value === undefined);
|
|
1039
|
+
if (!isUndef)
|
|
1040
|
+
return null;
|
|
1041
|
+
return {
|
|
1042
|
+
condition: exprToString(parsed.test),
|
|
1043
|
+
consequent: exprToString(parsed.consequent)
|
|
1044
|
+
};
|
|
752
1045
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
1046
|
+
resolveLiteralConst(name) {
|
|
1047
|
+
if (this.loopBoundNames?.has?.(name))
|
|
1048
|
+
return null;
|
|
1049
|
+
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
1050
|
+
if (c?.value === undefined)
|
|
1051
|
+
return null;
|
|
1052
|
+
const v = c.value.trim();
|
|
1053
|
+
if (/^-?\d+(\.\d+)?$/.test(v))
|
|
1054
|
+
return v;
|
|
1055
|
+
const strLit = /^'([^'\\]*)'$/.exec(v) ?? /^"([^"\\]*)"$/.exec(v);
|
|
1056
|
+
if (strLit)
|
|
1057
|
+
return `'${strLit[1].replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
1058
|
+
return null;
|
|
759
1059
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
emitExpression: (value, name) => {
|
|
763
|
-
if (name === "style") {
|
|
764
|
-
const css = this.tryLowerStyleObject(value.expr);
|
|
765
|
-
if (css !== null)
|
|
766
|
-
return `style="${css}"`;
|
|
767
|
-
}
|
|
768
|
-
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
769
|
-
return "";
|
|
770
|
-
}
|
|
771
|
-
const bareId = value.expr.trim();
|
|
772
|
-
const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
|
|
773
|
-
if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId)) {
|
|
774
|
-
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
775
|
-
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
776
|
-
return `<% if (defined ${perl2}) { %>${body}<% } %>`;
|
|
777
|
-
}
|
|
778
|
-
if (isBooleanAttr(name)) {
|
|
779
|
-
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
780
|
-
}
|
|
781
|
-
if (value.presenceOrUndefined) {
|
|
782
|
-
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
783
|
-
const tmp = `$bf_pu${this.presenceVarCounter++}`;
|
|
784
|
-
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${tmp}) %>"` : `${name}="<%= ${tmp} %>"`;
|
|
785
|
-
return `<% my ${tmp} = ${perl2}; if (${tmp}) { %>${body}<% } %>`;
|
|
786
|
-
}
|
|
787
|
-
{
|
|
788
|
-
const m = this.parseUndefinedAlternateTernary(value.expr);
|
|
789
|
-
if (m) {
|
|
790
|
-
const cond = this.convertExpressionToPerl(m.condition);
|
|
791
|
-
const val = this.convertExpressionToPerl(m.consequent);
|
|
792
|
-
return `<% if (${cond}) { %>${name}="<%= ${val} %>"<% } %>`;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
const perl = this.convertExpressionToPerl(value.expr);
|
|
796
|
-
if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr)) {
|
|
797
|
-
return `${name}="<%= bf->bool_str(${perl}) %>"`;
|
|
798
|
-
}
|
|
799
|
-
return `${name}="<%= ${perl} %>"`;
|
|
800
|
-
},
|
|
801
|
-
emitBooleanAttr: (_value, name) => name,
|
|
802
|
-
emitTemplate: (value, name) => `${name}="<%= ${this.convertTemplateLiteralPartsToPerl(value.parts)} %>"`,
|
|
803
|
-
emitSpread: (value) => {
|
|
804
|
-
if (this.refuseUnsupportedAttrExpression(value.expr, "...")) {
|
|
805
|
-
return "";
|
|
806
|
-
}
|
|
807
|
-
const trimmed = value.expr.trim();
|
|
808
|
-
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
809
|
-
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
810
|
-
return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
|
|
811
|
-
}
|
|
812
|
-
const ternaryHashref = this.conditionalSpreadToPerl(trimmed);
|
|
813
|
-
if (ternaryHashref !== null) {
|
|
814
|
-
return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
|
|
815
|
-
}
|
|
816
|
-
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
817
|
-
const localConst = this.localConstants.find((c) => c.name === trimmed && !c.isModule);
|
|
818
|
-
if (localConst?.value !== undefined) {
|
|
819
|
-
const initTrimmed = localConst.value.trim();
|
|
820
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
|
|
821
|
-
const resolved = this.conditionalSpreadToPerl(initTrimmed);
|
|
822
|
-
if (resolved !== null) {
|
|
823
|
-
return `<%== bf->spread_attrs(${resolved}) %>`;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
829
|
-
return `<%== bf->spread_attrs(${perlExpr}) %>`;
|
|
830
|
-
},
|
|
831
|
-
emitBooleanShorthand: () => "",
|
|
832
|
-
emitJsxChildren: () => ""
|
|
833
|
-
};
|
|
834
|
-
tryLowerStyleObject(expr) {
|
|
835
|
-
const entries = parseStyleObjectEntries(expr);
|
|
836
|
-
if (!entries)
|
|
1060
|
+
resolveStaticRecordLiteral(objectName, key) {
|
|
1061
|
+
if (this.loopBoundNames?.has?.(objectName))
|
|
837
1062
|
return null;
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:<%= ${this.convertExpressionToPerl(e.expr)} %>`).join(";");
|
|
1063
|
+
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
1064
|
+
if (!hit)
|
|
1065
|
+
return null;
|
|
1066
|
+
return hit.kind === "number" ? hit.text : `'${hit.text.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
843
1067
|
}
|
|
844
|
-
|
|
845
|
-
|
|
1068
|
+
resolveModuleStringConst(name) {
|
|
1069
|
+
if (this.loopBoundNames.has(name))
|
|
1070
|
+
return null;
|
|
1071
|
+
const value = this.moduleStringConsts.get(name);
|
|
1072
|
+
if (value === undefined)
|
|
1073
|
+
return null;
|
|
1074
|
+
return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
846
1075
|
}
|
|
847
|
-
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (lowered)
|
|
861
|
-
parts.push(lowered);
|
|
862
|
-
}
|
|
863
|
-
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
1076
|
+
generateScriptRegistrations(ir, scriptBaseName) {
|
|
1077
|
+
const hasInteractivity = hasClientInteractivity(ir);
|
|
1078
|
+
if (!hasInteractivity)
|
|
1079
|
+
return "";
|
|
1080
|
+
const name = scriptBaseName ?? ir.metadata.componentName;
|
|
1081
|
+
const runtimePath = this.options.barefootJsPath;
|
|
1082
|
+
const clientJsPath = `${this.options.clientJsBasePath}${name}.client.js`;
|
|
1083
|
+
const lines = [];
|
|
1084
|
+
lines.push(`% bf->register_script('${runtimePath}');`);
|
|
1085
|
+
lines.push(`% bf->register_script('${clientJsPath}');`);
|
|
1086
|
+
lines.push("");
|
|
1087
|
+
return lines.join(`
|
|
1088
|
+
`);
|
|
864
1089
|
}
|
|
865
|
-
|
|
866
|
-
return
|
|
1090
|
+
renderNode(node) {
|
|
1091
|
+
return emitIRNode(node, this, {});
|
|
867
1092
|
}
|
|
868
|
-
|
|
869
|
-
return
|
|
1093
|
+
emitElement(node, _ctx, _emit) {
|
|
1094
|
+
return this.renderElement(node);
|
|
870
1095
|
}
|
|
871
|
-
|
|
872
|
-
return
|
|
1096
|
+
emitText(node) {
|
|
1097
|
+
return node.value;
|
|
873
1098
|
}
|
|
874
|
-
|
|
875
|
-
return
|
|
1099
|
+
emitExpression(node) {
|
|
1100
|
+
return this.renderExpression(node);
|
|
876
1101
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
const paths = this.collectReturnPaths(statements, [], localVarMap, param);
|
|
880
|
-
if (paths.length === 0)
|
|
881
|
-
return "1";
|
|
882
|
-
if (paths.length === 1)
|
|
883
|
-
return this.buildSinglePathCondition(paths[0], param, localVarMap);
|
|
884
|
-
const parts = [];
|
|
885
|
-
for (const path of paths) {
|
|
886
|
-
if (path.result.kind === "literal" && path.result.literalType === "boolean" && path.result.value === false)
|
|
887
|
-
continue;
|
|
888
|
-
const cond = this.buildSinglePathCondition(path, param, localVarMap);
|
|
889
|
-
if (cond !== "0")
|
|
890
|
-
parts.push(cond);
|
|
891
|
-
}
|
|
892
|
-
if (parts.length === 0)
|
|
893
|
-
return "0";
|
|
894
|
-
if (parts.length === 1)
|
|
895
|
-
return parts[0];
|
|
896
|
-
return `(${parts.join(" || ")})`;
|
|
897
|
-
}
|
|
898
|
-
collectReturnPaths(statements, currentConditions, localVarMap, param) {
|
|
899
|
-
const paths = [];
|
|
900
|
-
for (const stmt of statements) {
|
|
901
|
-
if (stmt.kind === "var-decl") {
|
|
902
|
-
if (stmt.init.kind === "call" && stmt.init.callee.kind === "identifier") {
|
|
903
|
-
localVarMap.set(stmt.name, stmt.init.callee.name);
|
|
904
|
-
}
|
|
905
|
-
} else if (stmt.kind === "return") {
|
|
906
|
-
paths.push({ conditions: [...currentConditions], result: stmt.value });
|
|
907
|
-
break;
|
|
908
|
-
} else if (stmt.kind === "if") {
|
|
909
|
-
const thenPaths = this.collectReturnPaths(stmt.consequent, [...currentConditions, stmt.condition], localVarMap, param);
|
|
910
|
-
paths.push(...thenPaths);
|
|
911
|
-
if (stmt.alternate) {
|
|
912
|
-
const negated = { kind: "unary", op: "!", argument: stmt.condition };
|
|
913
|
-
const elsePaths = this.collectReturnPaths(stmt.alternate, [...currentConditions, negated], localVarMap, param);
|
|
914
|
-
paths.push(...elsePaths);
|
|
915
|
-
} else {
|
|
916
|
-
currentConditions.push({ kind: "unary", op: "!", argument: stmt.condition });
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
return paths;
|
|
1102
|
+
emitConditional(node, _ctx, _emit) {
|
|
1103
|
+
return this.renderConditional(node);
|
|
921
1104
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
if (path.result.value === true) {
|
|
925
|
-
if (path.conditions.length === 0)
|
|
926
|
-
return "1";
|
|
927
|
-
return this.renderConditionsAnd(path.conditions, param, localVarMap);
|
|
928
|
-
}
|
|
929
|
-
return "0";
|
|
930
|
-
}
|
|
931
|
-
if (path.conditions.length === 0) {
|
|
932
|
-
return this.renderPerlFilterExpr(path.result, param, localVarMap);
|
|
933
|
-
}
|
|
934
|
-
const condPart = this.renderConditionsAnd(path.conditions, param, localVarMap);
|
|
935
|
-
const resultPart = this.renderPerlFilterExpr(path.result, param, localVarMap);
|
|
936
|
-
return `(${condPart} && ${resultPart})`;
|
|
1105
|
+
emitLoop(node, _ctx, _emit) {
|
|
1106
|
+
return this.renderLoop(node);
|
|
937
1107
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return "1";
|
|
941
|
-
if (conditions.length === 1)
|
|
942
|
-
return this.renderPerlFilterExpr(conditions[0], param, localVarMap);
|
|
943
|
-
const parts = conditions.map((c) => this.renderPerlFilterExpr(c, param, localVarMap));
|
|
944
|
-
return `(${parts.join(" && ")})`;
|
|
1108
|
+
emitComponent(node, _ctx, _emit) {
|
|
1109
|
+
return this.renderComponent(node);
|
|
945
1110
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
for (const part of literalParts) {
|
|
949
|
-
if (part.type === "string") {
|
|
950
|
-
parts.push(this.substituteJsInterpolationsToPerl(part.value));
|
|
951
|
-
} else if (part.type === "ternary") {
|
|
952
|
-
const cond = this.convertExpressionToPerl(part.condition);
|
|
953
|
-
parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
|
|
954
|
-
} else if (part.type === "lookup") {
|
|
955
|
-
const keyExpr = this.convertExpressionToPerl(part.key);
|
|
956
|
-
const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
|
|
957
|
-
parts.push(`({ ${entries} }->{${keyExpr}} // '')`);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
return parts.length === 1 ? parts[0] : parts.join(" . ");
|
|
1111
|
+
emitFragment(node, _ctx, _emit) {
|
|
1112
|
+
return this.renderFragment(node);
|
|
961
1113
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const re = /\$\{([^}]+)\}/g;
|
|
965
|
-
let lastIndex = 0;
|
|
966
|
-
let m;
|
|
967
|
-
while ((m = re.exec(s)) !== null) {
|
|
968
|
-
if (m.index > lastIndex) {
|
|
969
|
-
segments.push(`'${s.slice(lastIndex, m.index)}'`);
|
|
970
|
-
}
|
|
971
|
-
segments.push(this.convertExpressionToPerl(m[1].trim()));
|
|
972
|
-
lastIndex = re.lastIndex;
|
|
973
|
-
}
|
|
974
|
-
if (lastIndex < s.length) {
|
|
975
|
-
segments.push(`'${s.slice(lastIndex)}'`);
|
|
976
|
-
}
|
|
977
|
-
if (segments.length === 0)
|
|
978
|
-
return `''`;
|
|
979
|
-
return segments.length === 1 ? segments[0] : `(${segments.join(" . ")})`;
|
|
1114
|
+
emitSlot(node) {
|
|
1115
|
+
return this.renderSlot(node);
|
|
980
1116
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
while (probe.startsWith("("))
|
|
984
|
-
probe = probe.slice(1).trimStart();
|
|
985
|
-
const startsAsObjectLiteral = probe.startsWith("{");
|
|
986
|
-
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
|
|
987
|
-
if (!startsAsObjectLiteral && !hasTaggedTemplate)
|
|
988
|
-
return false;
|
|
989
|
-
const parsed = parseExpression2(expr.trim());
|
|
990
|
-
const support = isSupported(parsed);
|
|
991
|
-
if (parsed.kind !== "unsupported" && support.supported)
|
|
992
|
-
return false;
|
|
993
|
-
const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
|
|
994
|
-
const reasonLine = reason ? `
|
|
995
|
-
${reason}` : "";
|
|
996
|
-
this.errors.push({
|
|
997
|
-
code: "BF101",
|
|
998
|
-
severity: "error",
|
|
999
|
-
message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
|
|
1000
|
-
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1001
|
-
suggestion: {
|
|
1002
|
-
message: "The Mojo adapter cannot lower JS object literals or tagged-template-literal expressions into Embedded Perl. Move the expression into a `'use client'` component (so hydration computes it), or expand it into discrete attributes whose values are values the adapter can lower."
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
return true;
|
|
1117
|
+
emitIfStatement(node, _ctx, _emit) {
|
|
1118
|
+
return this.renderIfStatement(node);
|
|
1006
1119
|
}
|
|
1007
|
-
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
if (!ts.isExpressionStatement(stmt))
|
|
1013
|
-
return null;
|
|
1014
|
-
let node = stmt.expression;
|
|
1015
|
-
while (ts.isParenthesizedExpression(node))
|
|
1016
|
-
node = node.expression;
|
|
1017
|
-
if (!ts.isConditionalExpression(node))
|
|
1018
|
-
return null;
|
|
1019
|
-
const unwrap = (e) => {
|
|
1020
|
-
let n = e;
|
|
1021
|
-
while (ts.isParenthesizedExpression(n))
|
|
1022
|
-
n = n.expression;
|
|
1023
|
-
return n;
|
|
1024
|
-
};
|
|
1025
|
-
const whenTrue = unwrap(node.whenTrue);
|
|
1026
|
-
const whenFalse = unwrap(node.whenFalse);
|
|
1027
|
-
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
1028
|
-
return null;
|
|
1029
|
-
}
|
|
1030
|
-
const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
|
|
1031
|
-
const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
|
|
1032
|
-
const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
|
|
1033
|
-
if (truePerl === null || falsePerl === null)
|
|
1034
|
-
return null;
|
|
1035
|
-
return `${condPerl} ? ${truePerl} : ${falsePerl}`;
|
|
1120
|
+
emitProvider(node, _ctx, _emit) {
|
|
1121
|
+
const value = this.providerValuePerl(node.valueProp);
|
|
1122
|
+
const children = this.renderChildren(node.children);
|
|
1123
|
+
const name = node.contextName;
|
|
1124
|
+
return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
|
|
1036
1125
|
}
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
} else {
|
|
1048
|
-
return null;
|
|
1049
|
-
}
|
|
1050
|
-
const initNode = (() => {
|
|
1051
|
-
let n = prop.initializer;
|
|
1052
|
-
while (ts.isParenthesizedExpression(n))
|
|
1053
|
-
n = n.expression;
|
|
1054
|
-
return n;
|
|
1055
|
-
})();
|
|
1056
|
-
const indexed = this.recordIndexAccessToPerl(initNode);
|
|
1057
|
-
if (indexed === null && ts.isElementAccessExpression(initNode) && initNode.argumentExpression && !ts.isNumericLiteral(initNode.argumentExpression) && !ts.isStringLiteral(initNode.argumentExpression)) {
|
|
1058
|
-
this.errors.push({
|
|
1059
|
-
code: "BF101",
|
|
1060
|
-
severity: "error",
|
|
1061
|
-
message: `Spread object value '${initNode.getText(sf)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Perl hashref.`,
|
|
1062
|
-
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1063
|
-
suggestion: {
|
|
1064
|
-
message: "Index a record whose values are number/string literals, or move the spread into a `'use client'` component so hydration computes it."
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
return null;
|
|
1068
|
-
}
|
|
1069
|
-
const valPerl = indexed !== null ? indexed : this.convertExpressionToPerl(prop.initializer.getText(sf));
|
|
1070
|
-
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
|
|
1126
|
+
providerValuePerl(valueProp) {
|
|
1127
|
+
const v = valueProp.value;
|
|
1128
|
+
if (v.kind === "literal") {
|
|
1129
|
+
return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
|
|
1130
|
+
}
|
|
1131
|
+
if (v.kind === "expression") {
|
|
1132
|
+
const hashref = this.providerObjectLiteralPerl(v.expr);
|
|
1133
|
+
if (hashref !== null)
|
|
1134
|
+
return hashref;
|
|
1135
|
+
return this.convertExpressionToPerl(v.expr);
|
|
1071
1136
|
}
|
|
1072
|
-
|
|
1137
|
+
if (v.kind === "template")
|
|
1138
|
+
return this.convertTemplateLiteralPartsToPerl(v.parts);
|
|
1139
|
+
return "undef";
|
|
1073
1140
|
}
|
|
1074
|
-
|
|
1075
|
-
const
|
|
1076
|
-
if (
|
|
1141
|
+
providerObjectLiteralPerl(expr) {
|
|
1142
|
+
const members = parseProviderObjectLiteral(expr.trim());
|
|
1143
|
+
if (members === null)
|
|
1077
1144
|
return null;
|
|
1078
|
-
const entries =
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1145
|
+
const entries = members.map((m) => {
|
|
1146
|
+
const key = `'${m.name.replace(/[\\']/g, (c) => `\\${c}`)}'`;
|
|
1147
|
+
if (m.kind === "function" || /^on[A-Z]/.test(m.name))
|
|
1148
|
+
return `${key} => undef`;
|
|
1149
|
+
const src = m.kind === "getter" ? m.body : m.expr;
|
|
1150
|
+
if (this.isClientOnlyContextIdentifier(src))
|
|
1151
|
+
return `${key} => undef`;
|
|
1152
|
+
return `${key} => ${this.convertExpressionToPerl(src)}`;
|
|
1081
1153
|
});
|
|
1082
|
-
return `{ ${entries.join(", ")} }
|
|
1083
|
-
}
|
|
1084
|
-
convertExpressionToPerl(expr) {
|
|
1085
|
-
const trimmed = expr.trim();
|
|
1086
|
-
if (trimmed === "")
|
|
1087
|
-
return "''";
|
|
1088
|
-
const parsed = parseExpression2(trimmed);
|
|
1089
|
-
const support = isSupported(parsed);
|
|
1090
|
-
if (!support.supported) {
|
|
1091
|
-
this.errors.push({
|
|
1092
|
-
code: "BF101",
|
|
1093
|
-
severity: "error",
|
|
1094
|
-
message: `Expression not supported: ${trimmed}`,
|
|
1095
|
-
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1096
|
-
suggestion: {
|
|
1097
|
-
message: support.reason ? `${support.reason}
|
|
1098
|
-
|
|
1099
|
-
Options:
|
|
1100
|
-
1. Use /* @client */ for client-side evaluation
|
|
1101
|
-
2. Pre-compute the value in Perl` : `Options:
|
|
1102
|
-
1. Use /* @client */ for client-side evaluation
|
|
1103
|
-
2. Pre-compute the value in Perl`
|
|
1104
|
-
}
|
|
1105
|
-
});
|
|
1106
|
-
return "''";
|
|
1107
|
-
}
|
|
1108
|
-
return this.renderParsedExprToPerl(parsed);
|
|
1109
|
-
}
|
|
1110
|
-
renderParsedExprToPerl(expr) {
|
|
1111
|
-
return emitParsedExpr(expr, new MojoTopLevelEmitter(this));
|
|
1112
|
-
}
|
|
1113
|
-
_isStringValueName(name) {
|
|
1114
|
-
return this.stringValueNames.has(name);
|
|
1154
|
+
return `{ ${entries.join(", ")} }`;
|
|
1115
1155
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1122
|
-
suggestion: {
|
|
1123
|
-
message: reason ? `${reason}
|
|
1124
|
-
|
|
1125
|
-
Options:
|
|
1126
|
-
1. Use /* @client */ for client-side evaluation
|
|
1127
|
-
2. Pre-compute the value in Perl` : `Options:
|
|
1128
|
-
1. Use /* @client */ for client-side evaluation
|
|
1129
|
-
2. Pre-compute the value in Perl`
|
|
1130
|
-
}
|
|
1131
|
-
});
|
|
1156
|
+
isClientOnlyContextIdentifier(src) {
|
|
1157
|
+
const t = src.trim();
|
|
1158
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(t))
|
|
1159
|
+
return false;
|
|
1160
|
+
return !this.providerDataNames.has(t) && !this.moduleStringConsts.has(t);
|
|
1132
1161
|
}
|
|
1133
|
-
|
|
1134
|
-
return this.
|
|
1162
|
+
emitAsync(node, _ctx, _emit) {
|
|
1163
|
+
return this.renderAsync(node);
|
|
1135
1164
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1165
|
+
renderElement(element) {
|
|
1166
|
+
const tag = element.tag;
|
|
1167
|
+
const attrs = this.renderAttributes(element);
|
|
1168
|
+
const children = this.renderChildren(element.children);
|
|
1169
|
+
let hydrationAttrs = "";
|
|
1170
|
+
if (element.needsScope) {
|
|
1171
|
+
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
1143
1172
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
const needle = emit(args[0]);
|
|
1147
|
-
return `bf->includes(${obj}, ${needle})`;
|
|
1173
|
+
if (this.rootScopeNodes.has(element) && element.needsScope) {
|
|
1174
|
+
hydrationAttrs += ` <%== bf->data_key_attr %>`;
|
|
1148
1175
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
const fn = method === "indexOf" ? "index_of" : "last_index_of";
|
|
1152
|
-
const obj = emit(object);
|
|
1153
|
-
const needle = emit(args[0]);
|
|
1154
|
-
return `bf->${fn}(${obj}, ${needle})`;
|
|
1176
|
+
if (element.slotId) {
|
|
1177
|
+
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
|
|
1155
1178
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const idx = args.length >= 1 ? emit(args[0]) : "0";
|
|
1159
|
-
return `bf->at(${obj}, ${idx})`;
|
|
1179
|
+
if (element.regionId) {
|
|
1180
|
+
hydrationAttrs += ` ${BF_REGION}="${element.regionId}"`;
|
|
1160
1181
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1182
|
+
const voidElements = [
|
|
1183
|
+
"area",
|
|
1184
|
+
"base",
|
|
1185
|
+
"br",
|
|
1186
|
+
"col",
|
|
1187
|
+
"embed",
|
|
1188
|
+
"hr",
|
|
1189
|
+
"img",
|
|
1190
|
+
"input",
|
|
1191
|
+
"link",
|
|
1192
|
+
"meta",
|
|
1193
|
+
"param",
|
|
1194
|
+
"source",
|
|
1195
|
+
"track",
|
|
1196
|
+
"wbr"
|
|
1197
|
+
];
|
|
1198
|
+
if (voidElements.includes(tag.toLowerCase())) {
|
|
1199
|
+
return `<${tag}${attrs}${hydrationAttrs}>`;
|
|
1200
|
+
}
|
|
1201
|
+
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
1202
|
+
}
|
|
1203
|
+
renderExpression(expr) {
|
|
1204
|
+
if (expr.clientOnly) {
|
|
1205
|
+
if (expr.slotId) {
|
|
1206
|
+
return `<%== bf->comment("client:${expr.slotId}") %>`;
|
|
1164
1207
|
}
|
|
1165
|
-
|
|
1166
|
-
const b = emit(args[0]);
|
|
1167
|
-
return `bf->concat(${a}, ${b})`;
|
|
1208
|
+
return "";
|
|
1168
1209
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
const end = args.length >= 2 ? emit(args[1]) : "undef";
|
|
1173
|
-
return `bf->slice(${recv}, ${start}, ${end})`;
|
|
1210
|
+
const perlExpr = this.convertExpressionToPerl(expr.expr);
|
|
1211
|
+
if (expr.slotId) {
|
|
1212
|
+
return `<%== bf->text_start("${expr.slotId}") %><%= ${perlExpr} %><%== bf->text_end %>`;
|
|
1174
1213
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1214
|
+
return `<%= ${perlExpr} %>`;
|
|
1215
|
+
}
|
|
1216
|
+
renderConditional(cond) {
|
|
1217
|
+
if (cond.clientOnly && cond.slotId) {
|
|
1218
|
+
return `<%== bf->comment("cond-start:${cond.slotId}") %><%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
1179
1219
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1220
|
+
const condition = this.convertExpressionToPerl(cond.condition);
|
|
1221
|
+
const whenTrue = this.renderNode(cond.whenTrue);
|
|
1222
|
+
const whenFalse = this.renderNodeOrNull(cond.whenFalse);
|
|
1223
|
+
const isFragmentBranch = cond.whenTrue.type === "fragment" || cond.whenFalse.type === "fragment";
|
|
1224
|
+
const useCommentMarkers = cond.slotId && isFragmentBranch;
|
|
1225
|
+
let markedTrue = whenTrue;
|
|
1226
|
+
let markedFalse = whenFalse;
|
|
1227
|
+
if (cond.slotId && !useCommentMarkers) {
|
|
1228
|
+
markedTrue = this.addCondMarkerToFirstElement(whenTrue, cond.slotId);
|
|
1229
|
+
markedFalse = whenFalse ? this.addCondMarkerToFirstElement(whenFalse, cond.slotId) : whenFalse;
|
|
1183
1230
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1231
|
+
let result;
|
|
1232
|
+
if (useCommentMarkers) {
|
|
1233
|
+
const inner = whenFalse ? `
|
|
1234
|
+
% if (${condition}) {
|
|
1235
|
+
${whenTrue}
|
|
1236
|
+
% } else {
|
|
1237
|
+
${whenFalse}
|
|
1238
|
+
% }
|
|
1239
|
+
` : `
|
|
1240
|
+
% if (${condition}) {
|
|
1241
|
+
${whenTrue}
|
|
1242
|
+
% }
|
|
1243
|
+
`;
|
|
1244
|
+
result = `<%== bf->comment("cond-start:${cond.slotId}") %>${inner}<%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
1245
|
+
} else if (markedFalse) {
|
|
1246
|
+
result = `
|
|
1247
|
+
% if (${condition}) {
|
|
1248
|
+
${markedTrue}
|
|
1249
|
+
% } else {
|
|
1250
|
+
${markedFalse}
|
|
1251
|
+
% }
|
|
1252
|
+
`;
|
|
1253
|
+
} else if (cond.slotId) {
|
|
1254
|
+
result = `<%== bf->comment("cond-start:${cond.slotId}") %>
|
|
1255
|
+
% if (${condition}) {
|
|
1256
|
+
${whenTrue}
|
|
1257
|
+
% }
|
|
1258
|
+
<%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
1259
|
+
} else {
|
|
1260
|
+
result = `
|
|
1261
|
+
% if (${condition}) {
|
|
1262
|
+
${whenTrue}
|
|
1263
|
+
% }
|
|
1264
|
+
`;
|
|
1187
1265
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1266
|
+
return result;
|
|
1267
|
+
}
|
|
1268
|
+
renderNodeOrNull(node) {
|
|
1269
|
+
if (node.type === "expression" && (node.expr === "null" || node.expr === "undefined")) {
|
|
1270
|
+
return null;
|
|
1191
1271
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1272
|
+
return this.renderNode(node);
|
|
1273
|
+
}
|
|
1274
|
+
addCondMarkerToFirstElement(content, condId) {
|
|
1275
|
+
const match = content.match(/^(<\w+)([\s>])/);
|
|
1276
|
+
if (match) {
|
|
1277
|
+
return content.replace(/^(<\w+)([\s>])/, `$1 ${BF_COND}="${condId}"$2`);
|
|
1196
1278
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
}
|
|
1202
|
-
const sep = emit(args[0]);
|
|
1203
|
-
if (args.length === 1) {
|
|
1204
|
-
return `bf->split(${recv}, ${sep})`;
|
|
1205
|
-
}
|
|
1206
|
-
const limit = emit(args[1]);
|
|
1207
|
-
return `bf->split(${recv}, ${sep}, ${limit})`;
|
|
1279
|
+
return `<%== bf->comment("cond-start:${condId}") %>${content}<%== bf->comment("cond-end:${condId}") %>`;
|
|
1280
|
+
}
|
|
1281
|
+
renderLoop(loop) {
|
|
1282
|
+
if (loop.clientOnly) {
|
|
1283
|
+
return `<%== bf->comment("loop:${loop.markerId}") %><%== bf->comment("/loop:${loop.markerId}") %>`;
|
|
1208
1284
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1285
|
+
const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
|
|
1286
|
+
const supportableDestructure = destructure && isLowerableObjectRestDestructure(loop);
|
|
1287
|
+
if (destructure && !supportableDestructure) {
|
|
1288
|
+
this.errors.push({
|
|
1289
|
+
code: "BF104",
|
|
1290
|
+
severity: "error",
|
|
1291
|
+
message: `Loop callback uses an array/object destructure pattern (\`${loop.param}\`) that the Mojo adapter cannot lower — Perl scalar bindings can't unpack a tuple in a single \`my\` declaration.`,
|
|
1292
|
+
loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1293
|
+
suggestion: {
|
|
1294
|
+
message: `Options:
|
|
1295
|
+
` + ` 1. Rename the parameter to a single name and access tuple elements with index syntax in the body (e.g. \`entry => entry->[0]\` instead of \`([k, v]) => ...\`).
|
|
1296
|
+
` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
|
|
1297
|
+
` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1218
1300
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1301
|
+
const rawArray = this.convertExpressionToPerl(loop.array);
|
|
1302
|
+
let sortedHoist = null;
|
|
1303
|
+
let array = rawArray;
|
|
1304
|
+
if (loop.sortComparator) {
|
|
1305
|
+
sortedHoist = `bf_iter_${perlIdentifierFromMarkerId(loop.markerId)}`;
|
|
1306
|
+
array = `$${sortedHoist}`;
|
|
1224
1307
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1308
|
+
const param = loop.param;
|
|
1309
|
+
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
1310
|
+
const loopBound = loop.iterationShape === "keys" ? [param] : supportableDestructure ? ["__bf_item", ...(loop.paramBindings ?? []).map((b) => b.name), loop.index ?? "_i"] : [param, loop.index ?? "_i"];
|
|
1311
|
+
for (const n of loopBound) {
|
|
1312
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
1229
1313
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1314
|
+
const prevInLoop = this.inLoop;
|
|
1315
|
+
this.inLoop = true;
|
|
1316
|
+
const renderedChildren = this.renderChildren(loop.children);
|
|
1317
|
+
this.inLoop = prevInLoop;
|
|
1318
|
+
const children = loop.bodyIsItemConditional && loop.key ? `<%== bf->comment("loop-i:" . ${this.convertExpressionToPerl(loop.key)}) %>
|
|
1319
|
+
${renderedChildren}` : renderedChildren;
|
|
1320
|
+
const lines = [];
|
|
1321
|
+
lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
|
|
1322
|
+
if (sortedHoist && loop.sortComparator) {
|
|
1323
|
+
for (const n of loopBound) {
|
|
1324
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
1325
|
+
if (c <= 0)
|
|
1326
|
+
this.loopBoundNames.delete(n);
|
|
1327
|
+
else
|
|
1328
|
+
this.loopBoundNames.set(n, c);
|
|
1236
1329
|
}
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1330
|
+
const sortEmit = (e) => this.convertExpressionToPerl("", e);
|
|
1331
|
+
const sortArrow = loop.sortComparator.arrow;
|
|
1332
|
+
let sorted = null;
|
|
1333
|
+
if (sortArrow.kind === "arrow") {
|
|
1334
|
+
sorted = renderSortEval(rawArray, sortArrow.body, sortArrow.params, sortEmit);
|
|
1240
1335
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1336
|
+
if (sorted === null) {
|
|
1337
|
+
const structured = sortComparatorFromArrow2(sortArrow);
|
|
1338
|
+
if (structured !== null)
|
|
1339
|
+
sorted = renderSortMethod(rawArray, structured);
|
|
1340
|
+
}
|
|
1341
|
+
if (sorted === null) {
|
|
1342
|
+
this._recordExprBF101(`.sort(...) loop comparator is not lowerable to a template sort`, `Pre-sort the array in the route handler, or mark the loop @client-only.`);
|
|
1343
|
+
sorted = rawArray;
|
|
1344
|
+
}
|
|
1345
|
+
for (const n of loopBound) {
|
|
1346
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
1347
|
+
}
|
|
1348
|
+
lines.push(`% my $${sortedHoist} = ${sorted};`);
|
|
1247
1349
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
|
|
1259
|
-
}
|
|
1260
|
-
function renderReduceMethod(recv, op, direction) {
|
|
1261
|
-
const keyEntry = op.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${op.key.field}'`;
|
|
1262
|
-
const init = op.type === "string" ? `'${op.init.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'` : op.init;
|
|
1263
|
-
return `bf->reduce(${recv}, { op => '${op.op}', ${keyEntry}, type => '${op.type}', init => ${init}, direction => '${direction}' })`;
|
|
1264
|
-
}
|
|
1265
|
-
function renderFlatMethod(recv, depth) {
|
|
1266
|
-
const d = depth === "infinity" ? -1 : depth;
|
|
1267
|
-
return `bf->flat(${recv}, ${d})`;
|
|
1268
|
-
}
|
|
1269
|
-
function renderFlatMapMethod(recv, op) {
|
|
1270
|
-
const proj = op.projection;
|
|
1271
|
-
if (proj.kind === "tuple") {
|
|
1272
|
-
const specs = proj.elements.map((l) => l.kind === "self" ? `['self', '']` : `['field', '${l.field}']`).join(", ");
|
|
1273
|
-
return `bf->flat_map_tuple(${recv}, ${specs})`;
|
|
1274
|
-
}
|
|
1275
|
-
if (proj.kind === "self")
|
|
1276
|
-
return `bf->flat_map(${recv}, 'self', '')`;
|
|
1277
|
-
return `bf->flat_map(${recv}, 'field', '${proj.field}')`;
|
|
1278
|
-
}
|
|
1279
|
-
function isStringTypeInfo(type) {
|
|
1280
|
-
return type?.kind === "primitive" && type.primitive === "string";
|
|
1281
|
-
}
|
|
1282
|
-
function isBareStringLiteral(initialValue) {
|
|
1283
|
-
if (!initialValue)
|
|
1284
|
-
return false;
|
|
1285
|
-
const v = initialValue.trim();
|
|
1286
|
-
return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
|
|
1287
|
-
}
|
|
1288
|
-
function isStringTypedOperand(expr, isStringName) {
|
|
1289
|
-
if (expr.kind === "literal" && expr.literalType === "string")
|
|
1290
|
-
return true;
|
|
1291
|
-
if (expr.kind === "call" && expr.callee.kind === "identifier" && expr.args.length === 0) {
|
|
1292
|
-
return isStringName(expr.callee.name);
|
|
1293
|
-
}
|
|
1294
|
-
if (expr.kind === "member" && expr.object.kind === "identifier" && expr.object.name === "props") {
|
|
1295
|
-
return isStringName(expr.property);
|
|
1296
|
-
}
|
|
1297
|
-
return false;
|
|
1298
|
-
}
|
|
1299
|
-
function emitIndexAccessPerl(object, index, emit, isStringName) {
|
|
1300
|
-
const i = emit(index);
|
|
1301
|
-
return isStringTypedOperand(index, isStringName) ? `${emit(object)}->{${i}}` : `${emit(object)}->[${i}]`;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
class MojoFilterEmitter {
|
|
1305
|
-
param;
|
|
1306
|
-
localVarMap;
|
|
1307
|
-
isStringName;
|
|
1308
|
-
constructor(param, localVarMap, isStringName = () => false) {
|
|
1309
|
-
this.param = param;
|
|
1310
|
-
this.localVarMap = localVarMap;
|
|
1311
|
-
this.isStringName = isStringName;
|
|
1312
|
-
}
|
|
1313
|
-
identifier(name) {
|
|
1314
|
-
if (name === this.param)
|
|
1315
|
-
return `$${this.param}`;
|
|
1316
|
-
const signal = this.localVarMap.get(name);
|
|
1317
|
-
if (signal)
|
|
1318
|
-
return `$${signal}`;
|
|
1319
|
-
return `$${name}`;
|
|
1320
|
-
}
|
|
1321
|
-
literal(value, literalType) {
|
|
1322
|
-
if (literalType === "string")
|
|
1323
|
-
return `'${value}'`;
|
|
1324
|
-
if (literalType === "boolean")
|
|
1325
|
-
return value ? "1" : "0";
|
|
1326
|
-
if (literalType === "null")
|
|
1327
|
-
return "undef";
|
|
1328
|
-
return String(value);
|
|
1329
|
-
}
|
|
1330
|
-
member(object, property, _computed, emit) {
|
|
1331
|
-
if (property === "length" && (object.kind === "higher-order" || object.kind === "array-literal")) {
|
|
1332
|
-
return `scalar(@{${emit(object)}})`;
|
|
1350
|
+
lines.push(`% for my ${indexVar} (0..$#{${array}}) {`);
|
|
1351
|
+
if (loop.iterationShape !== "keys") {
|
|
1352
|
+
if (supportableDestructure) {
|
|
1353
|
+
lines.push(`% my $__bf_item = ${array}->[${indexVar}];`);
|
|
1354
|
+
for (const b of loop.paramBindings ?? []) {
|
|
1355
|
+
lines.push(b.rest ? `% my $${b.name} = $__bf_item;` : `% my $${b.name} = $__bf_item->{${b.path.slice(1)}};`);
|
|
1356
|
+
}
|
|
1357
|
+
} else {
|
|
1358
|
+
lines.push(`% my $${param} = ${array}->[${indexVar}];`);
|
|
1359
|
+
}
|
|
1333
1360
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1361
|
+
if (loop.filterPredicate) {
|
|
1362
|
+
let filterCond;
|
|
1363
|
+
if (loop.filterPredicate.predicate) {
|
|
1364
|
+
filterCond = this.renderPerlFilterExpr(loop.filterPredicate.predicate, loop.filterPredicate.param);
|
|
1365
|
+
} else {
|
|
1366
|
+
filterCond = "1";
|
|
1367
|
+
}
|
|
1368
|
+
if (loop.filterPredicate.param !== param) {
|
|
1369
|
+
filterCond = filterCond.replace(new RegExp(`\\$${loop.filterPredicate.param}\\b`, "g"), `$${param}`);
|
|
1370
|
+
}
|
|
1371
|
+
lines.push(`% if (${filterCond}) {`);
|
|
1372
|
+
lines.push(children);
|
|
1373
|
+
lines.push(`% }`);
|
|
1374
|
+
} else {
|
|
1375
|
+
lines.push(children);
|
|
1342
1376
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
return needsParens ? `!(${arg})` : `!${arg}`;
|
|
1377
|
+
for (const n of loopBound) {
|
|
1378
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
1379
|
+
if (c <= 0)
|
|
1380
|
+
this.loopBoundNames.delete(n);
|
|
1381
|
+
else
|
|
1382
|
+
this.loopBoundNames.set(n, c);
|
|
1350
1383
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
return
|
|
1384
|
+
lines.push(`% }`);
|
|
1385
|
+
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
1386
|
+
return lines.join(`
|
|
1387
|
+
`);
|
|
1354
1388
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1389
|
+
componentPropEmitter = {
|
|
1390
|
+
emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
|
|
1391
|
+
emitExpression: (value, name) => {
|
|
1392
|
+
if (value.parts) {
|
|
1393
|
+
return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
|
|
1394
|
+
}
|
|
1395
|
+
if (value.parsed) {
|
|
1396
|
+
const hashref = objectLiteralExprToPerlHashref(this.spreadCtx, value.parsed);
|
|
1397
|
+
if (hashref !== null)
|
|
1398
|
+
return `${perlHashKey(name)} => ${hashref}`;
|
|
1399
|
+
}
|
|
1400
|
+
return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`;
|
|
1401
|
+
},
|
|
1402
|
+
emitSpread: (value) => {
|
|
1403
|
+
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
1404
|
+
return perlExpr.startsWith("%") ? perlExpr : `%{${perlExpr}}`;
|
|
1405
|
+
},
|
|
1406
|
+
emitTemplate: (value, name) => `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
|
|
1407
|
+
emitBooleanAttr: (_value, name) => `${perlHashKey(name)} => 1`,
|
|
1408
|
+
emitBooleanShorthand: (_value, name) => `${perlHashKey(name)} => 1`,
|
|
1409
|
+
emitJsxChildren: () => ""
|
|
1410
|
+
};
|
|
1411
|
+
renderComponent(comp) {
|
|
1412
|
+
const propParts = [];
|
|
1413
|
+
for (const p of comp.props) {
|
|
1414
|
+
if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
|
|
1415
|
+
continue;
|
|
1416
|
+
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
|
|
1417
|
+
if (lowered)
|
|
1418
|
+
propParts.push(lowered);
|
|
1362
1419
|
}
|
|
1363
|
-
if (
|
|
1364
|
-
|
|
1420
|
+
if (comp.slotId && !this.inLoop) {
|
|
1421
|
+
propParts.push(`_bf_slot => '${comp.slotId}'`);
|
|
1365
1422
|
}
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
"
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1378
|
-
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
1379
|
-
}
|
|
1380
|
-
logical(op, left, right, emit) {
|
|
1381
|
-
const l = emit(left);
|
|
1382
|
-
const r = emit(right);
|
|
1383
|
-
if (op === "&&")
|
|
1384
|
-
return `(${l} && ${r})`;
|
|
1385
|
-
if (op === "||")
|
|
1386
|
-
return `(${l} || ${r})`;
|
|
1387
|
-
return `(${l} // ${r})`;
|
|
1388
|
-
}
|
|
1389
|
-
higherOrder(method, object, param, predicate, emit) {
|
|
1390
|
-
const arrayExpr = emit(object);
|
|
1391
|
-
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName));
|
|
1392
|
-
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
|
|
1393
|
-
if (method === "filter")
|
|
1394
|
-
return `[grep { ${grepBody} } @{${arrayExpr}}]`;
|
|
1395
|
-
if (method === "every")
|
|
1396
|
-
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
1397
|
-
if (method === "some")
|
|
1398
|
-
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
1399
|
-
return arrayExpr;
|
|
1400
|
-
}
|
|
1401
|
-
arrayLiteral(elements, emit) {
|
|
1402
|
-
return `[${elements.map(emit).join(", ")}]`;
|
|
1423
|
+
const propsStr = propParts.length > 0 ? ", " + propParts.join(", ") : "";
|
|
1424
|
+
const tplName = this.toTemplateName(comp.name);
|
|
1425
|
+
const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
|
|
1426
|
+
if (effectiveChildren.length > 0) {
|
|
1427
|
+
const prevInLoop = this.inLoop;
|
|
1428
|
+
this.inLoop = false;
|
|
1429
|
+
const childrenBody = this.renderChildren(effectiveChildren);
|
|
1430
|
+
this.inLoop = prevInLoop;
|
|
1431
|
+
const varName = `$bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
1432
|
+
return `<% my ${varName} = begin %>${childrenBody}<% end %><%== bf->render_child('${tplName}'${propsStr}, children => ${varName}) %>`;
|
|
1433
|
+
}
|
|
1434
|
+
return `<%== bf->render_child('${tplName}'${propsStr}) %>`;
|
|
1403
1435
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1436
|
+
childrenCaptureCounter = 0;
|
|
1437
|
+
presenceVarCounter = 0;
|
|
1438
|
+
toTemplateName(componentName) {
|
|
1439
|
+
return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
1406
1440
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1441
|
+
renderIfStatement(ifStmt) {
|
|
1442
|
+
const condition = this.convertExpressionToPerl(ifStmt.condition);
|
|
1443
|
+
const consequent = ifStmt.consequent.type === "if-statement" ? this.renderIfStatement(ifStmt.consequent) : this.renderNode(ifStmt.consequent);
|
|
1444
|
+
let result = `% if (${condition}) {
|
|
1445
|
+
${consequent}
|
|
1446
|
+
`;
|
|
1447
|
+
if (ifStmt.alternate) {
|
|
1448
|
+
if (ifStmt.alternate.type === "if-statement") {
|
|
1449
|
+
const altResult = this.renderIfStatement(ifStmt.alternate);
|
|
1450
|
+
result += altResult.replace(/^% if/, "% } elsif");
|
|
1451
|
+
} else {
|
|
1452
|
+
const alternate = this.renderNode(ifStmt.alternate);
|
|
1453
|
+
result += `% } else {
|
|
1454
|
+
${alternate}
|
|
1455
|
+
`;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
result += `% }`;
|
|
1459
|
+
return result;
|
|
1409
1460
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1461
|
+
renderFragment(fragment) {
|
|
1462
|
+
const children = this.renderChildren(fragment.children);
|
|
1463
|
+
if (fragment.needsScopeComment) {
|
|
1464
|
+
return `<%== bf->scope_comment %>${children}`;
|
|
1465
|
+
}
|
|
1466
|
+
return children;
|
|
1412
1467
|
}
|
|
1413
|
-
|
|
1414
|
-
return
|
|
1468
|
+
renderSlot(_slot) {
|
|
1469
|
+
return `<%= content %>`;
|
|
1415
1470
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1471
|
+
renderAsync(node) {
|
|
1472
|
+
const fallback = this.renderNode(node.fallback);
|
|
1473
|
+
const children = this.renderChildren(node.children);
|
|
1474
|
+
const fallbackVar = `$bf_async_fallback_${node.id}`;
|
|
1475
|
+
return `<% my ${fallbackVar} = begin %>${fallback}<% end %><%== bf->async_boundary('${node.id}', ${fallbackVar}) %>
|
|
1476
|
+
${children}`;
|
|
1418
1477
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1478
|
+
elementAttrEmitter = {
|
|
1479
|
+
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
1480
|
+
emitExpression: (value, name) => {
|
|
1481
|
+
if (name === "style") {
|
|
1482
|
+
const css = this.tryLowerStyleObject(value.expr);
|
|
1483
|
+
if (css !== null)
|
|
1484
|
+
return `style="${css}"`;
|
|
1485
|
+
}
|
|
1486
|
+
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
1487
|
+
return "";
|
|
1488
|
+
}
|
|
1489
|
+
const bareId = value.expr.trim();
|
|
1490
|
+
const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
|
|
1491
|
+
if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId)) {
|
|
1492
|
+
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
1493
|
+
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
1494
|
+
return `<% if (defined ${perl2}) { %>${body}<% } %>`;
|
|
1495
|
+
}
|
|
1496
|
+
if (isBooleanAttr(name)) {
|
|
1497
|
+
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
1498
|
+
}
|
|
1499
|
+
if (value.presenceOrUndefined) {
|
|
1500
|
+
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
1501
|
+
const tmp = `$bf_pu${this.presenceVarCounter++}`;
|
|
1502
|
+
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<%= bf->bool_str(${tmp}) %>"` : `${name}="<%= ${tmp} %>"`;
|
|
1503
|
+
return `<% my ${tmp} = ${perl2}; if (${tmp}) { %>${body}<% } %>`;
|
|
1504
|
+
}
|
|
1505
|
+
{
|
|
1506
|
+
const m = this.parseUndefinedAlternateTernary(value.expr);
|
|
1507
|
+
if (m) {
|
|
1508
|
+
const cond = this.convertExpressionToPerl(m.condition);
|
|
1509
|
+
const val = this.convertExpressionToPerl(m.consequent);
|
|
1510
|
+
return `<% if (${cond}) { %>${name}="<%= ${val} %>"<% } %>`;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const perl = this.convertExpressionToPerl(value.expr);
|
|
1514
|
+
if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr)) {
|
|
1515
|
+
return `${name}="<%= bf->bool_str(${perl}) %>"`;
|
|
1516
|
+
}
|
|
1517
|
+
return `${name}="<%= ${perl} %>"`;
|
|
1518
|
+
},
|
|
1519
|
+
emitBooleanAttr: (_value, name) => name,
|
|
1520
|
+
emitTemplate: (value, name) => `${name}="<%= ${this.convertTemplateLiteralPartsToPerl(value.parts)} %>"`,
|
|
1521
|
+
emitSpread: (value) => {
|
|
1522
|
+
if (this.refuseUnsupportedAttrExpression(value.expr, "...")) {
|
|
1523
|
+
return "";
|
|
1524
|
+
}
|
|
1525
|
+
const trimmed = value.expr.trim();
|
|
1526
|
+
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
1527
|
+
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
1528
|
+
return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
|
|
1529
|
+
}
|
|
1530
|
+
const ternaryHashref = conditionalSpreadToPerl(this.spreadCtx, value.parsed);
|
|
1531
|
+
if (ternaryHashref !== null) {
|
|
1532
|
+
return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
|
|
1533
|
+
}
|
|
1534
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(trimmed)) {
|
|
1535
|
+
const localConst = this.localConstants.find((c) => c.name === trimmed && !c.isModule);
|
|
1536
|
+
if (localConst?.value !== undefined) {
|
|
1537
|
+
const initTrimmed = localConst.value.trim();
|
|
1538
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(initTrimmed)) {
|
|
1539
|
+
const resolved = conditionalSpreadToPerl(this.spreadCtx, parseExpression2(initTrimmed));
|
|
1540
|
+
if (resolved !== null) {
|
|
1541
|
+
return `<%== bf->spread_attrs(${resolved}) %>`;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
1547
|
+
return `<%== bf->spread_attrs(${perlExpr}) %>`;
|
|
1548
|
+
},
|
|
1549
|
+
emitBooleanShorthand: () => "",
|
|
1550
|
+
emitJsxChildren: () => ""
|
|
1551
|
+
};
|
|
1552
|
+
tryLowerStyleObject(expr) {
|
|
1553
|
+
const entries = parseStyleObjectEntries(expr);
|
|
1554
|
+
if (!entries)
|
|
1555
|
+
return null;
|
|
1556
|
+
for (const e of entries) {
|
|
1557
|
+
if (e.kind === "expr" && !isSupported(parseExpression2(e.expr)).supported)
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:<%= ${this.convertExpressionToPerl(e.expr)} %>`).join(";");
|
|
1421
1561
|
}
|
|
1422
|
-
|
|
1423
|
-
return "
|
|
1562
|
+
escapeAttrText(s) {
|
|
1563
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
1424
1564
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1565
|
+
renderAttributes(element) {
|
|
1566
|
+
const parts = [];
|
|
1567
|
+
for (const attr of element.attrs) {
|
|
1568
|
+
if (attr.clientOnly)
|
|
1569
|
+
continue;
|
|
1570
|
+
let attrName;
|
|
1571
|
+
if (attr.name === "className")
|
|
1572
|
+
attrName = "class";
|
|
1573
|
+
else if (attr.name === "key")
|
|
1574
|
+
attrName = "data-key";
|
|
1575
|
+
else
|
|
1576
|
+
attrName = attr.name;
|
|
1577
|
+
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
1578
|
+
if (lowered)
|
|
1579
|
+
parts.push(lowered);
|
|
1580
|
+
}
|
|
1581
|
+
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
1427
1582
|
}
|
|
1428
|
-
|
|
1429
|
-
return "
|
|
1583
|
+
renderScopeMarker(_instanceIdExpr) {
|
|
1584
|
+
return `bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %>`;
|
|
1430
1585
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
class MojoTopLevelEmitter {
|
|
1434
|
-
adapter;
|
|
1435
|
-
constructor(adapter) {
|
|
1436
|
-
this.adapter = adapter;
|
|
1586
|
+
renderSlotMarker(slotId) {
|
|
1587
|
+
return `${BF_SLOT}="${slotId}"`;
|
|
1437
1588
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
return "undef";
|
|
1441
|
-
const inlined = this.adapter.resolveModuleStringConst(name);
|
|
1442
|
-
if (inlined !== null)
|
|
1443
|
-
return inlined;
|
|
1444
|
-
const literalConst = this.adapter.resolveLiteralConst(name);
|
|
1445
|
-
if (literalConst !== null)
|
|
1446
|
-
return literalConst;
|
|
1447
|
-
return `$${name}`;
|
|
1589
|
+
renderCondMarker(condId) {
|
|
1590
|
+
return `${BF_COND}="${condId}"`;
|
|
1448
1591
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
return `'${value}'`;
|
|
1452
|
-
if (literalType === "boolean")
|
|
1453
|
-
return value ? "1" : "0";
|
|
1454
|
-
if (literalType === "null")
|
|
1455
|
-
return "undef";
|
|
1456
|
-
return String(value);
|
|
1592
|
+
renderPerlFilterExpr(expr, param, localVarMap = new Map) {
|
|
1593
|
+
return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason)));
|
|
1457
1594
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1595
|
+
convertTemplateLiteralPartsToPerl(literalParts) {
|
|
1596
|
+
const parts = [];
|
|
1597
|
+
for (const part of literalParts) {
|
|
1598
|
+
if (part.type === "string") {
|
|
1599
|
+
parts.push(this.substituteJsInterpolationsToPerl(part.value));
|
|
1600
|
+
} else if (part.type === "ternary") {
|
|
1601
|
+
const cond = this.convertExpressionToPerl(part.condition);
|
|
1602
|
+
parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
|
|
1603
|
+
} else if (part.type === "lookup") {
|
|
1604
|
+
const keyExpr = this.convertExpressionToPerl(part.key);
|
|
1605
|
+
const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
|
|
1606
|
+
parts.push(`({ ${entries} }->{${keyExpr}} // '')`);
|
|
1607
|
+
}
|
|
1466
1608
|
}
|
|
1467
|
-
|
|
1468
|
-
if (property === "length")
|
|
1469
|
-
return `scalar(@{${obj}})`;
|
|
1470
|
-
return `${obj}->{${property}}`;
|
|
1471
|
-
}
|
|
1472
|
-
indexAccess(object, index, emit) {
|
|
1473
|
-
return emitIndexAccessPerl(object, index, emit, (n) => this.adapter._isStringValueName(n));
|
|
1609
|
+
return parts.length === 1 ? parts[0] : parts.join(" . ");
|
|
1474
1610
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
if (
|
|
1482
|
-
|
|
1611
|
+
substituteJsInterpolationsToPerl(s) {
|
|
1612
|
+
const segments = [];
|
|
1613
|
+
const re = /\$\{([^}]+)\}/g;
|
|
1614
|
+
let lastIndex = 0;
|
|
1615
|
+
let m;
|
|
1616
|
+
while ((m = re.exec(s)) !== null) {
|
|
1617
|
+
if (m.index > lastIndex) {
|
|
1618
|
+
segments.push(`'${s.slice(lastIndex, m.index)}'`);
|
|
1483
1619
|
}
|
|
1620
|
+
segments.push(this.convertExpressionToPerl(m[1].trim()));
|
|
1621
|
+
lastIndex = re.lastIndex;
|
|
1484
1622
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
if (path && spec) {
|
|
1488
|
-
if (args.length === spec.arity) {
|
|
1489
|
-
return spec.emit(args.map(emit));
|
|
1490
|
-
}
|
|
1491
|
-
this.adapter._recordExprBF101(`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`, `Call '${path}' with exactly ${spec.arity} argument(s).`);
|
|
1492
|
-
return "''";
|
|
1623
|
+
if (lastIndex < s.length) {
|
|
1624
|
+
segments.push(`'${s.slice(lastIndex)}'`);
|
|
1493
1625
|
}
|
|
1494
|
-
|
|
1626
|
+
if (segments.length === 0)
|
|
1627
|
+
return `''`;
|
|
1628
|
+
return segments.length === 1 ? segments[0] : `(${segments.join(" . ")})`;
|
|
1495
1629
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1630
|
+
refuseUnsupportedAttrExpression(expr, attrName) {
|
|
1631
|
+
let probe = expr.trim();
|
|
1632
|
+
while (probe.startsWith("("))
|
|
1633
|
+
probe = probe.slice(1).trimStart();
|
|
1634
|
+
const startsAsObjectLiteral = probe.startsWith("{");
|
|
1635
|
+
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
|
|
1636
|
+
if (!startsAsObjectLiteral && !hasTaggedTemplate)
|
|
1637
|
+
return false;
|
|
1638
|
+
const parsed = parseExpression2(expr.trim());
|
|
1639
|
+
const support = isSupported(parsed);
|
|
1640
|
+
if (parsed.kind !== "unsupported" && support.supported)
|
|
1641
|
+
return false;
|
|
1642
|
+
const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
|
|
1643
|
+
const reasonLine = reason ? `
|
|
1644
|
+
${reason}` : "";
|
|
1645
|
+
this.errors.push({
|
|
1646
|
+
code: "BF101",
|
|
1647
|
+
severity: "error",
|
|
1648
|
+
message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
|
|
1649
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1650
|
+
suggestion: {
|
|
1651
|
+
message: "The Mojo adapter cannot lower JS object literals or tagged-template-literal expressions into Embedded Perl. Move the expression into a `'use client'` component (so hydration computes it), or expand it into discrete attributes whose values are values the adapter can lower."
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
return true;
|
|
1503
1655
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
return `${l} ne ${r}`;
|
|
1514
|
-
}
|
|
1515
|
-
const opMap = {
|
|
1516
|
-
"===": "==",
|
|
1517
|
-
"!==": "!=",
|
|
1518
|
-
">": ">",
|
|
1519
|
-
"<": "<",
|
|
1520
|
-
">=": ">=",
|
|
1521
|
-
"<=": "<=",
|
|
1522
|
-
"+": "+",
|
|
1523
|
-
"-": "-",
|
|
1524
|
-
"*": "*"
|
|
1656
|
+
get emitCtx() {
|
|
1657
|
+
return {
|
|
1658
|
+
_searchParamsLocals: this._searchParamsLocals,
|
|
1659
|
+
resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
|
|
1660
|
+
resolveLiteralConst: (name) => this.resolveLiteralConst(name),
|
|
1661
|
+
resolveStaticRecordLiteral: (o, k) => this.resolveStaticRecordLiteral(o, k),
|
|
1662
|
+
_isStringValueName: (name) => this._isStringValueName(name),
|
|
1663
|
+
_recordExprBF101: (message, reason) => this._recordExprBF101(message, reason),
|
|
1664
|
+
_renderPerlFilterExprPublic: (e, p) => this._renderPerlFilterExprPublic(e, p)
|
|
1525
1665
|
};
|
|
1526
|
-
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
1527
|
-
}
|
|
1528
|
-
logical(op, left, right, emit) {
|
|
1529
|
-
const l = emit(left);
|
|
1530
|
-
const r = emit(right);
|
|
1531
|
-
if (op === "&&")
|
|
1532
|
-
return `(${l} && ${r})`;
|
|
1533
|
-
if (op === "||")
|
|
1534
|
-
return `(${l} || ${r})`;
|
|
1535
|
-
return `(${l} // ${r})`;
|
|
1536
1666
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
1545
|
-
if (method === "some")
|
|
1546
|
-
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
1547
|
-
const findHelper = {
|
|
1548
|
-
find: "find",
|
|
1549
|
-
findIndex: "find_index",
|
|
1550
|
-
findLast: "find_last",
|
|
1551
|
-
findLastIndex: "find_last_index"
|
|
1667
|
+
get spreadCtx() {
|
|
1668
|
+
return {
|
|
1669
|
+
componentName: this.componentName,
|
|
1670
|
+
errors: this.errors,
|
|
1671
|
+
localConstants: this.localConstants,
|
|
1672
|
+
propsParams: this.propsParams,
|
|
1673
|
+
convertExpressionToPerl: (e, preParsed) => this.convertExpressionToPerl(e, preParsed)
|
|
1552
1674
|
};
|
|
1553
|
-
if (findHelper[method]) {
|
|
1554
|
-
return `bf->${findHelper[method]}(${arrayExpr}, sub { my $${param} = $_[0]; ${predBody} })`;
|
|
1555
|
-
}
|
|
1556
|
-
return arrayExpr;
|
|
1557
|
-
}
|
|
1558
|
-
arrayLiteral(elements, emit) {
|
|
1559
|
-
return `[${elements.map(emit).join(", ")}]`;
|
|
1560
|
-
}
|
|
1561
|
-
arrayMethod(method, object, args, emit) {
|
|
1562
|
-
return renderArrayMethod(method, object, args, emit);
|
|
1563
|
-
}
|
|
1564
|
-
sortMethod(_method, object, comparator, emit) {
|
|
1565
|
-
return renderSortMethod(emit(object), comparator);
|
|
1566
|
-
}
|
|
1567
|
-
reduceMethod(method, object, reduceOp, emit) {
|
|
1568
|
-
return renderReduceMethod(emit(object), reduceOp, method === "reduceRight" ? "right" : "left");
|
|
1569
|
-
}
|
|
1570
|
-
flatMethod(object, depth, emit) {
|
|
1571
|
-
return renderFlatMethod(emit(object), depth);
|
|
1572
1675
|
}
|
|
1573
|
-
|
|
1574
|
-
return
|
|
1575
|
-
}
|
|
1576
|
-
conditional(test, consequent, alternate, emit) {
|
|
1577
|
-
return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
|
|
1676
|
+
get memoCtx() {
|
|
1677
|
+
return { convertExpressionToPerl: (e, preParsed) => this.convertExpressionToPerl(e, preParsed) };
|
|
1578
1678
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1679
|
+
convertExpressionToPerl(expr, preParsed) {
|
|
1680
|
+
let parsed;
|
|
1681
|
+
if (preParsed) {
|
|
1682
|
+
parsed = preParsed;
|
|
1683
|
+
} else {
|
|
1684
|
+
const trimmed = expr.trim();
|
|
1685
|
+
if (trimmed === "")
|
|
1686
|
+
return "''";
|
|
1687
|
+
parsed = parseExpression2(trimmed);
|
|
1688
|
+
}
|
|
1689
|
+
if (parsed.kind === "call") {
|
|
1690
|
+
for (const matcher of this._loweringMatchers) {
|
|
1691
|
+
const node = matcher(parsed.callee, parsed.args);
|
|
1692
|
+
if (node?.kind === "guard-list" && node.helper === "query") {
|
|
1693
|
+
const argsGo = queryHrefArgs(node, (n) => this.renderParsedExprToPerl(n));
|
|
1694
|
+
return `bf->query(${argsGo.join(", ")})`;
|
|
1585
1695
|
}
|
|
1586
|
-
} else {
|
|
1587
|
-
const rendered = emit(part.expr);
|
|
1588
|
-
const needsParens = part.expr.kind === "binary" || part.expr.kind === "logical" || part.expr.kind === "conditional";
|
|
1589
|
-
terms.push(needsParens ? `(${rendered})` : rendered);
|
|
1590
1696
|
}
|
|
1591
1697
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1698
|
+
const support = isSupported(parsed);
|
|
1699
|
+
if (!support.supported) {
|
|
1700
|
+
this.errors.push({
|
|
1701
|
+
code: "BF101",
|
|
1702
|
+
severity: "error",
|
|
1703
|
+
message: `Expression not supported: ${preParsed ? stringifyParsedExpr2(parsed) : expr.trim()}`,
|
|
1704
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1705
|
+
suggestion: {
|
|
1706
|
+
message: support.reason ? `${support.reason}
|
|
1707
|
+
|
|
1708
|
+
Options:
|
|
1709
|
+
1. Use /* @client */ for client-side evaluation
|
|
1710
|
+
2. Pre-compute the value in Perl` : `Options:
|
|
1711
|
+
1. Use /* @client */ for client-side evaluation
|
|
1712
|
+
2. Pre-compute the value in Perl`
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
return "''";
|
|
1716
|
+
}
|
|
1717
|
+
return this.renderParsedExprToPerl(parsed);
|
|
1595
1718
|
}
|
|
1596
|
-
|
|
1597
|
-
return
|
|
1719
|
+
renderParsedExprToPerl(expr) {
|
|
1720
|
+
return emitParsedExpr2(expr, new MojoTopLevelEmitter(this.emitCtx));
|
|
1598
1721
|
}
|
|
1599
|
-
|
|
1600
|
-
return
|
|
1722
|
+
_isStringValueName(name) {
|
|
1723
|
+
return this.stringValueNames.has(name);
|
|
1724
|
+
}
|
|
1725
|
+
_recordExprBF101(message, reason) {
|
|
1726
|
+
this.errors.push({
|
|
1727
|
+
code: "BF101",
|
|
1728
|
+
severity: "error",
|
|
1729
|
+
message,
|
|
1730
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
1731
|
+
suggestion: {
|
|
1732
|
+
message: reason ? `${reason}
|
|
1733
|
+
|
|
1734
|
+
Options:
|
|
1735
|
+
1. Use /* @client */ for client-side evaluation
|
|
1736
|
+
2. Pre-compute the value in Perl` : `Options:
|
|
1737
|
+
1. Use /* @client */ for client-side evaluation
|
|
1738
|
+
2. Pre-compute the value in Perl`
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
_renderPerlFilterExprPublic(expr, param) {
|
|
1743
|
+
return this.renderPerlFilterExpr(expr, param);
|
|
1601
1744
|
}
|
|
1602
1745
|
}
|
|
1603
1746
|
var mojoAdapter = new MojoAdapter;
|