@barefootjs/mojolicious 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/__tests__/boolean-result.test.d.ts +2 -0
- package/dist/adapter/__tests__/boolean-result.test.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +42 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +1143 -0
- package/dist/adapter/mojo-adapter.d.ts +219 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +1163 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1143 -0
- package/dist/test-render.d.ts +38 -0
- package/dist/test-render.d.ts.map +1 -0
- package/lib/BarefootJS.pm +745 -0
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +150 -0
- package/lib/Mojolicious/Plugin/BarefootJS.pm +104 -0
- package/package.json +65 -0
- package/src/__tests__/mojo-adapter.test.ts +940 -0
- package/src/__tests__/mojo-streaming.test.ts +136 -0
- package/src/__tests__/scaffold.test.ts +224 -0
- package/src/__tests__/template-base-name.test.ts +26 -0
- package/src/adapter/__tests__/boolean-result.test.ts +106 -0
- package/src/adapter/boolean-result.ts +126 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/mojo-adapter.ts +1931 -0
- package/src/build.ts +37 -0
- package/src/index.ts +8 -0
- package/src/test-render.ts +704 -0
|
@@ -0,0 +1,1143 @@
|
|
|
1
|
+
// src/adapter/mojo-adapter.ts
|
|
2
|
+
import {
|
|
3
|
+
BaseAdapter,
|
|
4
|
+
isBooleanAttr,
|
|
5
|
+
parseExpression as parseExpression2,
|
|
6
|
+
isSupported,
|
|
7
|
+
identifierPath,
|
|
8
|
+
stringifyParsedExpr,
|
|
9
|
+
emitParsedExpr,
|
|
10
|
+
emitIRNode,
|
|
11
|
+
emitAttrValue
|
|
12
|
+
} from "@barefootjs/jsx";
|
|
13
|
+
|
|
14
|
+
// src/adapter/boolean-result.ts
|
|
15
|
+
import { parseExpression } from "@barefootjs/jsx";
|
|
16
|
+
var COMPARISON_OPS = new Set([
|
|
17
|
+
"<",
|
|
18
|
+
">",
|
|
19
|
+
"<=",
|
|
20
|
+
">=",
|
|
21
|
+
"==",
|
|
22
|
+
"===",
|
|
23
|
+
"!=",
|
|
24
|
+
"!=="
|
|
25
|
+
]);
|
|
26
|
+
function isBooleanResultParsed(node) {
|
|
27
|
+
switch (node.kind) {
|
|
28
|
+
case "literal":
|
|
29
|
+
return node.literalType === "boolean";
|
|
30
|
+
case "binary":
|
|
31
|
+
return COMPARISON_OPS.has(node.op);
|
|
32
|
+
case "unary":
|
|
33
|
+
return node.op === "!";
|
|
34
|
+
case "logical":
|
|
35
|
+
return isBooleanResultParsed(node.left) && isBooleanResultParsed(node.right);
|
|
36
|
+
case "conditional":
|
|
37
|
+
return isBooleanResultParsed(node.consequent) && isBooleanResultParsed(node.alternate);
|
|
38
|
+
default:
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isBooleanResultExpr(expr) {
|
|
43
|
+
const parsed = parseExpression(expr.trim());
|
|
44
|
+
if (!parsed)
|
|
45
|
+
return false;
|
|
46
|
+
return isBooleanResultParsed(parsed);
|
|
47
|
+
}
|
|
48
|
+
var ARIA_BOOLEAN_ATTRS = new Set([
|
|
49
|
+
"aria-atomic",
|
|
50
|
+
"aria-busy",
|
|
51
|
+
"aria-disabled",
|
|
52
|
+
"aria-hidden",
|
|
53
|
+
"aria-modal",
|
|
54
|
+
"aria-multiline",
|
|
55
|
+
"aria-multiselectable",
|
|
56
|
+
"aria-readonly",
|
|
57
|
+
"aria-required",
|
|
58
|
+
"aria-checked",
|
|
59
|
+
"aria-pressed"
|
|
60
|
+
]);
|
|
61
|
+
function isAriaBooleanAttr(name) {
|
|
62
|
+
return ARIA_BOOLEAN_ATTRS.has(name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/adapter/mojo-adapter.ts
|
|
66
|
+
import { BF_SLOT, BF_COND } from "@barefootjs/shared";
|
|
67
|
+
var MOJO_TEMPLATE_PRIMITIVES = {
|
|
68
|
+
"JSON.stringify": { arity: 1, emit: (args) => `bf->json(${args[0]})` },
|
|
69
|
+
String: { arity: 1, emit: (args) => `bf->string(${args[0]})` },
|
|
70
|
+
Number: { arity: 1, emit: (args) => `bf->number(${args[0]})` },
|
|
71
|
+
"Math.floor": { arity: 1, emit: (args) => `bf->floor(${args[0]})` },
|
|
72
|
+
"Math.ceil": { arity: 1, emit: (args) => `bf->ceil(${args[0]})` },
|
|
73
|
+
"Math.round": { arity: 1, emit: (args) => `bf->round(${args[0]})` }
|
|
74
|
+
};
|
|
75
|
+
var PRIMITIVE_SUBSTRING_RE = new RegExp(Object.keys(MOJO_TEMPLATE_PRIMITIVES).map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"));
|
|
76
|
+
var MOJO_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(MOJO_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
|
|
77
|
+
function resolveJsxChildrenProp(props) {
|
|
78
|
+
const prop = props.find((p) => p.name === "children");
|
|
79
|
+
if (!prop)
|
|
80
|
+
return [];
|
|
81
|
+
if (prop.value.kind !== "jsx-children")
|
|
82
|
+
return [];
|
|
83
|
+
return prop.value.children;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class MojoAdapter extends BaseAdapter {
|
|
87
|
+
name = "mojolicious";
|
|
88
|
+
extension = ".html.ep";
|
|
89
|
+
templatesPerComponent = true;
|
|
90
|
+
templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
|
|
91
|
+
componentName = "";
|
|
92
|
+
options;
|
|
93
|
+
errors = [];
|
|
94
|
+
inLoop = false;
|
|
95
|
+
higherOrderInFlight = new Set;
|
|
96
|
+
propsObjectName = null;
|
|
97
|
+
propsParams = [];
|
|
98
|
+
constructor(options = {}) {
|
|
99
|
+
super();
|
|
100
|
+
this.options = {
|
|
101
|
+
clientJsBasePath: options.clientJsBasePath ?? "/static/components/",
|
|
102
|
+
barefootJsPath: options.barefootJsPath ?? "/static/components/barefoot.js"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
generate(ir, options) {
|
|
106
|
+
this.componentName = ir.metadata.componentName;
|
|
107
|
+
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
108
|
+
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
109
|
+
this.errors = [];
|
|
110
|
+
this.higherOrderInFlight = new Set;
|
|
111
|
+
this.childrenCaptureCounter = 0;
|
|
112
|
+
if (!options?.siblingTemplatesRegistered) {
|
|
113
|
+
this.checkImportedLoopChildComponents(ir);
|
|
114
|
+
}
|
|
115
|
+
const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
|
|
116
|
+
const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
|
|
117
|
+
const template = `${scriptReg}${templateBody}
|
|
118
|
+
`;
|
|
119
|
+
if (this.errors.length > 0) {
|
|
120
|
+
ir.errors.push(...this.errors);
|
|
121
|
+
}
|
|
122
|
+
const sections = {
|
|
123
|
+
imports: "",
|
|
124
|
+
types: "",
|
|
125
|
+
component: template,
|
|
126
|
+
defaultExport: ""
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
template,
|
|
130
|
+
sections,
|
|
131
|
+
extension: this.extension
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
generateScriptRegistrations(ir, scriptBaseName) {
|
|
135
|
+
const hasInteractivity = this.hasClientInteractivity(ir);
|
|
136
|
+
if (!hasInteractivity)
|
|
137
|
+
return "";
|
|
138
|
+
const name = scriptBaseName ?? ir.metadata.componentName;
|
|
139
|
+
const runtimePath = this.options.barefootJsPath;
|
|
140
|
+
const clientJsPath = `${this.options.clientJsBasePath}${name}.client.js`;
|
|
141
|
+
const lines = [];
|
|
142
|
+
lines.push(`% bf->register_script('${runtimePath}');`);
|
|
143
|
+
lines.push(`% bf->register_script('${clientJsPath}');`);
|
|
144
|
+
lines.push("");
|
|
145
|
+
return lines.join(`
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
hasClientInteractivity(ir) {
|
|
149
|
+
return ir.metadata.signals.length > 0 || ir.metadata.effects.length > 0 || ir.metadata.onMounts.length > 0 || (ir.metadata.clientAnalysis?.needsInit ?? false);
|
|
150
|
+
}
|
|
151
|
+
renderNode(node) {
|
|
152
|
+
return emitIRNode(node, this, {});
|
|
153
|
+
}
|
|
154
|
+
emitElement(node, _ctx, _emit) {
|
|
155
|
+
return this.renderElement(node);
|
|
156
|
+
}
|
|
157
|
+
emitText(node) {
|
|
158
|
+
return node.value;
|
|
159
|
+
}
|
|
160
|
+
emitExpression(node) {
|
|
161
|
+
return this.renderExpression(node);
|
|
162
|
+
}
|
|
163
|
+
emitConditional(node, _ctx, _emit) {
|
|
164
|
+
return this.renderConditional(node);
|
|
165
|
+
}
|
|
166
|
+
emitLoop(node, _ctx, _emit) {
|
|
167
|
+
return this.renderLoop(node);
|
|
168
|
+
}
|
|
169
|
+
emitComponent(node, _ctx, _emit) {
|
|
170
|
+
return this.renderComponent(node);
|
|
171
|
+
}
|
|
172
|
+
emitFragment(node, _ctx, _emit) {
|
|
173
|
+
return this.renderFragment(node);
|
|
174
|
+
}
|
|
175
|
+
emitSlot(node) {
|
|
176
|
+
return this.renderSlot(node);
|
|
177
|
+
}
|
|
178
|
+
emitIfStatement(node, _ctx, _emit) {
|
|
179
|
+
return this.renderIfStatement(node);
|
|
180
|
+
}
|
|
181
|
+
emitProvider(node, _ctx, _emit) {
|
|
182
|
+
return this.renderChildren(node.children);
|
|
183
|
+
}
|
|
184
|
+
emitAsync(node, _ctx, _emit) {
|
|
185
|
+
return this.renderAsync(node);
|
|
186
|
+
}
|
|
187
|
+
renderElement(element) {
|
|
188
|
+
const tag = element.tag;
|
|
189
|
+
const attrs = this.renderAttributes(element);
|
|
190
|
+
const children = this.renderChildren(element.children);
|
|
191
|
+
let hydrationAttrs = "";
|
|
192
|
+
if (element.needsScope) {
|
|
193
|
+
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
194
|
+
}
|
|
195
|
+
if (element.slotId) {
|
|
196
|
+
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
|
|
197
|
+
}
|
|
198
|
+
const voidElements = [
|
|
199
|
+
"area",
|
|
200
|
+
"base",
|
|
201
|
+
"br",
|
|
202
|
+
"col",
|
|
203
|
+
"embed",
|
|
204
|
+
"hr",
|
|
205
|
+
"img",
|
|
206
|
+
"input",
|
|
207
|
+
"link",
|
|
208
|
+
"meta",
|
|
209
|
+
"param",
|
|
210
|
+
"source",
|
|
211
|
+
"track",
|
|
212
|
+
"wbr"
|
|
213
|
+
];
|
|
214
|
+
if (voidElements.includes(tag.toLowerCase())) {
|
|
215
|
+
return `<${tag}${attrs}${hydrationAttrs}>`;
|
|
216
|
+
}
|
|
217
|
+
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
218
|
+
}
|
|
219
|
+
renderExpression(expr) {
|
|
220
|
+
if (expr.clientOnly) {
|
|
221
|
+
if (expr.slotId) {
|
|
222
|
+
return `<%== bf->comment("client:${expr.slotId}") %>`;
|
|
223
|
+
}
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
const perlExpr = this.convertExpressionToPerl(expr.expr);
|
|
227
|
+
if (expr.slotId) {
|
|
228
|
+
return `<%== bf->text_start("${expr.slotId}") %><%= ${perlExpr} %><%== bf->text_end %>`;
|
|
229
|
+
}
|
|
230
|
+
return `<%= ${perlExpr} %>`;
|
|
231
|
+
}
|
|
232
|
+
renderConditional(cond) {
|
|
233
|
+
if (cond.clientOnly && cond.slotId) {
|
|
234
|
+
return `<%== bf->comment("cond-start:${cond.slotId}") %><%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
235
|
+
}
|
|
236
|
+
const condition = this.convertExpressionToPerl(cond.condition);
|
|
237
|
+
const whenTrue = this.renderNode(cond.whenTrue);
|
|
238
|
+
const whenFalse = this.renderNodeOrNull(cond.whenFalse);
|
|
239
|
+
const isFragmentBranch = cond.whenTrue.type === "fragment" || cond.whenFalse.type === "fragment";
|
|
240
|
+
const useCommentMarkers = cond.slotId && isFragmentBranch;
|
|
241
|
+
let markedTrue = whenTrue;
|
|
242
|
+
let markedFalse = whenFalse;
|
|
243
|
+
if (cond.slotId && !useCommentMarkers) {
|
|
244
|
+
markedTrue = this.addCondMarkerToFirstElement(whenTrue, cond.slotId);
|
|
245
|
+
markedFalse = whenFalse ? this.addCondMarkerToFirstElement(whenFalse, cond.slotId) : whenFalse;
|
|
246
|
+
}
|
|
247
|
+
let result;
|
|
248
|
+
if (useCommentMarkers) {
|
|
249
|
+
const inner = whenFalse ? `
|
|
250
|
+
% if (${condition}) {
|
|
251
|
+
${whenTrue}
|
|
252
|
+
% } else {
|
|
253
|
+
${whenFalse}
|
|
254
|
+
% }
|
|
255
|
+
` : `
|
|
256
|
+
% if (${condition}) {
|
|
257
|
+
${whenTrue}
|
|
258
|
+
% }
|
|
259
|
+
`;
|
|
260
|
+
result = `<%== bf->comment("cond-start:${cond.slotId}") %>${inner}<%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
261
|
+
} else if (markedFalse) {
|
|
262
|
+
result = `
|
|
263
|
+
% if (${condition}) {
|
|
264
|
+
${markedTrue}
|
|
265
|
+
% } else {
|
|
266
|
+
${markedFalse}
|
|
267
|
+
% }
|
|
268
|
+
`;
|
|
269
|
+
} else if (cond.slotId) {
|
|
270
|
+
result = `<%== bf->comment("cond-start:${cond.slotId}") %>
|
|
271
|
+
% if (${condition}) {
|
|
272
|
+
${whenTrue}
|
|
273
|
+
% }
|
|
274
|
+
<%== bf->comment("cond-end:${cond.slotId}") %>`;
|
|
275
|
+
} else {
|
|
276
|
+
result = `
|
|
277
|
+
% if (${condition}) {
|
|
278
|
+
${whenTrue}
|
|
279
|
+
% }
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
renderNodeOrNull(node) {
|
|
285
|
+
if (node.type === "expression" && (node.expr === "null" || node.expr === "undefined")) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return this.renderNode(node);
|
|
289
|
+
}
|
|
290
|
+
addCondMarkerToFirstElement(content, condId) {
|
|
291
|
+
const match = content.match(/^(<\w+)([\s>])/);
|
|
292
|
+
if (match) {
|
|
293
|
+
return content.replace(/^(<\w+)([\s>])/, `$1 ${BF_COND}="${condId}"$2`);
|
|
294
|
+
}
|
|
295
|
+
return `<%== bf->comment("cond-start:${condId}") %>${content}<%== bf->comment("cond-end:${condId}") %>`;
|
|
296
|
+
}
|
|
297
|
+
checkImportedLoopChildComponents(ir) {
|
|
298
|
+
const relativeImports = new Set;
|
|
299
|
+
for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
|
|
300
|
+
if (!imp.source.startsWith("./") && !imp.source.startsWith("../"))
|
|
301
|
+
continue;
|
|
302
|
+
if (imp.isTypeOnly)
|
|
303
|
+
continue;
|
|
304
|
+
for (const spec of imp.specifiers) {
|
|
305
|
+
relativeImports.add(spec.alias ?? spec.name);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (relativeImports.size === 0)
|
|
309
|
+
return;
|
|
310
|
+
const loc = { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
|
|
311
|
+
const visit = (node, inLoop) => {
|
|
312
|
+
switch (node.type) {
|
|
313
|
+
case "component": {
|
|
314
|
+
const comp = node;
|
|
315
|
+
if (inLoop && relativeImports.has(comp.name)) {
|
|
316
|
+
this.errors.push({
|
|
317
|
+
code: "BF103",
|
|
318
|
+
severity: "error",
|
|
319
|
+
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.`,
|
|
320
|
+
loc: comp.loc ?? loc,
|
|
321
|
+
suggestion: {
|
|
322
|
+
message: `Options:
|
|
323
|
+
` + ` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Mojo template alongside the parent at render time.
|
|
324
|
+
` + ` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.
|
|
325
|
+
` + ` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
for (const child of comp.children)
|
|
330
|
+
visit(child, inLoop);
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case "element":
|
|
334
|
+
for (const child of node.children)
|
|
335
|
+
visit(child, inLoop);
|
|
336
|
+
break;
|
|
337
|
+
case "fragment":
|
|
338
|
+
for (const child of node.children)
|
|
339
|
+
visit(child, inLoop);
|
|
340
|
+
break;
|
|
341
|
+
case "conditional": {
|
|
342
|
+
const cond = node;
|
|
343
|
+
visit(cond.whenTrue, inLoop);
|
|
344
|
+
if (cond.whenFalse)
|
|
345
|
+
visit(cond.whenFalse, inLoop);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case "loop":
|
|
349
|
+
for (const child of node.children)
|
|
350
|
+
visit(child, true);
|
|
351
|
+
break;
|
|
352
|
+
case "if-statement": {
|
|
353
|
+
const stmt = node;
|
|
354
|
+
visit(stmt.consequent, inLoop);
|
|
355
|
+
if (stmt.alternate)
|
|
356
|
+
visit(stmt.alternate, inLoop);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case "provider":
|
|
360
|
+
for (const child of node.children)
|
|
361
|
+
visit(child, inLoop);
|
|
362
|
+
break;
|
|
363
|
+
case "async": {
|
|
364
|
+
const a = node;
|
|
365
|
+
visit(a.fallback, inLoop);
|
|
366
|
+
for (const child of a.children)
|
|
367
|
+
visit(child, inLoop);
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
visit(ir.root, false);
|
|
373
|
+
}
|
|
374
|
+
renderLoop(loop) {
|
|
375
|
+
if (loop.clientOnly)
|
|
376
|
+
return "";
|
|
377
|
+
if (loop.paramBindings && loop.paramBindings.length > 0) {
|
|
378
|
+
this.errors.push({
|
|
379
|
+
code: "BF104",
|
|
380
|
+
severity: "error",
|
|
381
|
+
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.`,
|
|
382
|
+
loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
383
|
+
suggestion: {
|
|
384
|
+
message: `Options:
|
|
385
|
+
` + ` 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]) => ...\`).
|
|
386
|
+
` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
|
|
387
|
+
` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const rawArray = this.convertExpressionToPerl(loop.array);
|
|
392
|
+
let sortedHoist = null;
|
|
393
|
+
let array = rawArray;
|
|
394
|
+
if (loop.sortComparator) {
|
|
395
|
+
sortedHoist = `bf_iter_${perlIdentifierFromMarkerId(loop.markerId)}`;
|
|
396
|
+
array = `$${sortedHoist}`;
|
|
397
|
+
}
|
|
398
|
+
const param = loop.param;
|
|
399
|
+
const indexVar = loop.index ? `$${loop.index}` : "$_i";
|
|
400
|
+
const prevInLoop = this.inLoop;
|
|
401
|
+
this.inLoop = true;
|
|
402
|
+
const children = this.renderChildren(loop.children);
|
|
403
|
+
this.inLoop = prevInLoop;
|
|
404
|
+
const lines = [];
|
|
405
|
+
lines.push(`<%== bf->comment("loop:${loop.markerId}") %>`);
|
|
406
|
+
if (sortedHoist && loop.sortComparator) {
|
|
407
|
+
lines.push(`% my $${sortedHoist} = ${renderSortMethod(rawArray, loop.sortComparator)};`);
|
|
408
|
+
}
|
|
409
|
+
lines.push(`% for my ${indexVar} (0..$#{${array}}) {`);
|
|
410
|
+
lines.push(`% my $${param} = ${array}->[${indexVar}];`);
|
|
411
|
+
if (loop.filterPredicate) {
|
|
412
|
+
let filterCond;
|
|
413
|
+
if (loop.filterPredicate.blockBody) {
|
|
414
|
+
filterCond = this.renderBlockBodyCondition(loop.filterPredicate.blockBody, loop.filterPredicate.param);
|
|
415
|
+
} else if (loop.filterPredicate.predicate) {
|
|
416
|
+
filterCond = this.renderPerlFilterExpr(loop.filterPredicate.predicate, loop.filterPredicate.param);
|
|
417
|
+
} else {
|
|
418
|
+
filterCond = "1";
|
|
419
|
+
}
|
|
420
|
+
if (loop.filterPredicate.param !== param) {
|
|
421
|
+
filterCond = filterCond.replace(new RegExp(`\\$${loop.filterPredicate.param}\\b`, "g"), `$${param}`);
|
|
422
|
+
}
|
|
423
|
+
lines.push(`% if (${filterCond}) {`);
|
|
424
|
+
lines.push(children);
|
|
425
|
+
lines.push(`% }`);
|
|
426
|
+
} else {
|
|
427
|
+
lines.push(children);
|
|
428
|
+
}
|
|
429
|
+
lines.push(`% }`);
|
|
430
|
+
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
431
|
+
return lines.join(`
|
|
432
|
+
`);
|
|
433
|
+
}
|
|
434
|
+
componentPropEmitter = {
|
|
435
|
+
emitLiteral: (value, name) => `${name} => '${value.value}'`,
|
|
436
|
+
emitExpression: (value, name) => {
|
|
437
|
+
if (value.parts) {
|
|
438
|
+
return `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
|
|
439
|
+
}
|
|
440
|
+
return `${name} => ${this.convertExpressionToPerl(value.expr)}`;
|
|
441
|
+
},
|
|
442
|
+
emitSpread: (value) => {
|
|
443
|
+
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
444
|
+
return perlExpr.startsWith("%") ? perlExpr : `%{${perlExpr}}`;
|
|
445
|
+
},
|
|
446
|
+
emitTemplate: (value, name) => `${name} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
|
|
447
|
+
emitBooleanAttr: (_value, name) => `${name} => 1`,
|
|
448
|
+
emitBooleanShorthand: (_value, name) => `${name} => 1`,
|
|
449
|
+
emitJsxChildren: () => ""
|
|
450
|
+
};
|
|
451
|
+
renderComponent(comp) {
|
|
452
|
+
const propParts = [];
|
|
453
|
+
for (const p of comp.props) {
|
|
454
|
+
if (p.name.match(/^on[A-Z]/) && p.value.kind === "expression")
|
|
455
|
+
continue;
|
|
456
|
+
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
|
|
457
|
+
if (lowered)
|
|
458
|
+
propParts.push(lowered);
|
|
459
|
+
}
|
|
460
|
+
if (comp.slotId && !this.inLoop) {
|
|
461
|
+
propParts.push(`_bf_slot => '${comp.slotId}'`);
|
|
462
|
+
}
|
|
463
|
+
const propsStr = propParts.length > 0 ? ", " + propParts.join(", ") : "";
|
|
464
|
+
const tplName = this.toTemplateName(comp.name);
|
|
465
|
+
const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
|
|
466
|
+
if (effectiveChildren.length > 0) {
|
|
467
|
+
const childrenBody = this.renderChildren(effectiveChildren);
|
|
468
|
+
const varName = `$bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
469
|
+
return `<% my ${varName} = begin %>${childrenBody}<% end %><%== bf->render_child('${tplName}'${propsStr}, children => ${varName}) %>`;
|
|
470
|
+
}
|
|
471
|
+
return `<%== bf->render_child('${tplName}'${propsStr}) %>`;
|
|
472
|
+
}
|
|
473
|
+
childrenCaptureCounter = 0;
|
|
474
|
+
toTemplateName(componentName) {
|
|
475
|
+
return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
476
|
+
}
|
|
477
|
+
renderIfStatement(ifStmt) {
|
|
478
|
+
const condition = this.convertExpressionToPerl(ifStmt.condition);
|
|
479
|
+
const consequent = ifStmt.consequent.type === "if-statement" ? this.renderIfStatement(ifStmt.consequent) : this.renderNode(ifStmt.consequent);
|
|
480
|
+
let result = `% if (${condition}) {
|
|
481
|
+
${consequent}
|
|
482
|
+
`;
|
|
483
|
+
if (ifStmt.alternate) {
|
|
484
|
+
if (ifStmt.alternate.type === "if-statement") {
|
|
485
|
+
const altResult = this.renderIfStatement(ifStmt.alternate);
|
|
486
|
+
result += altResult.replace(/^% if/, "% } elsif");
|
|
487
|
+
} else {
|
|
488
|
+
const alternate = this.renderNode(ifStmt.alternate);
|
|
489
|
+
result += `% } else {
|
|
490
|
+
${alternate}
|
|
491
|
+
`;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
result += `% }`;
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
renderFragment(fragment) {
|
|
498
|
+
const children = this.renderChildren(fragment.children);
|
|
499
|
+
if (fragment.needsScopeComment) {
|
|
500
|
+
return `<%== bf->scope_comment %>${children}`;
|
|
501
|
+
}
|
|
502
|
+
return children;
|
|
503
|
+
}
|
|
504
|
+
renderSlot(_slot) {
|
|
505
|
+
return `<%= content %>`;
|
|
506
|
+
}
|
|
507
|
+
renderAsync(node) {
|
|
508
|
+
const fallback = this.renderNode(node.fallback);
|
|
509
|
+
const children = this.renderChildren(node.children);
|
|
510
|
+
const fallbackVar = `$bf_async_fallback_${node.id}`;
|
|
511
|
+
return `<% my ${fallbackVar} = begin %>${fallback}<% end %><%== bf->async_boundary('${node.id}', ${fallbackVar}) %>
|
|
512
|
+
${children}`;
|
|
513
|
+
}
|
|
514
|
+
elementAttrEmitter = {
|
|
515
|
+
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
516
|
+
emitExpression: (value, name) => {
|
|
517
|
+
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
518
|
+
return "";
|
|
519
|
+
}
|
|
520
|
+
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
521
|
+
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
522
|
+
}
|
|
523
|
+
const perl = this.convertExpressionToPerl(value.expr);
|
|
524
|
+
if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name)) {
|
|
525
|
+
return `${name}="<%= bf->bool_str(${perl}) %>"`;
|
|
526
|
+
}
|
|
527
|
+
return `${name}="<%= ${perl} %>"`;
|
|
528
|
+
},
|
|
529
|
+
emitBooleanAttr: (_value, name) => name,
|
|
530
|
+
emitTemplate: (value, name) => `${name}="<%= ${this.convertTemplateLiteralPartsToPerl(value.parts)} %>"`,
|
|
531
|
+
emitSpread: (value) => {
|
|
532
|
+
if (this.refuseUnsupportedAttrExpression(value.expr, "...")) {
|
|
533
|
+
return "";
|
|
534
|
+
}
|
|
535
|
+
const trimmed = value.expr.trim();
|
|
536
|
+
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
537
|
+
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
538
|
+
return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
|
|
539
|
+
}
|
|
540
|
+
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
541
|
+
return `<%== bf->spread_attrs(${perlExpr}) %>`;
|
|
542
|
+
},
|
|
543
|
+
emitBooleanShorthand: () => "",
|
|
544
|
+
emitJsxChildren: () => ""
|
|
545
|
+
};
|
|
546
|
+
renderAttributes(element) {
|
|
547
|
+
const parts = [];
|
|
548
|
+
for (const attr of element.attrs) {
|
|
549
|
+
let attrName;
|
|
550
|
+
if (attr.name === "className")
|
|
551
|
+
attrName = "class";
|
|
552
|
+
else if (attr.name === "key")
|
|
553
|
+
attrName = "data-key";
|
|
554
|
+
else
|
|
555
|
+
attrName = attr.name;
|
|
556
|
+
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
557
|
+
if (lowered)
|
|
558
|
+
parts.push(lowered);
|
|
559
|
+
}
|
|
560
|
+
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
561
|
+
}
|
|
562
|
+
renderScopeMarker(_instanceIdExpr) {
|
|
563
|
+
return `bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %>`;
|
|
564
|
+
}
|
|
565
|
+
renderSlotMarker(slotId) {
|
|
566
|
+
return `${BF_SLOT}="${slotId}"`;
|
|
567
|
+
}
|
|
568
|
+
renderCondMarker(condId) {
|
|
569
|
+
return `${BF_COND}="${condId}"`;
|
|
570
|
+
}
|
|
571
|
+
renderPerlFilterExpr(expr, param, localVarMap = new Map) {
|
|
572
|
+
return emitParsedExpr(expr, new MojoFilterEmitter(param, localVarMap));
|
|
573
|
+
}
|
|
574
|
+
renderBlockBodyCondition(statements, param) {
|
|
575
|
+
const localVarMap = new Map;
|
|
576
|
+
const paths = this.collectReturnPaths(statements, [], localVarMap, param);
|
|
577
|
+
if (paths.length === 0)
|
|
578
|
+
return "1";
|
|
579
|
+
if (paths.length === 1)
|
|
580
|
+
return this.buildSinglePathCondition(paths[0], param, localVarMap);
|
|
581
|
+
const parts = [];
|
|
582
|
+
for (const path of paths) {
|
|
583
|
+
if (path.result.kind === "literal" && path.result.literalType === "boolean" && path.result.value === false)
|
|
584
|
+
continue;
|
|
585
|
+
const cond = this.buildSinglePathCondition(path, param, localVarMap);
|
|
586
|
+
if (cond !== "0")
|
|
587
|
+
parts.push(cond);
|
|
588
|
+
}
|
|
589
|
+
if (parts.length === 0)
|
|
590
|
+
return "0";
|
|
591
|
+
if (parts.length === 1)
|
|
592
|
+
return parts[0];
|
|
593
|
+
return `(${parts.join(" || ")})`;
|
|
594
|
+
}
|
|
595
|
+
collectReturnPaths(statements, currentConditions, localVarMap, param) {
|
|
596
|
+
const paths = [];
|
|
597
|
+
for (const stmt of statements) {
|
|
598
|
+
if (stmt.kind === "var-decl") {
|
|
599
|
+
if (stmt.init.kind === "call" && stmt.init.callee.kind === "identifier") {
|
|
600
|
+
localVarMap.set(stmt.name, stmt.init.callee.name);
|
|
601
|
+
}
|
|
602
|
+
} else if (stmt.kind === "return") {
|
|
603
|
+
paths.push({ conditions: [...currentConditions], result: stmt.value });
|
|
604
|
+
break;
|
|
605
|
+
} else if (stmt.kind === "if") {
|
|
606
|
+
const thenPaths = this.collectReturnPaths(stmt.consequent, [...currentConditions, stmt.condition], localVarMap, param);
|
|
607
|
+
paths.push(...thenPaths);
|
|
608
|
+
if (stmt.alternate) {
|
|
609
|
+
const negated = { kind: "unary", op: "!", argument: stmt.condition };
|
|
610
|
+
const elsePaths = this.collectReturnPaths(stmt.alternate, [...currentConditions, negated], localVarMap, param);
|
|
611
|
+
paths.push(...elsePaths);
|
|
612
|
+
} else {
|
|
613
|
+
currentConditions.push({ kind: "unary", op: "!", argument: stmt.condition });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return paths;
|
|
618
|
+
}
|
|
619
|
+
buildSinglePathCondition(path, param, localVarMap) {
|
|
620
|
+
if (path.result.kind === "literal" && path.result.literalType === "boolean") {
|
|
621
|
+
if (path.result.value === true) {
|
|
622
|
+
if (path.conditions.length === 0)
|
|
623
|
+
return "1";
|
|
624
|
+
return this.renderConditionsAnd(path.conditions, param, localVarMap);
|
|
625
|
+
}
|
|
626
|
+
return "0";
|
|
627
|
+
}
|
|
628
|
+
if (path.conditions.length === 0) {
|
|
629
|
+
return this.renderPerlFilterExpr(path.result, param, localVarMap);
|
|
630
|
+
}
|
|
631
|
+
const condPart = this.renderConditionsAnd(path.conditions, param, localVarMap);
|
|
632
|
+
const resultPart = this.renderPerlFilterExpr(path.result, param, localVarMap);
|
|
633
|
+
return `(${condPart} && ${resultPart})`;
|
|
634
|
+
}
|
|
635
|
+
renderConditionsAnd(conditions, param, localVarMap) {
|
|
636
|
+
if (conditions.length === 0)
|
|
637
|
+
return "1";
|
|
638
|
+
if (conditions.length === 1)
|
|
639
|
+
return this.renderPerlFilterExpr(conditions[0], param, localVarMap);
|
|
640
|
+
const parts = conditions.map((c) => this.renderPerlFilterExpr(c, param, localVarMap));
|
|
641
|
+
return `(${parts.join(" && ")})`;
|
|
642
|
+
}
|
|
643
|
+
convertTemplateLiteralPartsToPerl(literalParts) {
|
|
644
|
+
const parts = [];
|
|
645
|
+
for (const part of literalParts) {
|
|
646
|
+
if (part.type === "string") {
|
|
647
|
+
parts.push(this.substituteJsInterpolationsToPerl(part.value));
|
|
648
|
+
} else if (part.type === "ternary") {
|
|
649
|
+
const cond = this.convertExpressionToPerl(part.condition);
|
|
650
|
+
parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
|
|
651
|
+
} else if (part.type === "lookup") {
|
|
652
|
+
const keyExpr = this.convertExpressionToPerl(part.key);
|
|
653
|
+
const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
|
|
654
|
+
parts.push(`({ ${entries} }->{${keyExpr}} // '')`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return parts.length === 1 ? parts[0] : parts.join(" . ");
|
|
658
|
+
}
|
|
659
|
+
substituteJsInterpolationsToPerl(s) {
|
|
660
|
+
const segments = [];
|
|
661
|
+
const re = /\$\{([^}]+)\}/g;
|
|
662
|
+
let lastIndex = 0;
|
|
663
|
+
let m;
|
|
664
|
+
while ((m = re.exec(s)) !== null) {
|
|
665
|
+
if (m.index > lastIndex) {
|
|
666
|
+
segments.push(`'${s.slice(lastIndex, m.index)}'`);
|
|
667
|
+
}
|
|
668
|
+
segments.push(this.convertExpressionToPerl(m[1].trim()));
|
|
669
|
+
lastIndex = re.lastIndex;
|
|
670
|
+
}
|
|
671
|
+
if (lastIndex < s.length) {
|
|
672
|
+
segments.push(`'${s.slice(lastIndex)}'`);
|
|
673
|
+
}
|
|
674
|
+
if (segments.length === 0)
|
|
675
|
+
return `''`;
|
|
676
|
+
return segments.length === 1 ? segments[0] : `(${segments.join(" . ")})`;
|
|
677
|
+
}
|
|
678
|
+
refuseUnsupportedAttrExpression(expr, attrName) {
|
|
679
|
+
let probe = expr.trim();
|
|
680
|
+
while (probe.startsWith("("))
|
|
681
|
+
probe = probe.slice(1).trimStart();
|
|
682
|
+
const startsAsObjectLiteral = probe.startsWith("{");
|
|
683
|
+
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
|
|
684
|
+
if (!startsAsObjectLiteral && !hasTaggedTemplate)
|
|
685
|
+
return false;
|
|
686
|
+
const parsed = parseExpression2(expr.trim());
|
|
687
|
+
const support = isSupported(parsed);
|
|
688
|
+
if (parsed.kind !== "unsupported" && support.supported)
|
|
689
|
+
return false;
|
|
690
|
+
const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
|
|
691
|
+
const reasonLine = reason ? `
|
|
692
|
+
${reason}` : "";
|
|
693
|
+
this.errors.push({
|
|
694
|
+
code: "BF101",
|
|
695
|
+
severity: "error",
|
|
696
|
+
message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
|
|
697
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
698
|
+
suggestion: {
|
|
699
|
+
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."
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
convertExpressionToPerl(expr) {
|
|
705
|
+
if (/\.\s*(?:filter|every|some|reduce|reduceRight|forEach|flatMap|flat|findLast|findLastIndex)\s*\(/.test(expr)) {
|
|
706
|
+
return this.convertHigherOrderExpr(expr);
|
|
707
|
+
}
|
|
708
|
+
if (/\.\s*(?:includes|indexOf|lastIndexOf|at|concat|slice|reverse|toReversed|toLowerCase|toUpperCase|trim)\s*\(/.test(expr)) {
|
|
709
|
+
return this.convertHigherOrderExpr(expr);
|
|
710
|
+
}
|
|
711
|
+
if (/\.\s*join\s*\(/.test(expr)) {
|
|
712
|
+
return this.convertHigherOrderExpr(expr);
|
|
713
|
+
}
|
|
714
|
+
const mojoOnlyMatch = /\.\s*(?<method>find|findIndex)\s*\(/.exec(expr);
|
|
715
|
+
if (mojoOnlyMatch) {
|
|
716
|
+
const methodName = mojoOnlyMatch.groups.method;
|
|
717
|
+
this.errors.push({
|
|
718
|
+
code: "BF101",
|
|
719
|
+
severity: "error",
|
|
720
|
+
message: `Mojo adapter has not lowered Array.prototype.${methodName} yet: ${expr.trim()}`,
|
|
721
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
722
|
+
suggestion: {
|
|
723
|
+
message: `Options:
|
|
724
|
+
1. Use /* @client */ for client-side evaluation
|
|
725
|
+
2. Pre-compute the value in Perl`
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
return "''";
|
|
729
|
+
}
|
|
730
|
+
expr = this.rewriteTemplatePrimitives(expr);
|
|
731
|
+
let result = expr.replace(/\b([a-z_]\w*)\(\)/g, (_, name) => `$${name}`);
|
|
732
|
+
result = result.replace(/\bprops\.(\w+)/g, (_, prop) => `$${prop}`);
|
|
733
|
+
result = result.replace(/(?<!\$)\b([a-z_]\w*)\.(\w+)/g, (match, obj, field) => {
|
|
734
|
+
if (match.startsWith("$"))
|
|
735
|
+
return match;
|
|
736
|
+
return `$${obj}->{${field}}`;
|
|
737
|
+
});
|
|
738
|
+
result = result.replace(/\$(\w+)\.(\w+)/g, (_, obj, field) => `$${obj}->{${field}}`);
|
|
739
|
+
result = result.replace(/\}->\{(\w+)\}\.(\w+)/g, (_, f1, f2) => `}->{${f1}}->{${f2}}`);
|
|
740
|
+
result = result.replace(/\$(\w+)->\{length\}/g, (_, arr) => `scalar(@{$${arr}})`);
|
|
741
|
+
result = result.replace(/\?\?/g, "//");
|
|
742
|
+
result = result.replace(/\s*===\s*(['"])/g, " eq $1");
|
|
743
|
+
result = result.replace(/\s*!==\s*(['"])/g, " ne $1");
|
|
744
|
+
result = result.replace(/(['"])\s*===\s*/g, "$1 eq ");
|
|
745
|
+
result = result.replace(/(['"])\s*!==\s*/g, "$1 ne ");
|
|
746
|
+
result = result.replace(/===/g, "==");
|
|
747
|
+
result = result.replace(/!==/g, "!=");
|
|
748
|
+
result = result.replace(/`([^`]*)`/g, (_, content) => {
|
|
749
|
+
const perlStr = content.replace(/\$\{([^}]+)\}/g, (_2, e) => `${this.convertExpressionToPerl(e)}`);
|
|
750
|
+
return `"${perlStr}"`;
|
|
751
|
+
});
|
|
752
|
+
if (/^[a-z_]\w*$/i.test(result) && !result.startsWith("$")) {
|
|
753
|
+
result = `$${result}`;
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
rewriteTemplatePrimitives(expr) {
|
|
758
|
+
if (!PRIMITIVE_SUBSTRING_RE.test(expr))
|
|
759
|
+
return expr;
|
|
760
|
+
const parsed = parseExpression2(expr);
|
|
761
|
+
if (parsed.kind === "unsupported")
|
|
762
|
+
return expr;
|
|
763
|
+
let mutated = false;
|
|
764
|
+
const walk = (n) => {
|
|
765
|
+
if (n.kind === "call") {
|
|
766
|
+
const path = identifierPath(n.callee);
|
|
767
|
+
const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
|
|
768
|
+
if (path && spec) {
|
|
769
|
+
if (n.args.length !== spec.arity) {
|
|
770
|
+
this.errors.push({
|
|
771
|
+
code: "BF101",
|
|
772
|
+
severity: "error",
|
|
773
|
+
message: `templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${n.args.length}`,
|
|
774
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
775
|
+
suggestion: {
|
|
776
|
+
message: `Call '${path}' with exactly ${spec.arity} argument(s), or wrap the JSX expression in /* @client */ to defer evaluation.`
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
return { kind: "call", callee: walk(n.callee), args: n.args.map(walk) };
|
|
780
|
+
}
|
|
781
|
+
const renderedArgs = n.args.map((a) => this.convertExpressionToPerl(stringifyParsedExpr(walk(a))));
|
|
782
|
+
mutated = true;
|
|
783
|
+
return { kind: "identifier", name: spec.emit(renderedArgs) };
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
switch (n.kind) {
|
|
787
|
+
case "call":
|
|
788
|
+
return { kind: "call", callee: walk(n.callee), args: n.args.map(walk) };
|
|
789
|
+
case "member":
|
|
790
|
+
return { kind: "member", object: walk(n.object), property: n.property, computed: n.computed };
|
|
791
|
+
case "binary":
|
|
792
|
+
return { kind: "binary", op: n.op, left: walk(n.left), right: walk(n.right) };
|
|
793
|
+
case "unary":
|
|
794
|
+
return { kind: "unary", op: n.op, argument: walk(n.argument) };
|
|
795
|
+
case "logical":
|
|
796
|
+
return { kind: "logical", op: n.op, left: walk(n.left), right: walk(n.right) };
|
|
797
|
+
case "conditional":
|
|
798
|
+
return { kind: "conditional", test: walk(n.test), consequent: walk(n.consequent), alternate: walk(n.alternate) };
|
|
799
|
+
default:
|
|
800
|
+
return n;
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
const transformed = walk(parsed);
|
|
804
|
+
if (!mutated)
|
|
805
|
+
return expr;
|
|
806
|
+
return stringifyParsedExpr(transformed);
|
|
807
|
+
}
|
|
808
|
+
convertHigherOrderExpr(expr) {
|
|
809
|
+
if (this.higherOrderInFlight.has(expr)) {
|
|
810
|
+
this.errors.push({
|
|
811
|
+
code: "BF101",
|
|
812
|
+
severity: "error",
|
|
813
|
+
message: `Cannot lower higher-order chain to Embedded Perl: ${expr.trim()}`,
|
|
814
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
815
|
+
suggestion: {
|
|
816
|
+
message: "The Mojo adapter cannot lower this `.filter()` / `.every()` / `.some()` chain — typically because the array source is a JS array literal or a non-signal expression the AST classifier doesn't recognise. Move the expression into a `'use client'` component (so hydration computes it client-side), or rewrite it to operate on a signal getter or a prop directly."
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
return "''";
|
|
820
|
+
}
|
|
821
|
+
this.higherOrderInFlight.add(expr);
|
|
822
|
+
try {
|
|
823
|
+
const parsed = parseExpression2(expr);
|
|
824
|
+
const support = isSupported(parsed);
|
|
825
|
+
if (!support.supported) {
|
|
826
|
+
this.errors.push({
|
|
827
|
+
code: "BF101",
|
|
828
|
+
severity: "error",
|
|
829
|
+
message: `Cannot lower higher-order chain to Embedded Perl: ${expr.trim()}`,
|
|
830
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
831
|
+
suggestion: {
|
|
832
|
+
message: support.reason ? `${support.reason}
|
|
833
|
+
|
|
834
|
+
Options:
|
|
835
|
+
1. Use /* @client */ for client-side evaluation
|
|
836
|
+
2. Pre-compute the value in Perl` : `Options:
|
|
837
|
+
1. Use /* @client */ for client-side evaluation
|
|
838
|
+
2. Pre-compute the value in Perl`
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
return "''";
|
|
842
|
+
}
|
|
843
|
+
return this.renderParsedExprToPerl(parsed);
|
|
844
|
+
} finally {
|
|
845
|
+
this.higherOrderInFlight.delete(expr);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
renderParsedExprToPerl(expr) {
|
|
849
|
+
return emitParsedExpr(expr, new MojoTopLevelEmitter(this));
|
|
850
|
+
}
|
|
851
|
+
_convertExpressionToPerlPublic(raw) {
|
|
852
|
+
return this.convertExpressionToPerl(raw);
|
|
853
|
+
}
|
|
854
|
+
_renderPerlFilterExprPublic(expr, param) {
|
|
855
|
+
return this.renderPerlFilterExpr(expr, param);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function renderArrayMethod(method, object, args, emit) {
|
|
859
|
+
switch (method) {
|
|
860
|
+
case "join": {
|
|
861
|
+
const obj = emit(object);
|
|
862
|
+
const sep = emit(args[0]);
|
|
863
|
+
return `join(${sep}, @{${obj}})`;
|
|
864
|
+
}
|
|
865
|
+
case "includes": {
|
|
866
|
+
const obj = emit(object);
|
|
867
|
+
const needle = emit(args[0]);
|
|
868
|
+
return `bf->includes(${obj}, ${needle})`;
|
|
869
|
+
}
|
|
870
|
+
case "indexOf":
|
|
871
|
+
case "lastIndexOf": {
|
|
872
|
+
const fn = method === "indexOf" ? "index_of" : "last_index_of";
|
|
873
|
+
const obj = emit(object);
|
|
874
|
+
const needle = emit(args[0]);
|
|
875
|
+
return `bf->${fn}(${obj}, ${needle})`;
|
|
876
|
+
}
|
|
877
|
+
case "at": {
|
|
878
|
+
const obj = emit(object);
|
|
879
|
+
const idx = emit(args[0]);
|
|
880
|
+
return `bf->at(${obj}, ${idx})`;
|
|
881
|
+
}
|
|
882
|
+
case "concat": {
|
|
883
|
+
const a = emit(object);
|
|
884
|
+
const b = emit(args[0]);
|
|
885
|
+
return `bf->concat(${a}, ${b})`;
|
|
886
|
+
}
|
|
887
|
+
case "slice": {
|
|
888
|
+
const recv = emit(object);
|
|
889
|
+
const start = emit(args[0]);
|
|
890
|
+
const end = args.length === 2 ? emit(args[1]) : "undef";
|
|
891
|
+
return `bf->slice(${recv}, ${start}, ${end})`;
|
|
892
|
+
}
|
|
893
|
+
case "reverse":
|
|
894
|
+
case "toReversed": {
|
|
895
|
+
const recv = emit(object);
|
|
896
|
+
return `bf->reverse(${recv})`;
|
|
897
|
+
}
|
|
898
|
+
case "toLowerCase": {
|
|
899
|
+
const recv = emit(object);
|
|
900
|
+
return `lc(${recv})`;
|
|
901
|
+
}
|
|
902
|
+
case "toUpperCase": {
|
|
903
|
+
const recv = emit(object);
|
|
904
|
+
return `uc(${recv})`;
|
|
905
|
+
}
|
|
906
|
+
case "trim": {
|
|
907
|
+
const recv = emit(object);
|
|
908
|
+
return `bf->trim(${recv})`;
|
|
909
|
+
}
|
|
910
|
+
default: {
|
|
911
|
+
const _exhaustive = method;
|
|
912
|
+
throw new Error(`renderArrayMethod: unhandled ArrayMethod '${_exhaustive}'`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function perlIdentifierFromMarkerId(markerId) {
|
|
917
|
+
return markerId.replace(/[^a-zA-Z0-9]/g, (ch) => ch === "_" ? "__" : `_x${ch.charCodeAt(0).toString(16)}`);
|
|
918
|
+
}
|
|
919
|
+
function renderSortMethod(recv, c) {
|
|
920
|
+
const keyEntry = c.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${c.key.field}'`;
|
|
921
|
+
return `bf->sort(${recv}, { ${keyEntry}, compare_type => '${c.type}', direction => '${c.direction}' })`;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
class MojoFilterEmitter {
|
|
925
|
+
param;
|
|
926
|
+
localVarMap;
|
|
927
|
+
constructor(param, localVarMap) {
|
|
928
|
+
this.param = param;
|
|
929
|
+
this.localVarMap = localVarMap;
|
|
930
|
+
}
|
|
931
|
+
identifier(name) {
|
|
932
|
+
if (name === this.param)
|
|
933
|
+
return `$${this.param}`;
|
|
934
|
+
const signal = this.localVarMap.get(name);
|
|
935
|
+
if (signal)
|
|
936
|
+
return `$${signal}`;
|
|
937
|
+
return `$${name}`;
|
|
938
|
+
}
|
|
939
|
+
literal(value, literalType) {
|
|
940
|
+
if (literalType === "string")
|
|
941
|
+
return `'${value}'`;
|
|
942
|
+
if (literalType === "boolean")
|
|
943
|
+
return value ? "1" : "0";
|
|
944
|
+
if (literalType === "null")
|
|
945
|
+
return "undef";
|
|
946
|
+
return String(value);
|
|
947
|
+
}
|
|
948
|
+
member(object, property, _computed, emit) {
|
|
949
|
+
if (property === "length" && (object.kind === "higher-order" || object.kind === "array-literal")) {
|
|
950
|
+
return `scalar(@{${emit(object)}})`;
|
|
951
|
+
}
|
|
952
|
+
return `${emit(object)}->{${property}}`;
|
|
953
|
+
}
|
|
954
|
+
call(callee, args, emit) {
|
|
955
|
+
if (callee.kind === "identifier" && args.length === 0) {
|
|
956
|
+
return `$${callee.name}`;
|
|
957
|
+
}
|
|
958
|
+
return emit(callee);
|
|
959
|
+
}
|
|
960
|
+
unary(op, argument, emit) {
|
|
961
|
+
const arg = emit(argument);
|
|
962
|
+
if (op === "!") {
|
|
963
|
+
const needsParens = argument.kind === "binary" || argument.kind === "logical";
|
|
964
|
+
return needsParens ? `!(${arg})` : `!${arg}`;
|
|
965
|
+
}
|
|
966
|
+
if (op === "-")
|
|
967
|
+
return `-${arg}`;
|
|
968
|
+
return arg;
|
|
969
|
+
}
|
|
970
|
+
binary(op, left, right, emit) {
|
|
971
|
+
const l = emit(left);
|
|
972
|
+
const r = emit(right);
|
|
973
|
+
if ((op === "===" || op === "==") && right.kind === "literal" && right.literalType === "string") {
|
|
974
|
+
return `${l} eq ${r}`;
|
|
975
|
+
}
|
|
976
|
+
if ((op === "!==" || op === "!=") && right.kind === "literal" && right.literalType === "string") {
|
|
977
|
+
return `${l} ne ${r}`;
|
|
978
|
+
}
|
|
979
|
+
const opMap = {
|
|
980
|
+
"===": "==",
|
|
981
|
+
"!==": "!=",
|
|
982
|
+
">": ">",
|
|
983
|
+
"<": "<",
|
|
984
|
+
">=": ">=",
|
|
985
|
+
"<=": "<=",
|
|
986
|
+
"+": "+",
|
|
987
|
+
"-": "-",
|
|
988
|
+
"*": "*",
|
|
989
|
+
"/": "/"
|
|
990
|
+
};
|
|
991
|
+
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
992
|
+
}
|
|
993
|
+
logical(op, left, right, emit) {
|
|
994
|
+
const l = emit(left);
|
|
995
|
+
const r = emit(right);
|
|
996
|
+
if (op === "&&")
|
|
997
|
+
return `(${l} && ${r})`;
|
|
998
|
+
if (op === "||")
|
|
999
|
+
return `(${l} || ${r})`;
|
|
1000
|
+
return `(${l} // ${r})`;
|
|
1001
|
+
}
|
|
1002
|
+
higherOrder(method, object, param, predicate, emit) {
|
|
1003
|
+
const arrayExpr = emit(object);
|
|
1004
|
+
const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap));
|
|
1005
|
+
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
|
|
1006
|
+
if (method === "filter")
|
|
1007
|
+
return `[grep { ${grepBody} } @{${arrayExpr}}]`;
|
|
1008
|
+
if (method === "every")
|
|
1009
|
+
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
1010
|
+
if (method === "some")
|
|
1011
|
+
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
1012
|
+
return arrayExpr;
|
|
1013
|
+
}
|
|
1014
|
+
arrayLiteral(elements, emit) {
|
|
1015
|
+
return `[${elements.map(emit).join(", ")}]`;
|
|
1016
|
+
}
|
|
1017
|
+
arrayMethod(method, object, args, emit) {
|
|
1018
|
+
return renderArrayMethod(method, object, args, emit);
|
|
1019
|
+
}
|
|
1020
|
+
sortMethod(_method, object, comparator, emit) {
|
|
1021
|
+
return renderSortMethod(emit(object), comparator);
|
|
1022
|
+
}
|
|
1023
|
+
conditional(_test, _consequent, _alternate) {
|
|
1024
|
+
return "1";
|
|
1025
|
+
}
|
|
1026
|
+
templateLiteral(_parts) {
|
|
1027
|
+
return "1";
|
|
1028
|
+
}
|
|
1029
|
+
arrowFn(_param, _body) {
|
|
1030
|
+
return "1";
|
|
1031
|
+
}
|
|
1032
|
+
unsupported(_raw, _reason) {
|
|
1033
|
+
return "1";
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
class MojoTopLevelEmitter {
|
|
1038
|
+
adapter;
|
|
1039
|
+
constructor(adapter) {
|
|
1040
|
+
this.adapter = adapter;
|
|
1041
|
+
}
|
|
1042
|
+
identifier(name) {
|
|
1043
|
+
return `$${name}`;
|
|
1044
|
+
}
|
|
1045
|
+
literal(value, literalType) {
|
|
1046
|
+
if (literalType === "string")
|
|
1047
|
+
return `'${value}'`;
|
|
1048
|
+
if (literalType === "boolean")
|
|
1049
|
+
return value ? "1" : "0";
|
|
1050
|
+
if (literalType === "null")
|
|
1051
|
+
return "undef";
|
|
1052
|
+
return String(value);
|
|
1053
|
+
}
|
|
1054
|
+
member(object, property, _computed, emit) {
|
|
1055
|
+
const obj = emit(object);
|
|
1056
|
+
if (property === "length")
|
|
1057
|
+
return `scalar(@{${obj}})`;
|
|
1058
|
+
return `${obj}->{${property}}`;
|
|
1059
|
+
}
|
|
1060
|
+
call(callee, args, emit) {
|
|
1061
|
+
if (callee.kind === "identifier" && args.length === 0) {
|
|
1062
|
+
return `$${callee.name}`;
|
|
1063
|
+
}
|
|
1064
|
+
return emit(callee);
|
|
1065
|
+
}
|
|
1066
|
+
unary(op, argument, emit) {
|
|
1067
|
+
const arg = emit(argument);
|
|
1068
|
+
if (op === "!")
|
|
1069
|
+
return `!${arg}`;
|
|
1070
|
+
if (op === "-")
|
|
1071
|
+
return `-${arg}`;
|
|
1072
|
+
return arg;
|
|
1073
|
+
}
|
|
1074
|
+
binary(op, left, right, emit) {
|
|
1075
|
+
const l = emit(left);
|
|
1076
|
+
const r = emit(right);
|
|
1077
|
+
if ((op === "===" || op === "==") && right.kind === "literal" && right.literalType === "string") {
|
|
1078
|
+
return `${l} eq ${r}`;
|
|
1079
|
+
}
|
|
1080
|
+
if ((op === "!==" || op === "!=") && right.kind === "literal" && right.literalType === "string") {
|
|
1081
|
+
return `${l} ne ${r}`;
|
|
1082
|
+
}
|
|
1083
|
+
const opMap = {
|
|
1084
|
+
"===": "==",
|
|
1085
|
+
"!==": "!=",
|
|
1086
|
+
">": ">",
|
|
1087
|
+
"<": "<",
|
|
1088
|
+
">=": ">=",
|
|
1089
|
+
"<=": "<=",
|
|
1090
|
+
"+": "+",
|
|
1091
|
+
"-": "-",
|
|
1092
|
+
"*": "*"
|
|
1093
|
+
};
|
|
1094
|
+
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
1095
|
+
}
|
|
1096
|
+
logical(op, left, right, emit) {
|
|
1097
|
+
const l = emit(left);
|
|
1098
|
+
const r = emit(right);
|
|
1099
|
+
if (op === "&&")
|
|
1100
|
+
return `(${l} && ${r})`;
|
|
1101
|
+
if (op === "||")
|
|
1102
|
+
return `(${l} || ${r})`;
|
|
1103
|
+
return `(${l} // ${r})`;
|
|
1104
|
+
}
|
|
1105
|
+
higherOrder(method, object, param, predicate, emit) {
|
|
1106
|
+
const arrayExpr = emit(object);
|
|
1107
|
+
const predBody = this.adapter._renderPerlFilterExprPublic(predicate, param);
|
|
1108
|
+
const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
|
|
1109
|
+
if (method === "filter")
|
|
1110
|
+
return `[grep { ${grepBody} } @{${arrayExpr}}]`;
|
|
1111
|
+
if (method === "every")
|
|
1112
|
+
return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
|
|
1113
|
+
if (method === "some")
|
|
1114
|
+
return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
|
|
1115
|
+
return arrayExpr;
|
|
1116
|
+
}
|
|
1117
|
+
arrayLiteral(elements, emit) {
|
|
1118
|
+
return `[${elements.map(emit).join(", ")}]`;
|
|
1119
|
+
}
|
|
1120
|
+
arrayMethod(method, object, args, emit) {
|
|
1121
|
+
return renderArrayMethod(method, object, args, emit);
|
|
1122
|
+
}
|
|
1123
|
+
sortMethod(_method, object, comparator, emit) {
|
|
1124
|
+
return renderSortMethod(emit(object), comparator);
|
|
1125
|
+
}
|
|
1126
|
+
conditional(test, consequent, alternate, emit) {
|
|
1127
|
+
return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
|
|
1128
|
+
}
|
|
1129
|
+
templateLiteral(_parts) {
|
|
1130
|
+
return "";
|
|
1131
|
+
}
|
|
1132
|
+
arrowFn(_param, _body) {
|
|
1133
|
+
return "";
|
|
1134
|
+
}
|
|
1135
|
+
unsupported(raw, _reason) {
|
|
1136
|
+
return this.adapter._convertExpressionToPerlPublic(raw);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
var mojoAdapter = new MojoAdapter;
|
|
1140
|
+
export {
|
|
1141
|
+
mojoAdapter,
|
|
1142
|
+
MojoAdapter
|
|
1143
|
+
};
|