@barefootjs/xslate 0.16.0 → 0.17.1

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