@barefootjs/xslate 0.18.4 → 0.18.7

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.
package/dist/index.js CHANGED
@@ -187308,7 +187308,13 @@ import {
187308
187308
  queryHrefArgs,
187309
187309
  isValidHelperId,
187310
187310
  sortComparatorFromArrow as sortComparatorFromArrow2,
187311
- isLowerableLoopDestructure
187311
+ isLowerableLoopDestructure,
187312
+ isDangerousInnerHtmlAttr,
187313
+ resolveDangerousInnerHtml,
187314
+ dangerousInnerHtmlMetacharViolation,
187315
+ dangerousInnerHtmlDiagnostic,
187316
+ resolveStaticLoopSource,
187317
+ collectLoopBoundNames as collectLoopBoundNames2
187312
187318
  } from "@barefootjs/jsx";
187313
187319
 
187314
187320
  // src/adapter/boolean-result.ts
@@ -187365,7 +187371,7 @@ function isAriaBooleanAttr(name) {
187365
187371
  }
187366
187372
 
187367
187373
  // src/adapter/xslate-adapter.ts
187368
- import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
187374
+ import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from "@barefootjs/shared";
187369
187375
 
187370
187376
  // src/adapter/lib/constants.ts
187371
187377
  var XSLATE_TEMPLATE_PRIMITIVES = {
@@ -187374,7 +187380,10 @@ var XSLATE_TEMPLATE_PRIMITIVES = {
187374
187380
  Number: { arity: 1, emit: (args) => `$bf.number(${args[0]})` },
187375
187381
  "Math.floor": { arity: 1, emit: (args) => `$bf.floor(${args[0]})` },
187376
187382
  "Math.ceil": { arity: 1, emit: (args) => `$bf.ceil(${args[0]})` },
187377
- "Math.round": { arity: 1, emit: (args) => `$bf.round(${args[0]})` }
187383
+ "Math.round": { arity: 1, emit: (args) => `$bf.round(${args[0]})` },
187384
+ "Math.min": { arity: 2, emit: (args) => `$bf.min(${args[0]}, ${args[1]})` },
187385
+ "Math.max": { arity: 2, emit: (args) => `$bf.max(${args[0]}, ${args[1]})` },
187386
+ "Math.abs": { arity: 1, emit: (args) => `$bf.abs(${args[0]})` }
187378
187387
  };
187379
187388
  var XSLATE_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(XSLATE_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
187380
187389
 
@@ -187479,6 +187488,12 @@ function renderArrayMethod(method, object, args, emit) {
187479
187488
  const recv = emit(object);
187480
187489
  return `$bf.trim(${recv})`;
187481
187490
  }
187491
+ case "trimStart":
187492
+ case "trimEnd": {
187493
+ const fn = method === "trimStart" ? "trim_start" : "trim_end";
187494
+ const recv = emit(object);
187495
+ return `$bf.${fn}(${recv})`;
187496
+ }
187482
187497
  case "toFixed": {
187483
187498
  const recv = emit(object);
187484
187499
  const digits = args.length >= 1 ? emit(args[0]) : "0";
@@ -187512,6 +187527,12 @@ function renderArrayMethod(method, object, args, emit) {
187512
187527
  const newS = emit(args[1]);
187513
187528
  return `$bf.replace(${recv}, ${oldS}, ${newS})`;
187514
187529
  }
187530
+ case "replaceAll": {
187531
+ const recv = emit(object);
187532
+ const oldS = emit(args[0]);
187533
+ const newS = emit(args[1]);
187534
+ return `$bf.replace_all(${recv}, ${oldS}, ${newS})`;
187535
+ }
187515
187536
  case "repeat": {
187516
187537
  const recv = emit(object);
187517
187538
  const count = args.length === 0 ? "0" : emit(args[0]);
@@ -187609,6 +187630,39 @@ function renderFlatMethod(recv, depth, emit) {
187609
187630
  return `$bf.flat(${recv}, ${d})`;
187610
187631
  }
187611
187632
 
187633
+ // src/adapter/lib/static-value.ts
187634
+ function staticValueToKolon(value) {
187635
+ if (value === null || value === undefined)
187636
+ return "nil";
187637
+ if (typeof value === "boolean")
187638
+ return null;
187639
+ if (typeof value === "number")
187640
+ return String(value);
187641
+ if (typeof value === "string")
187642
+ return `'${escapeKolonSingleQuoted(value)}'`;
187643
+ if (Array.isArray(value)) {
187644
+ const items = [];
187645
+ for (const el of value) {
187646
+ const serialized = staticValueToKolon(el);
187647
+ if (serialized === null)
187648
+ return null;
187649
+ items.push(serialized);
187650
+ }
187651
+ return `[${items.join(", ")}]`;
187652
+ }
187653
+ if (typeof value === "object") {
187654
+ const entries = [];
187655
+ for (const [key, val] of Object.entries(value)) {
187656
+ const serialized = staticValueToKolon(val);
187657
+ if (serialized === null)
187658
+ return null;
187659
+ entries.push(`${kolonHashKey(key)} => ${serialized}`);
187660
+ }
187661
+ return `{ ${entries.join(", ")} }`;
187662
+ }
187663
+ return null;
187664
+ }
187665
+
187612
187666
  // src/adapter/expr/emitters.ts
187613
187667
  import {
187614
187668
  groupBinaryOperand,
@@ -187655,7 +187709,7 @@ class XslateFilterEmitter {
187655
187709
  return "nil";
187656
187710
  return String(value);
187657
187711
  }
187658
- member(object, property, _computed, emit) {
187712
+ member(object, property, _computed, _optional, emit) {
187659
187713
  if (property === "length") {
187660
187714
  return `$bf.length(${emit(object)})`;
187661
187715
  }
@@ -187767,7 +187821,7 @@ class XslateTopLevelEmitter {
187767
187821
  return "nil";
187768
187822
  return String(value);
187769
187823
  }
187770
- member(object, property, _computed, emit) {
187824
+ member(object, property, _computed, _optional, emit) {
187771
187825
  if (object.kind === "identifier" && object.name === "props") {
187772
187826
  return `$${property}`;
187773
187827
  }
@@ -188152,6 +188206,9 @@ function generateDerivedMemoSeed(ctx, ir) {
188152
188206
  ` : "";
188153
188207
  }
188154
188208
 
188209
+ // src/adapter/props/prop-classes.ts
188210
+ import { collectLoopBoundNames } from "@barefootjs/jsx";
188211
+
188155
188212
  // src/adapter/value/parsed-literal.ts
188156
188213
  import { evalStringArrayJoin } from "@barefootjs/jsx";
188157
188214
  function isStringTypeInfo(type2) {
@@ -188182,6 +188239,12 @@ function collectStringValueNames(ir) {
188182
188239
  if (isStringTypeInfo(p.type))
188183
188240
  names.add(p.name);
188184
188241
  }
188242
+ for (const c of ir.metadata.localConstants) {
188243
+ if (isStringTypeInfo(c.type ?? undefined) || isBareStringLiteral(c.value))
188244
+ names.add(c.name);
188245
+ }
188246
+ for (const bound of collectLoopBoundNames(ir))
188247
+ names.delete(bound);
188185
188248
  return names;
188186
188249
  }
188187
188250
 
@@ -188208,6 +188271,7 @@ class XslateAdapter extends BaseAdapter {
188208
188271
  options;
188209
188272
  errors = [];
188210
188273
  inLoop = false;
188274
+ currentLoopKeyDepth = 0;
188211
188275
  propsObjectName = null;
188212
188276
  propsParams = [];
188213
188277
  booleanTypedProps = new Set;
@@ -188216,6 +188280,7 @@ class XslateAdapter extends BaseAdapter {
188216
188280
  _searchParamsLocals = new Set;
188217
188281
  _loweringMatchers = [];
188218
188282
  localConstants = [];
188283
+ staticLoopSourceBoundNames = new Set;
188219
188284
  nullableOptionalProps = new Set;
188220
188285
  constructor(options = {}) {
188221
188286
  super();
@@ -188231,6 +188296,7 @@ class XslateAdapter extends BaseAdapter {
188231
188296
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188232
188297
  this.booleanTypedProps = collectBooleanTypedProps(ir);
188233
188298
  this.localConstants = ir.metadata.localConstants ?? [];
188299
+ this.staticLoopSourceBoundNames = collectLoopBoundNames2(ir);
188234
188300
  this.nullableOptionalProps = collectNullableOptionalProps(ir);
188235
188301
  this.stringValueNames = collectStringValueNames(ir);
188236
188302
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
@@ -188284,7 +188350,7 @@ class XslateAdapter extends BaseAdapter {
188284
188350
  return this.renderElement(node);
188285
188351
  }
188286
188352
  emitText(node) {
188287
- return node.value;
188353
+ return escapeHtml(node.value);
188288
188354
  }
188289
188355
  emitExpression(node) {
188290
188356
  return this.renderExpression(node);
@@ -188347,7 +188413,8 @@ class XslateAdapter extends BaseAdapter {
188347
188413
  renderElement(element) {
188348
188414
  const tag = element.tag;
188349
188415
  const attrs = this.renderAttributes(element);
188350
- const children = this.renderChildren(element.children);
188416
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
188417
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
188351
188418
  let hydrationAttrs = "";
188352
188419
  if (element.needsScope) {
188353
188420
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
@@ -188382,6 +188449,22 @@ class XslateAdapter extends BaseAdapter {
188382
188449
  }
188383
188450
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188384
188451
  }
188452
+ renderDangerousInnerHtml(element) {
188453
+ const resolution = resolveDangerousInnerHtml(element);
188454
+ if (!resolution)
188455
+ return null;
188456
+ if (resolution.kind === "dynamic") {
188457
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
188458
+ return "";
188459
+ }
188460
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
188461
+ if (violation) {
188462
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
188463
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
188464
+ return "";
188465
+ }
188466
+ return resolution.html;
188467
+ }
188385
188468
  renderExpression(expr) {
188386
188469
  if (expr.clientOnly) {
188387
188470
  if (expr.slotId) {
@@ -188389,7 +188472,7 @@ class XslateAdapter extends BaseAdapter {
188389
188472
  }
188390
188473
  return "";
188391
188474
  }
188392
- const perlExpr = this.convertExpressionToKolon(expr.expr);
188475
+ const perlExpr = this.convertExpressionToKolon(expr.expr, expr.parsed);
188393
188476
  if (expr.slotId) {
188394
188477
  return `<: $bf.text_start("${expr.slotId}") | mark_raw :><: ${perlExpr} :><: $bf.text_end() | mark_raw :>`;
188395
188478
  }
@@ -188481,8 +188564,12 @@ ${whenTrue}
188481
188564
  }
188482
188565
  });
188483
188566
  }
188567
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
188568
+ isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
188569
+ });
188570
+ const staticArray = staticItems !== null ? staticValueToKolon(staticItems) : null;
188484
188571
  const arrayName = loop.array.trim();
188485
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188572
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188486
188573
  const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188487
188574
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188488
188575
  this.errors.push({
@@ -188496,7 +188583,7 @@ ${whenTrue}
188496
188583
  });
188497
188584
  }
188498
188585
  }
188499
- const rawArray = this.convertExpressionToKolon(loop.array);
188586
+ const rawArray = staticArray ?? this.convertExpressionToKolon(loop.array);
188500
188587
  let array = rawArray;
188501
188588
  if (loop.sortComparator) {
188502
188589
  const sort = loop.sortComparator;
@@ -188508,9 +188595,12 @@ ${whenTrue}
188508
188595
  }
188509
188596
  const param = loop.param;
188510
188597
  const renderedChildren = this.renderChildren(loop.children);
188511
- const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
188598
+ const loopVar = loop.objectIteration === "entries" ? "__bf_pair" : loop.objectIteration ? param : loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
188512
188599
  const indexLocalLines = [];
188513
- if (loop.iterationShape === "keys") {
188600
+ if (loop.objectIteration === "entries") {
188601
+ indexLocalLines.push(`: my $${loop.index ?? param} = $${loopVar}.key;`);
188602
+ indexLocalLines.push(`: my $${param} = $${loopVar}.value;`);
188603
+ } else if (loop.objectIteration) {} else if (loop.iterationShape === "keys") {
188514
188604
  indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`);
188515
188605
  } else if (loop.index) {
188516
188606
  indexLocalLines.push(`: my $${loop.index} = $~${loopVar}.index;`);
@@ -188530,13 +188620,17 @@ ${whenTrue}
188530
188620
  }
188531
188621
  const prevInLoop = this.inLoop;
188532
188622
  this.inLoop = true;
188623
+ const prevLoopKeyDepth = this.currentLoopKeyDepth;
188624
+ this.currentLoopKeyDepth = loop.depth;
188533
188625
  const childrenUnderLoop = this.renderChildren(loop.children);
188626
+ this.currentLoopKeyDepth = prevLoopKeyDepth;
188534
188627
  this.inLoop = prevInLoop;
188535
188628
  const bodyChildren = loop.bodyIsItemConditional && loop.key ? `<: $bf.comment("loop-i:" ~ ${this.convertExpressionToKolon(loop.key)}) | mark_raw :>
188536
188629
  ${childrenUnderLoop}` : childrenUnderLoop;
188537
188630
  const lines = [];
188538
188631
  lines.push(`<: $bf.comment("loop:${loop.markerId}") | mark_raw :>`);
188539
- lines.push(`: for ${array} -> $${loopVar} {`);
188632
+ const forHeader = loop.objectIteration === "entries" ? `: for ${array}.kv() -> $${loopVar} {` : loop.objectIteration === "keys" ? `: for ${array}.keys() -> $${loopVar} {` : loop.objectIteration === "values" ? `: for ${array}.values() -> $${loopVar} {` : `: for ${array} -> $${loopVar} {`;
188633
+ lines.push(forHeader);
188540
188634
  for (const il of indexLocalLines)
188541
188635
  lines.push(il);
188542
188636
  if (loop.filterPredicate) {
@@ -188607,9 +188701,20 @@ ${childrenUnderLoop}` : childrenUnderLoop;
188607
188701
  renderComponent(comp) {
188608
188702
  const segments = [{ kind: "entries", parts: [] }];
188609
188703
  const currentEntries = () => this.componentPropSegmentEntries(segments);
188704
+ const namedSlotMacros = [];
188610
188705
  for (const p of comp.props) {
188611
188706
  if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
188612
188707
  continue;
188708
+ if (p.value.kind === "jsx-children" && p.name !== "children") {
188709
+ const prevInLoop = this.inLoop;
188710
+ this.inLoop = false;
188711
+ const slotBody = this.renderChildren(p.value.children);
188712
+ this.inLoop = prevInLoop;
188713
+ const macroName = `bf_prop_${this.childrenCaptureCounter++}`;
188714
+ namedSlotMacros.push(`<: macro ${macroName} -> () { :>${slotBody}<: } :>`);
188715
+ currentEntries().push(`${kolonHashKey(p.name)} => ${macroName}()`);
188716
+ continue;
188717
+ }
188613
188718
  if (p.value.kind === "spread") {
188614
188719
  const trimmed = p.value.expr.trim();
188615
188720
  if (this.propsObjectName && this.propsObjectName === trimmed) {
@@ -188638,11 +188743,11 @@ ${childrenUnderLoop}` : childrenUnderLoop;
188638
188743
  const macroName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
188639
188744
  currentEntries().push(`children => ${macroName}()`);
188640
188745
  const dict = this.combineComponentPropSegments(segments);
188641
- return `<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', ${dict}) | mark_raw :>`;
188746
+ return `${namedSlotMacros.join("")}<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', ${dict}) | mark_raw :>`;
188642
188747
  }
188643
188748
  const isEmpty = segments.every((s) => s.kind === "entries" && s.parts.length === 0);
188644
188749
  const hashEntries = isEmpty ? "" : `, ${this.combineComponentPropSegments(segments)}`;
188645
- return `<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`;
188750
+ return `${namedSlotMacros.join("")}<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`;
188646
188751
  }
188647
188752
  childrenCaptureCounter = 0;
188648
188753
  presenceVarCounter = 0;
@@ -188687,7 +188792,7 @@ ${alternate}
188687
188792
  ${children}`;
188688
188793
  }
188689
188794
  elementAttrEmitter = {
188690
- emitLiteral: (value, name) => `${name}="${value.value}"`,
188795
+ emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
188691
188796
  emitExpression: (value, name) => {
188692
188797
  if (name === "style") {
188693
188798
  const css = this.tryLowerStyleObject(value.expr);
@@ -188791,12 +188896,15 @@ ${name}="<: ${val} :>"
188791
188896
  for (const attr of element.attrs) {
188792
188897
  if (attr.clientOnly)
188793
188898
  continue;
188899
+ if (isDangerousInnerHtmlAttr(attr))
188900
+ continue;
188794
188901
  let attrName;
188795
188902
  if (attr.name === "className")
188796
188903
  attrName = "class";
188797
- else if (attr.name === "key")
188798
- attrName = "data-key";
188799
- else
188904
+ else if (attr.name === "key") {
188905
+ const depth = this.currentLoopKeyDepth;
188906
+ attrName = depth > 0 ? `data-key-${depth}` : "data-key";
188907
+ } else
188800
188908
  attrName = attr.name;
188801
188909
  const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
188802
188910
  if (lowered)
@@ -188973,6 +189081,8 @@ Options:
188973
189081
  return this.booleanTypedProps.has(bare);
188974
189082
  }
188975
189083
  _resolveLiteralConst(name) {
189084
+ if (this.staticLoopSourceBoundNames.has(name))
189085
+ return null;
188976
189086
  const c = (this.localConstants ?? []).find((lc) => lc.name === name);
188977
189087
  if (c?.value === undefined)
188978
189088
  return null;
@@ -188985,6 +189095,8 @@ Options:
188985
189095
  return null;
188986
189096
  }
188987
189097
  _resolveStaticRecordLiteral(objectName, key) {
189098
+ if (this.staticLoopSourceBoundNames.has(objectName))
189099
+ return null;
188988
189100
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
188989
189101
  if (!hit)
188990
189102
  return null;
@@ -189022,35 +189134,18 @@ Options:
189022
189134
  var xslateAdapter = new XslateAdapter;
189023
189135
  // src/conformance-pins.ts
189024
189136
  var conformancePins = {
189025
- "static-array-children": [{ code: "BF103", severity: "error" }],
189026
- "todo-app": [{ code: "BF103", severity: "error" }],
189027
- "todo-app-ssr": [{ code: "BF103", severity: "error" }],
189028
189137
  "static-array-from-props": [{ code: "BF101", severity: "error" }],
189029
- "static-array-from-props-with-component": [
189030
- { code: "BF103", severity: "error" },
189031
- { code: "BF101", severity: "error" }
189032
- ],
189138
+ "static-array-from-props-with-component": [{ code: "BF101", severity: "error" }],
189033
189139
  "filter-nested-callback-predicate": [
189034
189140
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189035
189141
  ],
189036
189142
  "filter-nested-find-predicate": [
189037
189143
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189038
189144
  ],
189039
- "array-map-function-reference": [{ code: "BF101", severity: "error" }],
189040
- "dangerous-inner-html": [{ code: "BF101", severity: "error" }],
189041
- "string-replaceall": [{ code: "BF101", severity: "error" }]
189145
+ "dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
189042
189146
  };
189043
189147
  // src/render-divergences.ts
189044
- var renderDivergences = {
189045
- "html-entity-text": "`&copy;` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
189046
- "math-methods": "Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)",
189047
- "static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
189048
- "object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations",
189049
- "nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
189050
- "jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
189051
- "string-slice": "`.slice()` on a STRING misfires through the array slice helper",
189052
- "string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering)"
189053
- };
189148
+ var renderDivergences = {};
189054
189149
  export {
189055
189150
  xslateAdapter,
189056
189151
  renderDivergences,
@@ -1 +1 @@
1
- {"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,iBAiB/B,CAAA"}
1
+ {"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,iBAK/B,CAAA"}
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::Backend::Xslate;
2
- our $VERSION = "0.18.3";
2
+ our $VERSION = "0.18.5";
3
3
  use strict;
4
4
  use warnings;
5
5
  use utf8;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/xslate",
3
- "version": "0.18.4",
3
+ "version": "0.18.7",
4
4
  "description": "Text::Xslate (Kolon) adapter for BarefootJS — compiles IR to .tx templates and ships the Xslate rendering backend; runs under any PSGI/Plack app",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,14 +55,14 @@
55
55
  "directory": "packages/adapter-xslate"
56
56
  },
57
57
  "dependencies": {
58
- "@barefootjs/shared": "0.18.4"
58
+ "@barefootjs/shared": "0.18.7"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@barefootjs/adapter-tests": "0.1.0",
65
- "@barefootjs/jsx": "0.18.4",
65
+ "@barefootjs/jsx": "0.18.7",
66
66
  "typescript": "^5.0.0"
67
67
  }
68
68
  }
@@ -396,8 +396,147 @@ export { A }
396
396
  })
397
397
  })
398
398
 
399
+ describe('XslateAdapter - named-slot capture identifier safety (#2168 jsx-element-prop)', () => {
400
+ // A JSX-valued prop under a hyphenated name (`data-slot`, a valid JSX
401
+ // attribute name) must not leak into the Kolon macro's identifier — Kolon
402
+ // macro names can't contain `-`. The macro name is purely counter-based
403
+ // (never derived from the prop name); the hash KEY passed to
404
+ // `render_child` still carries the real name, quoted via `kolonHashKey`.
405
+ test('a hyphenated prop name does not appear in the macro name', () => {
406
+ const { template } = compileAndGenerate(`
407
+ function Card(props) { return null }
408
+ export function Parent() {
409
+ return <Card data-slot={<strong>Title</strong>}>text</Card>
410
+ }
411
+ `)
412
+ expect(template).toContain('<: macro bf_prop_0 -> ()')
413
+ expect(template).toContain("'data-slot' => bf_prop_0()")
414
+ expect(template).not.toContain('data-slot -> ()')
415
+ expect(template).not.toContain('data-slot_')
416
+ })
417
+ })
418
+
399
419
  // #2038 nested-callback-predicate loudness is pinned at the shared
400
420
  // conformance layer: `filter-nested-callback-predicate` /
401
421
  // `filter-nested-find-predicate` (BF101 via `expectedDiagnostics` above) and
402
422
  // `filter-nested-callback-predicate-client` (the `/* @client */` suppression
403
423
  // twin, which must render clean).
424
+
425
+ // #2221: `_resolveLiteralConst` is a flat name lookup against
426
+ // `ir.metadata.localConstants` with no notion of AST scope — it used to
427
+ // substitute an outer const's literal value even at an occurrence that is
428
+ // actually an enclosing loop callback's own (shadowing) parameter, so every
429
+ // iteration rendered the same hard-coded literal. Guarded with the same
430
+ // coarse `collectLoopBoundNames` exclusion as #2212: any name a loop binds
431
+ // anywhere in the component never inlines, falling back to the bare
432
+ // identifier.
433
+ describe('XslateAdapter - const inlining vs loop-param shadowing (#2221)', () => {
434
+ test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
435
+ const { template } = compileAndGenerate(`
436
+ function Widget() {
437
+ const label: string = 'x'
438
+ return <ul>{[2, 5].map((label) => <li key={label}>{1 + label}</li>)}</ul>
439
+ }
440
+ `)
441
+ // The loop body must reference the per-iteration loop var...
442
+ expect(template).toContain('1 + $label')
443
+ // ...never the outer const's hard-coded value.
444
+ expect(template).not.toContain("1 + 'x'")
445
+ })
446
+
447
+ test('a numeric const shadowed by a loop param emits the identifier too', () => {
448
+ const { template } = compileAndGenerate(`
449
+ function Widget() {
450
+ const count = 7
451
+ return <ul>{[2, 5].map((count) => <li key={count}>{1 + count}</li>)}</ul>
452
+ }
453
+ `)
454
+ expect(template).toContain('1 + $count')
455
+ expect(template).not.toContain('1 + 7')
456
+ })
457
+
458
+ test('a literal const NOT shadowed by any loop still inlines (#1897 pin)', () => {
459
+ const { template } = compileAndGenerate(`
460
+ function Widget({ values }: { values: number[] }) {
461
+ const totalPages = 5
462
+ return <div>
463
+ <p>Page 1 of {1 + totalPages}</p>
464
+ <ul>{values.map((v) => <li key={v}>{v}</li>)}</ul>
465
+ </div>
466
+ }
467
+ `)
468
+ expect(template).toContain('1 + 5')
469
+ })
470
+
471
+ // The accepted coarse-exclusion trade-off (same as #2212): a name that is
472
+ // loop-bound ANYWHERE in the component never inlines, even at a genuinely
473
+ // non-shadowed occurrence outside the loop — the bare identifier is
474
+ // emitted instead of the value.
475
+ test('a const referenced outside the loop whose name is loop-bound elsewhere falls back to the identifier (accepted trade-off)', () => {
476
+ const { template } = compileAndGenerate(`
477
+ function Widget({ values }: { values: number[] }) {
478
+ const label: string = 'x'
479
+ return <div>
480
+ <p>{1 + label}</p>
481
+ <ul>{values.map((label) => <li key={label}>{2 + label}</li>)}</ul>
482
+ </div>
483
+ }
484
+ `)
485
+ expect(template).not.toContain("1 + 'x'")
486
+ expect(template).toContain('2 + $label')
487
+ })
488
+ })
489
+
490
+ // #2237: `_resolveStaticRecordLiteral` (`IDENT.key` on a module-scope
491
+ // object-literal const, e.g. `variantClasses.ghost` — #1896/#1897) is a
492
+ // flat name lookup on `objectName` with no notion of AST scope, the
493
+ // record-literal sibling of #2221's `_resolveLiteralConst` bug. It used to
494
+ // substitute the outer const's member value even at an occurrence that is
495
+ // actually an enclosing loop callback's own (shadowing) parameter, so every
496
+ // iteration rendered the same hard-coded literal instead of the per-item
497
+ // value. Guarded with the same coarse `staticLoopSourceBoundNames`
498
+ // exclusion as #2221: any name a loop binds anywhere in the component
499
+ // never inlines, falling back to the bare `$cfg.x` member expression.
500
+ describe('XslateAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
501
+ test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
502
+ const { template } = compileAndGenerate(`
503
+ const cfg = { x: 'outer-lit' }
504
+ function Widget({ rows }: { rows: { x: string }[] }) {
505
+ return <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
506
+ }
507
+ `)
508
+ // The loop body must reference the per-iteration member access...
509
+ expect(template).toContain('<: $cfg.x :>')
510
+ // ...never the outer const's hard-coded value.
511
+ expect(template).not.toContain("<: 'outer-lit' :>")
512
+ })
513
+
514
+ test('a module object const NOT shadowed by any loop still inlines (variantClasses.ghost shape, #1896/#1897 pin)', () => {
515
+ const { template } = compileAndGenerate(`
516
+ const variantClasses = { solid: 'bg-solid', ghost: 'bg-ghost' }
517
+ function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
518
+ return <div>{variantClasses.ghost}</div>
519
+ }
520
+ `)
521
+ expect(template).toContain("<: 'bg-ghost' :>")
522
+ })
523
+
524
+ // The accepted coarse-exclusion trade-off (same as #2221/#2212): an
525
+ // object name that is loop-bound ANYWHERE in the component never
526
+ // inlines its member lookups, even at a genuinely non-shadowed
527
+ // occurrence outside the loop — the bare member expression is emitted
528
+ // instead of the value.
529
+ test('a record member referenced outside the loop whose object name is loop-bound elsewhere falls back to the member expression (accepted trade-off)', () => {
530
+ const { template } = compileAndGenerate(`
531
+ const cfg = { x: 'outer-lit' }
532
+ function Widget({ rows }: { rows: { x: string }[] }) {
533
+ return <div>
534
+ <p>{cfg.x}</p>
535
+ <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
536
+ </div>
537
+ }
538
+ `)
539
+ expect(template).not.toContain("<: 'outer-lit' :>")
540
+ expect(template).toContain('<: $cfg.x :>')
541
+ })
542
+ })
@@ -89,6 +89,15 @@ export function renderArrayMethod(
89
89
  const recv = emit(object)
90
90
  return `$bf.trim(${recv})`
91
91
  }
92
+ case 'trimStart':
93
+ case 'trimEnd': {
94
+ // `.trimStart()` / `.trimEnd()` — the one-sided siblings of
95
+ // `.trim()` (#2183 follow-up). Dedicated `$bf.trim_start` /
96
+ // `$bf.trim_end` helpers, not `$bf.trim` with a flag.
97
+ const fn = method === 'trimStart' ? 'trim_start' : 'trim_end'
98
+ const recv = emit(object)
99
+ return `$bf.${fn}(${recv})`
100
+ }
92
101
  case 'toFixed': {
93
102
  // `.toFixed(digits?)` — `$bf.to_fixed` mirrors JS rounding +
94
103
  // zero-padding (default 0 digits). #1897.
@@ -124,6 +133,16 @@ export function renderArrayMethod(
124
133
  const newS = emit(args[1])
125
134
  return `$bf.replace(${recv}, ${oldS}, ${newS})`
126
135
  }
136
+ case 'replaceAll': {
137
+ // `.replaceAll(old, new)` — string-pattern form, EVERY occurrence,
138
+ // via the dedicated `$bf.replace_all` helper (not `$bf.replace`
139
+ // with a flag) — the regex-pattern form is refused upstream at
140
+ // the parser, same as `.replace`. See #2182.
141
+ const recv = emit(object)
142
+ const oldS = emit(args[0])
143
+ const newS = emit(args[1])
144
+ return `$bf.replace_all(${recv}, ${oldS}, ${newS})`
145
+ }
127
146
  case 'repeat': {
128
147
  const recv = emit(object)
129
148
  const count = args.length === 0 ? '0' : emit(args[0])
@@ -94,7 +94,7 @@ export class XslateFilterEmitter implements ParsedExprEmitter {
94
94
  return String(value)
95
95
  }
96
96
 
97
- member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
97
+ member(object: ParsedExpr, property: string, _computed: boolean, _optional: boolean, emit: (e: ParsedExpr) => string): string {
98
98
  // `.length` — route through `$bf.length` (handles both array element
99
99
  // count and string char count, JS-compatibly). Kolon's builtin `.size()`
100
100
  // is array-only and faults on a string.
@@ -263,7 +263,7 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
263
263
  return String(value)
264
264
  }
265
265
 
266
- member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
266
+ member(object: ParsedExpr, property: string, _computed: boolean, _optional: boolean, emit: (e: ParsedExpr) => string): string {
267
267
  // `props.x` flattens to the bare `$x` the SSR caller binds each prop to
268
268
  // (props arrive as individual top-level vars, not a `$props` hashref).
269
269
  if (object.kind === 'identifier' && object.name === 'props') {
@@ -23,6 +23,9 @@ export const XSLATE_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
23
23
  'Math.floor': { arity: 1, emit: (args) => `$bf.floor(${args[0]})` },
24
24
  'Math.ceil': { arity: 1, emit: (args) => `$bf.ceil(${args[0]})` },
25
25
  'Math.round': { arity: 1, emit: (args) => `$bf.round(${args[0]})` },
26
+ 'Math.min': { arity: 2, emit: (args) => `$bf.min(${args[0]}, ${args[1]})` },
27
+ 'Math.max': { arity: 2, emit: (args) => `$bf.max(${args[0]}, ${args[1]})` },
28
+ 'Math.abs': { arity: 1, emit: (args) => `$bf.abs(${args[0]})` },
26
29
  }
27
30
 
28
31
  /**