@barefootjs/mojolicious 0.15.2 → 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.
Files changed (53) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +30 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +96 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/array-method.d.ts +75 -0
  6. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  7. package/dist/adapter/expr/emitters.d.ts +77 -0
  8. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  9. package/dist/adapter/expr/operand.d.ts +34 -0
  10. package/dist/adapter/expr/operand.d.ts.map +1 -0
  11. package/dist/adapter/index.js +1490 -1334
  12. package/dist/adapter/lib/constants.d.ts +27 -0
  13. package/dist/adapter/lib/constants.d.ts.map +1 -0
  14. package/dist/adapter/lib/ir-scope.d.ts +40 -0
  15. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  16. package/dist/adapter/lib/perl-naming.d.ts +29 -0
  17. package/dist/adapter/lib/perl-naming.d.ts.map +1 -0
  18. package/dist/adapter/lib/types.d.ts +28 -0
  19. package/dist/adapter/lib/types.d.ts.map +1 -0
  20. package/dist/adapter/memo/seed.d.ts +43 -0
  21. package/dist/adapter/memo/seed.d.ts.map +1 -0
  22. package/dist/adapter/mojo-adapter.d.ts +46 -104
  23. package/dist/adapter/mojo-adapter.d.ts.map +1 -1
  24. package/dist/adapter/props/prop-classes.d.ts +48 -0
  25. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  26. package/dist/adapter/spread/spread-codegen.d.ts +64 -0
  27. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  28. package/dist/adapter/value/parsed-literal.d.ts +26 -0
  29. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  30. package/dist/build.js +1490 -1334
  31. package/dist/index.js +1490 -1334
  32. package/dist/test-render.d.ts.map +1 -1
  33. package/lib/BarefootJS/Backend/Mojo.pm +1 -1
  34. package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
  35. package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
  36. package/package.json +3 -3
  37. package/src/__tests__/mojo-adapter.test.ts +163 -62
  38. package/src/__tests__/query-href.test.ts +94 -0
  39. package/src/adapter/analysis/component-tree.ts +128 -0
  40. package/src/adapter/emit-context.ts +107 -0
  41. package/src/adapter/expr/array-method.ts +408 -0
  42. package/src/adapter/expr/emitters.ts +607 -0
  43. package/src/adapter/expr/operand.ts +55 -0
  44. package/src/adapter/lib/constants.ts +39 -0
  45. package/src/adapter/lib/ir-scope.ts +70 -0
  46. package/src/adapter/lib/perl-naming.ts +36 -0
  47. package/src/adapter/lib/types.ts +31 -0
  48. package/src/adapter/memo/seed.ts +126 -0
  49. package/src/adapter/mojo-adapter.ts +230 -1476
  50. package/src/adapter/props/prop-classes.ts +87 -0
  51. package/src/adapter/spread/spread-codegen.ts +181 -0
  52. package/src/adapter/value/parsed-literal.ts +34 -0
  53. package/src/test-render.ts +2 -0
package/dist/build.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 parseExpression2,
5
+ parseExpression as parseExpression3,
6
+ stringifyParsedExpr as stringifyParsedExpr2,
7
7
  parseStyleObjectEntries,
8
- isSupported,
8
+ isSupported as isSupported2,
9
9
  exprToString,
10
10
  parseProviderObjectLiteral,
11
- identifierPath,
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
- matchSearchParamsMethodCall
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,1472 +136,1622 @@ function referencedVarsAreAvailable(expr, available) {
130
136
  return true;
131
137
  }
132
138
 
133
- class MojoAdapter extends BaseAdapter {
134
- name = "mojolicious";
135
- extension = ".html.ep";
136
- templatesPerComponent = true;
137
- importMapInjection = "html-snippet";
138
- templatePrimitives = MOJO_PRIMITIVE_EMIT_MAP;
139
- componentName = "";
140
- rootScopeNodes = new Set;
141
- options;
142
- errors = [];
143
- inLoop = false;
144
- propsObjectName = null;
145
- propsParams = [];
146
- booleanTypedProps = new Set;
147
- stringValueNames = new Set;
148
- _searchParamsLocals = new Set;
149
- moduleStringConsts = new Map;
150
- localConstants = [];
151
- loopBoundNames = new Map;
152
- nullableOptionalProps = new Set;
153
- constructor(options = {}) {
154
- super();
155
- this.options = {
156
- clientJsBasePath: options.clientJsBasePath ?? "/static/components/",
157
- barefootJsPath: options.barefootJsPath ?? "/static/components/barefoot.js"
158
- };
159
- }
160
- generate(ir, options) {
161
- this.componentName = ir.metadata.componentName;
162
- this.propsObjectName = ir.metadata.propsObjectName ?? null;
163
- augmentInheritedPropAccesses(ir);
164
- this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
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
- for (const p of ir.metadata.propsParams) {
174
- if (isStringTypeInfo(p.type))
175
- this.stringValueNames.add(p.name);
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
- this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
178
- this._searchParamsLocals = searchParamsLocalNames(ir.metadata);
179
- this.localConstants = ir.metadata.localConstants ?? [];
180
- this.loopBoundNames.clear();
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
- this.rootScopeNodes = collectRootScopeNodes(ir.root);
187
- const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
188
- const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
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
- const sections = {
197
- imports: "",
198
- types: "",
199
- component: template,
200
- defaultExport: ""
201
- };
202
- return {
203
- template,
204
- sections,
205
- extension: this.extension
206
- };
207
- }
208
- isBooleanTypedPropRef(expr) {
209
- let bare = expr.trim();
210
- if (this.propsObjectName && bare.startsWith(`${this.propsObjectName}.`)) {
211
- bare = bare.slice(this.propsObjectName.length + 1);
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
- resolveLiteralConst(name) {
231
- if (this.loopBoundNames?.has?.(name))
232
- return null;
233
- const c = (this.localConstants ?? []).find((lc) => lc.name === name);
234
- if (c?.value === undefined)
235
- return null;
236
- const v = c.value.trim();
237
- if (/^-?\d+(\.\d+)?$/.test(v))
238
- return v;
239
- const strLit = /^'([^'\\]*)'$/.exec(v) ?? /^"([^"\\]*)"$/.exec(v);
240
- if (strLit)
241
- return `'${strLit[1].replace(/[\\']/g, (m) => `\\${m}`)}'`;
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
- resolveStaticRecordLiteral(objectName, key) {
245
- if (this.loopBoundNames?.has?.(objectName))
246
- return null;
247
- const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
248
- if (!hit)
249
- return null;
250
- return hit.kind === "number" ? hit.text : `'${hit.text.replace(/[\\']/g, (m) => `\\${m}`)}'`;
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
- resolveModuleStringConst(name) {
253
- if (this.loopBoundNames.has(name))
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
- generateScriptRegistrations(ir, scriptBaseName) {
261
- const hasInteractivity = this.hasClientInteractivity(ir);
262
- if (!hasInteractivity)
263
- return "";
264
- const name = scriptBaseName ?? ir.metadata.componentName;
265
- const runtimePath = this.options.barefootJsPath;
266
- const clientJsPath = `${this.options.clientJsBasePath}${name}.client.js`;
267
- const lines = [];
268
- lines.push(`% bf->register_script('${runtimePath}');`);
269
- lines.push(`% bf->register_script('${clientJsPath}');`);
270
- lines.push("");
271
- return lines.join(`
272
- `);
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
- hasClientInteractivity(ir) {
275
- return ir.metadata.signals.length > 0 || ir.metadata.effects.length > 0 || ir.metadata.onMounts.length > 0 || (ir.metadata.clientAnalysis?.needsInit ?? false);
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
- renderNode(node) {
278
- return emitIRNode(node, this, {});
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
- emitElement(node, _ctx, _emit) {
281
- return this.renderElement(node);
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
- emitText(node) {
284
- return node.value;
391
+ indexAccess(object, index, emit) {
392
+ return emitIndexAccessPerl(object, index, emit, this.isStringName);
285
393
  }
286
- emitExpression(node) {
287
- return this.renderExpression(node);
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
- emitConditional(node, _ctx, _emit) {
290
- return this.renderConditional(node);
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
- emitLoop(node, _ctx, _emit) {
293
- return this.renderLoop(node);
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
- emitComponent(node, _ctx, _emit) {
296
- return this.renderComponent(node);
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
- emitFragment(node, _ctx, _emit) {
299
- return this.renderFragment(node);
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
- emitSlot(node) {
302
- return this.renderSlot(node);
460
+ arrayLiteral(elements, emit) {
461
+ return `[${elements.map(emit).join(", ")}]`;
303
462
  }
304
- emitIfStatement(node, _ctx, _emit) {
305
- return this.renderIfStatement(node);
463
+ arrayMethod(method, object, args, emit) {
464
+ return renderArrayMethod(method, object, args, emit);
306
465
  }
307
- emitProvider(node, _ctx, _emit) {
308
- const value = this.providerValuePerl(node.valueProp);
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
- providerValuePerl(valueProp) {
314
- const v = valueProp.value;
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
- providerObjectLiteralPerl(expr) {
329
- const members = parseProviderObjectLiteral(expr.trim());
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
- contextDefaultPerl(c) {
342
- const d = c.defaultValue;
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
- generateDerivedMemoSeed(ir) {
360
- const memos = ir.metadata.memos ?? [];
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
- tryLowerToPerl(expr, available) {
386
- const trimmed = expr.trim();
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
- emitAsync(node, _ctx, _emit) {
397
- return this.renderAsync(node);
484
+ objectLiteral(_properties, _raw, _emit) {
485
+ return "1";
398
486
  }
399
- renderElement(element) {
400
- const tag = element.tag;
401
- const attrs = this.renderAttributes(element);
402
- const children = this.renderChildren(element.children);
403
- let hydrationAttrs = "";
404
- if (element.needsScope) {
405
- hydrationAttrs += ` ${this.renderScopeMarker("")}`;
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 (this.rootScopeNodes.has(element) && element.needsScope) {
408
- hydrationAttrs += ` <%== bf->data_key_attr %>`;
518
+ if (object.kind === "identifier") {
519
+ const staticValue = this.ctx.resolveStaticRecordLiteral(object.name, property);
520
+ if (staticValue !== null)
521
+ return staticValue;
409
522
  }
410
- if (element.slotId) {
411
- hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
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 (element.regionId) {
414
- hydrationAttrs += ` ${BF_REGION}="${element.regionId}"`;
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 voidElements = [
417
- "area",
418
- "base",
419
- "br",
420
- "col",
421
- "embed",
422
- "hr",
423
- "img",
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 `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
550
+ return emit(callee);
436
551
  }
437
- renderExpression(expr) {
438
- if (expr.clientOnly) {
439
- if (expr.slotId) {
440
- return `<%== bf->comment("client:${expr.slotId}") %>`;
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
- const perlExpr = this.convertExpressionToPerl(expr.expr);
445
- if (expr.slotId) {
446
- return `<%== bf->text_start("${expr.slotId}") %><%= ${perlExpr} %><%== bf->text_end %>`;
568
+ if ((op === "!==" || op === "!=") && stringCmp) {
569
+ return `${l} ne ${r}`;
447
570
  }
448
- return `<%= ${perlExpr} %>`;
571
+ const opMap = {
572
+ "===": "==",
573
+ "!==": "!=",
574
+ ">": ">",
575
+ "<": "<",
576
+ ">=": ">=",
577
+ "<=": "<=",
578
+ "+": "+",
579
+ "-": "-",
580
+ "*": "*"
581
+ };
582
+ return `${l} ${opMap[op] ?? op} ${r}`;
449
583
  }
450
- renderConditional(cond) {
451
- if (cond.clientOnly && cond.slotId) {
452
- return `<%== bf->comment("cond-start:${cond.slotId}") %><%== bf->comment("cond-end:${cond.slotId}") %>`;
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
- const condition = this.convertExpressionToPerl(cond.condition);
455
- const whenTrue = this.renderNode(cond.whenTrue);
456
- const whenFalse = this.renderNodeOrNull(cond.whenFalse);
457
- const isFragmentBranch = cond.whenTrue.type === "fragment" || cond.whenFalse.type === "fragment";
458
- const useCommentMarkers = cond.slotId && isFragmentBranch;
459
- let markedTrue = whenTrue;
460
- let markedFalse = whenFalse;
461
- if (cond.slotId && !useCommentMarkers) {
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
- let result;
466
- if (useCommentMarkers) {
467
- const inner = whenFalse ? `
468
- % if (${condition}) {
469
- ${whenTrue}
470
- % } else {
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
- return result;
501
- }
502
- renderNodeOrNull(node) {
503
- if (node.type === "expression" && (node.expr === "null" || node.expr === "undefined")) {
504
- return null;
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 this.renderNode(node);
666
+ return arrayExpr;
507
667
  }
508
- addCondMarkerToFirstElement(content, condId) {
509
- const match = content.match(/^(<\w+)([\s>])/);
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
- checkImportedLoopChildComponents(ir) {
516
- const relativeImports = new Set;
517
- for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
518
- if (!imp.source.startsWith("./") && !imp.source.startsWith("../"))
519
- continue;
520
- if (imp.isTypeOnly)
521
- continue;
522
- for (const spec of imp.specifiers) {
523
- relativeImports.add(spec.alias ?? spec.name);
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 (relativeImports.size === 0)
527
- return;
528
- const loc = { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
529
- const visit = (node, inLoop) => {
530
- switch (node.type) {
531
- case "component": {
532
- const comp = node;
533
- if (inLoop && relativeImports.has(comp.name)) {
534
- this.errors.push({
535
- code: "BF103",
536
- severity: "error",
537
- 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.`,
538
- loc: comp.loc ?? loc,
539
- suggestion: {
540
- message: `Options:
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
- visit(ir.root, false);
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
- renderLoop(loop) {
593
- if (loop.clientOnly)
594
- return "";
595
- const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
596
- const supportableDestructure = destructure && isLowerableObjectRestDestructure(loop);
597
- if (destructure && !supportableDestructure) {
598
- this.errors.push({
599
- code: "BF104",
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: `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.`,
602
- loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
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: `Options:
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 rawArray = this.convertExpressionToPerl(loop.array);
612
- let sortedHoist = null;
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
- componentPropEmitter = {
677
- emitLiteral: (value, name) => `${perlHashKey(name)} => '${value.value}'`,
678
- emitExpression: (value, name) => {
679
- if (value.parts) {
680
- return `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`;
681
- }
682
- return `${perlHashKey(name)} => ${this.convertExpressionToPerl(value.expr)}`;
683
- },
684
- emitSpread: (value) => {
685
- const perlExpr = this.convertExpressionToPerl(value.expr);
686
- return perlExpr.startsWith("%") ? perlExpr : `%{${perlExpr}}`;
687
- },
688
- emitTemplate: (value, name) => `${perlHashKey(name)} => ${this.convertTemplateLiteralPartsToPerl(value.parts)}`,
689
- emitBooleanAttr: (_value, name) => `${perlHashKey(name)} => 1`,
690
- emitBooleanShorthand: (_value, name) => `${perlHashKey(name)} => 1`,
691
- emitJsxChildren: () => ""
692
- };
693
- renderComponent(comp) {
694
- const propParts = [];
695
- for (const p of comp.props) {
696
- if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
697
- continue;
698
- const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
699
- if (lowered)
700
- propParts.push(lowered);
701
- }
702
- if (comp.slotId && !this.inLoop) {
703
- propParts.push(`_bf_slot => '${comp.slotId}'`);
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
- const propsStr = propParts.length > 0 ? ", " + propParts.join(", ") : "";
706
- const tplName = this.toTemplateName(comp.name);
707
- const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
708
- if (effectiveChildren.length > 0) {
709
- const prevInLoop = this.inLoop;
710
- this.inLoop = false;
711
- const childrenBody = this.renderChildren(effectiveChildren);
712
- this.inLoop = prevInLoop;
713
- const varName = `$bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
714
- return `<% my ${varName} = begin %>${childrenBody}<% end %><%== bf->render_child('${tplName}'${propsStr}, children => ${varName}) %>`;
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
- childrenCaptureCounter = 0;
719
- presenceVarCounter = 0;
720
- toTemplateName(componentName) {
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
- renderIfStatement(ifStmt) {
724
- const condition = this.convertExpressionToPerl(ifStmt.condition);
725
- const consequent = ifStmt.consequent.type === "if-statement" ? this.renderIfStatement(ifStmt.consequent) : this.renderNode(ifStmt.consequent);
726
- let result = `% if (${condition}) {
727
- ${consequent}
728
- `;
729
- if (ifStmt.alternate) {
730
- if (ifStmt.alternate.type === "if-statement") {
731
- const altResult = this.renderIfStatement(ifStmt.alternate);
732
- result += altResult.replace(/^% if/, "% } elsif");
733
- } else {
734
- const alternate = this.renderNode(ifStmt.alternate);
735
- result += `% } else {
736
- ${alternate}
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
- result += `% }`;
741
- return result;
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
- renderFragment(fragment) {
744
- const children = this.renderChildren(fragment.children);
745
- if (fragment.needsScopeComment) {
746
- return `<%== bf->scope_comment %>${children}`;
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
- return children;
1041
+ if (!/^[A-Za-z_$][\w$]*$/.test(bare))
1042
+ return false;
1043
+ return this.booleanTypedProps.has(bare);
749
1044
  }
750
- renderSlot(_slot) {
751
- return `<%= content %>`;
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
- renderAsync(node) {
754
- const fallback = this.renderNode(node.fallback);
755
- const children = this.renderChildren(node.children);
756
- const fallbackVar = `$bf_async_fallback_${node.id}`;
757
- return `<% my ${fallbackVar} = begin %>${fallback}<% end %><%== bf->async_boundary('${node.id}', ${fallbackVar}) %>
758
- ${children}`;
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
- elementAttrEmitter = {
761
- emitLiteral: (value, name) => `${name}="${value.value}"`,
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
- for (const e of entries) {
839
- if (e.kind === "expr" && !isSupported(parseExpression2(e.expr)).supported)
840
- return null;
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}`)}'`;
1079
+ }
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}`)}'`;
1087
+ }
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
+ `);
1101
+ }
1102
+ renderNode(node) {
1103
+ return emitIRNode(node, this, {});
843
1104
  }
844
- escapeAttrText(s) {
845
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1105
+ emitElement(node, _ctx, _emit) {
1106
+ return this.renderElement(node);
846
1107
  }
847
- renderAttributes(element) {
848
- const parts = [];
849
- for (const attr of element.attrs) {
850
- let attrName;
851
- if (attr.name === "className")
852
- attrName = "class";
853
- else if (attr.name === "key")
854
- attrName = "data-key";
855
- else
856
- attrName = attr.name;
857
- const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
858
- if (lowered)
859
- parts.push(lowered);
860
- }
861
- return parts.length > 0 ? " " + parts.join(" ") : "";
1108
+ emitText(node) {
1109
+ return node.value;
862
1110
  }
863
- renderScopeMarker(_instanceIdExpr) {
864
- return `bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %>`;
1111
+ emitExpression(node) {
1112
+ return this.renderExpression(node);
865
1113
  }
866
- renderSlotMarker(slotId) {
867
- return `${BF_SLOT}="${slotId}"`;
1114
+ emitConditional(node, _ctx, _emit) {
1115
+ return this.renderConditional(node);
868
1116
  }
869
- renderCondMarker(condId) {
870
- return `${BF_COND}="${condId}"`;
1117
+ emitLoop(node, _ctx, _emit) {
1118
+ return this.renderLoop(node);
871
1119
  }
872
- renderPerlFilterExpr(expr, param, localVarMap = new Map) {
873
- return emitParsedExpr(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
1120
+ emitComponent(node, _ctx, _emit) {
1121
+ return this.renderComponent(node);
874
1122
  }
875
- renderBlockBodyCondition(statements, param) {
876
- const localVarMap = new Map;
877
- const paths = this.collectReturnPaths(statements, [], localVarMap, param);
878
- if (paths.length === 0)
879
- return "1";
880
- if (paths.length === 1)
881
- return this.buildSinglePathCondition(paths[0], param, localVarMap);
882
- const parts = [];
883
- for (const path of paths) {
884
- if (path.result.kind === "literal" && path.result.literalType === "boolean" && path.result.value === false)
885
- continue;
886
- const cond = this.buildSinglePathCondition(path, param, localVarMap);
887
- if (cond !== "0")
888
- parts.push(cond);
889
- }
890
- if (parts.length === 0)
891
- return "0";
892
- if (parts.length === 1)
893
- return parts[0];
894
- return `(${parts.join(" || ")})`;
895
- }
896
- collectReturnPaths(statements, currentConditions, localVarMap, param) {
897
- const paths = [];
898
- for (const stmt of statements) {
899
- if (stmt.kind === "var-decl") {
900
- if (stmt.init.kind === "call" && stmt.init.callee.kind === "identifier") {
901
- localVarMap.set(stmt.name, stmt.init.callee.name);
902
- }
903
- } else if (stmt.kind === "return") {
904
- paths.push({ conditions: [...currentConditions], result: stmt.value });
905
- break;
906
- } else if (stmt.kind === "if") {
907
- const thenPaths = this.collectReturnPaths(stmt.consequent, [...currentConditions, stmt.condition], localVarMap, param);
908
- paths.push(...thenPaths);
909
- if (stmt.alternate) {
910
- const negated = { kind: "unary", op: "!", argument: stmt.condition };
911
- const elsePaths = this.collectReturnPaths(stmt.alternate, [...currentConditions, negated], localVarMap, param);
912
- paths.push(...elsePaths);
913
- } else {
914
- currentConditions.push({ kind: "unary", op: "!", argument: stmt.condition });
915
- }
916
- }
917
- }
918
- return paths;
1123
+ emitFragment(node, _ctx, _emit) {
1124
+ return this.renderFragment(node);
919
1125
  }
920
- buildSinglePathCondition(path, param, localVarMap) {
921
- if (path.result.kind === "literal" && path.result.literalType === "boolean") {
922
- if (path.result.value === true) {
923
- if (path.conditions.length === 0)
924
- return "1";
925
- return this.renderConditionsAnd(path.conditions, param, localVarMap);
926
- }
927
- return "0";
1126
+ emitSlot(node) {
1127
+ return this.renderSlot(node);
1128
+ }
1129
+ emitIfStatement(node, _ctx, _emit) {
1130
+ return this.renderIfStatement(node);
1131
+ }
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}'); %>`;
1137
+ }
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);
928
1142
  }
929
- if (path.conditions.length === 0) {
930
- return this.renderPerlFilterExpr(path.result, param, localVarMap);
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);
931
1148
  }
932
- const condPart = this.renderConditionsAnd(path.conditions, param, localVarMap);
933
- const resultPart = this.renderPerlFilterExpr(path.result, param, localVarMap);
934
- return `(${condPart} && ${resultPart})`;
1149
+ if (v.kind === "template")
1150
+ return this.convertTemplateLiteralPartsToPerl(v.parts);
1151
+ return "undef";
935
1152
  }
936
- renderConditionsAnd(conditions, param, localVarMap) {
937
- if (conditions.length === 0)
938
- return "1";
939
- if (conditions.length === 1)
940
- return this.renderPerlFilterExpr(conditions[0], param, localVarMap);
941
- const parts = conditions.map((c) => this.renderPerlFilterExpr(c, param, localVarMap));
942
- return `(${parts.join(" && ")})`;
1153
+ providerObjectLiteralPerl(expr) {
1154
+ const members = parseProviderObjectLiteral(expr.trim());
1155
+ if (members === null)
1156
+ return null;
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)}`;
1165
+ });
1166
+ return `{ ${entries.join(", ")} }`;
943
1167
  }
944
- convertTemplateLiteralPartsToPerl(literalParts) {
945
- const parts = [];
946
- for (const part of literalParts) {
947
- if (part.type === "string") {
948
- parts.push(this.substituteJsInterpolationsToPerl(part.value));
949
- } else if (part.type === "ternary") {
950
- const cond = this.convertExpressionToPerl(part.condition);
951
- parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
952
- } else if (part.type === "lookup") {
953
- const keyExpr = this.convertExpressionToPerl(part.key);
954
- const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
955
- parts.push(`({ ${entries} }->{${keyExpr}} // '')`);
956
- }
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);
1173
+ }
1174
+ emitAsync(node, _ctx, _emit) {
1175
+ return this.renderAsync(node);
1176
+ }
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("")}`;
957
1184
  }
958
- return parts.length === 1 ? parts[0] : parts.join(" . ");
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}>`;
959
1214
  }
960
- substituteJsInterpolationsToPerl(s) {
961
- const segments = [];
962
- const re = /\$\{([^}]+)\}/g;
963
- let lastIndex = 0;
964
- let m;
965
- while ((m = re.exec(s)) !== null) {
966
- if (m.index > lastIndex) {
967
- segments.push(`'${s.slice(lastIndex, m.index)}'`);
1215
+ renderExpression(expr) {
1216
+ if (expr.clientOnly) {
1217
+ if (expr.slotId) {
1218
+ return `<%== bf->comment("client:${expr.slotId}") %>`;
968
1219
  }
969
- segments.push(this.convertExpressionToPerl(m[1].trim()));
970
- lastIndex = re.lastIndex;
1220
+ return "";
971
1221
  }
972
- if (lastIndex < s.length) {
973
- segments.push(`'${s.slice(lastIndex)}'`);
1222
+ const perlExpr = this.convertExpressionToPerl(expr.expr);
1223
+ if (expr.slotId) {
1224
+ return `<%== bf->text_start("${expr.slotId}") %><%= ${perlExpr} %><%== bf->text_end %>`;
974
1225
  }
975
- if (segments.length === 0)
976
- return `''`;
977
- return segments.length === 1 ? segments[0] : `(${segments.join(" . ")})`;
1226
+ return `<%= ${perlExpr} %>`;
978
1227
  }
979
- refuseUnsupportedAttrExpression(expr, attrName) {
980
- let probe = expr.trim();
981
- while (probe.startsWith("("))
982
- probe = probe.slice(1).trimStart();
983
- const startsAsObjectLiteral = probe.startsWith("{");
984
- const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
985
- if (!startsAsObjectLiteral && !hasTaggedTemplate)
986
- return false;
987
- const parsed = parseExpression2(expr.trim());
988
- const support = isSupported(parsed);
989
- if (parsed.kind !== "unsupported" && support.supported)
990
- return false;
991
- const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
992
- const reasonLine = reason ? `
993
- ${reason}` : "";
994
- this.errors.push({
995
- code: "BF101",
996
- severity: "error",
997
- message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
998
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
999
- suggestion: {
1000
- 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."
1001
- }
1002
- });
1003
- return true;
1228
+ renderConditional(cond) {
1229
+ if (cond.clientOnly && cond.slotId) {
1230
+ return `<%== bf->comment("cond-start:${cond.slotId}") %><%== bf->comment("cond-end:${cond.slotId}") %>`;
1231
+ }
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;
1242
+ }
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
+ `;
1277
+ }
1278
+ return result;
1004
1279
  }
1005
- conditionalSpreadToPerl(expr) {
1006
- const sf = ts.createSourceFile("__spread.ts", `(${expr})`, ts.ScriptTarget.Latest, true);
1007
- if (sf.statements.length !== 1)
1008
- return null;
1009
- const stmt = sf.statements[0];
1010
- if (!ts.isExpressionStatement(stmt))
1011
- return null;
1012
- let node = stmt.expression;
1013
- while (ts.isParenthesizedExpression(node))
1014
- node = node.expression;
1015
- if (!ts.isConditionalExpression(node))
1016
- return null;
1017
- const unwrap = (e) => {
1018
- let n = e;
1019
- while (ts.isParenthesizedExpression(n))
1020
- n = n.expression;
1021
- return n;
1022
- };
1023
- const whenTrue = unwrap(node.whenTrue);
1024
- const whenFalse = unwrap(node.whenFalse);
1025
- if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
1280
+ renderNodeOrNull(node) {
1281
+ if (node.type === "expression" && (node.expr === "null" || node.expr === "undefined")) {
1026
1282
  return null;
1027
1283
  }
1028
- const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
1029
- const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
1030
- const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
1031
- if (truePerl === null || falsePerl === null)
1032
- return null;
1033
- return `${condPerl} ? ${truePerl} : ${falsePerl}`;
1284
+ return this.renderNode(node);
1034
1285
  }
1035
- objectLiteralToPerlHashref(obj, sf) {
1036
- const entries = [];
1037
- for (const prop of obj.properties) {
1038
- if (!ts.isPropertyAssignment(prop))
1039
- return null;
1040
- let key;
1041
- if (ts.isIdentifier(prop.name)) {
1042
- key = prop.name.text;
1043
- } else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
1044
- key = prop.name.text;
1045
- } else {
1046
- return null;
1047
- }
1048
- const initNode = (() => {
1049
- let n = prop.initializer;
1050
- while (ts.isParenthesizedExpression(n))
1051
- n = n.expression;
1052
- return n;
1053
- })();
1054
- const indexed = this.recordIndexAccessToPerl(initNode);
1055
- if (indexed === null && ts.isElementAccessExpression(initNode) && initNode.argumentExpression && !ts.isNumericLiteral(initNode.argumentExpression) && !ts.isStringLiteral(initNode.argumentExpression)) {
1056
- this.errors.push({
1057
- code: "BF101",
1058
- severity: "error",
1059
- 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.`,
1060
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1061
- suggestion: {
1062
- message: "Index a record whose values are number/string literals, or move the spread into a `'use client'` component so hydration computes it."
1063
- }
1064
- });
1065
- return null;
1066
- }
1067
- const valPerl = indexed !== null ? indexed : this.convertExpressionToPerl(prop.initializer.getText(sf));
1068
- entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
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`);
1069
1290
  }
1070
- return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
1071
- }
1072
- recordIndexAccessToPerl(val) {
1073
- const parsed = parseRecordIndexAccess(val, this.localConstants, this.propsParams);
1074
- if (!parsed)
1075
- return null;
1076
- const entries = parsed.entries.map((e) => {
1077
- const mapVal = e.value.kind === "number" ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`;
1078
- return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`;
1079
- });
1080
- return `{ ${entries.join(", ")} }->{$${parsed.indexPropName}}`;
1291
+ return `<%== bf->comment("cond-start:${condId}") %>${content}<%== bf->comment("cond-end:${condId}") %>`;
1081
1292
  }
1082
- convertExpressionToPerl(expr) {
1083
- const trimmed = expr.trim();
1084
- if (trimmed === "")
1085
- return "''";
1086
- const parsed = parseExpression2(trimmed);
1087
- const support = isSupported(parsed);
1088
- if (!support.supported) {
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) {
1089
1299
  this.errors.push({
1090
- code: "BF101",
1300
+ code: "BF104",
1091
1301
  severity: "error",
1092
- message: `Expression not supported: ${trimmed}`,
1093
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
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 } },
1094
1304
  suggestion: {
1095
- message: support.reason ? `${support.reason}
1096
-
1097
- Options:
1098
- 1. Use /* @client */ for client-side evaluation
1099
- 2. Pre-compute the value in Perl` : `Options:
1100
- 1. Use /* @client */ for client-side evaluation
1101
- 2. Pre-compute the value in Perl`
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.`
1102
1309
  }
1103
1310
  });
1104
- return "''";
1105
- }
1106
- return this.renderParsedExprToPerl(parsed);
1107
- }
1108
- renderParsedExprToPerl(expr) {
1109
- return emitParsedExpr(expr, new MojoTopLevelEmitter(this));
1110
- }
1111
- _isStringValueName(name) {
1112
- return this.stringValueNames.has(name);
1113
- }
1114
- _recordExprBF101(message, reason) {
1115
- this.errors.push({
1116
- code: "BF101",
1117
- severity: "error",
1118
- message,
1119
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1120
- suggestion: {
1121
- message: reason ? `${reason}
1122
-
1123
- Options:
1124
- 1. Use /* @client */ for client-side evaluation
1125
- 2. Pre-compute the value in Perl` : `Options:
1126
- 1. Use /* @client */ for client-side evaluation
1127
- 2. Pre-compute the value in Perl`
1128
- }
1129
- });
1130
- }
1131
- _renderPerlFilterExprPublic(expr, param) {
1132
- return this.renderPerlFilterExpr(expr, param);
1133
- }
1134
- }
1135
- function renderArrayMethod(method, object, args, emit) {
1136
- switch (method) {
1137
- case "join": {
1138
- const obj = emit(object);
1139
- const sep = args.length >= 1 ? emit(args[0]) : `','`;
1140
- return `join(${sep}, @{${obj}})`;
1141
- }
1142
- case "includes": {
1143
- const obj = emit(object);
1144
- const needle = emit(args[0]);
1145
- return `bf->includes(${obj}, ${needle})`;
1146
- }
1147
- case "indexOf":
1148
- case "lastIndexOf": {
1149
- const fn = method === "indexOf" ? "index_of" : "last_index_of";
1150
- const obj = emit(object);
1151
- const needle = emit(args[0]);
1152
- return `bf->${fn}(${obj}, ${needle})`;
1153
- }
1154
- case "at": {
1155
- const obj = emit(object);
1156
- const idx = args.length >= 1 ? emit(args[0]) : "0";
1157
- return `bf->at(${obj}, ${idx})`;
1158
- }
1159
- case "concat": {
1160
- if (args.length === 0) {
1161
- return emit(object);
1162
- }
1163
- const a = emit(object);
1164
- const b = emit(args[0]);
1165
- return `bf->concat(${a}, ${b})`;
1166
- }
1167
- case "slice": {
1168
- const recv = emit(object);
1169
- const start = args.length >= 1 ? emit(args[0]) : "0";
1170
- const end = args.length >= 2 ? emit(args[1]) : "undef";
1171
- return `bf->slice(${recv}, ${start}, ${end})`;
1172
1311
  }
1173
- case "reverse":
1174
- case "toReversed": {
1175
- const recv = emit(object);
1176
- return `bf->reverse(${recv})`;
1177
- }
1178
- case "toLowerCase": {
1179
- const recv = emit(object);
1180
- return `lc(${recv})`;
1181
- }
1182
- case "toUpperCase": {
1183
- const recv = emit(object);
1184
- return `uc(${recv})`;
1185
- }
1186
- case "trim": {
1187
- const recv = emit(object);
1188
- return `bf->trim(${recv})`;
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}`;
1189
1318
  }
1190
- case "toFixed": {
1191
- const recv = emit(object);
1192
- const digits = args.length >= 1 ? emit(args[0]) : "0";
1193
- return `bf->to_fixed(${recv}, ${digits})`;
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);
1194
1324
  }
1195
- case "split": {
1196
- const recv = emit(object);
1197
- if (args.length === 0) {
1198
- return `bf->split(${recv})`;
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);
1199
1340
  }
1200
- const sep = emit(args[0]);
1201
- if (args.length === 1) {
1202
- return `bf->split(${recv}, ${sep})`;
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);
1203
1346
  }
1204
- const limit = emit(args[1]);
1205
- return `bf->split(${recv}, ${sep}, ${limit})`;
1206
- }
1207
- case "startsWith":
1208
- case "endsWith": {
1209
- const fn = method === "startsWith" ? "starts_with" : "ends_with";
1210
- const recv = emit(object);
1211
- const arg = emit(args[0]);
1212
- if (args.length >= 2) {
1213
- 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);
1214
1351
  }
1215
- return `bf->${fn}(${recv}, ${arg})`;
1216
- }
1217
- case "replace": {
1218
- const recv = emit(object);
1219
- const oldS = emit(args[0]);
1220
- const newS = emit(args[1]);
1221
- return `bf->replace(${recv}, ${oldS}, ${newS})`;
1222
- }
1223
- case "repeat": {
1224
- const recv = emit(object);
1225
- const count = args.length === 0 ? "0" : emit(args[0]);
1226
- return `bf->repeat(${recv}, ${count})`;
1227
- }
1228
- case "padStart":
1229
- case "padEnd": {
1230
- const fn = method === "padStart" ? "pad_start" : "pad_end";
1231
- const recv = emit(object);
1232
- if (args.length === 0) {
1233
- 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;
1234
1355
  }
1235
- const target = emit(args[0]);
1236
- if (args.length === 1) {
1237
- return `bf->${fn}(${recv}, ${target})`;
1356
+ for (const n of loopBound) {
1357
+ this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
1238
1358
  }
1239
- const pad = emit(args[1]);
1240
- return `bf->${fn}(${recv}, ${target}, ${pad})`;
1241
- }
1242
- default: {
1243
- const _exhaustive = method;
1244
- throw new Error(`renderArrayMethod: unhandled ArrayMethod '${_exhaustive}'`);
1359
+ lines.push(`% my $${sortedHoist} = ${sorted};`);
1245
1360
  }
1246
- }
1247
- }
1248
- function perlIdentifierFromMarkerId(markerId) {
1249
- return markerId.replace(/[^a-zA-Z0-9]/g, (ch) => ch === "_" ? "__" : `_x${ch.charCodeAt(0).toString(16)}`);
1250
- }
1251
- function renderSortMethod(recv, c) {
1252
- const keyHashes = c.keys.map((k) => {
1253
- const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
1254
- return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
1255
- });
1256
- return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
1257
- }
1258
- function renderReduceMethod(recv, op, direction) {
1259
- const keyEntry = op.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${op.key.field}'`;
1260
- const init = op.type === "string" ? `'${op.init.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'` : op.init;
1261
- return `bf->reduce(${recv}, { op => '${op.op}', ${keyEntry}, type => '${op.type}', init => ${init}, direction => '${direction}' })`;
1262
- }
1263
- function renderFlatMethod(recv, depth) {
1264
- const d = depth === "infinity" ? -1 : depth;
1265
- return `bf->flat(${recv}, ${d})`;
1266
- }
1267
- function renderFlatMapMethod(recv, op) {
1268
- const proj = op.projection;
1269
- if (proj.kind === "tuple") {
1270
- const specs = proj.elements.map((l) => l.kind === "self" ? `['self', '']` : `['field', '${l.field}']`).join(", ");
1271
- return `bf->flat_map_tuple(${recv}, ${specs})`;
1272
- }
1273
- if (proj.kind === "self")
1274
- return `bf->flat_map(${recv}, 'self', '')`;
1275
- return `bf->flat_map(${recv}, 'field', '${proj.field}')`;
1276
- }
1277
- function isStringTypeInfo(type) {
1278
- return type?.kind === "primitive" && type.primitive === "string";
1279
- }
1280
- function isBareStringLiteral(initialValue) {
1281
- if (!initialValue)
1282
- return false;
1283
- const v = initialValue.trim();
1284
- return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
1285
- }
1286
- function isStringTypedOperand(expr, isStringName) {
1287
- if (expr.kind === "literal" && expr.literalType === "string")
1288
- return true;
1289
- if (expr.kind === "call" && expr.callee.kind === "identifier" && expr.args.length === 0) {
1290
- return isStringName(expr.callee.name);
1291
- }
1292
- if (expr.kind === "member" && expr.object.kind === "identifier" && expr.object.name === "props") {
1293
- return isStringName(expr.property);
1294
- }
1295
- return false;
1296
- }
1297
- function emitIndexAccessPerl(object, index, emit, isStringName) {
1298
- const i = emit(index);
1299
- return isStringTypedOperand(index, isStringName) ? `${emit(object)}->{${i}}` : `${emit(object)}->[${i}]`;
1300
- }
1301
-
1302
- class MojoFilterEmitter {
1303
- param;
1304
- localVarMap;
1305
- isStringName;
1306
- constructor(param, localVarMap, isStringName = () => false) {
1307
- this.param = param;
1308
- this.localVarMap = localVarMap;
1309
- this.isStringName = isStringName;
1310
- }
1311
- identifier(name) {
1312
- if (name === this.param)
1313
- return `$${this.param}`;
1314
- const signal = this.localVarMap.get(name);
1315
- if (signal)
1316
- return `$${signal}`;
1317
- return `$${name}`;
1318
- }
1319
- literal(value, literalType) {
1320
- if (literalType === "string")
1321
- return `'${value}'`;
1322
- if (literalType === "boolean")
1323
- return value ? "1" : "0";
1324
- if (literalType === "null")
1325
- return "undef";
1326
- return String(value);
1327
- }
1328
- member(object, property, _computed, emit) {
1329
- if (property === "length" && (object.kind === "higher-order" || object.kind === "array-literal")) {
1330
- 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
+ }
1331
1371
  }
1332
- return `${emit(object)}->{${property}}`;
1333
- }
1334
- indexAccess(object, index, emit) {
1335
- return emitIndexAccessPerl(object, index, emit, this.isStringName);
1336
- }
1337
- call(callee, args, emit) {
1338
- if (callee.kind === "identifier" && args.length === 0) {
1339
- return `$${callee.name}`;
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);
1340
1387
  }
1341
- return emit(callee);
1342
- }
1343
- unary(op, argument, emit) {
1344
- const arg = emit(argument);
1345
- if (op === "!") {
1346
- const needsParens = argument.kind === "binary" || argument.kind === "logical";
1347
- 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);
1348
1394
  }
1349
- if (op === "-")
1350
- return `-${arg}`;
1351
- return arg;
1395
+ lines.push(`% }`);
1396
+ lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
1397
+ return lines.join(`
1398
+ `);
1352
1399
  }
1353
- binary(op, left, right, emit) {
1354
- const l = emit(left);
1355
- const r = emit(right);
1356
- const isStr = (e) => isStringTypedOperand(e, this.isStringName);
1357
- const stringCmp = isStr(left) || isStr(right);
1358
- if ((op === "===" || op === "==") && stringCmp) {
1359
- return `${l} eq ${r}`;
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);
1360
1430
  }
1361
- if ((op === "!==" || op === "!=") && stringCmp) {
1362
- return `${l} ne ${r}`;
1431
+ if (comp.slotId && !this.inLoop) {
1432
+ propParts.push(`_bf_slot => '${comp.slotId}'`);
1363
1433
  }
1364
- const opMap = {
1365
- "===": "==",
1366
- "!==": "!=",
1367
- ">": ">",
1368
- "<": "<",
1369
- ">=": ">=",
1370
- "<=": "<=",
1371
- "+": "+",
1372
- "-": "-",
1373
- "*": "*",
1374
- "/": "/"
1375
- };
1376
- return `${l} ${opMap[op] ?? op} ${r}`;
1377
- }
1378
- logical(op, left, right, emit) {
1379
- const l = emit(left);
1380
- const r = emit(right);
1381
- if (op === "&&")
1382
- return `(${l} && ${r})`;
1383
- if (op === "||")
1384
- return `(${l} || ${r})`;
1385
- return `(${l} // ${r})`;
1386
- }
1387
- higherOrder(method, object, param, predicate, emit) {
1388
- const arrayExpr = emit(object);
1389
- const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName));
1390
- const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
1391
- if (method === "filter")
1392
- return `[grep { ${grepBody} } @{${arrayExpr}}]`;
1393
- if (method === "every")
1394
- return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
1395
- if (method === "some")
1396
- return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
1397
- return arrayExpr;
1398
- }
1399
- arrayLiteral(elements, emit) {
1400
- return `[${elements.map(emit).join(", ")}]`;
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}) %>`;
1401
1446
  }
1402
- arrayMethod(method, object, args, emit) {
1403
- return renderArrayMethod(method, object, args, emit);
1447
+ childrenCaptureCounter = 0;
1448
+ presenceVarCounter = 0;
1449
+ toTemplateName(componentName) {
1450
+ return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
1404
1451
  }
1405
- sortMethod(_method, object, comparator, emit) {
1406
- return renderSortMethod(emit(object), comparator);
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;
1407
1471
  }
1408
- reduceMethod(method, object, reduceOp, emit) {
1409
- return renderReduceMethod(emit(object), reduceOp, method === "reduceRight" ? "right" : "left");
1472
+ renderFragment(fragment) {
1473
+ const children = this.renderChildren(fragment.children);
1474
+ if (fragment.needsScopeComment) {
1475
+ return `<%== bf->scope_comment %>${children}`;
1476
+ }
1477
+ return children;
1410
1478
  }
1411
- flatMethod(object, depth, emit) {
1412
- return renderFlatMethod(emit(object), depth);
1479
+ renderSlot(_slot) {
1480
+ return `<%= content %>`;
1413
1481
  }
1414
- flatMapMethod(object, op, emit) {
1415
- return renderFlatMapMethod(emit(object), op);
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}`;
1416
1488
  }
1417
- conditional(_test, _consequent, _alternate) {
1418
- return "1";
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(";");
1419
1572
  }
1420
- templateLiteral(_parts) {
1421
- return "1";
1573
+ escapeAttrText(s) {
1574
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1422
1575
  }
1423
- arrowFn(_param, _body) {
1424
- return "1";
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(" ") : "";
1425
1593
  }
1426
- unsupported(_raw, _reason) {
1427
- return "1";
1594
+ renderScopeMarker(_instanceIdExpr) {
1595
+ return `bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %>`;
1428
1596
  }
1429
- }
1430
-
1431
- class MojoTopLevelEmitter {
1432
- adapter;
1433
- constructor(adapter) {
1434
- this.adapter = adapter;
1597
+ renderSlotMarker(slotId) {
1598
+ return `${BF_SLOT}="${slotId}"`;
1435
1599
  }
1436
- identifier(name) {
1437
- if (name === "undefined" || name === "null")
1438
- return "undef";
1439
- const inlined = this.adapter.resolveModuleStringConst(name);
1440
- if (inlined !== null)
1441
- return inlined;
1442
- const literalConst = this.adapter.resolveLiteralConst(name);
1443
- if (literalConst !== null)
1444
- return literalConst;
1445
- return `$${name}`;
1600
+ renderCondMarker(condId) {
1601
+ return `${BF_COND}="${condId}"`;
1446
1602
  }
1447
- literal(value, literalType) {
1448
- if (literalType === "string")
1449
- return `'${value}'`;
1450
- if (literalType === "boolean")
1451
- return value ? "1" : "0";
1452
- if (literalType === "null")
1453
- return "undef";
1454
- return String(value);
1603
+ renderPerlFilterExpr(expr, param, localVarMap = new Map) {
1604
+ return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
1455
1605
  }
1456
- member(object, property, _computed, emit) {
1457
- if (object.kind === "identifier" && object.name === "props") {
1458
- return `$${property}`;
1459
- }
1460
- if (object.kind === "identifier") {
1461
- const staticValue = this.adapter.resolveStaticRecordLiteral(object.name, property);
1462
- if (staticValue !== null)
1463
- return staticValue;
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}} // '')`);
1618
+ }
1464
1619
  }
1465
- const obj = emit(object);
1466
- if (property === "length")
1467
- return `scalar(@{${obj}})`;
1468
- return `${obj}->{${property}}`;
1469
- }
1470
- indexAccess(object, index, emit) {
1471
- return emitIndexAccessPerl(object, index, emit, (n) => this.adapter._isStringValueName(n));
1620
+ return parts.length === 1 ? parts[0] : parts.join(" . ");
1472
1621
  }
1473
- call(callee, args, emit) {
1474
- if (callee.kind === "identifier" && args.length === 0) {
1475
- return `$${callee.name}`;
1476
- }
1477
- if (this.adapter._searchParamsLocals.size > 0) {
1478
- const sp = matchSearchParamsMethodCall(callee, args, this.adapter._searchParamsLocals);
1479
- if (sp) {
1480
- return `$searchParams->${sp.method}(${sp.args.map(emit).join(", ")})`;
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)}'`);
1481
1630
  }
1631
+ segments.push(this.convertExpressionToPerl(m[1].trim()));
1632
+ lastIndex = re.lastIndex;
1482
1633
  }
1483
- const path = identifierPath(callee);
1484
- const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
1485
- if (path && spec) {
1486
- if (args.length === spec.arity) {
1487
- return spec.emit(args.map(emit));
1488
- }
1489
- this.adapter._recordExprBF101(`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`, `Call '${path}' with exactly ${spec.arity} argument(s).`);
1490
- return "''";
1634
+ if (lastIndex < s.length) {
1635
+ segments.push(`'${s.slice(lastIndex)}'`);
1491
1636
  }
1492
- return emit(callee);
1637
+ if (segments.length === 0)
1638
+ return `''`;
1639
+ return segments.length === 1 ? segments[0] : `(${segments.join(" . ")})`;
1493
1640
  }
1494
- unary(op, argument, emit) {
1495
- const arg = emit(argument);
1496
- if (op === "!")
1497
- return `!${arg}`;
1498
- if (op === "-")
1499
- return `-${arg}`;
1500
- return arg;
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;
1501
1666
  }
1502
- binary(op, left, right, emit) {
1503
- const l = emit(left);
1504
- const r = emit(right);
1505
- const isStr = (e) => isStringTypedOperand(e, (n) => this.adapter._isStringValueName(n));
1506
- const stringCmp = isStr(left) || isStr(right);
1507
- if ((op === "===" || op === "==") && stringCmp) {
1508
- return `${l} eq ${r}`;
1509
- }
1510
- if ((op === "!==" || op === "!=") && stringCmp) {
1511
- return `${l} ne ${r}`;
1512
- }
1513
- const opMap = {
1514
- "===": "==",
1515
- "!==": "!=",
1516
- ">": ">",
1517
- "<": "<",
1518
- ">=": ">=",
1519
- "<=": "<=",
1520
- "+": "+",
1521
- "-": "-",
1522
- "*": "*"
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)
1523
1676
  };
1524
- return `${l} ${opMap[op] ?? op} ${r}`;
1525
- }
1526
- logical(op, left, right, emit) {
1527
- const l = emit(left);
1528
- const r = emit(right);
1529
- if (op === "&&")
1530
- return `(${l} && ${r})`;
1531
- if (op === "||")
1532
- return `(${l} || ${r})`;
1533
- return `(${l} // ${r})`;
1534
1677
  }
1535
- higherOrder(method, object, param, predicate, emit) {
1536
- const arrayExpr = emit(object);
1537
- const predBody = this.adapter._renderPerlFilterExprPublic(predicate, param);
1538
- const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
1539
- if (method === "filter")
1540
- return `[grep { ${grepBody} } @{${arrayExpr}}]`;
1541
- if (method === "every")
1542
- return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
1543
- if (method === "some")
1544
- return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
1545
- const findHelper = {
1546
- find: "find",
1547
- findIndex: "find_index",
1548
- findLast: "find_last",
1549
- findLastIndex: "find_last_index"
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)
1550
1685
  };
1551
- if (findHelper[method]) {
1552
- return `bf->${findHelper[method]}(${arrayExpr}, sub { my $${param} = $_[0]; ${predBody} })`;
1553
- }
1554
- return arrayExpr;
1555
- }
1556
- arrayLiteral(elements, emit) {
1557
- return `[${elements.map(emit).join(", ")}]`;
1558
- }
1559
- arrayMethod(method, object, args, emit) {
1560
- return renderArrayMethod(method, object, args, emit);
1561
- }
1562
- sortMethod(_method, object, comparator, emit) {
1563
- return renderSortMethod(emit(object), comparator);
1564
- }
1565
- reduceMethod(method, object, reduceOp, emit) {
1566
- return renderReduceMethod(emit(object), reduceOp, method === "reduceRight" ? "right" : "left");
1567
1686
  }
1568
- flatMethod(object, depth, emit) {
1569
- return renderFlatMethod(emit(object), depth);
1570
- }
1571
- flatMapMethod(object, op, emit) {
1572
- return renderFlatMapMethod(emit(object), op);
1573
- }
1574
- conditional(test, consequent, alternate, emit) {
1575
- return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
1687
+ get memoCtx() {
1688
+ return { convertExpressionToPerl: (e, preParsed) => this.convertExpressionToPerl(e, preParsed) };
1576
1689
  }
1577
- templateLiteral(parts, emit) {
1578
- const terms = [];
1579
- for (const part of parts) {
1580
- if (part.type === "string") {
1581
- if (part.value !== "") {
1582
- terms.push(`"${part.value.replace(/[\\"$@]/g, (m) => `\\${m}`)}"`);
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(", ")})`;
1583
1706
  }
1584
- } else {
1585
- const rendered = emit(part.expr);
1586
- const needsParens = part.expr.kind === "binary" || part.expr.kind === "logical" || part.expr.kind === "conditional";
1587
- terms.push(needsParens ? `(${rendered})` : rendered);
1588
1707
  }
1589
1708
  }
1590
- if (terms.length === 0)
1591
- return '""';
1592
- return terms.join(" . ");
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);
1593
1729
  }
1594
- arrowFn(_param, _body) {
1595
- return "''";
1730
+ renderParsedExprToPerl(expr) {
1731
+ return emitParsedExpr2(expr, new MojoTopLevelEmitter(this.emitCtx));
1596
1732
  }
1597
- unsupported(_raw, _reason) {
1598
- 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);
1599
1755
  }
1600
1756
  }
1601
1757
  var mojoAdapter = new MojoAdapter;