@barefootjs/mojolicious 0.16.0 → 0.17.1

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