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