@barefootjs/xslate 0.15.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) 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 +94 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/array-method.d.ts +63 -0
  6. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  7. package/dist/adapter/expr/emitters.d.ts +97 -0
  8. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  9. package/dist/adapter/expr/operand.d.ts +25 -0
  10. package/dist/adapter/expr/operand.d.ts.map +1 -0
  11. package/dist/adapter/index.js +1428 -1324
  12. package/dist/adapter/lib/constants.d.ts +22 -0
  13. package/dist/adapter/lib/constants.d.ts.map +1 -0
  14. package/dist/adapter/lib/ir-scope.d.ts +34 -0
  15. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  16. package/dist/adapter/lib/kolon-naming.d.ts +22 -0
  17. package/dist/adapter/lib/kolon-naming.d.ts.map +1 -0
  18. package/dist/adapter/lib/types.d.ts +27 -0
  19. package/dist/adapter/lib/types.d.ts.map +1 -0
  20. package/dist/adapter/memo/seed.d.ts +38 -0
  21. package/dist/adapter/memo/seed.d.ts.map +1 -0
  22. package/dist/adapter/props/prop-classes.d.ts +32 -0
  23. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  24. package/dist/adapter/spread/spread-codegen.d.ts +69 -0
  25. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  26. package/dist/adapter/value/parsed-literal.d.ts +32 -0
  27. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  28. package/dist/adapter/xslate-adapter.d.ts +38 -92
  29. package/dist/adapter/xslate-adapter.d.ts.map +1 -1
  30. package/dist/build.js +1428 -1324
  31. package/dist/index.js +1428 -1324
  32. package/lib/BarefootJS/Backend/Xslate.pm +1 -1
  33. package/package.json +3 -3
  34. package/src/__tests__/query-href.test.ts +96 -0
  35. package/src/__tests__/xslate-adapter.test.ts +94 -2
  36. package/src/adapter/analysis/component-tree.ts +123 -0
  37. package/src/adapter/emit-context.ts +104 -0
  38. package/src/adapter/expr/array-method.ts +311 -0
  39. package/src/adapter/expr/emitters.ts +530 -0
  40. package/src/adapter/expr/operand.ts +35 -0
  41. package/src/adapter/lib/constants.ts +34 -0
  42. package/src/adapter/lib/ir-scope.ts +64 -0
  43. package/src/adapter/lib/kolon-naming.ts +27 -0
  44. package/src/adapter/lib/types.ts +30 -0
  45. package/src/adapter/memo/seed.ts +125 -0
  46. package/src/adapter/props/prop-classes.ts +64 -0
  47. package/src/adapter/spread/spread-codegen.ts +179 -0
  48. package/src/adapter/value/parsed-literal.ts +80 -0
  49. package/src/adapter/xslate-adapter.ts +176 -1233
  50. package/src/test-render.ts +3 -0
@@ -187291,25 +187291,23 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
187291
187291
  import {
187292
187292
  BaseAdapter,
187293
187293
  isBooleanAttr,
187294
- parseExpression as parseExpression2,
187294
+ parseExpression as parseExpression3,
187295
+ stringifyParsedExpr as stringifyParsedExpr2,
187295
187296
  exprToString,
187296
187297
  parseProviderObjectLiteral,
187297
187298
  parseStyleObjectEntries,
187298
- isSupported,
187299
- identifierPath,
187299
+ isSupported as isSupported2,
187300
187300
  emitParsedExpr,
187301
187301
  emitIRNode,
187302
187302
  emitAttrValue,
187303
187303
  augmentInheritedPropAccesses,
187304
- parseRecordIndexAccess,
187305
- evalStringArrayJoin,
187306
187304
  collectModuleStringConsts,
187307
- extractArrowBodyExpression,
187308
- collectContextConsumers,
187309
187305
  isLowerableObjectRestDestructure,
187310
187306
  lookupStaticRecordLiteral,
187311
187307
  searchParamsLocalNames,
187312
- matchSearchParamsMethodCall
187308
+ prepareLoweringMatchers,
187309
+ queryHrefArgs,
187310
+ sortComparatorFromArrow as sortComparatorFromArrow2
187313
187311
  } from "@barefootjs/jsx";
187314
187312
 
187315
187313
  // src/adapter/boolean-result.ts
@@ -187366,8 +187364,9 @@ function isAriaBooleanAttr(name) {
187366
187364
  }
187367
187365
 
187368
187366
  // src/adapter/xslate-adapter.ts
187369
- var import_typescript = __toESM(require_typescript(), 1);
187370
187367
  import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
187368
+
187369
+ // src/adapter/lib/constants.ts
187371
187370
  var XSLATE_TEMPLATE_PRIMITIVES = {
187372
187371
  "JSON.stringify": { arity: 1, emit: (args) => `$bf.json(${args[0]})` },
187373
187372
  String: { arity: 1, emit: (args) => `$bf.string(${args[0]})` },
@@ -187377,12 +187376,16 @@ var XSLATE_TEMPLATE_PRIMITIVES = {
187377
187376
  "Math.round": { arity: 1, emit: (args) => `$bf.round(${args[0]})` }
187378
187377
  };
187379
187378
  var XSLATE_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(XSLATE_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
187379
+
187380
+ // src/adapter/lib/kolon-naming.ts
187380
187381
  function escapeKolonSingleQuoted(s) {
187381
187382
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
187382
187383
  }
187383
187384
  function kolonHashKey(name) {
187384
187385
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${escapeKolonSingleQuoted(name)}'`;
187385
187386
  }
187387
+
187388
+ // src/adapter/lib/ir-scope.ts
187386
187389
  function resolveJsxChildrenProp(props) {
187387
187390
  const prop = props.find((p) => p.name === "children");
187388
187391
  if (!prop)
@@ -187422,1164 +187425,737 @@ function referencedVarsAreAvailable(expr, available) {
187422
187425
  return true;
187423
187426
  }
187424
187427
 
187425
- class XslateAdapter extends BaseAdapter {
187426
- name = "xslate";
187427
- extension = ".tx";
187428
- templatesPerComponent = true;
187429
- importMapInjection = "html-snippet";
187430
- templatePrimitives = XSLATE_PRIMITIVE_EMIT_MAP;
187431
- componentName = "";
187432
- rootScopeNodes = new Set;
187433
- options;
187434
- errors = [];
187435
- inLoop = false;
187436
- propsObjectName = null;
187437
- propsParams = [];
187438
- booleanTypedProps = new Set;
187439
- stringValueNames = new Set;
187440
- moduleStringConsts = new Map;
187441
- _searchParamsLocals = new Set;
187442
- localConstants = [];
187443
- nullableOptionalProps = new Set;
187444
- constructor(options = {}) {
187445
- super();
187446
- this.options = {
187447
- clientJsBasePath: options.clientJsBasePath ?? "/static/components/",
187448
- barefootJsPath: options.barefootJsPath ?? "/static/components/barefoot.js"
187449
- };
187450
- }
187451
- generate(ir, options) {
187452
- this.componentName = ir.metadata.componentName;
187453
- this.propsObjectName = ir.metadata.propsObjectName ?? null;
187454
- augmentInheritedPropAccesses(ir);
187455
- this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
187456
- this.booleanTypedProps = new Set(ir.metadata.propsParams.filter((prop) => prop.type?.primitive === "boolean" || prop.type?.raw === "boolean").map((prop) => prop.name));
187457
- this.localConstants = ir.metadata.localConstants ?? [];
187458
- this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
187459
- this.stringValueNames = new Set;
187460
- for (const s of ir.metadata.signals) {
187461
- if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
187462
- this.stringValueNames.add(s.getter);
187428
+ // src/adapter/expr/array-method.ts
187429
+ import {
187430
+ serializeParsedExpr,
187431
+ freeVarsInBody
187432
+ } from "@barefootjs/jsx";
187433
+ function renderArrayMethod(method, object, args, emit) {
187434
+ switch (method) {
187435
+ case "join": {
187436
+ const obj = emit(object);
187437
+ const sep = args.length >= 1 ? emit(args[0]) : `','`;
187438
+ return `$bf.join(${obj}, ${sep})`;
187439
+ }
187440
+ case "includes": {
187441
+ const obj = emit(object);
187442
+ const needle = emit(args[0]);
187443
+ return `$bf.includes(${obj}, ${needle})`;
187444
+ }
187445
+ case "indexOf":
187446
+ case "lastIndexOf": {
187447
+ const fn = method === "indexOf" ? "index_of" : "last_index_of";
187448
+ const obj = emit(object);
187449
+ const needle = emit(args[0]);
187450
+ return `$bf.${fn}(${obj}, ${needle})`;
187451
+ }
187452
+ case "at": {
187453
+ const obj = emit(object);
187454
+ const idx = args.length >= 1 ? emit(args[0]) : "0";
187455
+ return `$bf.at(${obj}, ${idx})`;
187456
+ }
187457
+ case "concat": {
187458
+ if (args.length === 0) {
187459
+ return emit(object);
187463
187460
  }
187461
+ const a = emit(object);
187462
+ const b = emit(args[0]);
187463
+ return `$bf.concat(${a}, ${b})`;
187464
187464
  }
187465
- for (const p of ir.metadata.propsParams) {
187466
- if (isStringTypeInfo(p.type))
187467
- this.stringValueNames.add(p.name);
187465
+ case "slice": {
187466
+ const recv = emit(object);
187467
+ const start = args.length >= 1 ? emit(args[0]) : "0";
187468
+ const end = args.length >= 2 ? emit(args[1]) : "nil";
187469
+ return `$bf.slice(${recv}, ${start}, ${end})`;
187468
187470
  }
187469
- this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
187470
- this._searchParamsLocals = searchParamsLocalNames(ir.metadata);
187471
- this.errors = [];
187472
- this.childrenCaptureCounter = 0;
187473
- if (!options?.siblingTemplatesRegistered) {
187474
- this.checkImportedLoopChildComponents(ir);
187471
+ case "reverse":
187472
+ case "toReversed": {
187473
+ const recv = emit(object);
187474
+ return `$bf.reverse(${recv})`;
187475
187475
  }
187476
- this.rootScopeNodes = collectRootScopeNodes(ir.root);
187477
- const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
187478
- const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
187479
- const ctxSeed = this.generateContextConsumerSeed(ir);
187480
- const memoSeed = this.generateDerivedMemoSeed(ir);
187481
- const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
187482
- `;
187483
- if (this.errors.length > 0) {
187484
- ir.errors.push(...this.errors);
187476
+ case "toLowerCase": {
187477
+ const recv = emit(object);
187478
+ return `$bf.lc(${recv})`;
187485
187479
  }
187486
- const sections = {
187487
- imports: "",
187488
- types: "",
187489
- component: template,
187490
- defaultExport: ""
187491
- };
187492
- return {
187493
- template,
187494
- sections,
187495
- extension: this.extension
187496
- };
187497
- }
187498
- generateScriptRegistrations(ir, scriptBaseName) {
187499
- const hasInteractivity = this.hasClientInteractivity(ir);
187500
- if (!hasInteractivity)
187501
- return "";
187502
- const name = scriptBaseName ?? ir.metadata.componentName;
187503
- const runtimePath = this.options.barefootJsPath;
187504
- const clientJsPath = `${this.options.clientJsBasePath}${name}.client.js`;
187505
- const lines = [];
187506
- lines.push(`: my $_bf_reg0 = $bf.register_script('${runtimePath}');`);
187507
- lines.push(`: my $_bf_reg1 = $bf.register_script('${clientJsPath}');`);
187508
- lines.push("");
187509
- return lines.join(`
187510
- `);
187511
- }
187512
- hasClientInteractivity(ir) {
187513
- return ir.metadata.signals.length > 0 || ir.metadata.effects.length > 0 || ir.metadata.onMounts.length > 0 || (ir.metadata.clientAnalysis?.needsInit ?? false);
187514
- }
187515
- renderNode(node) {
187516
- return emitIRNode(node, this, {});
187517
- }
187518
- emitElement(node, _ctx, _emit) {
187519
- return this.renderElement(node);
187520
- }
187521
- emitText(node) {
187522
- return node.value;
187523
- }
187524
- emitExpression(node) {
187525
- return this.renderExpression(node);
187526
- }
187527
- emitConditional(node, _ctx, _emit) {
187528
- return this.renderConditional(node);
187529
- }
187530
- emitLoop(node, _ctx, _emit) {
187531
- return this.renderLoop(node);
187532
- }
187533
- emitComponent(node, _ctx, _emit) {
187534
- return this.renderComponent(node);
187535
- }
187536
- emitFragment(node, _ctx, _emit) {
187537
- return this.renderFragment(node);
187538
- }
187539
- emitSlot(node) {
187540
- return this.renderSlot(node);
187541
- }
187542
- emitIfStatement(node, _ctx, _emit) {
187543
- return this.renderIfStatement(node);
187544
- }
187545
- emitProvider(node, _ctx, _emit) {
187546
- const value = this.providerValueKolon(node.valueProp);
187547
- const children = this.renderChildren(node.children);
187548
- const name = node.contextName;
187549
- return `<: $bf.provide_context('${name}', ${value}) :>` + children + `<: $bf.revoke_context('${name}') :>`;
187550
- }
187551
- providerValueKolon(valueProp) {
187552
- const v = valueProp.value;
187553
- if (v.kind === "literal") {
187554
- return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
187480
+ case "toUpperCase": {
187481
+ const recv = emit(object);
187482
+ return `$bf.uc(${recv})`;
187555
187483
  }
187556
- if (v.kind === "expression") {
187557
- const hashref = this.providerObjectLiteralKolon(v.expr);
187558
- if (hashref !== null)
187559
- return hashref;
187560
- return this.convertExpressionToKolon(v.expr);
187484
+ case "trim": {
187485
+ const recv = emit(object);
187486
+ return `$bf.trim(${recv})`;
187561
187487
  }
187562
- if (v.kind === "template")
187563
- return this.convertTemplateLiteralPartsToKolon(v.parts);
187564
- return "nil";
187565
- }
187566
- providerObjectLiteralKolon(expr) {
187567
- const members = parseProviderObjectLiteral(expr.trim());
187568
- if (members === null)
187569
- return null;
187570
- const entries = members.map((m) => {
187571
- const key = `'${m.name.replace(/[\\']/g, (c) => `\\${c}`)}'`;
187572
- if (m.kind === "function" || /^on[A-Z]/.test(m.name))
187573
- return `${key} => nil`;
187574
- const src = m.kind === "getter" ? m.body : m.expr;
187575
- return `${key} => ${this.convertExpressionToKolon(src)}`;
187576
- });
187577
- return `{ ${entries.join(", ")} }`;
187578
- }
187579
- contextDefaultKolon(c) {
187580
- const d = c.defaultValue;
187581
- if (d === null || d === undefined)
187582
- return "nil";
187583
- if (typeof d === "string")
187584
- return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
187585
- if (typeof d === "boolean")
187586
- return d ? "1" : "0";
187587
- return String(d);
187588
- }
187589
- generateContextConsumerSeed(ir) {
187590
- const consumers = collectContextConsumers(ir.metadata);
187591
- if (consumers.length === 0)
187592
- return "";
187593
- return consumers.map((c) => `: my $${c.localName} = $bf.use_context('${c.contextName}', ${this.contextDefaultKolon(c)});`).join(`
187594
- `) + `
187595
- `;
187596
- }
187597
- generateDerivedMemoSeed(ir) {
187598
- const memos = ir.metadata.memos ?? [];
187599
- const signals = ir.metadata.signals ?? [];
187600
- if (memos.length === 0 && signals.length === 0)
187601
- return "";
187602
- const available = new Set(ir.metadata.propsParams.map((p) => p.name));
187603
- const lines = [];
187604
- for (const signal of signals) {
187605
- const kolon = this.tryLowerToKolon(signal.initialValue, available);
187606
- const refsSelf = kolon !== null && new RegExp(`\\$${signal.getter}\\b`).test(kolon);
187607
- if (kolon !== null && !refsSelf)
187608
- lines.push(`: my $${signal.getter} = ${kolon};`);
187609
- available.add(signal.getter);
187488
+ case "toFixed": {
187489
+ const recv = emit(object);
187490
+ const digits = args.length >= 1 ? emit(args[0]) : "0";
187491
+ return `$bf.to_fixed(${recv}, ${digits})`;
187610
187492
  }
187611
- for (const memo of memos) {
187612
- const body = extractArrowBodyExpression(memo.computation);
187613
- if (body !== null) {
187614
- const kolon = this.tryLowerToKolon(body, available);
187615
- const refsSelf = kolon !== null && new RegExp(`\\$${memo.name}\\b`).test(kolon);
187616
- if (kolon !== null && !refsSelf)
187617
- lines.push(`: my $${memo.name} = ${kolon};`);
187493
+ case "split": {
187494
+ const recv = emit(object);
187495
+ if (args.length === 0) {
187496
+ return `$bf.split(${recv})`;
187618
187497
  }
187619
- available.add(memo.name);
187620
- }
187621
- return lines.length > 0 ? lines.join(`
187622
- `) + `
187623
- ` : "";
187624
- }
187625
- tryLowerToKolon(expr, available) {
187626
- const trimmed = expr.trim();
187627
- if (!trimmed)
187628
- return null;
187629
- if (!isSupported(parseExpression2(trimmed)).supported)
187630
- return null;
187631
- const kolon = this.convertExpressionToKolon(trimmed);
187632
- if (kolon === "" || !/\$[A-Za-z_]\w*/.test(kolon))
187633
- return null;
187634
- return referencedVarsAreAvailable(kolon, available) ? kolon : null;
187635
- }
187636
- emitAsync(node, _ctx, _emit) {
187637
- return this.renderAsync(node);
187638
- }
187639
- renderElement(element) {
187640
- const tag = element.tag;
187641
- const attrs = this.renderAttributes(element);
187642
- const children = this.renderChildren(element.children);
187643
- let hydrationAttrs = "";
187644
- if (element.needsScope) {
187645
- hydrationAttrs += ` ${this.renderScopeMarker("")}`;
187498
+ const sep = emit(args[0]);
187499
+ if (args.length === 1) {
187500
+ return `$bf.split(${recv}, ${sep})`;
187501
+ }
187502
+ const limit = emit(args[1]);
187503
+ return `$bf.split(${recv}, ${sep}, ${limit})`;
187646
187504
  }
187647
- if (this.rootScopeNodes.has(element) && element.needsScope) {
187648
- hydrationAttrs += ` <: $bf.data_key_attr() | mark_raw :>`;
187505
+ case "startsWith":
187506
+ case "endsWith": {
187507
+ const fn = method === "startsWith" ? "starts_with" : "ends_with";
187508
+ const recv = emit(object);
187509
+ const arg = emit(args[0]);
187510
+ if (args.length >= 2) {
187511
+ return `$bf.${fn}(${recv}, ${arg}, ${emit(args[1])})`;
187512
+ }
187513
+ return `$bf.${fn}(${recv}, ${arg})`;
187649
187514
  }
187650
- if (element.slotId) {
187651
- hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
187515
+ case "replace": {
187516
+ const recv = emit(object);
187517
+ const oldS = emit(args[0]);
187518
+ const newS = emit(args[1]);
187519
+ return `$bf.replace(${recv}, ${oldS}, ${newS})`;
187652
187520
  }
187653
- if (element.regionId) {
187654
- hydrationAttrs += ` ${BF_REGION}="${element.regionId}"`;
187521
+ case "repeat": {
187522
+ const recv = emit(object);
187523
+ const count = args.length === 0 ? "0" : emit(args[0]);
187524
+ return `$bf.repeat(${recv}, ${count})`;
187655
187525
  }
187656
- const voidElements = [
187657
- "area",
187658
- "base",
187659
- "br",
187660
- "col",
187661
- "embed",
187662
- "hr",
187663
- "img",
187664
- "input",
187665
- "link",
187666
- "meta",
187667
- "param",
187668
- "source",
187669
- "track",
187670
- "wbr"
187671
- ];
187672
- if (voidElements.includes(tag.toLowerCase())) {
187673
- return `<${tag}${attrs}${hydrationAttrs}>`;
187674
- }
187675
- return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
187676
- }
187677
- renderExpression(expr) {
187678
- if (expr.clientOnly) {
187679
- if (expr.slotId) {
187680
- return `<: $bf.comment("client:${expr.slotId}") | mark_raw :>`;
187526
+ case "padStart":
187527
+ case "padEnd": {
187528
+ const fn = method === "padStart" ? "pad_start" : "pad_end";
187529
+ const recv = emit(object);
187530
+ if (args.length === 0) {
187531
+ return `$bf.${fn}(${recv}, 0)`;
187681
187532
  }
187682
- return "";
187533
+ const target = emit(args[0]);
187534
+ if (args.length === 1) {
187535
+ return `$bf.${fn}(${recv}, ${target})`;
187536
+ }
187537
+ const pad = emit(args[1]);
187538
+ return `$bf.${fn}(${recv}, ${target}, ${pad})`;
187683
187539
  }
187684
- const perlExpr = this.convertExpressionToKolon(expr.expr);
187685
- if (expr.slotId) {
187686
- return `<: $bf.text_start("${expr.slotId}") | mark_raw :><: ${perlExpr} :><: $bf.text_end() | mark_raw :>`;
187540
+ default: {
187541
+ const _exhaustive = method;
187542
+ throw new Error(`renderArrayMethod: unhandled ArrayMethod '${_exhaustive}'`);
187687
187543
  }
187688
- return `<: ${perlExpr} :>`;
187689
187544
  }
187690
- renderConditional(cond) {
187691
- if (cond.clientOnly && cond.slotId) {
187692
- return `<: $bf.comment("cond-start:${cond.slotId}") | mark_raw :><: $bf.comment("cond-end:${cond.slotId}") | mark_raw :>`;
187693
- }
187694
- const condition = this.convertExpressionToKolon(cond.condition);
187695
- const whenTrue = this.renderNode(cond.whenTrue);
187696
- const whenFalse = this.renderNodeOrNull(cond.whenFalse);
187697
- const isFragmentBranch = cond.whenTrue.type === "fragment" || cond.whenFalse.type === "fragment";
187698
- const useCommentMarkers = cond.slotId && isFragmentBranch;
187699
- let markedTrue = whenTrue;
187700
- let markedFalse = whenFalse;
187701
- if (cond.slotId && !useCommentMarkers) {
187702
- markedTrue = this.addCondMarkerToFirstElement(whenTrue, cond.slotId);
187703
- markedFalse = whenFalse ? this.addCondMarkerToFirstElement(whenFalse, cond.slotId) : whenFalse;
187704
- }
187705
- let result;
187706
- if (useCommentMarkers) {
187707
- const inner = whenFalse ? `
187708
- : if (${condition}) {
187709
- ${whenTrue}
187710
- : } else {
187711
- ${whenFalse}
187712
- : }
187713
- ` : `
187714
- : if (${condition}) {
187715
- ${whenTrue}
187716
- : }
187717
- `;
187718
- result = `<: $bf.comment("cond-start:${cond.slotId}") | mark_raw :>${inner}<: $bf.comment("cond-end:${cond.slotId}") | mark_raw :>`;
187719
- } else if (markedFalse) {
187720
- result = `
187721
- : if (${condition}) {
187722
- ${markedTrue}
187723
- : } else {
187724
- ${markedFalse}
187725
- : }
187726
- `;
187727
- } else if (cond.slotId) {
187728
- result = `<: $bf.comment("cond-start:${cond.slotId}") | mark_raw :>
187729
- : if (${condition}) {
187730
- ${whenTrue}
187731
- : }
187732
- <: $bf.comment("cond-end:${cond.slotId}") | mark_raw :>`;
187733
- } else {
187734
- result = `
187735
- : if (${condition}) {
187736
- ${whenTrue}
187737
- : }
187738
- `;
187739
- }
187740
- return result;
187545
+ }
187546
+ function escapePerlSingleQuote(s) {
187547
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
187548
+ }
187549
+ function emitEvalEnvArg(body, params, emit) {
187550
+ const free = freeVarsInBody(body, new Set(params));
187551
+ if (free.length === 0)
187552
+ return "{}";
187553
+ const pairs = free.map((n) => `'${escapePerlSingleQuote(n)}' => ${emit({ kind: "identifier", name: n })}`);
187554
+ return `{ ${pairs.join(", ")} }`;
187555
+ }
187556
+ function renderSortEval(recv, body, params, emit) {
187557
+ const json = serializeParsedExpr(body);
187558
+ if (json === null)
187559
+ return null;
187560
+ if (params.length < 2)
187561
+ return null;
187562
+ const [paramA, paramB] = params;
187563
+ const env = emitEvalEnvArg(body, params, emit);
187564
+ return `$bf.sort_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramA}', '${paramB}', ${env})`;
187565
+ }
187566
+ function renderReduceEval(recv, body, params, init, direction, emit) {
187567
+ const json = serializeParsedExpr(body);
187568
+ if (json === null)
187569
+ return null;
187570
+ if (init.kind !== "literal")
187571
+ return null;
187572
+ const initOut = init.literalType === "string" ? `'${escapePerlSingleQuote(String(init.value))}'` : init.literalType === "number" ? String(init.value) : null;
187573
+ if (initOut === null)
187574
+ return null;
187575
+ if (params.length < 2)
187576
+ return null;
187577
+ const [paramAcc, paramItem] = params;
187578
+ const env = emitEvalEnvArg(body, params, emit);
187579
+ return `$bf.reduce_eval(${recv}, '${escapePerlSingleQuote(json)}', '${paramAcc}', '${paramItem}', ${initOut}, '${direction}', ${env})`;
187580
+ }
187581
+ function renderPredicateEval(funcName, recv, predicate, param, emit, forward) {
187582
+ const json = serializeParsedExpr(predicate);
187583
+ if (json === null)
187584
+ return null;
187585
+ const env = emitEvalEnvArg(predicate, [param], emit);
187586
+ const fwd = forward === undefined ? "" : `, ${forward ? 1 : 0}`;
187587
+ return `$bf.${funcName}(${recv}, '${escapePerlSingleQuote(json)}', '${param}'${fwd}, ${env})`;
187588
+ }
187589
+ function renderFlatMapEval(recv, body, param, emit) {
187590
+ const json = serializeParsedExpr(body);
187591
+ if (json === null)
187592
+ return null;
187593
+ const env = emitEvalEnvArg(body, [param], emit);
187594
+ return `$bf.flat_map_eval(${recv}, '${escapePerlSingleQuote(json)}', '${param}', ${env})`;
187595
+ }
187596
+ function renderSortMethod(recv, c) {
187597
+ const keyHashes = c.keys.map((k) => {
187598
+ const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
187599
+ return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
187600
+ });
187601
+ return `$bf.sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
187602
+ }
187603
+ function renderFlatMethod(recv, depth) {
187604
+ const d = depth === "infinity" ? -1 : depth;
187605
+ return `$bf.flat(${recv}, ${d})`;
187606
+ }
187607
+
187608
+ // src/adapter/expr/emitters.ts
187609
+ import {
187610
+ identifierPath,
187611
+ matchSearchParamsMethodCall,
187612
+ sortComparatorFromArrow
187613
+ } from "@barefootjs/jsx";
187614
+ var PREDICATE_METHODS = new Set([
187615
+ "filter",
187616
+ "find",
187617
+ "findIndex",
187618
+ "findLast",
187619
+ "findLastIndex",
187620
+ "every",
187621
+ "some"
187622
+ ]);
187623
+
187624
+ class XslateFilterEmitter {
187625
+ param;
187626
+ localVarMap;
187627
+ isStringName;
187628
+ constructor(param, localVarMap, isStringName = () => false) {
187629
+ this.param = param;
187630
+ this.localVarMap = localVarMap;
187631
+ this.isStringName = isStringName;
187741
187632
  }
187742
- renderNodeOrNull(node) {
187743
- if (node.type === "expression" && (node.expr === "null" || node.expr === "undefined")) {
187744
- return null;
187633
+ identifier(name) {
187634
+ if (name === this.param)
187635
+ return `$${this.param}`;
187636
+ const signal = this.localVarMap.get(name);
187637
+ if (signal)
187638
+ return `$${signal}`;
187639
+ return `$${name}`;
187640
+ }
187641
+ literal(value, literalType) {
187642
+ if (literalType === "string")
187643
+ return `'${value}'`;
187644
+ if (literalType === "boolean")
187645
+ return value ? "1" : "0";
187646
+ if (literalType === "null")
187647
+ return "nil";
187648
+ return String(value);
187649
+ }
187650
+ member(object, property, _computed, emit) {
187651
+ if (property === "length") {
187652
+ return `$bf.length(${emit(object)})`;
187745
187653
  }
187746
- return this.renderNode(node);
187654
+ return `${emit(object)}.${property}`;
187747
187655
  }
187748
- addCondMarkerToFirstElement(content, condId) {
187749
- const match = content.match(/^(<\w+)([\s>])/);
187750
- if (match) {
187751
- return content.replace(/^(<\w+)([\s>])/, `$1 ${BF_COND}="${condId}"$2`);
187656
+ indexAccess(object, index, emit) {
187657
+ return `${emit(object)}[${emit(index)}]`;
187658
+ }
187659
+ call(callee, args, emit) {
187660
+ if (callee.kind === "identifier" && args.length === 0) {
187661
+ return `$${callee.name}`;
187752
187662
  }
187753
- return `<: $bf.comment("cond-start:${condId}") | mark_raw :>${content}<: $bf.comment("cond-end:${condId}") | mark_raw :>`;
187663
+ return emit(callee);
187754
187664
  }
187755
- checkImportedLoopChildComponents(ir) {
187756
- const relativeImports = new Set;
187757
- for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
187758
- if (!imp.source.startsWith("./") && !imp.source.startsWith("../"))
187759
- continue;
187760
- if (imp.isTypeOnly)
187761
- continue;
187762
- for (const spec of imp.specifiers) {
187763
- relativeImports.add(spec.alias ?? spec.name);
187764
- }
187665
+ unary(op, argument, emit) {
187666
+ const arg = emit(argument);
187667
+ if (op === "!") {
187668
+ const needsParens = argument.kind === "binary" || argument.kind === "logical";
187669
+ return needsParens ? `!(${arg})` : `!${arg}`;
187765
187670
  }
187766
- if (relativeImports.size === 0)
187767
- return;
187768
- const loc = { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
187769
- const visit = (node, inLoop) => {
187770
- switch (node.type) {
187771
- case "component": {
187772
- const comp = node;
187773
- if (inLoop && relativeImports.has(comp.name)) {
187774
- this.errors.push({
187775
- code: "BF103",
187776
- severity: "error",
187777
- message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Xslate adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
187778
- loc: comp.loc ?? loc,
187779
- suggestion: {
187780
- message: `Options:
187781
- ` + ` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Xslate template alongside the parent at render time.
187782
- ` + ` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.
187783
- ` + ` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`
187784
- }
187785
- });
187786
- }
187787
- for (const child of comp.children)
187788
- visit(child, inLoop);
187789
- break;
187790
- }
187791
- case "element":
187792
- for (const child of node.children)
187793
- visit(child, inLoop);
187794
- break;
187795
- case "fragment":
187796
- for (const child of node.children)
187797
- visit(child, inLoop);
187798
- break;
187799
- case "conditional": {
187800
- const cond = node;
187801
- visit(cond.whenTrue, inLoop);
187802
- if (cond.whenFalse)
187803
- visit(cond.whenFalse, inLoop);
187804
- break;
187805
- }
187806
- case "loop":
187807
- for (const child of node.children)
187808
- visit(child, true);
187809
- break;
187810
- case "if-statement": {
187811
- const stmt = node;
187812
- visit(stmt.consequent, inLoop);
187813
- if (stmt.alternate)
187814
- visit(stmt.alternate, inLoop);
187815
- break;
187816
- }
187817
- case "provider":
187818
- for (const child of node.children)
187819
- visit(child, inLoop);
187820
- break;
187821
- case "async": {
187822
- const a = node;
187823
- visit(a.fallback, inLoop);
187824
- for (const child of a.children)
187825
- visit(child, inLoop);
187826
- break;
187827
- }
187828
- }
187671
+ if (op === "-")
187672
+ return `-${arg}`;
187673
+ return arg;
187674
+ }
187675
+ binary(op, left, right, emit) {
187676
+ const l = emit(left);
187677
+ const r = emit(right);
187678
+ const opMap = {
187679
+ "===": "==",
187680
+ "!==": "!=",
187681
+ ">": ">",
187682
+ "<": "<",
187683
+ ">=": ">=",
187684
+ "<=": "<=",
187685
+ "+": "+",
187686
+ "-": "-",
187687
+ "*": "*",
187688
+ "/": "/"
187829
187689
  };
187830
- visit(ir.root, false);
187690
+ return `${l} ${opMap[op] ?? op} ${r}`;
187831
187691
  }
187832
- renderLoop(loop) {
187833
- if (loop.clientOnly)
187834
- return "";
187835
- const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
187836
- const supportableDestructure = destructure && isLowerableObjectRestDestructure(loop);
187837
- if (destructure && !supportableDestructure) {
187838
- this.errors.push({
187839
- code: "BF104",
187840
- severity: "error",
187841
- message: `Loop callback uses an array/object destructure pattern (\`${loop.param}\`) that the Xslate adapter cannot lower — Kolon \`for LIST -> $item\` binds a single scalar and can't unpack a tuple.`,
187842
- loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
187843
- suggestion: {
187844
- message: `Options:
187845
- ` + ` 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]) => ...\`).
187846
- ` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
187847
- ` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
187848
- }
187849
- });
187850
- }
187851
- const rawArray = this.convertExpressionToKolon(loop.array);
187852
- let array = rawArray;
187853
- if (loop.sortComparator) {
187854
- array = renderSortMethod(rawArray, loop.sortComparator);
187855
- }
187856
- const param = loop.param;
187857
- const renderedChildren = this.renderChildren(loop.children);
187858
- const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
187859
- const indexLocalLines = [];
187860
- if (loop.iterationShape === "keys") {
187861
- indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`);
187862
- } else if (loop.index) {
187863
- indexLocalLines.push(`: my $${loop.index} = $~${loopVar}.index;`);
187864
- }
187865
- if (supportableDestructure) {
187866
- for (const b of loop.paramBindings ?? []) {
187867
- indexLocalLines.push(b.rest ? `: my $${b.name} = $${loopVar};` : `: my $${b.name} = $${loopVar}${b.path};`);
187868
- }
187869
- }
187870
- const prevInLoop = this.inLoop;
187871
- this.inLoop = true;
187872
- const childrenUnderLoop = this.renderChildren(loop.children);
187873
- this.inLoop = prevInLoop;
187874
- const bodyChildren = loop.bodyIsItemConditional && loop.key ? `<: $bf.comment("loop-i:" ~ ${this.convertExpressionToKolon(loop.key)}) | mark_raw :>
187875
- ${childrenUnderLoop}` : childrenUnderLoop;
187876
- const lines = [];
187877
- lines.push(`<: $bf.comment("loop:${loop.markerId}") | mark_raw :>`);
187878
- lines.push(`: for ${array} -> $${loopVar} {`);
187879
- for (const il of indexLocalLines)
187880
- lines.push(il);
187881
- if (loop.filterPredicate) {
187882
- let filterCond;
187883
- if (loop.filterPredicate.blockBody) {
187884
- filterCond = this.renderBlockBodyCondition(loop.filterPredicate.blockBody, loop.filterPredicate.param);
187885
- } else if (loop.filterPredicate.predicate) {
187886
- filterCond = this.renderKolonFilterExpr(loop.filterPredicate.predicate, loop.filterPredicate.param);
187887
- } else {
187888
- filterCond = "1";
187889
- }
187890
- if (loop.filterPredicate.param !== param) {
187891
- filterCond = filterCond.replace(new RegExp(`\\$${loop.filterPredicate.param}\\b`, "g"), `$${param}`);
187892
- }
187893
- lines.push(`: if (${filterCond}) {`);
187894
- lines.push(bodyChildren);
187895
- lines.push(`: }`);
187896
- } else {
187897
- lines.push(bodyChildren);
187898
- }
187899
- lines.push(`: }`);
187900
- lines.push(`<: $bf.comment("/loop:${loop.markerId}") | mark_raw :>`);
187901
- return lines.join(`
187902
- `);
187692
+ logical(op, left, right, emit) {
187693
+ const l = emit(left);
187694
+ const r = emit(right);
187695
+ if (op === "&&")
187696
+ return `(${l} && ${r})`;
187697
+ if (op === "||")
187698
+ return `(${l} || ${r})`;
187699
+ return `(${l} // ${r})`;
187903
187700
  }
187904
- componentPropEmitter = {
187905
- emitLiteral: (value, name) => `${kolonHashKey(name)} => '${value.value}'`,
187906
- emitExpression: (value, name) => {
187907
- if (value.parts) {
187908
- return `${kolonHashKey(name)} => ${this.convertTemplateLiteralPartsToKolon(value.parts)}`;
187909
- }
187910
- return `${kolonHashKey(name)} => ${this.convertExpressionToKolon(value.expr)}`;
187911
- },
187912
- emitSpread: (value) => {
187913
- return this.convertExpressionToKolon(value.expr);
187914
- },
187915
- emitTemplate: (value, name) => `${kolonHashKey(name)} => ${this.convertTemplateLiteralPartsToKolon(value.parts)}`,
187916
- emitBooleanAttr: (_value, name) => `${kolonHashKey(name)} => 1`,
187917
- emitBooleanShorthand: (_value, name) => `${kolonHashKey(name)} => 1`,
187918
- emitJsxChildren: () => ""
187919
- };
187920
- renderComponent(comp) {
187921
- const propParts = [];
187922
- for (const p of comp.props) {
187923
- if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
187924
- continue;
187925
- if (p.value.kind === "spread") {
187926
- const trimmed = p.value.expr.trim();
187927
- if (this.propsObjectName && this.propsObjectName === trimmed) {
187928
- for (const pp of this.propsParams) {
187929
- propParts.push(`${pp.name} => $${pp.name}`);
187930
- }
187931
- continue;
187932
- }
187933
- this.errors.push({
187934
- code: "BF101",
187935
- severity: "error",
187936
- message: `Spread props (\`{...${trimmed}}\`) on a child component cannot be lowered to Kolon — Kolon hashref method args can't splat a runtime hash into named entries.`,
187937
- loc: comp.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
187938
- suggestion: {
187939
- message: "Pass the child component its props explicitly rather than spreading a runtime object."
187940
- }
187941
- });
187942
- continue;
187943
- }
187944
- const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
187945
- if (lowered)
187946
- propParts.push(lowered);
187947
- }
187948
- if (comp.slotId && !this.inLoop) {
187949
- propParts.push(`_bf_slot => '${comp.slotId}'`);
187701
+ callbackMethod(method, object, _arrow, _restArgs, emit) {
187702
+ return emit(object);
187703
+ }
187704
+ arrayLiteral(elements, emit) {
187705
+ return `[${elements.map(emit).join(", ")}]`;
187706
+ }
187707
+ arrayMethod(method, object, args, emit) {
187708
+ return renderArrayMethod(method, object, args, emit);
187709
+ }
187710
+ flatMethod(object, depth, emit) {
187711
+ return renderFlatMethod(emit(object), depth);
187712
+ }
187713
+ conditional(_test, _consequent, _alternate) {
187714
+ return "1";
187715
+ }
187716
+ templateLiteral(_parts) {
187717
+ return "1";
187718
+ }
187719
+ arrow(_params, _body) {
187720
+ return "1";
187721
+ }
187722
+ regex(_raw) {
187723
+ return "1";
187724
+ }
187725
+ unsupported(_raw, _reason) {
187726
+ return "1";
187727
+ }
187728
+ objectLiteral(_properties, _raw, _emit) {
187729
+ return "1";
187730
+ }
187731
+ }
187732
+
187733
+ class XslateTopLevelEmitter {
187734
+ ctx;
187735
+ constructor(ctx) {
187736
+ this.ctx = ctx;
187737
+ }
187738
+ identifier(name) {
187739
+ if (name === "undefined" || name === "null")
187740
+ return "nil";
187741
+ const inlined = this.ctx._resolveModuleStringConst(name);
187742
+ if (inlined !== null)
187743
+ return inlined;
187744
+ const literalConst = this.ctx._resolveLiteralConst(name);
187745
+ if (literalConst !== null)
187746
+ return literalConst;
187747
+ return `$${name}`;
187748
+ }
187749
+ literal(value, literalType) {
187750
+ if (literalType === "string")
187751
+ return `'${value}'`;
187752
+ if (literalType === "boolean")
187753
+ return value ? "1" : "0";
187754
+ if (literalType === "null")
187755
+ return "nil";
187756
+ return String(value);
187757
+ }
187758
+ member(object, property, _computed, emit) {
187759
+ if (object.kind === "identifier" && object.name === "props") {
187760
+ return `$${property}`;
187950
187761
  }
187951
- const tplName = this.toTemplateName(comp.name);
187952
- const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
187953
- if (effectiveChildren.length > 0) {
187954
- const prevInLoop = this.inLoop;
187955
- this.inLoop = false;
187956
- const childrenBody = this.renderChildren(effectiveChildren);
187957
- this.inLoop = prevInLoop;
187958
- const macroName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
187959
- const childrenEntry = `children => ${macroName}()`;
187960
- const allParts = [...propParts, childrenEntry];
187961
- return `<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', { ${allParts.join(", ")} }) | mark_raw :>`;
187762
+ if (object.kind === "identifier") {
187763
+ const staticValue = this.ctx._resolveStaticRecordLiteral(object.name, property);
187764
+ if (staticValue !== null)
187765
+ return staticValue;
187962
187766
  }
187963
- const hashEntries = propParts.length > 0 ? `, { ${propParts.join(", ")} }` : "";
187964
- return `<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`;
187767
+ const obj = emit(object);
187768
+ if (property === "length")
187769
+ return `$bf.length(${obj})`;
187770
+ return `${obj}.${property}`;
187965
187771
  }
187966
- childrenCaptureCounter = 0;
187967
- presenceVarCounter = 0;
187968
- toTemplateName(componentName) {
187969
- return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
187772
+ indexAccess(object, index, emit) {
187773
+ return `${emit(object)}[${emit(index)}]`;
187970
187774
  }
187971
- renderIfStatement(ifStmt) {
187972
- const condition = this.convertExpressionToKolon(ifStmt.condition);
187973
- const consequent = ifStmt.consequent.type === "if-statement" ? this.renderIfStatement(ifStmt.consequent) : this.renderNode(ifStmt.consequent);
187974
- let result = `: if (${condition}) {
187975
- ${consequent}
187976
- `;
187977
- if (ifStmt.alternate) {
187978
- if (ifStmt.alternate.type === "if-statement") {
187979
- const altResult = this.renderIfStatement(ifStmt.alternate);
187980
- result += altResult.replace(/^: if/, ": } elsif");
187981
- } else {
187982
- const alternate = this.renderNode(ifStmt.alternate);
187983
- result += `: } else {
187984
- ${alternate}
187985
- `;
187775
+ call(callee, args, emit) {
187776
+ if (callee.kind === "identifier" && args.length === 0) {
187777
+ return `$${callee.name}`;
187778
+ }
187779
+ if (this.ctx._searchParamsLocals.size > 0) {
187780
+ const sp = matchSearchParamsMethodCall(callee, args, this.ctx._searchParamsLocals);
187781
+ if (sp) {
187782
+ return `$searchParams.${sp.method}(${sp.args.map(emit).join(", ")})`;
187986
187783
  }
187987
187784
  }
187988
- result += `: }`;
187989
- return result;
187785
+ const path = identifierPath(callee);
187786
+ const spec = path ? XSLATE_TEMPLATE_PRIMITIVES[path] : undefined;
187787
+ if (path && spec) {
187788
+ if (args.length === spec.arity) {
187789
+ return spec.emit(args.map(emit));
187790
+ }
187791
+ this.ctx._recordExprBF101(`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`, `Call '${path}' with exactly ${spec.arity} argument(s).`);
187792
+ return "''";
187793
+ }
187794
+ return emit(callee);
187990
187795
  }
187991
- renderFragment(fragment) {
187992
- const children = this.renderChildren(fragment.children);
187993
- if (fragment.needsScopeComment) {
187994
- return `<: $bf.scope_comment() | mark_raw :>${children}`;
187796
+ unary(op, argument, emit) {
187797
+ const arg = emit(argument);
187798
+ if (op === "!")
187799
+ return `!${arg}`;
187800
+ if (op === "-")
187801
+ return `-${arg}`;
187802
+ return arg;
187803
+ }
187804
+ binary(op, left, right, emit) {
187805
+ const l = emit(left);
187806
+ const r = emit(right);
187807
+ const opMap = {
187808
+ "===": "==",
187809
+ "!==": "!=",
187810
+ ">": ">",
187811
+ "<": "<",
187812
+ ">=": ">=",
187813
+ "<=": "<=",
187814
+ "+": "+",
187815
+ "-": "-",
187816
+ "*": "*"
187817
+ };
187818
+ return `${l} ${opMap[op] ?? op} ${r}`;
187819
+ }
187820
+ logical(op, left, right, emit) {
187821
+ const l = emit(left);
187822
+ const r = emit(right);
187823
+ if (op === "&&")
187824
+ return `(${l} && ${r})`;
187825
+ if (op === "||")
187826
+ return `(${l} || ${r})`;
187827
+ return `(${l} // ${r})`;
187828
+ }
187829
+ callbackMethod(method, object, arrow, restArgs, emit) {
187830
+ const recv = emit(object);
187831
+ const body = arrow.body;
187832
+ const params = arrow.params;
187833
+ if (PREDICATE_METHODS.has(method)) {
187834
+ return this._emitPredicateCallback({ method, object, param: params[0], predicate: body }, recv, emit);
187835
+ }
187836
+ if (method === "sort" || method === "toSorted") {
187837
+ const evalForm = renderSortEval(recv, body, params, emit);
187838
+ if (evalForm !== null)
187839
+ return evalForm;
187840
+ const structured = sortComparatorFromArrow(arrow);
187841
+ if (structured !== null)
187842
+ return renderSortMethod(recv, structured);
187843
+ this.ctx._recordExprBF101(`'.${method}(...)' comparator is outside the Xslate adapter's evaluable / structured surface`, `Pre-compute the sorted array, or move this position to a '/* @client */' boundary.`);
187844
+ return "''";
187995
187845
  }
187996
- return children;
187846
+ if (method === "reduce" || method === "reduceRight") {
187847
+ const direction = method === "reduceRight" ? "right" : "left";
187848
+ const init = restArgs[0];
187849
+ const evalForm = init !== undefined ? renderReduceEval(recv, body, params, init, direction, emit) : null;
187850
+ if (evalForm !== null)
187851
+ return evalForm;
187852
+ this.ctx._recordExprBF101(`'.${method}(...)' is outside the Xslate adapter's evaluable surface (needs a literal initial value and an evaluable reducer body)`, `Pre-compute the reduced value, or move this position to a '/* @client */' boundary.`);
187853
+ return "''";
187854
+ }
187855
+ if (method === "flatMap") {
187856
+ const evalForm = renderFlatMapEval(recv, body, params[0], emit);
187857
+ if (evalForm !== null)
187858
+ return evalForm;
187859
+ this.ctx._recordExprBF101(`'.flatMap(...)' projection is outside the Xslate adapter's evaluable surface`, `Pre-compute the projected array, or move this position to a '/* @client */' boundary.`);
187860
+ return "''";
187861
+ }
187862
+ return recv;
187997
187863
  }
187998
- renderSlot(_slot) {
187999
- return `<: $children | mark_raw :>`;
187864
+ _emitPredicateCallback(call, arrayExpr, emit) {
187865
+ const { method, object, param, predicate } = call;
187866
+ const evalFn = {
187867
+ filter: ["filter_eval"],
187868
+ every: ["every_eval"],
187869
+ some: ["some_eval"],
187870
+ find: ["find_eval", true],
187871
+ findLast: ["find_eval", false],
187872
+ findIndex: ["find_index_eval", true],
187873
+ findLastIndex: ["find_index_eval", false]
187874
+ };
187875
+ const isIdentity = method === "filter" && predicate.kind === "identifier" && predicate.name === param;
187876
+ const spec = evalFn[method];
187877
+ if (spec && !isIdentity) {
187878
+ const evalForm = renderPredicateEval(spec[0], arrayExpr, predicate, param, emit, spec[1]);
187879
+ if (evalForm !== null)
187880
+ return evalForm;
187881
+ }
187882
+ const predBody = this.ctx._renderKolonFilterExprPublic(predicate, param);
187883
+ const lambda = `-> $${param} { ${predBody} }`;
187884
+ const fn = {
187885
+ filter: "filter",
187886
+ every: "every",
187887
+ some: "some",
187888
+ find: "find",
187889
+ findIndex: "find_index",
187890
+ findLast: "find_last",
187891
+ findLastIndex: "find_last_index"
187892
+ };
187893
+ if (fn[method])
187894
+ return `$bf.${fn[method]}(${arrayExpr}, ${lambda})`;
187895
+ return emit(object);
188000
187896
  }
188001
- renderAsync(node) {
188002
- const fallback = this.renderNode(node.fallback);
188003
- const children = this.renderChildren(node.children);
188004
- const macroName = `bf_async_fallback_${node.id}`;
188005
- return `<: macro ${macroName} -> () { :>${fallback}<: } :><: $bf.async_boundary('${node.id}', ${macroName}()) | mark_raw :>
188006
- ${children}`;
187897
+ arrayLiteral(elements, emit) {
187898
+ return `[${elements.map(emit).join(", ")}]`;
188007
187899
  }
188008
- elementAttrEmitter = {
188009
- emitLiteral: (value, name) => `${name}="${value.value}"`,
188010
- emitExpression: (value, name) => {
188011
- if (name === "style") {
188012
- const css = this.tryLowerStyleObject(value.expr);
188013
- if (css !== null)
188014
- return `style="${css}"`;
188015
- }
188016
- if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
188017
- return "";
188018
- }
188019
- const bareId = value.expr.trim();
188020
- const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
188021
- if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId)) {
188022
- const perl2 = this.convertExpressionToKolon(value.expr);
188023
- const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<: $bf.bool_str(${perl2}) :>"` : `${name}="<: ${perl2} :>"`;
188024
- return `
188025
- : if (defined ${perl2}) {
188026
- ${body}
188027
- : }
188028
- `;
188029
- }
188030
- if (isBooleanAttr(name)) {
188031
- return `<: ${this.convertExpressionToKolon(value.expr)} ? '${name}' : '' :>`;
188032
- }
188033
- if (value.presenceOrUndefined) {
188034
- const perl2 = this.convertExpressionToKolon(value.expr);
188035
- const tmp = `$bf_pu${this.presenceVarCounter++}`;
188036
- const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<: $bf.bool_str(${tmp}) :>"` : `${name}="<: ${tmp} :>"`;
188037
- return `
188038
- : my ${tmp} = ${perl2};
188039
- : if (${tmp}) {
188040
- ${body}
188041
- : }
188042
- `;
188043
- }
188044
- {
188045
- const m = this.parseUndefinedAlternateTernary(value.expr);
188046
- if (m) {
188047
- const cond = this.convertExpressionToKolon(m.condition);
188048
- const val = this.convertExpressionToKolon(m.consequent);
188049
- return `
188050
- : if (${cond}) {
188051
- ${name}="<: ${val} :>"
188052
- : }
188053
- `;
188054
- }
188055
- }
188056
- const perl = this.convertExpressionToKolon(value.expr);
188057
- if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr)) {
188058
- return `${name}="<: $bf.bool_str(${perl}) :>"`;
188059
- }
188060
- return `${name}="<: ${perl} :>"`;
188061
- },
188062
- emitBooleanAttr: (_value, name) => name,
188063
- emitTemplate: (value, name) => `${name}="<: ${this.convertTemplateLiteralPartsToKolon(value.parts)} :>"`,
188064
- emitSpread: (value) => {
188065
- if (this.refuseUnsupportedAttrExpression(value.expr, "...")) {
188066
- return "";
188067
- }
188068
- const trimmed = value.expr.trim();
188069
- if (this.propsObjectName && this.propsObjectName === trimmed) {
188070
- const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
188071
- return `<: $bf.spread_attrs({${entries.join(", ")}}) | mark_raw :>`;
188072
- }
188073
- const ternaryHashref = this.conditionalSpreadToKolon(trimmed);
188074
- if (ternaryHashref !== null) {
188075
- return `<: $bf.spread_attrs(${ternaryHashref}) | mark_raw :>`;
188076
- }
188077
- if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
188078
- const localConst = (this.localConstants ?? []).find((c) => c.name === trimmed && !c.isModule);
188079
- if (localConst?.value !== undefined) {
188080
- const initTrimmed = localConst.value.trim();
188081
- if (!/^[A-Za-z_$][\w$]*$/.test(initTrimmed)) {
188082
- const resolved = this.conditionalSpreadToKolon(initTrimmed);
188083
- if (resolved !== null) {
188084
- return `<: $bf.spread_attrs(${resolved}) | mark_raw :>`;
188085
- }
188086
- }
188087
- }
188088
- }
188089
- const perlExpr = this.convertExpressionToKolon(value.expr);
188090
- return `<: $bf.spread_attrs(${perlExpr}) | mark_raw :>`;
188091
- },
188092
- emitBooleanShorthand: () => "",
188093
- emitJsxChildren: () => ""
188094
- };
188095
- tryLowerStyleObject(expr) {
188096
- const entries = parseStyleObjectEntries(expr);
188097
- if (!entries)
188098
- return null;
188099
- for (const e of entries) {
188100
- if (e.kind === "expr" && !isSupported(parseExpression2(e.expr)).supported)
188101
- return null;
188102
- }
188103
- return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:<: ${this.convertExpressionToKolon(e.expr)} :>`).join(";");
187900
+ arrayMethod(method, object, args, emit) {
187901
+ return renderArrayMethod(method, object, args, emit);
188104
187902
  }
188105
- escapeAttrText(s) {
188106
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
187903
+ flatMethod(object, depth, emit) {
187904
+ return renderFlatMethod(emit(object), depth);
188107
187905
  }
188108
- renderAttributes(element) {
188109
- const parts = [];
188110
- for (const attr of element.attrs) {
188111
- let attrName;
188112
- if (attr.name === "className")
188113
- attrName = "class";
188114
- else if (attr.name === "key")
188115
- attrName = "data-key";
188116
- else
188117
- attrName = attr.name;
188118
- const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
188119
- if (lowered)
188120
- parts.push(lowered);
187906
+ conditional(test, consequent, alternate, emit) {
187907
+ return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
187908
+ }
187909
+ templateLiteral(parts, emit) {
187910
+ const terms = [];
187911
+ for (const part of parts) {
187912
+ if (part.type === "string") {
187913
+ if (part.value !== "") {
187914
+ terms.push(`'${part.value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`);
187915
+ }
187916
+ } else {
187917
+ const rendered = emit(part.expr);
187918
+ const needsParens = part.expr.kind === "binary" || part.expr.kind === "logical" || part.expr.kind === "conditional";
187919
+ terms.push(needsParens ? `(${rendered})` : rendered);
187920
+ }
188121
187921
  }
188122
- return parts.length > 0 ? " " + parts.join(" ") : "";
187922
+ if (terms.length === 0)
187923
+ return `''`;
187924
+ return terms.join(" ~ ");
188123
187925
  }
188124
- renderScopeMarker(_instanceIdExpr) {
188125
- return `bf-s="<: $bf.scope_attr() :>" <: $bf.hydration_attrs() | mark_raw :> <: $bf.props_attr() | mark_raw :>`;
187926
+ arrow(_params, _body) {
187927
+ return "''";
188126
187928
  }
188127
- renderSlotMarker(slotId) {
188128
- return `${BF_SLOT}="${slotId}"`;
187929
+ regex(_raw) {
187930
+ return "''";
188129
187931
  }
188130
- renderCondMarker(condId) {
188131
- return `${BF_COND}="${condId}"`;
187932
+ unsupported(_raw, _reason) {
187933
+ return "''";
188132
187934
  }
188133
- renderKolonFilterExpr(expr, param, localVarMap = new Map) {
188134
- return emitParsedExpr(expr, new XslateFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
187935
+ objectLiteral(_properties, _raw, _emit) {
187936
+ return "''";
188135
187937
  }
188136
- renderBlockBodyCondition(statements, param) {
188137
- const localVarMap = new Map;
188138
- const paths = this.collectReturnPaths(statements, [], localVarMap, param);
188139
- if (paths.length === 0)
188140
- return "1";
188141
- if (paths.length === 1)
188142
- return this.buildSinglePathCondition(paths[0], param, localVarMap);
188143
- const parts = [];
188144
- for (const path of paths) {
188145
- if (path.result.kind === "literal" && path.result.literalType === "boolean" && path.result.value === false)
188146
- continue;
188147
- const cond = this.buildSinglePathCondition(path, param, localVarMap);
188148
- if (cond !== "0")
188149
- parts.push(cond);
188150
- }
188151
- if (parts.length === 0)
188152
- return "0";
188153
- if (parts.length === 1)
188154
- return parts[0];
188155
- return `(${parts.join(" || ")})`;
187938
+ }
187939
+
187940
+ // src/adapter/analysis/component-tree.ts
187941
+ function hasClientInteractivity(ir) {
187942
+ return ir.metadata.signals.length > 0 || ir.metadata.effects.length > 0 || ir.metadata.onMounts.length > 0 || (ir.metadata.clientAnalysis?.needsInit ?? false);
187943
+ }
187944
+ function collectImportedLoopChildComponentErrors(ir, componentName) {
187945
+ const errors = [];
187946
+ const relativeImports = new Set;
187947
+ for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
187948
+ if (!imp.source.startsWith("./") && !imp.source.startsWith("../"))
187949
+ continue;
187950
+ if (imp.isTypeOnly)
187951
+ continue;
187952
+ for (const spec of imp.specifiers) {
187953
+ relativeImports.add(spec.alias ?? spec.name);
187954
+ }
188156
187955
  }
188157
- collectReturnPaths(statements, currentConditions, localVarMap, param) {
188158
- const paths = [];
188159
- for (const stmt of statements) {
188160
- if (stmt.kind === "var-decl") {
188161
- if (stmt.init.kind === "call" && stmt.init.callee.kind === "identifier") {
188162
- localVarMap.set(stmt.name, stmt.init.callee.name);
188163
- }
188164
- } else if (stmt.kind === "return") {
188165
- paths.push({ conditions: [...currentConditions], result: stmt.value });
188166
- break;
188167
- } else if (stmt.kind === "if") {
188168
- const thenPaths = this.collectReturnPaths(stmt.consequent, [...currentConditions, stmt.condition], localVarMap, param);
188169
- paths.push(...thenPaths);
188170
- if (stmt.alternate) {
188171
- const negated = { kind: "unary", op: "!", argument: stmt.condition };
188172
- const elsePaths = this.collectReturnPaths(stmt.alternate, [...currentConditions, negated], localVarMap, param);
188173
- paths.push(...elsePaths);
188174
- } else {
188175
- currentConditions.push({ kind: "unary", op: "!", argument: stmt.condition });
187956
+ if (relativeImports.size === 0)
187957
+ return errors;
187958
+ const loc = { file: componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
187959
+ const visit = (node, inLoop) => {
187960
+ switch (node.type) {
187961
+ case "component": {
187962
+ const comp = node;
187963
+ if (inLoop && relativeImports.has(comp.name)) {
187964
+ errors.push({
187965
+ code: "BF103",
187966
+ severity: "error",
187967
+ message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Xslate adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
187968
+ loc: comp.loc ?? loc,
187969
+ suggestion: {
187970
+ message: `Options:
187971
+ ` + ` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Xslate template alongside the parent at render time.
187972
+ ` + ` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.
187973
+ ` + ` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`
187974
+ }
187975
+ });
188176
187976
  }
187977
+ for (const child of comp.children)
187978
+ visit(child, inLoop);
187979
+ break;
188177
187980
  }
188178
- }
188179
- return paths;
188180
- }
188181
- buildSinglePathCondition(path, param, localVarMap) {
188182
- if (path.result.kind === "literal" && path.result.literalType === "boolean") {
188183
- if (path.result.value === true) {
188184
- if (path.conditions.length === 0)
188185
- return "1";
188186
- return this.renderConditionsAnd(path.conditions, param, localVarMap);
187981
+ case "element":
187982
+ for (const child of node.children)
187983
+ visit(child, inLoop);
187984
+ break;
187985
+ case "fragment":
187986
+ for (const child of node.children)
187987
+ visit(child, inLoop);
187988
+ break;
187989
+ case "conditional": {
187990
+ const cond = node;
187991
+ visit(cond.whenTrue, inLoop);
187992
+ if (cond.whenFalse)
187993
+ visit(cond.whenFalse, inLoop);
187994
+ break;
188187
187995
  }
188188
- return "0";
188189
- }
188190
- if (path.conditions.length === 0) {
188191
- return this.renderKolonFilterExpr(path.result, param, localVarMap);
188192
- }
188193
- const condPart = this.renderConditionsAnd(path.conditions, param, localVarMap);
188194
- const resultPart = this.renderKolonFilterExpr(path.result, param, localVarMap);
188195
- return `(${condPart} && ${resultPart})`;
188196
- }
188197
- renderConditionsAnd(conditions, param, localVarMap) {
188198
- if (conditions.length === 0)
188199
- return "1";
188200
- if (conditions.length === 1)
188201
- return this.renderKolonFilterExpr(conditions[0], param, localVarMap);
188202
- const parts = conditions.map((c) => this.renderKolonFilterExpr(c, param, localVarMap));
188203
- return `(${parts.join(" && ")})`;
188204
- }
188205
- convertTemplateLiteralPartsToKolon(literalParts) {
188206
- const parts = [];
188207
- for (const part of literalParts) {
188208
- if (part.type === "string") {
188209
- parts.push(this.substituteJsInterpolationsToKolon(part.value));
188210
- } else if (part.type === "ternary") {
188211
- const cond = this.convertExpressionToKolon(part.condition);
188212
- parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
188213
- } else if (part.type === "lookup") {
188214
- const keyExpr = this.convertExpressionToKolon(part.key);
188215
- const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
188216
- parts.push(`({ ${entries} }[${keyExpr}] // '')`);
187996
+ case "loop":
187997
+ for (const child of node.children)
187998
+ visit(child, true);
187999
+ break;
188000
+ case "if-statement": {
188001
+ const stmt = node;
188002
+ visit(stmt.consequent, inLoop);
188003
+ if (stmt.alternate)
188004
+ visit(stmt.alternate, inLoop);
188005
+ break;
188217
188006
  }
188218
- }
188219
- return parts.length === 1 ? parts[0] : parts.join(" ~ ");
188220
- }
188221
- substituteJsInterpolationsToKolon(s) {
188222
- const segments = [];
188223
- const re = /\$\{([^}]+)\}/g;
188224
- let lastIndex = 0;
188225
- let m;
188226
- while ((m = re.exec(s)) !== null) {
188227
- if (m.index > lastIndex) {
188228
- segments.push(`'${s.slice(lastIndex, m.index)}'`);
188007
+ case "provider":
188008
+ for (const child of node.children)
188009
+ visit(child, inLoop);
188010
+ break;
188011
+ case "async": {
188012
+ const a = node;
188013
+ visit(a.fallback, inLoop);
188014
+ for (const child of a.children)
188015
+ visit(child, inLoop);
188016
+ break;
188229
188017
  }
188230
- segments.push(this.convertExpressionToKolon(m[1].trim()));
188231
- lastIndex = re.lastIndex;
188232
188018
  }
188233
- if (lastIndex < s.length) {
188234
- segments.push(`'${s.slice(lastIndex)}'`);
188235
- }
188236
- if (segments.length === 0)
188237
- return `''`;
188238
- return segments.length === 1 ? segments[0] : `(${segments.join(" ~ ")})`;
188239
- }
188240
- refuseUnsupportedAttrExpression(expr, attrName) {
188241
- let probe = expr.trim();
188242
- while (probe.startsWith("("))
188243
- probe = probe.slice(1).trimStart();
188244
- const startsAsObjectLiteral = probe.startsWith("{");
188245
- const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
188246
- if (!startsAsObjectLiteral && !hasTaggedTemplate)
188247
- return false;
188248
- const parsed = parseExpression2(expr.trim());
188249
- const support = isSupported(parsed);
188250
- if (parsed.kind !== "unsupported" && support.supported)
188251
- return false;
188252
- const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
188253
- const reasonLine = reason ? `
188254
- ${reason}` : "";
188255
- this.errors.push({
188256
- code: "BF101",
188257
- severity: "error",
188258
- message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
188259
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188260
- suggestion: {
188261
- message: "The Xslate adapter cannot lower JS object literals or tagged-template-literal expressions into Kolon. 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."
188262
- }
188263
- });
188264
- return true;
188019
+ };
188020
+ visit(ir.root, false);
188021
+ return errors;
188022
+ }
188023
+
188024
+ // src/adapter/spread/spread-codegen.ts
188025
+ var import_typescript = __toESM(require_typescript(), 1);
188026
+ import { parseRecordIndexAccess, stringifyParsedExpr } from "@barefootjs/jsx";
188027
+ function conditionalSpreadToKolon(ctx, expr) {
188028
+ if (!expr || expr.kind !== "conditional")
188029
+ return null;
188030
+ const whenTrue = expr.consequent;
188031
+ const whenFalse = expr.alternate;
188032
+ if (whenTrue.kind !== "object-literal" || whenFalse.kind !== "object-literal") {
188033
+ return null;
188265
188034
  }
188266
- conditionalSpreadToKolon(expr) {
188267
- const sf = import_typescript.default.createSourceFile("__spread.ts", `(${expr})`, import_typescript.default.ScriptTarget.Latest, true);
188268
- if (sf.statements.length !== 1)
188269
- return null;
188270
- const stmt = sf.statements[0];
188271
- if (!import_typescript.default.isExpressionStatement(stmt))
188035
+ const condKolon = ctx.convertExpressionToKolon("", expr.test);
188036
+ const trueKolon = objectLiteralToKolonHashref(ctx, whenTrue);
188037
+ const falseKolon = objectLiteralToKolonHashref(ctx, whenFalse);
188038
+ if (trueKolon === null || falseKolon === null)
188039
+ return null;
188040
+ return `${condKolon} ? ${trueKolon} : ${falseKolon}`;
188041
+ }
188042
+ function objectLiteralExprToKolonHashref(ctx, expr) {
188043
+ if (!expr || expr.kind !== "object-literal")
188044
+ return null;
188045
+ return objectLiteralToKolonHashref(ctx, expr);
188046
+ }
188047
+ function objectLiteralToKolonHashref(ctx, obj) {
188048
+ const entries = [];
188049
+ for (const prop of obj.properties) {
188050
+ if (prop.shorthand)
188272
188051
  return null;
188273
- let node = stmt.expression;
188274
- while (import_typescript.default.isParenthesizedExpression(node))
188275
- node = node.expression;
188276
- if (!import_typescript.default.isConditionalExpression(node))
188052
+ if (prop.keyKind === "numeric")
188277
188053
  return null;
188278
- const unwrap = (e) => {
188279
- let n = e;
188280
- while (import_typescript.default.isParenthesizedExpression(n))
188281
- n = n.expression;
188282
- return n;
188283
- };
188284
- const whenTrue = unwrap(node.whenTrue);
188285
- const whenFalse = unwrap(node.whenFalse);
188286
- if (!import_typescript.default.isObjectLiteralExpression(whenTrue) || !import_typescript.default.isObjectLiteralExpression(whenFalse)) {
188054
+ const key = prop.key;
188055
+ const val = prop.value;
188056
+ const indexed = recordIndexAccessToKolon(ctx, val);
188057
+ if (indexed === null && val.kind === "index-access" && !isLiteralIndex(val.index)) {
188058
+ ctx.errors.push({
188059
+ code: "BF101",
188060
+ severity: "error",
188061
+ message: `Spread object value '${stringifyParsedExpr(val)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Kolon hashref.`,
188062
+ loc: { file: ctx.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188063
+ suggestion: {
188064
+ message: "Index a record whose values are number/string literals, or move the spread into a `'use client'` component so hydration computes it."
188065
+ }
188066
+ });
188287
188067
  return null;
188288
188068
  }
188289
- const condPerl = this.convertExpressionToKolon(node.condition.getText(sf));
188290
- const truePerl = this.objectLiteralToKolonHashref(whenTrue, sf);
188291
- const falsePerl = this.objectLiteralToKolonHashref(whenFalse, sf);
188292
- if (truePerl === null || falsePerl === null)
188293
- return null;
188294
- return `${condPerl} ? ${truePerl} : ${falsePerl}`;
188295
- }
188296
- objectLiteralToKolonHashref(obj, sf) {
188297
- const entries = [];
188298
- for (const prop of obj.properties) {
188299
- if (!import_typescript.default.isPropertyAssignment(prop))
188300
- return null;
188301
- let key;
188302
- if (import_typescript.default.isIdentifier(prop.name)) {
188303
- key = prop.name.text;
188304
- } else if (import_typescript.default.isStringLiteral(prop.name) || import_typescript.default.isNoSubstitutionTemplateLiteral(prop.name)) {
188305
- key = prop.name.text;
188306
- } else {
188307
- return null;
188308
- }
188309
- const initNode = (() => {
188310
- let n = prop.initializer;
188311
- while (import_typescript.default.isParenthesizedExpression(n))
188312
- n = n.expression;
188313
- return n;
188314
- })();
188315
- const indexed = this.recordIndexAccessToKolon(initNode);
188316
- if (indexed === null && import_typescript.default.isElementAccessExpression(initNode) && initNode.argumentExpression && !import_typescript.default.isNumericLiteral(initNode.argumentExpression) && !import_typescript.default.isStringLiteral(initNode.argumentExpression)) {
188317
- this.errors.push({
188318
- code: "BF101",
188319
- severity: "error",
188320
- message: `Spread object value '${initNode.getText(sf)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Kolon hashref.`,
188321
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188322
- suggestion: {
188323
- message: "Index a record whose values are number/string literals, or move the spread into a `'use client'` component so hydration computes it."
188324
- }
188325
- });
188326
- return null;
188327
- }
188328
- const valPerl = indexed !== null ? indexed : this.convertExpressionToKolon(prop.initializer.getText(sf));
188329
- entries.push(`'${escapeKolonSingleQuoted(key)}' => ${valPerl}`);
188330
- }
188331
- return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
188332
- }
188333
- recordIndexAccessToKolon(val) {
188334
- const parsed = parseRecordIndexAccess(val, this.localConstants ?? [], this.propsParams);
188335
- if (!parsed)
188336
- return null;
188337
- const entries = parsed.entries.map((e) => {
188338
- const mapVal = e.value.kind === "number" ? e.value.text : `'${escapeKolonSingleQuoted(e.value.text)}'`;
188339
- return `'${escapeKolonSingleQuoted(e.key)}' => ${mapVal}`;
188340
- });
188341
- return `{ ${entries.join(", ")} }[$${parsed.indexPropName}]`;
188342
- }
188343
- convertExpressionToKolon(expr) {
188344
- const trimmed = expr.trim();
188345
- if (trimmed === "")
188346
- return "''";
188347
- const parsed = parseExpression2(trimmed);
188348
- const support = isSupported(parsed);
188349
- if (!support.supported) {
188350
- this.errors.push({
188351
- code: "BF101",
188352
- severity: "error",
188353
- message: `Expression not supported: ${trimmed}`,
188354
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188355
- suggestion: {
188356
- message: support.reason ? `${support.reason}
188357
-
188358
- Options:
188359
- 1. Use /* @client */ for client-side evaluation
188360
- 2. Pre-compute the value in the backend` : `Options:
188361
- 1. Use /* @client */ for client-side evaluation
188362
- 2. Pre-compute the value in the backend`
188363
- }
188364
- });
188365
- return "''";
188366
- }
188367
- return this.renderParsedExprToKolon(parsed);
188368
- }
188369
- renderParsedExprToKolon(expr) {
188370
- return emitParsedExpr(expr, new XslateTopLevelEmitter(this));
188371
- }
188372
- _isStringValueName(name) {
188373
- return this.stringValueNames.has(name);
188374
- }
188375
- parseUndefinedAlternateTernary(expr) {
188376
- const parsed = parseExpression2(expr.trim());
188377
- if (parsed?.kind !== "conditional")
188378
- return null;
188379
- const alt = parsed.alternate;
188380
- const isUndef = alt.kind === "identifier" && (alt.name === "undefined" || alt.name === "null") || alt.kind === "literal" && (alt.value === null || alt.value === undefined);
188381
- if (!isUndef)
188382
- return null;
188383
- return {
188384
- condition: exprToString(parsed.test),
188385
- consequent: exprToString(parsed.consequent)
188386
- };
188387
- }
188388
- isBooleanTypedPropRef(expr) {
188389
- let bare = expr.trim();
188390
- if (this.propsObjectName && bare.startsWith(`${this.propsObjectName}.`)) {
188391
- bare = bare.slice(this.propsObjectName.length + 1);
188392
- }
188393
- if (!/^[A-Za-z_$][\w$]*$/.test(bare))
188394
- return false;
188395
- return this.booleanTypedProps.has(bare);
188396
- }
188397
- _resolveLiteralConst(name) {
188398
- const c = (this.localConstants ?? []).find((lc) => lc.name === name);
188399
- if (c?.value === undefined)
188400
- return null;
188401
- const v = c.value.trim();
188402
- if (/^-?\d+(\.\d+)?$/.test(v))
188403
- return v;
188404
- const strLit = /^'([^'\\]*)'$/.exec(v) ?? /^"([^"\\]*)"$/.exec(v);
188405
- if (strLit)
188406
- return `'${strLit[1].replace(/[\\']/g, (m) => `\\${m}`)}'`;
188407
- return null;
188408
- }
188409
- _resolveStaticRecordLiteral(objectName, key) {
188410
- const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
188411
- if (!hit)
188412
- return null;
188413
- return hit.kind === "number" ? hit.text : `'${hit.text.replace(/[\\']/g, (m) => `\\${m}`)}'`;
188414
- }
188415
- _resolveModuleStringConst(name) {
188416
- if (this.inLoop)
188417
- return null;
188418
- const value = this.moduleStringConsts.get(name);
188419
- if (value === undefined)
188420
- return null;
188421
- return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
188422
- }
188423
- _recordExprBF101(message, reason) {
188424
- this.errors.push({
188425
- code: "BF101",
188426
- severity: "error",
188427
- message,
188428
- loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188429
- suggestion: {
188430
- message: reason ? `${reason}
188431
-
188432
- Options:
188433
- 1. Use /* @client */ for client-side evaluation
188434
- 2. Pre-compute the value in the backend` : `Options:
188435
- 1. Use /* @client */ for client-side evaluation
188436
- 2. Pre-compute the value in the backend`
188437
- }
188438
- });
188439
- }
188440
- _renderKolonFilterExprPublic(expr, param) {
188441
- return this.renderKolonFilterExpr(expr, param);
188069
+ const valKolon = indexed !== null ? indexed : ctx.convertExpressionToKolon("", val);
188070
+ entries.push(`'${escapeKolonSingleQuoted(key)}' => ${valKolon}`);
188442
188071
  }
188072
+ return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
188443
188073
  }
188444
- function renderArrayMethod(method, object, args, emit) {
188445
- switch (method) {
188446
- case "join": {
188447
- const obj = emit(object);
188448
- const sep2 = args.length >= 1 ? emit(args[0]) : `','`;
188449
- return `$bf.join(${obj}, ${sep2})`;
188450
- }
188451
- case "includes": {
188452
- const obj = emit(object);
188453
- const needle = emit(args[0]);
188454
- return `$bf.includes(${obj}, ${needle})`;
188455
- }
188456
- case "indexOf":
188457
- case "lastIndexOf": {
188458
- const fn = method === "indexOf" ? "index_of" : "last_index_of";
188459
- const obj = emit(object);
188460
- const needle = emit(args[0]);
188461
- return `$bf.${fn}(${obj}, ${needle})`;
188462
- }
188463
- case "at": {
188464
- const obj = emit(object);
188465
- const idx = args.length >= 1 ? emit(args[0]) : "0";
188466
- return `$bf.at(${obj}, ${idx})`;
188467
- }
188468
- case "concat": {
188469
- if (args.length === 0) {
188470
- return emit(object);
188471
- }
188472
- const a = emit(object);
188473
- const b = emit(args[0]);
188474
- return `$bf.concat(${a}, ${b})`;
188475
- }
188476
- case "slice": {
188477
- const recv = emit(object);
188478
- const start = args.length >= 1 ? emit(args[0]) : "0";
188479
- const end = args.length >= 2 ? emit(args[1]) : "nil";
188480
- return `$bf.slice(${recv}, ${start}, ${end})`;
188481
- }
188482
- case "reverse":
188483
- case "toReversed": {
188484
- const recv = emit(object);
188485
- return `$bf.reverse(${recv})`;
188486
- }
188487
- case "toLowerCase": {
188488
- const recv = emit(object);
188489
- return `$bf.lc(${recv})`;
188490
- }
188491
- case "toUpperCase": {
188492
- const recv = emit(object);
188493
- return `$bf.uc(${recv})`;
188494
- }
188495
- case "trim": {
188496
- const recv = emit(object);
188497
- return `$bf.trim(${recv})`;
188498
- }
188499
- case "toFixed": {
188500
- const recv = emit(object);
188501
- const digits = args.length >= 1 ? emit(args[0]) : "0";
188502
- return `$bf.to_fixed(${recv}, ${digits})`;
188503
- }
188504
- case "split": {
188505
- const recv = emit(object);
188506
- if (args.length === 0) {
188507
- return `$bf.split(${recv})`;
188508
- }
188509
- const sep2 = emit(args[0]);
188510
- if (args.length === 1) {
188511
- return `$bf.split(${recv}, ${sep2})`;
188512
- }
188513
- const limit = emit(args[1]);
188514
- return `$bf.split(${recv}, ${sep2}, ${limit})`;
188515
- }
188516
- case "startsWith":
188517
- case "endsWith": {
188518
- const fn = method === "startsWith" ? "starts_with" : "ends_with";
188519
- const recv = emit(object);
188520
- const arg = emit(args[0]);
188521
- if (args.length >= 2) {
188522
- return `$bf.${fn}(${recv}, ${arg}, ${emit(args[1])})`;
188523
- }
188524
- return `$bf.${fn}(${recv}, ${arg})`;
188525
- }
188526
- case "replace": {
188527
- const recv = emit(object);
188528
- const oldS = emit(args[0]);
188529
- const newS = emit(args[1]);
188530
- return `$bf.replace(${recv}, ${oldS}, ${newS})`;
188531
- }
188532
- case "repeat": {
188533
- const recv = emit(object);
188534
- const count = args.length === 0 ? "0" : emit(args[0]);
188535
- return `$bf.repeat(${recv}, ${count})`;
188536
- }
188537
- case "padStart":
188538
- case "padEnd": {
188539
- const fn = method === "padStart" ? "pad_start" : "pad_end";
188540
- const recv = emit(object);
188541
- if (args.length === 0) {
188542
- return `$bf.${fn}(${recv}, 0)`;
188543
- }
188544
- const target = emit(args[0]);
188545
- if (args.length === 1) {
188546
- return `$bf.${fn}(${recv}, ${target})`;
188547
- }
188548
- const pad2 = emit(args[1]);
188549
- return `$bf.${fn}(${recv}, ${target}, ${pad2})`;
188550
- }
188551
- default: {
188552
- const _exhaustive = method;
188553
- throw new Error(`renderArrayMethod: unhandled ArrayMethod '${_exhaustive}'`);
188554
- }
188555
- }
188074
+ function isLiteralIndex(index) {
188075
+ return index.kind === "literal" && (index.literalType === "number" || index.literalType === "string");
188556
188076
  }
188557
- function renderSortMethod(recv, c) {
188558
- const keyHashes = c.keys.map((k) => {
188559
- const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
188560
- return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
188077
+ function recordIndexAccessToKolon(ctx, val) {
188078
+ if (val.kind !== "index-access" || val.object.kind !== "identifier" || val.index.kind !== "identifier") {
188079
+ return null;
188080
+ }
188081
+ const tsVal = import_typescript.default.factory.createElementAccessExpression(import_typescript.default.factory.createIdentifier(val.object.name), import_typescript.default.factory.createIdentifier(val.index.name));
188082
+ const parsed = parseRecordIndexAccess(tsVal, ctx.localConstants ?? [], ctx.propsParams);
188083
+ if (!parsed)
188084
+ return null;
188085
+ const entries = parsed.entries.map((e) => {
188086
+ const mapVal = e.value.kind === "number" ? e.value.text : `'${escapeKolonSingleQuoted(e.value.text)}'`;
188087
+ return `'${escapeKolonSingleQuoted(e.key)}' => ${mapVal}`;
188561
188088
  });
188562
- return `$bf.sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
188089
+ return `{ ${entries.join(", ")} }[$${parsed.indexPropName}]`;
188563
188090
  }
188564
- function renderReduceMethod(recv, op, direction) {
188565
- const keyEntry = op.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${op.key.field}'`;
188566
- const init = op.type === "string" ? `'${op.init.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'` : op.init;
188567
- return `$bf.reduce(${recv}, { op => '${op.op}', ${keyEntry}, type => '${op.type}', init => ${init}, direction => '${direction}' })`;
188091
+
188092
+ // src/adapter/memo/seed.ts
188093
+ import {
188094
+ collectContextConsumers,
188095
+ extractArrowBodyExpression,
188096
+ isSupported,
188097
+ parseExpression as parseExpression2
188098
+ } from "@barefootjs/jsx";
188099
+ function contextDefaultKolon(c) {
188100
+ const d = c.defaultValue;
188101
+ if (d === null || d === undefined)
188102
+ return "nil";
188103
+ if (typeof d === "string")
188104
+ return `'${d.replace(/[\\']/g, (m) => `\\${m}`)}'`;
188105
+ if (typeof d === "boolean")
188106
+ return d ? "1" : "0";
188107
+ return String(d);
188568
188108
  }
188569
- function renderFlatMethod(recv, depth) {
188570
- const d = depth === "infinity" ? -1 : depth;
188571
- return `$bf.flat(${recv}, ${d})`;
188109
+ function generateContextConsumerSeed(ir) {
188110
+ const consumers = collectContextConsumers(ir.metadata);
188111
+ if (consumers.length === 0)
188112
+ return "";
188113
+ return consumers.map((c) => `: my $${c.localName} = $bf.use_context('${c.contextName}', ${contextDefaultKolon(c)});`).join(`
188114
+ `) + `
188115
+ `;
188572
188116
  }
188573
- function renderFlatMapMethod(recv, op) {
188574
- const proj = op.projection;
188575
- if (proj.kind === "tuple") {
188576
- const specs = proj.elements.map((l) => l.kind === "self" ? `['self', '']` : `['field', '${l.field}']`).join(", ");
188577
- return `$bf.flat_map_tuple(${recv}, ${specs})`;
188117
+ function generateDerivedMemoSeed(ctx, ir) {
188118
+ const memos = ir.metadata.memos ?? [];
188119
+ const signals = ir.metadata.signals ?? [];
188120
+ if (memos.length === 0 && signals.length === 0)
188121
+ return "";
188122
+ const available = new Set(ir.metadata.propsParams.map((p) => p.name));
188123
+ const lines = [];
188124
+ for (const signal of signals) {
188125
+ const kolon = tryLowerToKolon(ctx, signal.initialValue, available);
188126
+ const refsSelf = kolon !== null && new RegExp(`\\$${signal.getter}\\b`).test(kolon);
188127
+ if (kolon !== null && !refsSelf)
188128
+ lines.push(`: my $${signal.getter} = ${kolon};`);
188129
+ available.add(signal.getter);
188578
188130
  }
188579
- if (proj.kind === "self")
188580
- return `$bf.flat_map(${recv}, 'self', '')`;
188581
- return `$bf.flat_map(${recv}, 'field', '${proj.field}')`;
188131
+ for (const memo of memos) {
188132
+ const body = extractArrowBodyExpression(memo.computation);
188133
+ if (body !== null) {
188134
+ const kolon = tryLowerToKolon(ctx, body, available);
188135
+ const refsSelf = kolon !== null && new RegExp(`\\$${memo.name}\\b`).test(kolon);
188136
+ if (kolon !== null && !refsSelf)
188137
+ lines.push(`: my $${memo.name} = ${kolon};`);
188138
+ }
188139
+ available.add(memo.name);
188140
+ }
188141
+ return lines.length > 0 ? lines.join(`
188142
+ `) + `
188143
+ ` : "";
188144
+ }
188145
+ function tryLowerToKolon(ctx, expr, available) {
188146
+ const trimmed = expr.trim();
188147
+ if (!trimmed)
188148
+ return null;
188149
+ if (!isSupported(parseExpression2(trimmed)).supported)
188150
+ return null;
188151
+ const kolon = ctx.convertExpressionToKolon(trimmed);
188152
+ if (kolon === "" || !/\$[A-Za-z_]\w*/.test(kolon))
188153
+ return null;
188154
+ return referencedVarsAreAvailable(kolon, available) ? kolon : null;
188582
188155
  }
188156
+
188157
+ // src/adapter/value/parsed-literal.ts
188158
+ import { evalStringArrayJoin } from "@barefootjs/jsx";
188583
188159
  function isStringTypeInfo(type2) {
188584
188160
  return type2?.kind === "primitive" && type2.primitive === "string";
188585
188161
  }
@@ -188589,274 +188165,802 @@ function isBareStringLiteral(initialValue) {
188589
188165
  const v = initialValue.trim();
188590
188166
  return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
188591
188167
  }
188592
- class XslateFilterEmitter {
188593
- param;
188594
- localVarMap;
188595
- isStringName;
188596
- constructor(param, localVarMap, isStringName = () => false) {
188597
- this.param = param;
188598
- this.localVarMap = localVarMap;
188599
- this.isStringName = isStringName;
188168
+
188169
+ // src/adapter/props/prop-classes.ts
188170
+ function collectBooleanTypedProps(ir) {
188171
+ return new Set(ir.metadata.propsParams.filter((prop) => prop.type?.primitive === "boolean" || prop.type?.raw === "boolean").map((prop) => prop.name));
188172
+ }
188173
+ function collectNullableOptionalProps(ir) {
188174
+ return new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
188175
+ }
188176
+ function collectStringValueNames(ir) {
188177
+ const names = new Set;
188178
+ for (const s of ir.metadata.signals) {
188179
+ if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
188180
+ names.add(s.getter);
188181
+ }
188600
188182
  }
188601
- identifier(name) {
188602
- if (name === this.param)
188603
- return `$${this.param}`;
188604
- const signal = this.localVarMap.get(name);
188605
- if (signal)
188606
- return `$${signal}`;
188607
- return `$${name}`;
188183
+ for (const p of ir.metadata.propsParams) {
188184
+ if (isStringTypeInfo(p.type))
188185
+ names.add(p.name);
188608
188186
  }
188609
- literal(value, literalType) {
188610
- if (literalType === "string")
188611
- return `'${value}'`;
188612
- if (literalType === "boolean")
188613
- return value ? "1" : "0";
188614
- if (literalType === "null")
188615
- return "nil";
188616
- return String(value);
188187
+ return names;
188188
+ }
188189
+
188190
+ // src/adapter/xslate-adapter.ts
188191
+ class XslateAdapter extends BaseAdapter {
188192
+ name = "xslate";
188193
+ extension = ".tx";
188194
+ templatesPerComponent = true;
188195
+ importMapInjection = "html-snippet";
188196
+ templatePrimitives = XSLATE_PRIMITIVE_EMIT_MAP;
188197
+ componentName = "";
188198
+ rootScopeNodes = new Set;
188199
+ options;
188200
+ errors = [];
188201
+ inLoop = false;
188202
+ propsObjectName = null;
188203
+ propsParams = [];
188204
+ booleanTypedProps = new Set;
188205
+ stringValueNames = new Set;
188206
+ moduleStringConsts = new Map;
188207
+ _searchParamsLocals = new Set;
188208
+ _loweringMatchers = [];
188209
+ localConstants = [];
188210
+ nullableOptionalProps = new Set;
188211
+ constructor(options = {}) {
188212
+ super();
188213
+ this.options = {
188214
+ clientJsBasePath: options.clientJsBasePath ?? "/static/components/",
188215
+ barefootJsPath: options.barefootJsPath ?? "/static/components/barefoot.js"
188216
+ };
188617
188217
  }
188618
- member(object, property, _computed, emit) {
188619
- if (property === "length") {
188620
- return `$bf.length(${emit(object)})`;
188218
+ generate(ir, options) {
188219
+ this.componentName = ir.metadata.componentName;
188220
+ this.propsObjectName = ir.metadata.propsObjectName ?? null;
188221
+ augmentInheritedPropAccesses(ir);
188222
+ this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188223
+ this.booleanTypedProps = collectBooleanTypedProps(ir);
188224
+ this.localConstants = ir.metadata.localConstants ?? [];
188225
+ this.nullableOptionalProps = collectNullableOptionalProps(ir);
188226
+ this.stringValueNames = collectStringValueNames(ir);
188227
+ this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
188228
+ this._searchParamsLocals = searchParamsLocalNames(ir.metadata);
188229
+ this._loweringMatchers = prepareLoweringMatchers(ir.metadata);
188230
+ this.errors = [];
188231
+ this.childrenCaptureCounter = 0;
188232
+ if (!options?.siblingTemplatesRegistered) {
188233
+ this.errors.push(...collectImportedLoopChildComponentErrors(ir, this.componentName));
188621
188234
  }
188622
- return `${emit(object)}.${property}`;
188235
+ this.rootScopeNodes = collectRootScopeNodes(ir.root);
188236
+ const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
188237
+ const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
188238
+ const ctxSeed = generateContextConsumerSeed(ir);
188239
+ const memoSeed = generateDerivedMemoSeed(this.memoCtx, ir);
188240
+ const template = `${scriptReg}${ctxSeed}${memoSeed}${templateBody}
188241
+ `;
188242
+ if (this.errors.length > 0) {
188243
+ ir.errors.push(...this.errors);
188244
+ }
188245
+ const sections = {
188246
+ imports: "",
188247
+ types: "",
188248
+ component: template,
188249
+ defaultExport: ""
188250
+ };
188251
+ return {
188252
+ template,
188253
+ sections,
188254
+ extension: this.extension
188255
+ };
188623
188256
  }
188624
- indexAccess(object, index, emit) {
188625
- return `${emit(object)}[${emit(index)}]`;
188257
+ generateScriptRegistrations(ir, scriptBaseName) {
188258
+ const hasInteractivity = hasClientInteractivity(ir);
188259
+ if (!hasInteractivity)
188260
+ return "";
188261
+ const name = scriptBaseName ?? ir.metadata.componentName;
188262
+ const runtimePath = this.options.barefootJsPath;
188263
+ const clientJsPath = `${this.options.clientJsBasePath}${name}.client.js`;
188264
+ const lines = [];
188265
+ lines.push(`: my $_bf_reg0 = $bf.register_script('${runtimePath}');`);
188266
+ lines.push(`: my $_bf_reg1 = $bf.register_script('${clientJsPath}');`);
188267
+ lines.push("");
188268
+ return lines.join(`
188269
+ `);
188626
188270
  }
188627
- call(callee, args, emit) {
188628
- if (callee.kind === "identifier" && args.length === 0) {
188629
- return `$${callee.name}`;
188630
- }
188631
- return emit(callee);
188271
+ renderNode(node) {
188272
+ return emitIRNode(node, this, {});
188632
188273
  }
188633
- unary(op, argument, emit) {
188634
- const arg = emit(argument);
188635
- if (op === "!") {
188636
- const needsParens = argument.kind === "binary" || argument.kind === "logical";
188637
- return needsParens ? `!(${arg})` : `!${arg}`;
188638
- }
188639
- if (op === "-")
188640
- return `-${arg}`;
188641
- return arg;
188274
+ emitElement(node, _ctx, _emit) {
188275
+ return this.renderElement(node);
188642
188276
  }
188643
- binary(op, left, right, emit) {
188644
- const l = emit(left);
188645
- const r = emit(right);
188646
- const opMap = {
188647
- "===": "==",
188648
- "!==": "!=",
188649
- ">": ">",
188650
- "<": "<",
188651
- ">=": ">=",
188652
- "<=": "<=",
188653
- "+": "+",
188654
- "-": "-",
188655
- "*": "*",
188656
- "/": "/"
188657
- };
188658
- return `${l} ${opMap[op] ?? op} ${r}`;
188277
+ emitText(node) {
188278
+ return node.value;
188659
188279
  }
188660
- logical(op, left, right, emit) {
188661
- const l = emit(left);
188662
- const r = emit(right);
188663
- if (op === "&&")
188664
- return `(${l} && ${r})`;
188665
- if (op === "||")
188666
- return `(${l} || ${r})`;
188667
- return `(${l} // ${r})`;
188280
+ emitExpression(node) {
188281
+ return this.renderExpression(node);
188668
188282
  }
188669
- higherOrder(method, object, param, predicate, emit) {
188670
- return emit(object);
188283
+ emitConditional(node, _ctx, _emit) {
188284
+ return this.renderConditional(node);
188671
188285
  }
188672
- arrayLiteral(elements, emit) {
188673
- return `[${elements.map(emit).join(", ")}]`;
188286
+ emitLoop(node, _ctx, _emit) {
188287
+ return this.renderLoop(node);
188674
188288
  }
188675
- arrayMethod(method, object, args, emit) {
188676
- return renderArrayMethod(method, object, args, emit);
188289
+ emitComponent(node, _ctx, _emit) {
188290
+ return this.renderComponent(node);
188677
188291
  }
188678
- sortMethod(_method, object, comparator, emit) {
188679
- return renderSortMethod(emit(object), comparator);
188292
+ emitFragment(node, _ctx, _emit) {
188293
+ return this.renderFragment(node);
188680
188294
  }
188681
- reduceMethod(method, object, reduceOp, emit) {
188682
- return renderReduceMethod(emit(object), reduceOp, method === "reduceRight" ? "right" : "left");
188295
+ emitSlot(node) {
188296
+ return this.renderSlot(node);
188683
188297
  }
188684
- flatMethod(object, depth, emit) {
188685
- return renderFlatMethod(emit(object), depth);
188298
+ emitIfStatement(node, _ctx, _emit) {
188299
+ return this.renderIfStatement(node);
188686
188300
  }
188687
- flatMapMethod(object, op, emit) {
188688
- return renderFlatMapMethod(emit(object), op);
188301
+ emitProvider(node, _ctx, _emit) {
188302
+ const value = this.providerValueKolon(node.valueProp);
188303
+ const children = this.renderChildren(node.children);
188304
+ const name = node.contextName;
188305
+ return `<: $bf.provide_context('${name}', ${value}) :>` + children + `<: $bf.revoke_context('${name}') :>`;
188689
188306
  }
188690
- conditional(_test, _consequent, _alternate) {
188691
- return "1";
188307
+ providerValueKolon(valueProp) {
188308
+ const v = valueProp.value;
188309
+ if (v.kind === "literal") {
188310
+ return typeof v.value === "string" ? `'${v.value.replace(/[\\']/g, (m) => `\\${m}`)}'` : String(v.value);
188311
+ }
188312
+ if (v.kind === "expression") {
188313
+ const hashref = this.providerObjectLiteralKolon(v.expr);
188314
+ if (hashref !== null)
188315
+ return hashref;
188316
+ return this.convertExpressionToKolon(v.expr);
188317
+ }
188318
+ if (v.kind === "template")
188319
+ return this.convertTemplateLiteralPartsToKolon(v.parts);
188320
+ return "nil";
188692
188321
  }
188693
- templateLiteral(_parts) {
188694
- return "1";
188322
+ providerObjectLiteralKolon(expr) {
188323
+ const members = parseProviderObjectLiteral(expr.trim());
188324
+ if (members === null)
188325
+ return null;
188326
+ const entries = members.map((m) => {
188327
+ const key = `'${m.name.replace(/[\\']/g, (c) => `\\${c}`)}'`;
188328
+ if (m.kind === "function" || /^on[A-Z]/.test(m.name))
188329
+ return `${key} => nil`;
188330
+ const src = m.kind === "getter" ? m.body : m.expr;
188331
+ return `${key} => ${this.convertExpressionToKolon(src)}`;
188332
+ });
188333
+ return `{ ${entries.join(", ")} }`;
188695
188334
  }
188696
- arrowFn(_param, _body) {
188697
- return "1";
188335
+ emitAsync(node, _ctx, _emit) {
188336
+ return this.renderAsync(node);
188698
188337
  }
188699
- unsupported(_raw, _reason) {
188700
- return "1";
188338
+ renderElement(element) {
188339
+ const tag = element.tag;
188340
+ const attrs = this.renderAttributes(element);
188341
+ const children = this.renderChildren(element.children);
188342
+ let hydrationAttrs = "";
188343
+ if (element.needsScope) {
188344
+ hydrationAttrs += ` ${this.renderScopeMarker("")}`;
188345
+ }
188346
+ if (this.rootScopeNodes.has(element) && element.needsScope) {
188347
+ hydrationAttrs += ` <: $bf.data_key_attr() | mark_raw :>`;
188348
+ }
188349
+ if (element.slotId) {
188350
+ hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
188351
+ }
188352
+ if (element.regionId) {
188353
+ hydrationAttrs += ` ${BF_REGION}="${element.regionId}"`;
188354
+ }
188355
+ const voidElements = [
188356
+ "area",
188357
+ "base",
188358
+ "br",
188359
+ "col",
188360
+ "embed",
188361
+ "hr",
188362
+ "img",
188363
+ "input",
188364
+ "link",
188365
+ "meta",
188366
+ "param",
188367
+ "source",
188368
+ "track",
188369
+ "wbr"
188370
+ ];
188371
+ if (voidElements.includes(tag.toLowerCase())) {
188372
+ return `<${tag}${attrs}${hydrationAttrs}>`;
188373
+ }
188374
+ return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188701
188375
  }
188702
- }
188703
-
188704
- class XslateTopLevelEmitter {
188705
- adapter;
188706
- constructor(adapter) {
188707
- this.adapter = adapter;
188376
+ renderExpression(expr) {
188377
+ if (expr.clientOnly) {
188378
+ if (expr.slotId) {
188379
+ return `<: $bf.comment("client:${expr.slotId}") | mark_raw :>`;
188380
+ }
188381
+ return "";
188382
+ }
188383
+ const perlExpr = this.convertExpressionToKolon(expr.expr);
188384
+ if (expr.slotId) {
188385
+ return `<: $bf.text_start("${expr.slotId}") | mark_raw :><: ${perlExpr} :><: $bf.text_end() | mark_raw :>`;
188386
+ }
188387
+ return `<: ${perlExpr} :>`;
188708
188388
  }
188709
- identifier(name) {
188710
- if (name === "undefined" || name === "null")
188711
- return "nil";
188712
- const inlined = this.adapter._resolveModuleStringConst(name);
188713
- if (inlined !== null)
188714
- return inlined;
188715
- const literalConst = this.adapter._resolveLiteralConst(name);
188716
- if (literalConst !== null)
188717
- return literalConst;
188718
- return `$${name}`;
188389
+ renderConditional(cond) {
188390
+ if (cond.clientOnly && cond.slotId) {
188391
+ return `<: $bf.comment("cond-start:${cond.slotId}") | mark_raw :><: $bf.comment("cond-end:${cond.slotId}") | mark_raw :>`;
188392
+ }
188393
+ const condition = this.convertExpressionToKolon(cond.condition);
188394
+ const whenTrue = this.renderNode(cond.whenTrue);
188395
+ const whenFalse = this.renderNodeOrNull(cond.whenFalse);
188396
+ const isFragmentBranch = cond.whenTrue.type === "fragment" || cond.whenFalse.type === "fragment";
188397
+ const useCommentMarkers = cond.slotId && isFragmentBranch;
188398
+ let markedTrue = whenTrue;
188399
+ let markedFalse = whenFalse;
188400
+ if (cond.slotId && !useCommentMarkers) {
188401
+ markedTrue = this.addCondMarkerToFirstElement(whenTrue, cond.slotId);
188402
+ markedFalse = whenFalse ? this.addCondMarkerToFirstElement(whenFalse, cond.slotId) : whenFalse;
188403
+ }
188404
+ let result;
188405
+ if (useCommentMarkers) {
188406
+ const inner = whenFalse ? `
188407
+ : if (${condition}) {
188408
+ ${whenTrue}
188409
+ : } else {
188410
+ ${whenFalse}
188411
+ : }
188412
+ ` : `
188413
+ : if (${condition}) {
188414
+ ${whenTrue}
188415
+ : }
188416
+ `;
188417
+ result = `<: $bf.comment("cond-start:${cond.slotId}") | mark_raw :>${inner}<: $bf.comment("cond-end:${cond.slotId}") | mark_raw :>`;
188418
+ } else if (markedFalse) {
188419
+ result = `
188420
+ : if (${condition}) {
188421
+ ${markedTrue}
188422
+ : } else {
188423
+ ${markedFalse}
188424
+ : }
188425
+ `;
188426
+ } else if (cond.slotId) {
188427
+ result = `<: $bf.comment("cond-start:${cond.slotId}") | mark_raw :>
188428
+ : if (${condition}) {
188429
+ ${whenTrue}
188430
+ : }
188431
+ <: $bf.comment("cond-end:${cond.slotId}") | mark_raw :>`;
188432
+ } else {
188433
+ result = `
188434
+ : if (${condition}) {
188435
+ ${whenTrue}
188436
+ : }
188437
+ `;
188438
+ }
188439
+ return result;
188440
+ }
188441
+ renderNodeOrNull(node) {
188442
+ if (node.type === "expression" && (node.expr === "null" || node.expr === "undefined")) {
188443
+ return null;
188444
+ }
188445
+ return this.renderNode(node);
188446
+ }
188447
+ addCondMarkerToFirstElement(content, condId) {
188448
+ const match = content.match(/^(<\w+)([\s>])/);
188449
+ if (match) {
188450
+ return content.replace(/^(<\w+)([\s>])/, `$1 ${BF_COND}="${condId}"$2`);
188451
+ }
188452
+ return `<: $bf.comment("cond-start:${condId}") | mark_raw :>${content}<: $bf.comment("cond-end:${condId}") | mark_raw :>`;
188453
+ }
188454
+ renderLoop(loop) {
188455
+ if (loop.clientOnly)
188456
+ return "";
188457
+ const destructure = !!(loop.paramBindings && loop.paramBindings.length > 0);
188458
+ const supportableDestructure = destructure && isLowerableObjectRestDestructure(loop);
188459
+ if (destructure && !supportableDestructure) {
188460
+ this.errors.push({
188461
+ code: "BF104",
188462
+ severity: "error",
188463
+ message: `Loop callback uses an array/object destructure pattern (\`${loop.param}\`) that the Xslate adapter cannot lower — Kolon \`for LIST -> $item\` binds a single scalar and can't unpack a tuple.`,
188464
+ loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188465
+ suggestion: {
188466
+ message: `Options:
188467
+ ` + ` 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]) => ...\`).
188468
+ ` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
188469
+ ` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
188470
+ }
188471
+ });
188472
+ }
188473
+ const rawArray = this.convertExpressionToKolon(loop.array);
188474
+ let array = rawArray;
188475
+ if (loop.sortComparator) {
188476
+ const sort = loop.sortComparator;
188477
+ const sortEmit = (e) => this.convertExpressionToKolon("", e);
188478
+ const arrow = sort.arrow;
188479
+ const params = arrow.kind === "arrow" ? arrow.params : [sort.paramA, sort.paramB];
188480
+ const structured = sortComparatorFromArrow2(arrow);
188481
+ array = renderSortEval(rawArray, arrow.kind === "arrow" ? arrow.body : arrow, params, sortEmit) ?? (structured !== null ? renderSortMethod(rawArray, structured) : rawArray);
188482
+ }
188483
+ const param = loop.param;
188484
+ const renderedChildren = this.renderChildren(loop.children);
188485
+ const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
188486
+ const indexLocalLines = [];
188487
+ if (loop.iterationShape === "keys") {
188488
+ indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`);
188489
+ } else if (loop.index) {
188490
+ indexLocalLines.push(`: my $${loop.index} = $~${loopVar}.index;`);
188491
+ }
188492
+ if (supportableDestructure) {
188493
+ for (const b of loop.paramBindings ?? []) {
188494
+ indexLocalLines.push(b.rest ? `: my $${b.name} = $${loopVar};` : `: my $${b.name} = $${loopVar}${b.path};`);
188495
+ }
188496
+ }
188497
+ const prevInLoop = this.inLoop;
188498
+ this.inLoop = true;
188499
+ const childrenUnderLoop = this.renderChildren(loop.children);
188500
+ this.inLoop = prevInLoop;
188501
+ const bodyChildren = loop.bodyIsItemConditional && loop.key ? `<: $bf.comment("loop-i:" ~ ${this.convertExpressionToKolon(loop.key)}) | mark_raw :>
188502
+ ${childrenUnderLoop}` : childrenUnderLoop;
188503
+ const lines = [];
188504
+ lines.push(`<: $bf.comment("loop:${loop.markerId}") | mark_raw :>`);
188505
+ lines.push(`: for ${array} -> $${loopVar} {`);
188506
+ for (const il of indexLocalLines)
188507
+ lines.push(il);
188508
+ if (loop.filterPredicate) {
188509
+ let filterCond;
188510
+ if (loop.filterPredicate.predicate) {
188511
+ filterCond = this.renderKolonFilterExpr(loop.filterPredicate.predicate, loop.filterPredicate.param);
188512
+ } else {
188513
+ filterCond = "1";
188514
+ }
188515
+ if (loop.filterPredicate.param !== param) {
188516
+ filterCond = filterCond.replace(new RegExp(`\\$${loop.filterPredicate.param}\\b`, "g"), `$${param}`);
188517
+ }
188518
+ lines.push(`: if (${filterCond}) {`);
188519
+ lines.push(bodyChildren);
188520
+ lines.push(`: }`);
188521
+ } else {
188522
+ lines.push(bodyChildren);
188523
+ }
188524
+ lines.push(`: }`);
188525
+ lines.push(`<: $bf.comment("/loop:${loop.markerId}") | mark_raw :>`);
188526
+ return lines.join(`
188527
+ `);
188528
+ }
188529
+ componentPropEmitter = {
188530
+ emitLiteral: (value, name) => `${kolonHashKey(name)} => '${value.value}'`,
188531
+ emitExpression: (value, name) => {
188532
+ if (value.parts) {
188533
+ return `${kolonHashKey(name)} => ${this.convertTemplateLiteralPartsToKolon(value.parts)}`;
188534
+ }
188535
+ if (value.parsed) {
188536
+ const hashref = objectLiteralExprToKolonHashref(this.spreadCtx, value.parsed);
188537
+ if (hashref !== null)
188538
+ return `${kolonHashKey(name)} => ${hashref}`;
188539
+ }
188540
+ return `${kolonHashKey(name)} => ${this.convertExpressionToKolon(value.expr)}`;
188541
+ },
188542
+ emitSpread: (value) => {
188543
+ return this.convertExpressionToKolon(value.expr);
188544
+ },
188545
+ emitTemplate: (value, name) => `${kolonHashKey(name)} => ${this.convertTemplateLiteralPartsToKolon(value.parts)}`,
188546
+ emitBooleanAttr: (_value, name) => `${kolonHashKey(name)} => 1`,
188547
+ emitBooleanShorthand: (_value, name) => `${kolonHashKey(name)} => 1`,
188548
+ emitJsxChildren: () => ""
188549
+ };
188550
+ renderComponent(comp) {
188551
+ const propParts = [];
188552
+ for (const p of comp.props) {
188553
+ if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
188554
+ continue;
188555
+ if (p.value.kind === "spread") {
188556
+ const trimmed = p.value.expr.trim();
188557
+ if (this.propsObjectName && this.propsObjectName === trimmed) {
188558
+ for (const pp of this.propsParams) {
188559
+ propParts.push(`${pp.name} => $${pp.name}`);
188560
+ }
188561
+ continue;
188562
+ }
188563
+ this.errors.push({
188564
+ code: "BF101",
188565
+ severity: "error",
188566
+ message: `Spread props (\`{...${trimmed}}\`) on a child component cannot be lowered to Kolon — Kolon hashref method args can't splat a runtime hash into named entries.`,
188567
+ loc: comp.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188568
+ suggestion: {
188569
+ message: "Pass the child component its props explicitly rather than spreading a runtime object."
188570
+ }
188571
+ });
188572
+ continue;
188573
+ }
188574
+ const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
188575
+ if (lowered)
188576
+ propParts.push(lowered);
188577
+ }
188578
+ if (comp.slotId && !this.inLoop) {
188579
+ propParts.push(`_bf_slot => '${comp.slotId}'`);
188580
+ }
188581
+ const tplName = this.toTemplateName(comp.name);
188582
+ const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
188583
+ if (effectiveChildren.length > 0) {
188584
+ const prevInLoop = this.inLoop;
188585
+ this.inLoop = false;
188586
+ const childrenBody = this.renderChildren(effectiveChildren);
188587
+ this.inLoop = prevInLoop;
188588
+ const macroName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
188589
+ const childrenEntry = `children => ${macroName}()`;
188590
+ const allParts = [...propParts, childrenEntry];
188591
+ return `<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', { ${allParts.join(", ")} }) | mark_raw :>`;
188592
+ }
188593
+ const hashEntries = propParts.length > 0 ? `, { ${propParts.join(", ")} }` : "";
188594
+ return `<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`;
188595
+ }
188596
+ childrenCaptureCounter = 0;
188597
+ presenceVarCounter = 0;
188598
+ toTemplateName(componentName) {
188599
+ return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
188600
+ }
188601
+ renderIfStatement(ifStmt) {
188602
+ const condition = this.convertExpressionToKolon(ifStmt.condition);
188603
+ const consequent = ifStmt.consequent.type === "if-statement" ? this.renderIfStatement(ifStmt.consequent) : this.renderNode(ifStmt.consequent);
188604
+ let result = `: if (${condition}) {
188605
+ ${consequent}
188606
+ `;
188607
+ if (ifStmt.alternate) {
188608
+ if (ifStmt.alternate.type === "if-statement") {
188609
+ const altResult = this.renderIfStatement(ifStmt.alternate);
188610
+ result += altResult.replace(/^: if/, ": } elsif");
188611
+ } else {
188612
+ const alternate = this.renderNode(ifStmt.alternate);
188613
+ result += `: } else {
188614
+ ${alternate}
188615
+ `;
188616
+ }
188617
+ }
188618
+ result += `: }`;
188619
+ return result;
188620
+ }
188621
+ renderFragment(fragment) {
188622
+ const children = this.renderChildren(fragment.children);
188623
+ if (fragment.needsScopeComment) {
188624
+ return `<: $bf.scope_comment() | mark_raw :>${children}`;
188625
+ }
188626
+ return children;
188627
+ }
188628
+ renderSlot(_slot) {
188629
+ return `<: $children | mark_raw :>`;
188630
+ }
188631
+ renderAsync(node) {
188632
+ const fallback = this.renderNode(node.fallback);
188633
+ const children = this.renderChildren(node.children);
188634
+ const macroName = `bf_async_fallback_${node.id}`;
188635
+ return `<: macro ${macroName} -> () { :>${fallback}<: } :><: $bf.async_boundary('${node.id}', ${macroName}()) | mark_raw :>
188636
+ ${children}`;
188637
+ }
188638
+ elementAttrEmitter = {
188639
+ emitLiteral: (value, name) => `${name}="${value.value}"`,
188640
+ emitExpression: (value, name) => {
188641
+ if (name === "style") {
188642
+ const css = this.tryLowerStyleObject(value.expr);
188643
+ if (css !== null)
188644
+ return `style="${css}"`;
188645
+ }
188646
+ if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
188647
+ return "";
188648
+ }
188649
+ const bareId = value.expr.trim();
188650
+ const normalizedBareId = this.propsObjectName && bareId.startsWith(`${this.propsObjectName}.`) ? bareId.slice(this.propsObjectName.length + 1) : bareId;
188651
+ if (!isBooleanAttr(name) && !value.presenceOrUndefined && /^[A-Za-z_$][\w$]*$/.test(normalizedBareId) && this.nullableOptionalProps.has(normalizedBareId)) {
188652
+ const perl2 = this.convertExpressionToKolon(value.expr);
188653
+ const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<: $bf.bool_str(${perl2}) :>"` : `${name}="<: ${perl2} :>"`;
188654
+ return `
188655
+ : if (defined ${perl2}) {
188656
+ ${body}
188657
+ : }
188658
+ `;
188659
+ }
188660
+ if (isBooleanAttr(name)) {
188661
+ return `<: ${this.convertExpressionToKolon(value.expr)} ? '${name}' : '' :>`;
188662
+ }
188663
+ if (value.presenceOrUndefined) {
188664
+ const perl2 = this.convertExpressionToKolon(value.expr);
188665
+ const tmp = `$bf_pu${this.presenceVarCounter++}`;
188666
+ const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr) ? `${name}="<: $bf.bool_str(${tmp}) :>"` : `${name}="<: ${tmp} :>"`;
188667
+ return `
188668
+ : my ${tmp} = ${perl2};
188669
+ : if (${tmp}) {
188670
+ ${body}
188671
+ : }
188672
+ `;
188673
+ }
188674
+ {
188675
+ const m = this.parseUndefinedAlternateTernary(value.expr);
188676
+ if (m) {
188677
+ const cond = this.convertExpressionToKolon(m.condition);
188678
+ const val = this.convertExpressionToKolon(m.consequent);
188679
+ return `
188680
+ : if (${cond}) {
188681
+ ${name}="<: ${val} :>"
188682
+ : }
188683
+ `;
188684
+ }
188685
+ }
188686
+ const perl = this.convertExpressionToKolon(value.expr);
188687
+ if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(value.expr)) {
188688
+ return `${name}="<: $bf.bool_str(${perl}) :>"`;
188689
+ }
188690
+ return `${name}="<: ${perl} :>"`;
188691
+ },
188692
+ emitBooleanAttr: (_value, name) => name,
188693
+ emitTemplate: (value, name) => `${name}="<: ${this.convertTemplateLiteralPartsToKolon(value.parts)} :>"`,
188694
+ emitSpread: (value) => {
188695
+ if (this.refuseUnsupportedAttrExpression(value.expr, "...")) {
188696
+ return "";
188697
+ }
188698
+ const trimmed = value.expr.trim();
188699
+ if (this.propsObjectName && this.propsObjectName === trimmed) {
188700
+ const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
188701
+ return `<: $bf.spread_attrs({${entries.join(", ")}}) | mark_raw :>`;
188702
+ }
188703
+ const ternaryHashref = conditionalSpreadToKolon(this.spreadCtx, value.parsed);
188704
+ if (ternaryHashref !== null) {
188705
+ return `<: $bf.spread_attrs(${ternaryHashref}) | mark_raw :>`;
188706
+ }
188707
+ if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
188708
+ const localConst = (this.localConstants ?? []).find((c) => c.name === trimmed && !c.isModule);
188709
+ if (localConst?.value !== undefined) {
188710
+ const initTrimmed = localConst.value.trim();
188711
+ if (!/^[A-Za-z_$][\w$]*$/.test(initTrimmed)) {
188712
+ const resolved = conditionalSpreadToKolon(this.spreadCtx, parseExpression3(initTrimmed));
188713
+ if (resolved !== null) {
188714
+ return `<: $bf.spread_attrs(${resolved}) | mark_raw :>`;
188715
+ }
188716
+ }
188717
+ }
188718
+ }
188719
+ const perlExpr = this.convertExpressionToKolon(value.expr);
188720
+ return `<: $bf.spread_attrs(${perlExpr}) | mark_raw :>`;
188721
+ },
188722
+ emitBooleanShorthand: () => "",
188723
+ emitJsxChildren: () => ""
188724
+ };
188725
+ tryLowerStyleObject(expr) {
188726
+ const entries = parseStyleObjectEntries(expr);
188727
+ if (!entries)
188728
+ return null;
188729
+ for (const e of entries) {
188730
+ if (e.kind === "expr" && !isSupported2(parseExpression3(e.expr)).supported)
188731
+ return null;
188732
+ }
188733
+ return entries.map((e) => e.kind === "literal" ? `${this.escapeAttrText(e.cssKey)}:${this.escapeAttrText(e.value)}` : `${this.escapeAttrText(e.cssKey)}:<: ${this.convertExpressionToKolon(e.expr)} :>`).join(";");
188734
+ }
188735
+ escapeAttrText(s) {
188736
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
188737
+ }
188738
+ renderAttributes(element) {
188739
+ const parts = [];
188740
+ for (const attr of element.attrs) {
188741
+ if (attr.clientOnly)
188742
+ continue;
188743
+ let attrName;
188744
+ if (attr.name === "className")
188745
+ attrName = "class";
188746
+ else if (attr.name === "key")
188747
+ attrName = "data-key";
188748
+ else
188749
+ attrName = attr.name;
188750
+ const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
188751
+ if (lowered)
188752
+ parts.push(lowered);
188753
+ }
188754
+ return parts.length > 0 ? " " + parts.join(" ") : "";
188719
188755
  }
188720
- literal(value, literalType) {
188721
- if (literalType === "string")
188722
- return `'${value}'`;
188723
- if (literalType === "boolean")
188724
- return value ? "1" : "0";
188725
- if (literalType === "null")
188726
- return "nil";
188727
- return String(value);
188756
+ renderScopeMarker(_instanceIdExpr) {
188757
+ return `bf-s="<: $bf.scope_attr() :>" <: $bf.hydration_attrs() | mark_raw :> <: $bf.props_attr() | mark_raw :>`;
188728
188758
  }
188729
- member(object, property, _computed, emit) {
188730
- if (object.kind === "identifier" && object.name === "props") {
188731
- return `$${property}`;
188732
- }
188733
- if (object.kind === "identifier") {
188734
- const staticValue = this.adapter._resolveStaticRecordLiteral(object.name, property);
188735
- if (staticValue !== null)
188736
- return staticValue;
188737
- }
188738
- const obj = emit(object);
188739
- if (property === "length")
188740
- return `$bf.length(${obj})`;
188741
- return `${obj}.${property}`;
188759
+ renderSlotMarker(slotId) {
188760
+ return `${BF_SLOT}="${slotId}"`;
188742
188761
  }
188743
- indexAccess(object, index, emit) {
188744
- return `${emit(object)}[${emit(index)}]`;
188762
+ renderCondMarker(condId) {
188763
+ return `${BF_COND}="${condId}"`;
188745
188764
  }
188746
- call(callee, args, emit) {
188747
- if (callee.kind === "identifier" && args.length === 0) {
188748
- return `$${callee.name}`;
188749
- }
188750
- if (this.adapter._searchParamsLocals.size > 0) {
188751
- const sp = matchSearchParamsMethodCall(callee, args, this.adapter._searchParamsLocals);
188752
- if (sp) {
188753
- return `$searchParams.${sp.method}(${sp.args.map(emit).join(", ")})`;
188765
+ renderKolonFilterExpr(expr, param, localVarMap = new Map) {
188766
+ return emitParsedExpr(expr, new XslateFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
188767
+ }
188768
+ convertTemplateLiteralPartsToKolon(literalParts) {
188769
+ const parts = [];
188770
+ for (const part of literalParts) {
188771
+ if (part.type === "string") {
188772
+ parts.push(this.substituteJsInterpolationsToKolon(part.value));
188773
+ } else if (part.type === "ternary") {
188774
+ const cond = this.convertExpressionToKolon(part.condition);
188775
+ parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
188776
+ } else if (part.type === "lookup") {
188777
+ const keyExpr = this.convertExpressionToKolon(part.key);
188778
+ const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
188779
+ parts.push(`({ ${entries} }[${keyExpr}] // '')`);
188754
188780
  }
188755
188781
  }
188756
- const path = identifierPath(callee);
188757
- const spec = path ? XSLATE_TEMPLATE_PRIMITIVES[path] : undefined;
188758
- if (path && spec) {
188759
- if (args.length === spec.arity) {
188760
- return spec.emit(args.map(emit));
188782
+ return parts.length === 1 ? parts[0] : parts.join(" ~ ");
188783
+ }
188784
+ substituteJsInterpolationsToKolon(s) {
188785
+ const segments = [];
188786
+ const re = /\$\{([^}]+)\}/g;
188787
+ let lastIndex = 0;
188788
+ let m;
188789
+ while ((m = re.exec(s)) !== null) {
188790
+ if (m.index > lastIndex) {
188791
+ segments.push(`'${s.slice(lastIndex, m.index)}'`);
188761
188792
  }
188762
- this.adapter._recordExprBF101(`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`, `Call '${path}' with exactly ${spec.arity} argument(s).`);
188763
- return "''";
188793
+ segments.push(this.convertExpressionToKolon(m[1].trim()));
188794
+ lastIndex = re.lastIndex;
188764
188795
  }
188765
- return emit(callee);
188796
+ if (lastIndex < s.length) {
188797
+ segments.push(`'${s.slice(lastIndex)}'`);
188798
+ }
188799
+ if (segments.length === 0)
188800
+ return `''`;
188801
+ return segments.length === 1 ? segments[0] : `(${segments.join(" ~ ")})`;
188766
188802
  }
188767
- unary(op, argument, emit) {
188768
- const arg = emit(argument);
188769
- if (op === "!")
188770
- return `!${arg}`;
188771
- if (op === "-")
188772
- return `-${arg}`;
188773
- return arg;
188803
+ refuseUnsupportedAttrExpression(expr, attrName) {
188804
+ let probe = expr.trim();
188805
+ while (probe.startsWith("("))
188806
+ probe = probe.slice(1).trimStart();
188807
+ const startsAsObjectLiteral = probe.startsWith("{");
188808
+ const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
188809
+ if (!startsAsObjectLiteral && !hasTaggedTemplate)
188810
+ return false;
188811
+ const parsed = parseExpression3(expr.trim());
188812
+ const support = isSupported2(parsed);
188813
+ if (parsed.kind !== "unsupported" && support.supported)
188814
+ return false;
188815
+ const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
188816
+ const reasonLine = reason ? `
188817
+ ${reason}` : "";
188818
+ this.errors.push({
188819
+ code: "BF101",
188820
+ severity: "error",
188821
+ message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
188822
+ loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188823
+ suggestion: {
188824
+ message: "The Xslate adapter cannot lower JS object literals or tagged-template-literal expressions into Kolon. 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."
188825
+ }
188826
+ });
188827
+ return true;
188774
188828
  }
188775
- binary(op, left, right, emit) {
188776
- const l = emit(left);
188777
- const r = emit(right);
188778
- const opMap = {
188779
- "===": "==",
188780
- "!==": "!=",
188781
- ">": ">",
188782
- "<": "<",
188783
- ">=": ">=",
188784
- "<=": "<=",
188785
- "+": "+",
188786
- "-": "-",
188787
- "*": "*"
188829
+ get emitCtx() {
188830
+ return {
188831
+ _searchParamsLocals: this._searchParamsLocals,
188832
+ _resolveModuleStringConst: (name) => this._resolveModuleStringConst(name),
188833
+ _resolveLiteralConst: (name) => this._resolveLiteralConst(name),
188834
+ _resolveStaticRecordLiteral: (o, k) => this._resolveStaticRecordLiteral(o, k),
188835
+ _recordExprBF101: (message, reason) => this._recordExprBF101(message, reason),
188836
+ _renderKolonFilterExprPublic: (e, p) => this._renderKolonFilterExprPublic(e, p)
188788
188837
  };
188789
- return `${l} ${opMap[op] ?? op} ${r}`;
188790
- }
188791
- logical(op, left, right, emit) {
188792
- const l = emit(left);
188793
- const r = emit(right);
188794
- if (op === "&&")
188795
- return `(${l} && ${r})`;
188796
- if (op === "||")
188797
- return `(${l} || ${r})`;
188798
- return `(${l} // ${r})`;
188799
188838
  }
188800
- higherOrder(method, object, param, predicate, emit) {
188801
- const arrayExpr = emit(object);
188802
- const predBody = this.adapter._renderKolonFilterExprPublic(predicate, param);
188803
- const lambda = `-> $${param} { ${predBody} }`;
188804
- const fn = {
188805
- filter: "filter",
188806
- every: "every",
188807
- some: "some",
188808
- find: "find",
188809
- findIndex: "find_index",
188810
- findLast: "find_last",
188811
- findLastIndex: "find_last_index"
188839
+ get spreadCtx() {
188840
+ return {
188841
+ componentName: this.componentName,
188842
+ errors: this.errors,
188843
+ localConstants: this.localConstants,
188844
+ propsParams: this.propsParams,
188845
+ convertExpressionToKolon: (e, preParsed) => this.convertExpressionToKolon(e, preParsed)
188812
188846
  };
188813
- if (fn[method])
188814
- return `$bf.${fn[method]}(${arrayExpr}, ${lambda})`;
188815
- return emit(object);
188816
188847
  }
188817
- arrayLiteral(elements, emit) {
188818
- return `[${elements.map(emit).join(", ")}]`;
188848
+ get memoCtx() {
188849
+ return { convertExpressionToKolon: (e, preParsed) => this.convertExpressionToKolon(e, preParsed) };
188819
188850
  }
188820
- arrayMethod(method, object, args, emit) {
188821
- return renderArrayMethod(method, object, args, emit);
188851
+ convertExpressionToKolon(expr, preParsed) {
188852
+ let parsed;
188853
+ if (preParsed) {
188854
+ parsed = preParsed;
188855
+ } else {
188856
+ const trimmed = expr.trim();
188857
+ if (trimmed === "")
188858
+ return "''";
188859
+ parsed = parseExpression3(trimmed);
188860
+ }
188861
+ if (parsed.kind === "call") {
188862
+ for (const matcher of this._loweringMatchers) {
188863
+ const node = matcher(parsed.callee, parsed.args);
188864
+ if (node?.kind === "guard-list" && node.helper === "query") {
188865
+ const qArgs = queryHrefArgs(node, (n) => this.renderParsedExprToKolon(n));
188866
+ return `$bf.query(${qArgs.join(", ")})`;
188867
+ }
188868
+ }
188869
+ }
188870
+ const support = isSupported2(parsed);
188871
+ if (!support.supported) {
188872
+ this.errors.push({
188873
+ code: "BF101",
188874
+ severity: "error",
188875
+ message: `Expression not supported: ${preParsed ? stringifyParsedExpr2(parsed) : expr.trim()}`,
188876
+ loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188877
+ suggestion: {
188878
+ message: support.reason ? `${support.reason}
188879
+
188880
+ Options:
188881
+ 1. Use /* @client */ for client-side evaluation
188882
+ 2. Pre-compute the value in the backend` : `Options:
188883
+ 1. Use /* @client */ for client-side evaluation
188884
+ 2. Pre-compute the value in the backend`
188885
+ }
188886
+ });
188887
+ return "''";
188888
+ }
188889
+ return this.renderParsedExprToKolon(parsed);
188822
188890
  }
188823
- sortMethod(_method, object, comparator, emit) {
188824
- return renderSortMethod(emit(object), comparator);
188891
+ renderParsedExprToKolon(expr) {
188892
+ return emitParsedExpr(expr, new XslateTopLevelEmitter(this.emitCtx));
188825
188893
  }
188826
- reduceMethod(method, object, reduceOp, emit) {
188827
- return renderReduceMethod(emit(object), reduceOp, method === "reduceRight" ? "right" : "left");
188894
+ _isStringValueName(name) {
188895
+ return this.stringValueNames.has(name);
188828
188896
  }
188829
- flatMethod(object, depth, emit) {
188830
- return renderFlatMethod(emit(object), depth);
188897
+ parseUndefinedAlternateTernary(expr) {
188898
+ const parsed = parseExpression3(expr.trim());
188899
+ if (parsed?.kind !== "conditional")
188900
+ return null;
188901
+ const alt = parsed.alternate;
188902
+ const isUndef = alt.kind === "identifier" && (alt.name === "undefined" || alt.name === "null") || alt.kind === "literal" && (alt.value === null || alt.value === undefined);
188903
+ if (!isUndef)
188904
+ return null;
188905
+ return {
188906
+ condition: exprToString(parsed.test),
188907
+ consequent: exprToString(parsed.consequent)
188908
+ };
188909
+ }
188910
+ isBooleanTypedPropRef(expr) {
188911
+ let bare = expr.trim();
188912
+ if (this.propsObjectName && bare.startsWith(`${this.propsObjectName}.`)) {
188913
+ bare = bare.slice(this.propsObjectName.length + 1);
188914
+ }
188915
+ if (!/^[A-Za-z_$][\w$]*$/.test(bare))
188916
+ return false;
188917
+ return this.booleanTypedProps.has(bare);
188831
188918
  }
188832
- flatMapMethod(object, op, emit) {
188833
- return renderFlatMapMethod(emit(object), op);
188919
+ _resolveLiteralConst(name) {
188920
+ const c = (this.localConstants ?? []).find((lc) => lc.name === name);
188921
+ if (c?.value === undefined)
188922
+ return null;
188923
+ const v = c.value.trim();
188924
+ if (/^-?\d+(\.\d+)?$/.test(v))
188925
+ return v;
188926
+ const strLit = /^'([^'\\]*)'$/.exec(v) ?? /^"([^"\\]*)"$/.exec(v);
188927
+ if (strLit)
188928
+ return `'${strLit[1].replace(/[\\']/g, (m) => `\\${m}`)}'`;
188929
+ return null;
188834
188930
  }
188835
- conditional(test, consequent, alternate, emit) {
188836
- return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
188931
+ _resolveStaticRecordLiteral(objectName, key) {
188932
+ const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
188933
+ if (!hit)
188934
+ return null;
188935
+ return hit.kind === "number" ? hit.text : `'${hit.text.replace(/[\\']/g, (m) => `\\${m}`)}'`;
188837
188936
  }
188838
- templateLiteral(parts, emit) {
188839
- const terms = [];
188840
- for (const part of parts) {
188841
- if (part.type === "string") {
188842
- if (part.value !== "") {
188843
- terms.push(`'${part.value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`);
188844
- }
188845
- } else {
188846
- const rendered = emit(part.expr);
188847
- const needsParens = part.expr.kind === "binary" || part.expr.kind === "logical" || part.expr.kind === "conditional";
188848
- terms.push(needsParens ? `(${rendered})` : rendered);
188849
- }
188850
- }
188851
- if (terms.length === 0)
188852
- return `''`;
188853
- return terms.join(" ~ ");
188937
+ _resolveModuleStringConst(name) {
188938
+ if (this.inLoop)
188939
+ return null;
188940
+ const value = this.moduleStringConsts.get(name);
188941
+ if (value === undefined)
188942
+ return null;
188943
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
188854
188944
  }
188855
- arrowFn(_param, _body) {
188856
- return "''";
188945
+ _recordExprBF101(message, reason) {
188946
+ this.errors.push({
188947
+ code: "BF101",
188948
+ severity: "error",
188949
+ message,
188950
+ loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
188951
+ suggestion: {
188952
+ message: reason ? `${reason}
188953
+
188954
+ Options:
188955
+ 1. Use /* @client */ for client-side evaluation
188956
+ 2. Pre-compute the value in the backend` : `Options:
188957
+ 1. Use /* @client */ for client-side evaluation
188958
+ 2. Pre-compute the value in the backend`
188959
+ }
188960
+ });
188857
188961
  }
188858
- unsupported(_raw, _reason) {
188859
- return "''";
188962
+ _renderKolonFilterExprPublic(expr, param) {
188963
+ return this.renderKolonFilterExpr(expr, param);
188860
188964
  }
188861
188965
  }
188862
188966
  var xslateAdapter = new XslateAdapter;