@barefootjs/mojolicious 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 -1336
  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 -1336
  31. package/dist/index.js +1490 -1336
  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 +124 -60
  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 +223 -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/index.js CHANGED
@@ -1,27 +1,24 @@
1
1
  // src/adapter/mojo-adapter.ts
2
- import ts from "typescript";
3
2
  import {
4
3
  BaseAdapter,
5
4
  isBooleanAttr,
6
- parseExpression as 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,1474 +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}`)}'`;
843
1079
  }
844
- escapeAttrText(s) {
845
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1080
+ resolveModuleStringConst(name) {
1081
+ if (this.loopBoundNames.has(name))
1082
+ return null;
1083
+ const value = this.moduleStringConsts.get(name);
1084
+ if (value === undefined)
1085
+ return null;
1086
+ return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
846
1087
  }
847
- renderAttributes(element) {
848
- const parts = [];
849
- for (const attr of element.attrs) {
850
- if (attr.clientOnly)
851
- continue;
852
- let attrName;
853
- if (attr.name === "className")
854
- attrName = "class";
855
- else if (attr.name === "key")
856
- attrName = "data-key";
857
- else
858
- attrName = attr.name;
859
- const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
860
- if (lowered)
861
- parts.push(lowered);
862
- }
863
- return parts.length > 0 ? " " + parts.join(" ") : "";
1088
+ generateScriptRegistrations(ir, scriptBaseName) {
1089
+ const hasInteractivity = hasClientInteractivity(ir);
1090
+ if (!hasInteractivity)
1091
+ return "";
1092
+ const name = scriptBaseName ?? ir.metadata.componentName;
1093
+ const runtimePath = this.options.barefootJsPath;
1094
+ const clientJsPath = `${this.options.clientJsBasePath}${name}.client.js`;
1095
+ const lines = [];
1096
+ lines.push(`% bf->register_script('${runtimePath}');`);
1097
+ lines.push(`% bf->register_script('${clientJsPath}');`);
1098
+ lines.push("");
1099
+ return lines.join(`
1100
+ `);
864
1101
  }
865
- renderScopeMarker(_instanceIdExpr) {
866
- return `bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %>`;
1102
+ renderNode(node) {
1103
+ return emitIRNode(node, this, {});
867
1104
  }
868
- renderSlotMarker(slotId) {
869
- return `${BF_SLOT}="${slotId}"`;
1105
+ emitElement(node, _ctx, _emit) {
1106
+ return this.renderElement(node);
870
1107
  }
871
- renderCondMarker(condId) {
872
- return `${BF_COND}="${condId}"`;
1108
+ emitText(node) {
1109
+ return node.value;
873
1110
  }
874
- renderPerlFilterExpr(expr, param, localVarMap = new Map) {
875
- return emitParsedExpr(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
1111
+ emitExpression(node) {
1112
+ return this.renderExpression(node);
876
1113
  }
877
- renderBlockBodyCondition(statements, param) {
878
- const localVarMap = new Map;
879
- const paths = this.collectReturnPaths(statements, [], localVarMap, param);
880
- if (paths.length === 0)
881
- return "1";
882
- if (paths.length === 1)
883
- return this.buildSinglePathCondition(paths[0], param, localVarMap);
884
- const parts = [];
885
- for (const path of paths) {
886
- if (path.result.kind === "literal" && path.result.literalType === "boolean" && path.result.value === false)
887
- continue;
888
- const cond = this.buildSinglePathCondition(path, param, localVarMap);
889
- if (cond !== "0")
890
- parts.push(cond);
891
- }
892
- if (parts.length === 0)
893
- return "0";
894
- if (parts.length === 1)
895
- return parts[0];
896
- return `(${parts.join(" || ")})`;
897
- }
898
- collectReturnPaths(statements, currentConditions, localVarMap, param) {
899
- const paths = [];
900
- for (const stmt of statements) {
901
- if (stmt.kind === "var-decl") {
902
- if (stmt.init.kind === "call" && stmt.init.callee.kind === "identifier") {
903
- localVarMap.set(stmt.name, stmt.init.callee.name);
904
- }
905
- } else if (stmt.kind === "return") {
906
- paths.push({ conditions: [...currentConditions], result: stmt.value });
907
- break;
908
- } else if (stmt.kind === "if") {
909
- const thenPaths = this.collectReturnPaths(stmt.consequent, [...currentConditions, stmt.condition], localVarMap, param);
910
- paths.push(...thenPaths);
911
- if (stmt.alternate) {
912
- const negated = { kind: "unary", op: "!", argument: stmt.condition };
913
- const elsePaths = this.collectReturnPaths(stmt.alternate, [...currentConditions, negated], localVarMap, param);
914
- paths.push(...elsePaths);
915
- } else {
916
- currentConditions.push({ kind: "unary", op: "!", argument: stmt.condition });
917
- }
918
- }
919
- }
920
- return paths;
1114
+ emitConditional(node, _ctx, _emit) {
1115
+ return this.renderConditional(node);
921
1116
  }
922
- buildSinglePathCondition(path, param, localVarMap) {
923
- if (path.result.kind === "literal" && path.result.literalType === "boolean") {
924
- if (path.result.value === true) {
925
- if (path.conditions.length === 0)
926
- return "1";
927
- return this.renderConditionsAnd(path.conditions, param, localVarMap);
928
- }
929
- return "0";
930
- }
931
- if (path.conditions.length === 0) {
932
- return this.renderPerlFilterExpr(path.result, param, localVarMap);
933
- }
934
- const condPart = this.renderConditionsAnd(path.conditions, param, localVarMap);
935
- const resultPart = this.renderPerlFilterExpr(path.result, param, localVarMap);
936
- return `(${condPart} && ${resultPart})`;
1117
+ emitLoop(node, _ctx, _emit) {
1118
+ return this.renderLoop(node);
937
1119
  }
938
- renderConditionsAnd(conditions, param, localVarMap) {
939
- if (conditions.length === 0)
940
- return "1";
941
- if (conditions.length === 1)
942
- return this.renderPerlFilterExpr(conditions[0], param, localVarMap);
943
- const parts = conditions.map((c) => this.renderPerlFilterExpr(c, param, localVarMap));
944
- return `(${parts.join(" && ")})`;
1120
+ emitComponent(node, _ctx, _emit) {
1121
+ return this.renderComponent(node);
945
1122
  }
946
- convertTemplateLiteralPartsToPerl(literalParts) {
947
- const parts = [];
948
- for (const part of literalParts) {
949
- if (part.type === "string") {
950
- parts.push(this.substituteJsInterpolationsToPerl(part.value));
951
- } else if (part.type === "ternary") {
952
- const cond = this.convertExpressionToPerl(part.condition);
953
- parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
954
- } else if (part.type === "lookup") {
955
- const keyExpr = this.convertExpressionToPerl(part.key);
956
- const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
957
- parts.push(`({ ${entries} }->{${keyExpr}} // '')`);
958
- }
959
- }
960
- return parts.length === 1 ? parts[0] : parts.join(" . ");
1123
+ emitFragment(node, _ctx, _emit) {
1124
+ return this.renderFragment(node);
961
1125
  }
962
- substituteJsInterpolationsToPerl(s) {
963
- const segments = [];
964
- const re = /\$\{([^}]+)\}/g;
965
- let lastIndex = 0;
966
- let m;
967
- while ((m = re.exec(s)) !== null) {
968
- if (m.index > lastIndex) {
969
- segments.push(`'${s.slice(lastIndex, m.index)}'`);
970
- }
971
- segments.push(this.convertExpressionToPerl(m[1].trim()));
972
- lastIndex = re.lastIndex;
973
- }
974
- if (lastIndex < s.length) {
975
- segments.push(`'${s.slice(lastIndex)}'`);
976
- }
977
- if (segments.length === 0)
978
- return `''`;
979
- return segments.length === 1 ? segments[0] : `(${segments.join(" . ")})`;
1126
+ emitSlot(node) {
1127
+ return this.renderSlot(node);
980
1128
  }
981
- refuseUnsupportedAttrExpression(expr, attrName) {
982
- let probe = expr.trim();
983
- while (probe.startsWith("("))
984
- probe = probe.slice(1).trimStart();
985
- const startsAsObjectLiteral = probe.startsWith("{");
986
- const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
987
- if (!startsAsObjectLiteral && !hasTaggedTemplate)
988
- return false;
989
- const parsed = parseExpression2(expr.trim());
990
- const support = isSupported(parsed);
991
- if (parsed.kind !== "unsupported" && support.supported)
992
- return false;
993
- const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
994
- const reasonLine = reason ? `
995
- ${reason}` : "";
996
- this.errors.push({
997
- code: "BF101",
998
- severity: "error",
999
- message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
1000
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1001
- suggestion: {
1002
- message: "The Mojo adapter cannot lower JS object literals or tagged-template-literal expressions into Embedded Perl. Move the expression into a `'use client'` component (so hydration computes it), or expand it into discrete attributes whose values are values the adapter can lower."
1003
- }
1004
- });
1005
- return true;
1129
+ emitIfStatement(node, _ctx, _emit) {
1130
+ return this.renderIfStatement(node);
1006
1131
  }
1007
- conditionalSpreadToPerl(expr) {
1008
- const sf = ts.createSourceFile("__spread.ts", `(${expr})`, ts.ScriptTarget.Latest, true);
1009
- if (sf.statements.length !== 1)
1010
- return null;
1011
- const stmt = sf.statements[0];
1012
- if (!ts.isExpressionStatement(stmt))
1013
- return null;
1014
- let node = stmt.expression;
1015
- while (ts.isParenthesizedExpression(node))
1016
- node = node.expression;
1017
- if (!ts.isConditionalExpression(node))
1018
- return null;
1019
- const unwrap = (e) => {
1020
- let n = e;
1021
- while (ts.isParenthesizedExpression(n))
1022
- n = n.expression;
1023
- return n;
1024
- };
1025
- const whenTrue = unwrap(node.whenTrue);
1026
- const whenFalse = unwrap(node.whenFalse);
1027
- if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
1028
- return null;
1029
- }
1030
- const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
1031
- const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
1032
- const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
1033
- if (truePerl === null || falsePerl === null)
1034
- return null;
1035
- return `${condPerl} ? ${truePerl} : ${falsePerl}`;
1132
+ emitProvider(node, _ctx, _emit) {
1133
+ const value = this.providerValuePerl(node.valueProp);
1134
+ const children = this.renderChildren(node.children);
1135
+ const name = node.contextName;
1136
+ return `<% bf->provide_context('${name}', ${value}); %>` + children + `<% bf->revoke_context('${name}'); %>`;
1036
1137
  }
1037
- objectLiteralToPerlHashref(obj, sf) {
1038
- const entries = [];
1039
- for (const prop of obj.properties) {
1040
- if (!ts.isPropertyAssignment(prop))
1041
- return null;
1042
- let key;
1043
- if (ts.isIdentifier(prop.name)) {
1044
- key = prop.name.text;
1045
- } else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
1046
- key = prop.name.text;
1047
- } else {
1048
- return null;
1049
- }
1050
- const initNode = (() => {
1051
- let n = prop.initializer;
1052
- while (ts.isParenthesizedExpression(n))
1053
- n = n.expression;
1054
- return n;
1055
- })();
1056
- const indexed = this.recordIndexAccessToPerl(initNode);
1057
- if (indexed === null && ts.isElementAccessExpression(initNode) && initNode.argumentExpression && !ts.isNumericLiteral(initNode.argumentExpression) && !ts.isStringLiteral(initNode.argumentExpression)) {
1058
- this.errors.push({
1059
- code: "BF101",
1060
- severity: "error",
1061
- message: `Spread object value '${initNode.getText(sf)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Perl hashref.`,
1062
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1063
- suggestion: {
1064
- message: "Index a record whose values are number/string literals, or move the spread into a `'use client'` component so hydration computes it."
1065
- }
1066
- });
1067
- return null;
1068
- }
1069
- const valPerl = indexed !== null ? indexed : this.convertExpressionToPerl(prop.initializer.getText(sf));
1070
- entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
1138
+ providerValuePerl(valueProp) {
1139
+ const v = valueProp.value;
1140
+ if (v.kind === "literal") {
1141
+ return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
1142
+ }
1143
+ if (v.kind === "expression") {
1144
+ const hashref = this.providerObjectLiteralPerl(v.expr);
1145
+ if (hashref !== null)
1146
+ return hashref;
1147
+ return this.convertExpressionToPerl(v.expr);
1071
1148
  }
1072
- return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
1149
+ if (v.kind === "template")
1150
+ return this.convertTemplateLiteralPartsToPerl(v.parts);
1151
+ return "undef";
1073
1152
  }
1074
- recordIndexAccessToPerl(val) {
1075
- const parsed = parseRecordIndexAccess(val, this.localConstants, this.propsParams);
1076
- if (!parsed)
1153
+ providerObjectLiteralPerl(expr) {
1154
+ const members = parseProviderObjectLiteral(expr.trim());
1155
+ if (members === null)
1077
1156
  return null;
1078
- const entries = parsed.entries.map((e) => {
1079
- const mapVal = e.value.kind === "number" ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`;
1080
- return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`;
1157
+ const entries = members.map((m) => {
1158
+ const key = `'${m.name.replace(/[\\']/g, (c) => `\\${c}`)}'`;
1159
+ if (m.kind === "function" || /^on[A-Z]/.test(m.name))
1160
+ return `${key} => undef`;
1161
+ const src = m.kind === "getter" ? m.body : m.expr;
1162
+ if (this.isClientOnlyContextIdentifier(src))
1163
+ return `${key} => undef`;
1164
+ return `${key} => ${this.convertExpressionToPerl(src)}`;
1081
1165
  });
1082
- return `{ ${entries.join(", ")} }->{$${parsed.indexPropName}}`;
1166
+ return `{ ${entries.join(", ")} }`;
1083
1167
  }
1084
- convertExpressionToPerl(expr) {
1085
- const trimmed = expr.trim();
1086
- if (trimmed === "")
1087
- return "''";
1088
- const parsed = parseExpression2(trimmed);
1089
- const support = isSupported(parsed);
1090
- if (!support.supported) {
1091
- this.errors.push({
1092
- code: "BF101",
1093
- severity: "error",
1094
- message: `Expression not supported: ${trimmed}`,
1095
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1096
- suggestion: {
1097
- message: support.reason ? `${support.reason}
1098
-
1099
- Options:
1100
- 1. Use /* @client */ for client-side evaluation
1101
- 2. Pre-compute the value in Perl` : `Options:
1102
- 1. Use /* @client */ for client-side evaluation
1103
- 2. Pre-compute the value in Perl`
1104
- }
1105
- });
1106
- return "''";
1107
- }
1108
- return this.renderParsedExprToPerl(parsed);
1168
+ isClientOnlyContextIdentifier(src) {
1169
+ const t = src.trim();
1170
+ if (!/^[A-Za-z_$][\w$]*$/.test(t))
1171
+ return false;
1172
+ return !this.providerDataNames.has(t) && !this.moduleStringConsts.has(t);
1109
1173
  }
1110
- renderParsedExprToPerl(expr) {
1111
- return emitParsedExpr(expr, new MojoTopLevelEmitter(this));
1174
+ emitAsync(node, _ctx, _emit) {
1175
+ return this.renderAsync(node);
1112
1176
  }
1113
- _isStringValueName(name) {
1114
- return this.stringValueNames.has(name);
1177
+ renderElement(element) {
1178
+ const tag = element.tag;
1179
+ const attrs = this.renderAttributes(element);
1180
+ const children = this.renderChildren(element.children);
1181
+ let hydrationAttrs = "";
1182
+ if (element.needsScope) {
1183
+ hydrationAttrs += ` ${this.renderScopeMarker("")}`;
1184
+ }
1185
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
1186
+ hydrationAttrs += ` <%== bf->data_key_attr %>`;
1187
+ }
1188
+ if (element.slotId) {
1189
+ hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
1190
+ }
1191
+ if (element.regionId) {
1192
+ hydrationAttrs += ` ${BF_REGION}="${element.regionId}"`;
1193
+ }
1194
+ const voidElements = [
1195
+ "area",
1196
+ "base",
1197
+ "br",
1198
+ "col",
1199
+ "embed",
1200
+ "hr",
1201
+ "img",
1202
+ "input",
1203
+ "link",
1204
+ "meta",
1205
+ "param",
1206
+ "source",
1207
+ "track",
1208
+ "wbr"
1209
+ ];
1210
+ if (voidElements.includes(tag.toLowerCase())) {
1211
+ return `<${tag}${attrs}${hydrationAttrs}>`;
1212
+ }
1213
+ return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
1115
1214
  }
1116
- _recordExprBF101(message, reason) {
1117
- this.errors.push({
1118
- code: "BF101",
1119
- severity: "error",
1120
- message,
1121
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1122
- suggestion: {
1123
- message: reason ? `${reason}
1124
-
1125
- Options:
1126
- 1. Use /* @client */ for client-side evaluation
1127
- 2. Pre-compute the value in Perl` : `Options:
1128
- 1. Use /* @client */ for client-side evaluation
1129
- 2. Pre-compute the value in Perl`
1215
+ renderExpression(expr) {
1216
+ if (expr.clientOnly) {
1217
+ if (expr.slotId) {
1218
+ return `<%== bf->comment("client:${expr.slotId}") %>`;
1130
1219
  }
1131
- });
1132
- }
1133
- _renderPerlFilterExprPublic(expr, param) {
1134
- return this.renderPerlFilterExpr(expr, param);
1135
- }
1136
- }
1137
- function renderArrayMethod(method, object, args, emit) {
1138
- switch (method) {
1139
- case "join": {
1140
- const obj = emit(object);
1141
- const sep = args.length >= 1 ? emit(args[0]) : `','`;
1142
- return `join(${sep}, @{${obj}})`;
1143
- }
1144
- case "includes": {
1145
- const obj = emit(object);
1146
- const needle = emit(args[0]);
1147
- return `bf->includes(${obj}, ${needle})`;
1220
+ return "";
1148
1221
  }
1149
- case "indexOf":
1150
- case "lastIndexOf": {
1151
- const fn = method === "indexOf" ? "index_of" : "last_index_of";
1152
- const obj = emit(object);
1153
- const needle = emit(args[0]);
1154
- return `bf->${fn}(${obj}, ${needle})`;
1222
+ const perlExpr = this.convertExpressionToPerl(expr.expr);
1223
+ if (expr.slotId) {
1224
+ return `<%== bf->text_start("${expr.slotId}") %><%= ${perlExpr} %><%== bf->text_end %>`;
1155
1225
  }
1156
- case "at": {
1157
- const obj = emit(object);
1158
- const idx = args.length >= 1 ? emit(args[0]) : "0";
1159
- return `bf->at(${obj}, ${idx})`;
1226
+ return `<%= ${perlExpr} %>`;
1227
+ }
1228
+ renderConditional(cond) {
1229
+ if (cond.clientOnly && cond.slotId) {
1230
+ return `<%== bf->comment("cond-start:${cond.slotId}") %><%== bf->comment("cond-end:${cond.slotId}") %>`;
1160
1231
  }
1161
- case "concat": {
1162
- if (args.length === 0) {
1163
- return emit(object);
1164
- }
1165
- const a = emit(object);
1166
- const b = emit(args[0]);
1167
- return `bf->concat(${a}, ${b})`;
1232
+ const condition = this.convertExpressionToPerl(cond.condition);
1233
+ const whenTrue = this.renderNode(cond.whenTrue);
1234
+ const whenFalse = this.renderNodeOrNull(cond.whenFalse);
1235
+ const isFragmentBranch = cond.whenTrue.type === "fragment" || cond.whenFalse.type === "fragment";
1236
+ const useCommentMarkers = cond.slotId && isFragmentBranch;
1237
+ let markedTrue = whenTrue;
1238
+ let markedFalse = whenFalse;
1239
+ if (cond.slotId && !useCommentMarkers) {
1240
+ markedTrue = this.addCondMarkerToFirstElement(whenTrue, cond.slotId);
1241
+ markedFalse = whenFalse ? this.addCondMarkerToFirstElement(whenFalse, cond.slotId) : whenFalse;
1168
1242
  }
1169
- case "slice": {
1170
- const recv = emit(object);
1171
- const start = args.length >= 1 ? emit(args[0]) : "0";
1172
- const end = args.length >= 2 ? emit(args[1]) : "undef";
1173
- return `bf->slice(${recv}, ${start}, ${end})`;
1243
+ let result;
1244
+ if (useCommentMarkers) {
1245
+ const inner = whenFalse ? `
1246
+ % if (${condition}) {
1247
+ ${whenTrue}
1248
+ % } else {
1249
+ ${whenFalse}
1250
+ % }
1251
+ ` : `
1252
+ % if (${condition}) {
1253
+ ${whenTrue}
1254
+ % }
1255
+ `;
1256
+ result = `<%== bf->comment("cond-start:${cond.slotId}") %>${inner}<%== bf->comment("cond-end:${cond.slotId}") %>`;
1257
+ } else if (markedFalse) {
1258
+ result = `
1259
+ % if (${condition}) {
1260
+ ${markedTrue}
1261
+ % } else {
1262
+ ${markedFalse}
1263
+ % }
1264
+ `;
1265
+ } else if (cond.slotId) {
1266
+ result = `<%== bf->comment("cond-start:${cond.slotId}") %>
1267
+ % if (${condition}) {
1268
+ ${whenTrue}
1269
+ % }
1270
+ <%== bf->comment("cond-end:${cond.slotId}") %>`;
1271
+ } else {
1272
+ result = `
1273
+ % if (${condition}) {
1274
+ ${whenTrue}
1275
+ % }
1276
+ `;
1174
1277
  }
1175
- case "reverse":
1176
- case "toReversed": {
1177
- const recv = emit(object);
1178
- return `bf->reverse(${recv})`;
1278
+ return result;
1279
+ }
1280
+ renderNodeOrNull(node) {
1281
+ if (node.type === "expression" && (node.expr === "null" || node.expr === "undefined")) {
1282
+ return null;
1179
1283
  }
1180
- case "toLowerCase": {
1181
- const recv = emit(object);
1182
- return `lc(${recv})`;
1284
+ return this.renderNode(node);
1285
+ }
1286
+ addCondMarkerToFirstElement(content, condId) {
1287
+ const match = content.match(/^(<\w+)([\s>])/);
1288
+ if (match) {
1289
+ return content.replace(/^(<\w+)([\s>])/, `$1 ${BF_COND}="${condId}"$2`);
1183
1290
  }
1184
- case "toUpperCase": {
1185
- const recv = emit(object);
1186
- return `uc(${recv})`;
1291
+ return `<%== bf->comment("cond-start:${condId}") %>${content}<%== bf->comment("cond-end:${condId}") %>`;
1292
+ }
1293
+ renderLoop(loop) {
1294
+ if (loop.clientOnly)
1295
+ return "";
1296
+ const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
1297
+ const supportableDestructure = destructure && isLowerableObjectRestDestructure(loop);
1298
+ if (destructure && !supportableDestructure) {
1299
+ this.errors.push({
1300
+ code: "BF104",
1301
+ severity: "error",
1302
+ message: `Loop callback uses an array/object destructure pattern (\`${loop.param}\`) that the Mojo adapter cannot lower — Perl scalar bindings can't unpack a tuple in a single \`my\` declaration.`,
1303
+ loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1304
+ suggestion: {
1305
+ message: `Options:
1306
+ ` + ` 1. Rename the parameter to a single name and access tuple elements with index syntax in the body (e.g. \`entry => entry->[0]\` instead of \`([k, v]) => ...\`).
1307
+ ` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
1308
+ ` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
1309
+ }
1310
+ });
1187
1311
  }
1188
- case "trim": {
1189
- const recv = emit(object);
1190
- 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}`;
1191
1318
  }
1192
- case "toFixed": {
1193
- const recv = emit(object);
1194
- const digits = args.length >= 1 ? emit(args[0]) : "0";
1195
- 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);
1196
1324
  }
1197
- case "split": {
1198
- const recv = emit(object);
1199
- if (args.length === 0) {
1200
- 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);
1201
1340
  }
1202
- const sep = emit(args[0]);
1203
- if (args.length === 1) {
1204
- 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);
1205
1346
  }
1206
- const limit = emit(args[1]);
1207
- return `bf->split(${recv}, ${sep}, ${limit})`;
1208
- }
1209
- case "startsWith":
1210
- case "endsWith": {
1211
- const fn = method === "startsWith" ? "starts_with" : "ends_with";
1212
- const recv = emit(object);
1213
- const arg = emit(args[0]);
1214
- if (args.length >= 2) {
1215
- return `bf->${fn}(${recv}, ${arg}, ${emit(args[1])})`;
1347
+ if (sorted === null) {
1348
+ const structured = sortComparatorFromArrow2(sortArrow);
1349
+ if (structured !== null)
1350
+ sorted = renderSortMethod(rawArray, structured);
1216
1351
  }
1217
- return `bf->${fn}(${recv}, ${arg})`;
1218
- }
1219
- case "replace": {
1220
- const recv = emit(object);
1221
- const oldS = emit(args[0]);
1222
- const newS = emit(args[1]);
1223
- return `bf->replace(${recv}, ${oldS}, ${newS})`;
1224
- }
1225
- case "repeat": {
1226
- const recv = emit(object);
1227
- const count = args.length === 0 ? "0" : emit(args[0]);
1228
- return `bf->repeat(${recv}, ${count})`;
1229
- }
1230
- case "padStart":
1231
- case "padEnd": {
1232
- const fn = method === "padStart" ? "pad_start" : "pad_end";
1233
- const recv = emit(object);
1234
- if (args.length === 0) {
1235
- return `bf->${fn}(${recv}, 0)`;
1352
+ if (sorted === null) {
1353
+ this._recordExprBF101(`.sort(...) loop comparator is not lowerable to a template sort`, `Pre-sort the array in the route handler, or mark the loop @client-only.`);
1354
+ sorted = rawArray;
1236
1355
  }
1237
- const target = emit(args[0]);
1238
- if (args.length === 1) {
1239
- return `bf->${fn}(${recv}, ${target})`;
1356
+ for (const n of loopBound) {
1357
+ this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
1240
1358
  }
1241
- const pad = emit(args[1]);
1242
- return `bf->${fn}(${recv}, ${target}, ${pad})`;
1243
- }
1244
- default: {
1245
- const _exhaustive = method;
1246
- throw new Error(`renderArrayMethod: unhandled ArrayMethod '${_exhaustive}'`);
1359
+ lines.push(`% my $${sortedHoist} = ${sorted};`);
1247
1360
  }
1248
- }
1249
- }
1250
- function perlIdentifierFromMarkerId(markerId) {
1251
- return markerId.replace(/[^a-zA-Z0-9]/g, (ch) => ch === "_" ? "__" : `_x${ch.charCodeAt(0).toString(16)}`);
1252
- }
1253
- function renderSortMethod(recv, c) {
1254
- const keyHashes = c.keys.map((k) => {
1255
- const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
1256
- return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
1257
- });
1258
- return `bf->sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
1259
- }
1260
- function renderReduceMethod(recv, op, direction) {
1261
- const keyEntry = op.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${op.key.field}'`;
1262
- const init = op.type === "string" ? `'${op.init.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'` : op.init;
1263
- return `bf->reduce(${recv}, { op => '${op.op}', ${keyEntry}, type => '${op.type}', init => ${init}, direction => '${direction}' })`;
1264
- }
1265
- function renderFlatMethod(recv, depth) {
1266
- const d = depth === "infinity" ? -1 : depth;
1267
- return `bf->flat(${recv}, ${d})`;
1268
- }
1269
- function renderFlatMapMethod(recv, op) {
1270
- const proj = op.projection;
1271
- if (proj.kind === "tuple") {
1272
- const specs = proj.elements.map((l) => l.kind === "self" ? `['self', '']` : `['field', '${l.field}']`).join(", ");
1273
- return `bf->flat_map_tuple(${recv}, ${specs})`;
1274
- }
1275
- if (proj.kind === "self")
1276
- return `bf->flat_map(${recv}, 'self', '')`;
1277
- return `bf->flat_map(${recv}, 'field', '${proj.field}')`;
1278
- }
1279
- function isStringTypeInfo(type) {
1280
- return type?.kind === "primitive" && type.primitive === "string";
1281
- }
1282
- function isBareStringLiteral(initialValue) {
1283
- if (!initialValue)
1284
- return false;
1285
- const v = initialValue.trim();
1286
- return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
1287
- }
1288
- function isStringTypedOperand(expr, isStringName) {
1289
- if (expr.kind === "literal" && expr.literalType === "string")
1290
- return true;
1291
- if (expr.kind === "call" && expr.callee.kind === "identifier" && expr.args.length === 0) {
1292
- return isStringName(expr.callee.name);
1293
- }
1294
- if (expr.kind === "member" && expr.object.kind === "identifier" && expr.object.name === "props") {
1295
- return isStringName(expr.property);
1296
- }
1297
- return false;
1298
- }
1299
- function emitIndexAccessPerl(object, index, emit, isStringName) {
1300
- const i = emit(index);
1301
- return isStringTypedOperand(index, isStringName) ? `${emit(object)}->{${i}}` : `${emit(object)}->[${i}]`;
1302
- }
1303
-
1304
- class MojoFilterEmitter {
1305
- param;
1306
- localVarMap;
1307
- isStringName;
1308
- constructor(param, localVarMap, isStringName = () => false) {
1309
- this.param = param;
1310
- this.localVarMap = localVarMap;
1311
- this.isStringName = isStringName;
1312
- }
1313
- identifier(name) {
1314
- if (name === this.param)
1315
- return `$${this.param}`;
1316
- const signal = this.localVarMap.get(name);
1317
- if (signal)
1318
- return `$${signal}`;
1319
- return `$${name}`;
1320
- }
1321
- literal(value, literalType) {
1322
- if (literalType === "string")
1323
- return `'${value}'`;
1324
- if (literalType === "boolean")
1325
- return value ? "1" : "0";
1326
- if (literalType === "null")
1327
- return "undef";
1328
- return String(value);
1329
- }
1330
- member(object, property, _computed, emit) {
1331
- if (property === "length" && (object.kind === "higher-order" || object.kind === "array-literal")) {
1332
- return `scalar(@{${emit(object)}})`;
1361
+ lines.push(`% for my ${indexVar} (0..$#{${array}}) {`);
1362
+ if (loop.iterationShape !== "keys") {
1363
+ if (supportableDestructure) {
1364
+ lines.push(`% my $__bf_item = ${array}->[${indexVar}];`);
1365
+ for (const b of loop.paramBindings ?? []) {
1366
+ lines.push(b.rest ? `% my $${b.name} = $__bf_item;` : `% my $${b.name} = $__bf_item->{${b.path.slice(1)}};`);
1367
+ }
1368
+ } else {
1369
+ lines.push(`% my $${param} = ${array}->[${indexVar}];`);
1370
+ }
1333
1371
  }
1334
- return `${emit(object)}->{${property}}`;
1335
- }
1336
- indexAccess(object, index, emit) {
1337
- return emitIndexAccessPerl(object, index, emit, this.isStringName);
1338
- }
1339
- call(callee, args, emit) {
1340
- if (callee.kind === "identifier" && args.length === 0) {
1341
- 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);
1342
1387
  }
1343
- return emit(callee);
1344
- }
1345
- unary(op, argument, emit) {
1346
- const arg = emit(argument);
1347
- if (op === "!") {
1348
- const needsParens = argument.kind === "binary" || argument.kind === "logical";
1349
- return needsParens ? `!(${arg})` : `!${arg}`;
1388
+ for (const n of loopBound) {
1389
+ const c = (this.loopBoundNames.get(n) ?? 1) - 1;
1390
+ if (c <= 0)
1391
+ this.loopBoundNames.delete(n);
1392
+ else
1393
+ this.loopBoundNames.set(n, c);
1350
1394
  }
1351
- if (op === "-")
1352
- return `-${arg}`;
1353
- return arg;
1395
+ lines.push(`% }`);
1396
+ lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
1397
+ return lines.join(`
1398
+ `);
1354
1399
  }
1355
- binary(op, left, right, emit) {
1356
- const l = emit(left);
1357
- const r = emit(right);
1358
- const isStr = (e) => isStringTypedOperand(e, this.isStringName);
1359
- const stringCmp = isStr(left) || isStr(right);
1360
- if ((op === "===" || op === "==") && stringCmp) {
1361
- 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);
1362
1430
  }
1363
- if ((op === "!==" || op === "!=") && stringCmp) {
1364
- return `${l} ne ${r}`;
1431
+ if (comp.slotId && !this.inLoop) {
1432
+ propParts.push(`_bf_slot => '${comp.slotId}'`);
1365
1433
  }
1366
- const opMap = {
1367
- "===": "==",
1368
- "!==": "!=",
1369
- ">": ">",
1370
- "<": "<",
1371
- ">=": ">=",
1372
- "<=": "<=",
1373
- "+": "+",
1374
- "-": "-",
1375
- "*": "*",
1376
- "/": "/"
1377
- };
1378
- return `${l} ${opMap[op] ?? op} ${r}`;
1379
- }
1380
- logical(op, left, right, emit) {
1381
- const l = emit(left);
1382
- const r = emit(right);
1383
- if (op === "&&")
1384
- return `(${l} && ${r})`;
1385
- if (op === "||")
1386
- return `(${l} || ${r})`;
1387
- return `(${l} // ${r})`;
1388
- }
1389
- higherOrder(method, object, param, predicate, emit) {
1390
- const arrayExpr = emit(object);
1391
- const predBody = emitParsedExpr(predicate, new MojoFilterEmitter(param, this.localVarMap, this.isStringName));
1392
- const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
1393
- if (method === "filter")
1394
- return `[grep { ${grepBody} } @{${arrayExpr}}]`;
1395
- if (method === "every")
1396
- return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
1397
- if (method === "some")
1398
- return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
1399
- return arrayExpr;
1400
- }
1401
- arrayLiteral(elements, emit) {
1402
- return `[${elements.map(emit).join(", ")}]`;
1403
- }
1404
- arrayMethod(method, object, args, emit) {
1405
- return renderArrayMethod(method, object, args, emit);
1406
- }
1407
- sortMethod(_method, object, comparator, emit) {
1408
- return renderSortMethod(emit(object), comparator);
1434
+ const propsStr = propParts.length > 0 ? ", " + propParts.join(", ") : "";
1435
+ const tplName = this.toTemplateName(comp.name);
1436
+ const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
1437
+ if (effectiveChildren.length > 0) {
1438
+ const prevInLoop = this.inLoop;
1439
+ this.inLoop = false;
1440
+ const childrenBody = this.renderChildren(effectiveChildren);
1441
+ this.inLoop = prevInLoop;
1442
+ const varName = `$bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
1443
+ return `<% my ${varName} = begin %>${childrenBody}<% end %><%== bf->render_child('${tplName}'${propsStr}, children => ${varName}) %>`;
1444
+ }
1445
+ return `<%== bf->render_child('${tplName}'${propsStr}) %>`;
1409
1446
  }
1410
- reduceMethod(method, object, reduceOp, emit) {
1411
- return renderReduceMethod(emit(object), reduceOp, method === "reduceRight" ? "right" : "left");
1447
+ childrenCaptureCounter = 0;
1448
+ presenceVarCounter = 0;
1449
+ toTemplateName(componentName) {
1450
+ return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
1412
1451
  }
1413
- flatMethod(object, depth, emit) {
1414
- return renderFlatMethod(emit(object), depth);
1452
+ renderIfStatement(ifStmt) {
1453
+ const condition = this.convertExpressionToPerl(ifStmt.condition);
1454
+ const consequent = ifStmt.consequent.type === "if-statement" ? this.renderIfStatement(ifStmt.consequent) : this.renderNode(ifStmt.consequent);
1455
+ let result = `% if (${condition}) {
1456
+ ${consequent}
1457
+ `;
1458
+ if (ifStmt.alternate) {
1459
+ if (ifStmt.alternate.type === "if-statement") {
1460
+ const altResult = this.renderIfStatement(ifStmt.alternate);
1461
+ result += altResult.replace(/^% if/, "% } elsif");
1462
+ } else {
1463
+ const alternate = this.renderNode(ifStmt.alternate);
1464
+ result += `% } else {
1465
+ ${alternate}
1466
+ `;
1467
+ }
1468
+ }
1469
+ result += `% }`;
1470
+ return result;
1415
1471
  }
1416
- flatMapMethod(object, op, emit) {
1417
- return renderFlatMapMethod(emit(object), op);
1472
+ renderFragment(fragment) {
1473
+ const children = this.renderChildren(fragment.children);
1474
+ if (fragment.needsScopeComment) {
1475
+ return `<%== bf->scope_comment %>${children}`;
1476
+ }
1477
+ return children;
1418
1478
  }
1419
- conditional(_test, _consequent, _alternate) {
1420
- return "1";
1479
+ renderSlot(_slot) {
1480
+ return `<%= content %>`;
1421
1481
  }
1422
- templateLiteral(_parts) {
1423
- return "1";
1482
+ renderAsync(node) {
1483
+ const fallback = this.renderNode(node.fallback);
1484
+ const children = this.renderChildren(node.children);
1485
+ const fallbackVar = `$bf_async_fallback_${node.id}`;
1486
+ return `<% my ${fallbackVar} = begin %>${fallback}<% end %><%== bf->async_boundary('${node.id}', ${fallbackVar}) %>
1487
+ ${children}`;
1424
1488
  }
1425
- arrowFn(_param, _body) {
1426
- 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(";");
1427
1572
  }
1428
- unsupported(_raw, _reason) {
1429
- return "1";
1573
+ escapeAttrText(s) {
1574
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1430
1575
  }
1431
- }
1432
-
1433
- class MojoTopLevelEmitter {
1434
- adapter;
1435
- constructor(adapter) {
1436
- this.adapter = adapter;
1576
+ renderAttributes(element) {
1577
+ const parts = [];
1578
+ for (const attr of element.attrs) {
1579
+ if (attr.clientOnly)
1580
+ continue;
1581
+ let attrName;
1582
+ if (attr.name === "className")
1583
+ attrName = "class";
1584
+ else if (attr.name === "key")
1585
+ attrName = "data-key";
1586
+ else
1587
+ attrName = attr.name;
1588
+ const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
1589
+ if (lowered)
1590
+ parts.push(lowered);
1591
+ }
1592
+ return parts.length > 0 ? " " + parts.join(" ") : "";
1437
1593
  }
1438
- identifier(name) {
1439
- if (name === "undefined" || name === "null")
1440
- return "undef";
1441
- const inlined = this.adapter.resolveModuleStringConst(name);
1442
- if (inlined !== null)
1443
- return inlined;
1444
- const literalConst = this.adapter.resolveLiteralConst(name);
1445
- if (literalConst !== null)
1446
- return literalConst;
1447
- return `$${name}`;
1594
+ renderScopeMarker(_instanceIdExpr) {
1595
+ return `bf-s="<%= bf->scope_attr %>" <%== bf->hydration_attrs %> <%== bf->props_attr %>`;
1448
1596
  }
1449
- literal(value, literalType) {
1450
- if (literalType === "string")
1451
- return `'${value}'`;
1452
- if (literalType === "boolean")
1453
- return value ? "1" : "0";
1454
- if (literalType === "null")
1455
- return "undef";
1456
- return String(value);
1597
+ renderSlotMarker(slotId) {
1598
+ return `${BF_SLOT}="${slotId}"`;
1457
1599
  }
1458
- member(object, property, _computed, emit) {
1459
- if (object.kind === "identifier" && object.name === "props") {
1460
- return `$${property}`;
1461
- }
1462
- if (object.kind === "identifier") {
1463
- const staticValue = this.adapter.resolveStaticRecordLiteral(object.name, property);
1464
- if (staticValue !== null)
1465
- return staticValue;
1466
- }
1467
- const obj = emit(object);
1468
- if (property === "length")
1469
- return `scalar(@{${obj}})`;
1470
- return `${obj}->{${property}}`;
1600
+ renderCondMarker(condId) {
1601
+ return `${BF_COND}="${condId}"`;
1471
1602
  }
1472
- indexAccess(object, index, emit) {
1473
- return emitIndexAccessPerl(object, index, emit, (n) => this.adapter._isStringValueName(n));
1603
+ renderPerlFilterExpr(expr, param, localVarMap = new Map) {
1604
+ return emitParsedExpr2(expr, new MojoFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
1474
1605
  }
1475
- call(callee, args, emit) {
1476
- if (callee.kind === "identifier" && args.length === 0) {
1477
- return `$${callee.name}`;
1478
- }
1479
- if (this.adapter._searchParamsLocals.size > 0) {
1480
- const sp = matchSearchParamsMethodCall(callee, args, this.adapter._searchParamsLocals);
1481
- if (sp) {
1482
- return `$searchParams->${sp.method}(${sp.args.map(emit).join(", ")})`;
1483
- }
1484
- }
1485
- const path = identifierPath(callee);
1486
- const spec = path ? MOJO_TEMPLATE_PRIMITIVES[path] : undefined;
1487
- if (path && spec) {
1488
- if (args.length === spec.arity) {
1489
- return spec.emit(args.map(emit));
1606
+ convertTemplateLiteralPartsToPerl(literalParts) {
1607
+ const parts = [];
1608
+ for (const part of literalParts) {
1609
+ if (part.type === "string") {
1610
+ parts.push(this.substituteJsInterpolationsToPerl(part.value));
1611
+ } else if (part.type === "ternary") {
1612
+ const cond = this.convertExpressionToPerl(part.condition);
1613
+ parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
1614
+ } else if (part.type === "lookup") {
1615
+ const keyExpr = this.convertExpressionToPerl(part.key);
1616
+ const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
1617
+ parts.push(`({ ${entries} }->{${keyExpr}} // '')`);
1490
1618
  }
1491
- this.adapter._recordExprBF101(`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`, `Call '${path}' with exactly ${spec.arity} argument(s).`);
1492
- return "''";
1493
1619
  }
1494
- return emit(callee);
1495
- }
1496
- unary(op, argument, emit) {
1497
- const arg = emit(argument);
1498
- if (op === "!")
1499
- return `!${arg}`;
1500
- if (op === "-")
1501
- return `-${arg}`;
1502
- return arg;
1620
+ return parts.length === 1 ? parts[0] : parts.join(" . ");
1503
1621
  }
1504
- binary(op, left, right, emit) {
1505
- const l = emit(left);
1506
- const r = emit(right);
1507
- const isStr = (e) => isStringTypedOperand(e, (n) => this.adapter._isStringValueName(n));
1508
- const stringCmp = isStr(left) || isStr(right);
1509
- if ((op === "===" || op === "==") && stringCmp) {
1510
- return `${l} eq ${r}`;
1622
+ substituteJsInterpolationsToPerl(s) {
1623
+ const segments = [];
1624
+ const re = /\$\{([^}]+)\}/g;
1625
+ let lastIndex = 0;
1626
+ let m;
1627
+ while ((m = re.exec(s)) !== null) {
1628
+ if (m.index > lastIndex) {
1629
+ segments.push(`'${s.slice(lastIndex, m.index)}'`);
1630
+ }
1631
+ segments.push(this.convertExpressionToPerl(m[1].trim()));
1632
+ lastIndex = re.lastIndex;
1511
1633
  }
1512
- if ((op === "!==" || op === "!=") && stringCmp) {
1513
- return `${l} ne ${r}`;
1634
+ if (lastIndex < s.length) {
1635
+ segments.push(`'${s.slice(lastIndex)}'`);
1514
1636
  }
1515
- const opMap = {
1516
- "===": "==",
1517
- "!==": "!=",
1518
- ">": ">",
1519
- "<": "<",
1520
- ">=": ">=",
1521
- "<=": "<=",
1522
- "+": "+",
1523
- "-": "-",
1524
- "*": "*"
1525
- };
1526
- return `${l} ${opMap[op] ?? op} ${r}`;
1637
+ if (segments.length === 0)
1638
+ return `''`;
1639
+ return segments.length === 1 ? segments[0] : `(${segments.join(" . ")})`;
1527
1640
  }
1528
- logical(op, left, right, emit) {
1529
- const l = emit(left);
1530
- const r = emit(right);
1531
- if (op === "&&")
1532
- return `(${l} && ${r})`;
1533
- if (op === "||")
1534
- return `(${l} || ${r})`;
1535
- return `(${l} // ${r})`;
1641
+ refuseUnsupportedAttrExpression(expr, attrName) {
1642
+ let probe = expr.trim();
1643
+ while (probe.startsWith("("))
1644
+ probe = probe.slice(1).trimStart();
1645
+ const startsAsObjectLiteral = probe.startsWith("{");
1646
+ const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
1647
+ if (!startsAsObjectLiteral && !hasTaggedTemplate)
1648
+ return false;
1649
+ const parsed = parseExpression3(expr.trim());
1650
+ const support = isSupported2(parsed);
1651
+ if (parsed.kind !== "unsupported" && support.supported)
1652
+ return false;
1653
+ const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
1654
+ const reasonLine = reason ? `
1655
+ ${reason}` : "";
1656
+ this.errors.push({
1657
+ code: "BF101",
1658
+ severity: "error",
1659
+ message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
1660
+ loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1661
+ suggestion: {
1662
+ message: "The Mojo adapter cannot lower JS object literals or tagged-template-literal expressions into Embedded Perl. Move the expression into a `'use client'` component (so hydration computes it), or expand it into discrete attributes whose values are values the adapter can lower."
1663
+ }
1664
+ });
1665
+ return true;
1536
1666
  }
1537
- higherOrder(method, object, param, predicate, emit) {
1538
- const arrayExpr = emit(object);
1539
- const predBody = this.adapter._renderPerlFilterExprPublic(predicate, param);
1540
- const grepBody = predBody.replace(new RegExp(`\\$${param}\\b`, "g"), "$_");
1541
- if (method === "filter")
1542
- return `[grep { ${grepBody} } @{${arrayExpr}}]`;
1543
- if (method === "every")
1544
- return `!(grep { !(${grepBody}) } @{${arrayExpr}})`;
1545
- if (method === "some")
1546
- return `!!(grep { ${grepBody} } @{${arrayExpr}})`;
1547
- const findHelper = {
1548
- find: "find",
1549
- findIndex: "find_index",
1550
- findLast: "find_last",
1551
- findLastIndex: "find_last_index"
1667
+ get emitCtx() {
1668
+ return {
1669
+ _searchParamsLocals: this._searchParamsLocals,
1670
+ resolveModuleStringConst: (name) => this.resolveModuleStringConst(name),
1671
+ resolveLiteralConst: (name) => this.resolveLiteralConst(name),
1672
+ resolveStaticRecordLiteral: (o, k) => this.resolveStaticRecordLiteral(o, k),
1673
+ _isStringValueName: (name) => this._isStringValueName(name),
1674
+ _recordExprBF101: (message, reason) => this._recordExprBF101(message, reason),
1675
+ _renderPerlFilterExprPublic: (e, p) => this._renderPerlFilterExprPublic(e, p)
1552
1676
  };
1553
- if (findHelper[method]) {
1554
- return `bf->${findHelper[method]}(${arrayExpr}, sub { my $${param} = $_[0]; ${predBody} })`;
1555
- }
1556
- return arrayExpr;
1557
- }
1558
- arrayLiteral(elements, emit) {
1559
- return `[${elements.map(emit).join(", ")}]`;
1560
- }
1561
- arrayMethod(method, object, args, emit) {
1562
- return renderArrayMethod(method, object, args, emit);
1563
- }
1564
- sortMethod(_method, object, comparator, emit) {
1565
- return renderSortMethod(emit(object), comparator);
1566
- }
1567
- reduceMethod(method, object, reduceOp, emit) {
1568
- return renderReduceMethod(emit(object), reduceOp, method === "reduceRight" ? "right" : "left");
1569
- }
1570
- flatMethod(object, depth, emit) {
1571
- return renderFlatMethod(emit(object), depth);
1572
1677
  }
1573
- flatMapMethod(object, op, emit) {
1574
- return renderFlatMapMethod(emit(object), op);
1678
+ get spreadCtx() {
1679
+ return {
1680
+ componentName: this.componentName,
1681
+ errors: this.errors,
1682
+ localConstants: this.localConstants,
1683
+ propsParams: this.propsParams,
1684
+ convertExpressionToPerl: (e, preParsed) => this.convertExpressionToPerl(e, preParsed)
1685
+ };
1575
1686
  }
1576
- conditional(test, consequent, alternate, emit) {
1577
- return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
1687
+ get memoCtx() {
1688
+ return { convertExpressionToPerl: (e, preParsed) => this.convertExpressionToPerl(e, preParsed) };
1578
1689
  }
1579
- templateLiteral(parts, emit) {
1580
- const terms = [];
1581
- for (const part of parts) {
1582
- if (part.type === "string") {
1583
- if (part.value !== "") {
1584
- 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(", ")})`;
1585
1706
  }
1586
- } else {
1587
- const rendered = emit(part.expr);
1588
- const needsParens = part.expr.kind === "binary" || part.expr.kind === "logical" || part.expr.kind === "conditional";
1589
- terms.push(needsParens ? `(${rendered})` : rendered);
1590
1707
  }
1591
1708
  }
1592
- if (terms.length === 0)
1593
- return '""';
1594
- 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);
1595
1729
  }
1596
- arrowFn(_param, _body) {
1597
- return "''";
1730
+ renderParsedExprToPerl(expr) {
1731
+ return emitParsedExpr2(expr, new MojoTopLevelEmitter(this.emitCtx));
1598
1732
  }
1599
- unsupported(_raw, _reason) {
1600
- return "''";
1733
+ _isStringValueName(name) {
1734
+ return this.stringValueNames.has(name);
1735
+ }
1736
+ _recordExprBF101(message, reason) {
1737
+ this.errors.push({
1738
+ code: "BF101",
1739
+ severity: "error",
1740
+ message,
1741
+ loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
1742
+ suggestion: {
1743
+ message: reason ? `${reason}
1744
+
1745
+ Options:
1746
+ 1. Use /* @client */ for client-side evaluation
1747
+ 2. Pre-compute the value in Perl` : `Options:
1748
+ 1. Use /* @client */ for client-side evaluation
1749
+ 2. Pre-compute the value in Perl`
1750
+ }
1751
+ });
1752
+ }
1753
+ _renderPerlFilterExprPublic(expr, param) {
1754
+ return this.renderPerlFilterExpr(expr, param);
1601
1755
  }
1602
1756
  }
1603
1757
  var mojoAdapter = new MojoAdapter;