@barefootjs/jinja 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
  prepareLoweringMatchers,
187309
187309
  queryHrefArgs,
187310
187310
  isValidHelperId,
187311
- sortComparatorFromArrow as sortComparatorFromArrow2
187311
+ sortComparatorFromArrow as sortComparatorFromArrow2,
187312
+ isDangerousInnerHtmlAttr,
187313
+ resolveDangerousInnerHtml,
187314
+ dangerousInnerHtmlMetacharViolation,
187315
+ dangerousInnerHtmlDiagnostic,
187316
+ resolveStaticLoopSource,
187317
+ collectLoopBoundNames
187312
187318
  } from "@barefootjs/jsx";
187313
187319
 
187314
187320
  // src/adapter/boolean-result.ts
@@ -187369,7 +187375,7 @@ function isAriaBooleanAttr(name) {
187369
187375
  }
187370
187376
 
187371
187377
  // src/adapter/jinja-adapter.ts
187372
- import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
187378
+ import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from "@barefootjs/shared";
187373
187379
 
187374
187380
  // src/adapter/lib/constants.ts
187375
187381
  var JINJA_TEMPLATE_PRIMITIVES = {
@@ -187378,7 +187384,10 @@ var JINJA_TEMPLATE_PRIMITIVES = {
187378
187384
  Number: { arity: 1, emit: (args) => `bf.number(${args[0]})` },
187379
187385
  "Math.floor": { arity: 1, emit: (args) => `bf.floor(${args[0]})` },
187380
187386
  "Math.ceil": { arity: 1, emit: (args) => `bf.ceil(${args[0]})` },
187381
- "Math.round": { arity: 1, emit: (args) => `bf.round(${args[0]})` }
187387
+ "Math.round": { arity: 1, emit: (args) => `bf.round(${args[0]})` },
187388
+ "Math.min": { arity: 2, emit: (args) => `bf.min(${args[0]}, ${args[1]})` },
187389
+ "Math.max": { arity: 2, emit: (args) => `bf.max(${args[0]}, ${args[1]})` },
187390
+ "Math.abs": { arity: 1, emit: (args) => `bf.abs(${args[0]})` }
187382
187391
  };
187383
187392
  var JINJA_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(JINJA_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
187384
187393
 
@@ -187557,6 +187566,12 @@ function renderArrayMethod(method, object, args, emit) {
187557
187566
  const recv = emit(object);
187558
187567
  return `bf.trim(${recv})`;
187559
187568
  }
187569
+ case "trimStart":
187570
+ case "trimEnd": {
187571
+ const fn = method === "trimStart" ? "trim_start" : "trim_end";
187572
+ const recv = emit(object);
187573
+ return `bf.${fn}(${recv})`;
187574
+ }
187560
187575
  case "toFixed": {
187561
187576
  const recv = emit(object);
187562
187577
  const digits = args.length >= 1 ? emit(args[0]) : "0";
@@ -187590,6 +187605,12 @@ function renderArrayMethod(method, object, args, emit) {
187590
187605
  const newS = emit(args[1]);
187591
187606
  return `bf.replace(${recv}, ${oldS}, ${newS})`;
187592
187607
  }
187608
+ case "replaceAll": {
187609
+ const recv = emit(object);
187610
+ const oldS = emit(args[0]);
187611
+ const newS = emit(args[1]);
187612
+ return `bf.replace_all(${recv}, ${oldS}, ${newS})`;
187613
+ }
187593
187614
  case "repeat": {
187594
187615
  const recv = emit(object);
187595
187616
  const count = args.length === 0 ? "0" : emit(args[0]);
@@ -187684,6 +187705,39 @@ function renderFlatMethod(recv, depth, emit) {
187684
187705
  return `bf.flat(${recv}, ${d})`;
187685
187706
  }
187686
187707
 
187708
+ // src/adapter/lib/static-value.ts
187709
+ function staticValueToJinja(value) {
187710
+ if (value === null || value === undefined)
187711
+ return "none";
187712
+ if (typeof value === "boolean")
187713
+ return value ? "true" : "false";
187714
+ if (typeof value === "number")
187715
+ return String(value);
187716
+ if (typeof value === "string")
187717
+ return `'${escapeJinjaSingleQuoted(value)}'`;
187718
+ if (Array.isArray(value)) {
187719
+ const items = [];
187720
+ for (const el of value) {
187721
+ const serialized = staticValueToJinja(el);
187722
+ if (serialized === null)
187723
+ return null;
187724
+ items.push(serialized);
187725
+ }
187726
+ return `[${items.join(", ")}]`;
187727
+ }
187728
+ if (typeof value === "object") {
187729
+ const entries = [];
187730
+ for (const [key, val] of Object.entries(value)) {
187731
+ const serialized = staticValueToJinja(val);
187732
+ if (serialized === null)
187733
+ return null;
187734
+ entries.push(`${jinjaHashKey(key)}: ${serialized}`);
187735
+ }
187736
+ return `{${entries.join(", ")}}`;
187737
+ }
187738
+ return null;
187739
+ }
187740
+
187687
187741
  // src/adapter/expr/emitters.ts
187688
187742
  import {
187689
187743
  groupBinaryOperand,
@@ -187732,11 +187786,11 @@ class JinjaFilterEmitter {
187732
187786
  return "none";
187733
187787
  return String(value);
187734
187788
  }
187735
- member(object, property, _computed, emit) {
187789
+ member(object, property, _computed, _optional, emit) {
187736
187790
  if (property === "length") {
187737
187791
  return `bf.length(${emit(object)})`;
187738
187792
  }
187739
- return `${emit(object)}.${property}`;
187793
+ return `${emit(object)}['${escapeJinjaSingleQuoted(property)}']`;
187740
187794
  }
187741
187795
  indexAccess(object, index, emit) {
187742
187796
  return `${emit(object)}[${emit(index)}]`;
@@ -187838,7 +187892,7 @@ class JinjaTopLevelEmitter {
187838
187892
  return "none";
187839
187893
  return String(value);
187840
187894
  }
187841
- member(object, property, _computed, emit) {
187895
+ member(object, property, _computed, _optional, emit) {
187842
187896
  if (object.kind === "identifier" && object.name === "props") {
187843
187897
  return jinjaIdent(property);
187844
187898
  }
@@ -187850,7 +187904,7 @@ class JinjaTopLevelEmitter {
187850
187904
  const obj = emit(object);
187851
187905
  if (property === "length")
187852
187906
  return `bf.length(${obj})`;
187853
- return `${obj}.${property}`;
187907
+ return `${obj}['${escapeJinjaSingleQuoted(property)}']`;
187854
187908
  }
187855
187909
  indexAccess(object, index, emit) {
187856
187910
  return `${emit(object)}[${emit(index)}]`;
@@ -188257,6 +188311,7 @@ class JinjaAdapter extends BaseAdapter {
188257
188311
  options;
188258
188312
  errors = [];
188259
188313
  inLoop = false;
188314
+ currentLoopKeyDepth = 0;
188260
188315
  propsObjectName = null;
188261
188316
  propsParams = [];
188262
188317
  booleanTypedProps = new Set;
@@ -188265,6 +188320,7 @@ class JinjaAdapter extends BaseAdapter {
188265
188320
  _searchParamsLocals = new Set;
188266
188321
  _loweringMatchers = [];
188267
188322
  localConstants = [];
188323
+ staticLoopSourceBoundNames = new Set;
188268
188324
  nullableOptionalProps = new Set;
188269
188325
  constructor(options = {}) {
188270
188326
  super();
@@ -188280,6 +188336,7 @@ class JinjaAdapter extends BaseAdapter {
188280
188336
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188281
188337
  this.booleanTypedProps = collectBooleanTypedProps(ir);
188282
188338
  this.localConstants = ir.metadata.localConstants ?? [];
188339
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
188283
188340
  this.nullableOptionalProps = collectNullableOptionalProps(ir);
188284
188341
  this.stringValueNames = collectStringValueNames(ir);
188285
188342
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
@@ -188333,7 +188390,7 @@ class JinjaAdapter extends BaseAdapter {
188333
188390
  return this.renderElement(node);
188334
188391
  }
188335
188392
  emitText(node) {
188336
- return node.value;
188393
+ return escapeHtml(node.value);
188337
188394
  }
188338
188395
  emitExpression(node) {
188339
188396
  return this.renderExpression(node);
@@ -188401,7 +188458,8 @@ class JinjaAdapter extends BaseAdapter {
188401
188458
  renderElement(element) {
188402
188459
  const tag = element.tag;
188403
188460
  const attrs = this.renderAttributes(element);
188404
- const children = this.renderChildren(element.children);
188461
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
188462
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
188405
188463
  let hydrationAttrs = "";
188406
188464
  if (element.needsScope) {
188407
188465
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
@@ -188436,6 +188494,22 @@ class JinjaAdapter extends BaseAdapter {
188436
188494
  }
188437
188495
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188438
188496
  }
188497
+ renderDangerousInnerHtml(element) {
188498
+ const resolution = resolveDangerousInnerHtml(element);
188499
+ if (!resolution)
188500
+ return null;
188501
+ if (resolution.kind === "dynamic") {
188502
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
188503
+ return "";
188504
+ }
188505
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
188506
+ if (violation) {
188507
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
188508
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
188509
+ return "";
188510
+ }
188511
+ return resolution.html;
188512
+ }
188439
188513
  renderExpression(expr) {
188440
188514
  if (expr.clientOnly) {
188441
188515
  if (expr.slotId) {
@@ -188443,7 +188517,7 @@ class JinjaAdapter extends BaseAdapter {
188443
188517
  }
188444
188518
  return "";
188445
188519
  }
188446
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
188520
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
188447
188521
  if (expr.slotId) {
188448
188522
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
188449
188523
  }
@@ -188535,8 +188609,12 @@ ${whenTrue}
188535
188609
  }
188536
188610
  });
188537
188611
  }
188612
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
188613
+ isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
188614
+ });
188615
+ const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null;
188538
188616
  const arrayName = loop.array.trim();
188539
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188617
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188540
188618
  const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188541
188619
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188542
188620
  this.errors.push({
@@ -188550,7 +188628,7 @@ ${whenTrue}
188550
188628
  });
188551
188629
  }
188552
188630
  }
188553
- const rawArray = this.convertExpressionToJinja(loop.array);
188631
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
188554
188632
  let array = rawArray;
188555
188633
  if (loop.sortComparator) {
188556
188634
  const sort = loop.sortComparator;
@@ -188564,7 +188642,7 @@ ${whenTrue}
188564
188642
  const renderedChildren = this.renderChildren(loop.children);
188565
188643
  const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
188566
188644
  const indexLocalLines = [];
188567
- if (loop.iterationShape === "keys") {
188645
+ if (loop.objectIteration) {} else if (loop.iterationShape === "keys") {
188568
188646
  indexLocalLines.push(`{% set ${jinjaIdent(param)} = loop.index0 %}`);
188569
188647
  } else if (loop.index) {
188570
188648
  indexLocalLines.push(`{% set ${jinjaIdent(loop.index)} = loop.index0 %}`);
@@ -188584,13 +188662,17 @@ ${whenTrue}
188584
188662
  }
188585
188663
  const prevInLoop = this.inLoop;
188586
188664
  this.inLoop = true;
188665
+ const prevLoopKeyDepth = this.currentLoopKeyDepth;
188666
+ this.currentLoopKeyDepth = loop.depth;
188587
188667
  const childrenUnderLoop = this.renderChildren(loop.children);
188668
+ this.currentLoopKeyDepth = prevLoopKeyDepth;
188588
188669
  this.inLoop = prevInLoop;
188589
188670
  const bodyChildren = loop.bodyIsItemConditional && loop.key ? `{{ bf.comment("loop-i:" ~ bf.string(${this.convertExpressionToJinja(loop.key)})) | safe }}
188590
188671
  ${childrenUnderLoop}` : childrenUnderLoop;
188591
188672
  const lines = [];
188592
188673
  lines.push(`{{ bf.comment("loop:${loop.markerId}") | safe }}`);
188593
- lines.push(`{% for ${jinjaIdent(loopVar)} in ${array} %}`);
188674
+ const forHeader = loop.objectIteration === "entries" ? `{% for ${jinjaIdent(loop.index ?? param)}, ${jinjaIdent(param)} in ${array}.items() %}` : loop.objectIteration === "keys" ? `{% for ${jinjaIdent(param)} in ${array}.keys() %}` : loop.objectIteration === "values" ? `{% for ${jinjaIdent(param)} in ${array}.values() %}` : `{% for ${jinjaIdent(loopVar)} in ${array} %}`;
188675
+ lines.push(forHeader);
188594
188676
  for (const il of indexLocalLines)
188595
188677
  lines.push(il);
188596
188678
  if (loop.filterPredicate) {
@@ -188662,9 +188744,20 @@ ${childrenUnderLoop}` : childrenUnderLoop;
188662
188744
  renderComponent(comp) {
188663
188745
  const segments = [{ kind: "entries", parts: [] }];
188664
188746
  const currentEntries = () => this.componentPropSegmentEntries(segments);
188747
+ const namedSlotSetBlocks = [];
188665
188748
  for (const p of comp.props) {
188666
188749
  if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
188667
188750
  continue;
188751
+ if (p.value.kind === "jsx-children" && p.name !== "children") {
188752
+ const prevInLoop = this.inLoop;
188753
+ this.inLoop = false;
188754
+ const slotBody = this.renderChildren(p.value.children);
188755
+ this.inLoop = prevInLoop;
188756
+ const captureName = `bf_prop_${this.childrenCaptureCounter++}`;
188757
+ namedSlotSetBlocks.push(`{% set ${captureName} %}${slotBody}{% endset %}`);
188758
+ currentEntries().push(`${jinjaHashKey(p.name)}: ${captureName}`);
188759
+ continue;
188760
+ }
188668
188761
  if (p.value.kind === "spread") {
188669
188762
  const trimmed = p.value.expr.trim();
188670
188763
  if (this.propsObjectName && this.propsObjectName === trimmed) {
@@ -188693,11 +188786,11 @@ ${childrenUnderLoop}` : childrenUnderLoop;
188693
188786
  const captureName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
188694
188787
  currentEntries().push(`${jinjaHashKey("children")}: ${captureName}`);
188695
188788
  const dict = this.combineComponentPropSegments(segments);
188696
- return `{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`;
188789
+ return `${namedSlotSetBlocks.join("")}{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`;
188697
188790
  }
188698
188791
  const isEmpty = segments.every((s) => s.kind === "entries" && s.parts.length === 0);
188699
188792
  const dictEntries = isEmpty ? "" : `, ${this.combineComponentPropSegments(segments)}`;
188700
- return `{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`;
188793
+ return `${namedSlotSetBlocks.join("")}{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`;
188701
188794
  }
188702
188795
  childrenCaptureCounter = 0;
188703
188796
  presenceVarCounter = 0;
@@ -188742,7 +188835,7 @@ ${alternate}
188742
188835
  ${children}`;
188743
188836
  }
188744
188837
  elementAttrEmitter = {
188745
- emitLiteral: (value, name) => `${name}="${value.value}"`,
188838
+ emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
188746
188839
  emitExpression: (value, name) => {
188747
188840
  if (name === "style") {
188748
188841
  const css = this.tryLowerStyleObject(value.expr);
@@ -188847,12 +188940,15 @@ ${name}="{{ bf.string(${val}) }}"
188847
188940
  for (const attr of element.attrs) {
188848
188941
  if (attr.clientOnly)
188849
188942
  continue;
188943
+ if (isDangerousInnerHtmlAttr(attr))
188944
+ continue;
188850
188945
  let attrName;
188851
188946
  if (attr.name === "className")
188852
188947
  attrName = "class";
188853
- else if (attr.name === "key")
188854
- attrName = "data-key";
188855
- else
188948
+ else if (attr.name === "key") {
188949
+ const depth = this.currentLoopKeyDepth;
188950
+ attrName = depth > 0 ? `data-key-${depth}` : "data-key";
188951
+ } else
188856
188952
  attrName = attr.name;
188857
188953
  const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
188858
188954
  if (lowered)
@@ -189045,6 +189141,8 @@ Options:
189045
189141
  return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
189046
189142
  }
189047
189143
  _resolveLiteralConst(name) {
189144
+ if (this.staticLoopSourceBoundNames.has(name))
189145
+ return null;
189048
189146
  const c = (this.localConstants ?? []).find((lc) => lc.name === name);
189049
189147
  if (c?.value === undefined)
189050
189148
  return null;
@@ -189057,6 +189155,8 @@ Options:
189057
189155
  return null;
189058
189156
  }
189059
189157
  _resolveStaticRecordLiteral(objectName, key) {
189158
+ if (this.staticLoopSourceBoundNames.has(objectName))
189159
+ return null;
189060
189160
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
189061
189161
  if (!hit)
189062
189162
  return null;
@@ -189094,14 +189194,10 @@ Options:
189094
189194
  var jinjaAdapter = new JinjaAdapter;
189095
189195
  // src/conformance-pins.ts
189096
189196
  var conformancePins = {
189097
- "static-array-children": [{ code: "BF103", severity: "error" }],
189098
- "todo-app": [{ code: "BF103", severity: "error" }],
189099
- "todo-app-ssr": [{ code: "BF103", severity: "error" }],
189100
189197
  "static-array-from-props": [
189101
189198
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189102
189199
  ],
189103
189200
  "static-array-from-props-with-component": [
189104
- { code: "BF103", severity: "error" },
189105
189201
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189106
189202
  ],
189107
189203
  "filter-nested-callback-predicate": [
@@ -189110,21 +189206,10 @@ var conformancePins = {
189110
189206
  "filter-nested-find-predicate": [
189111
189207
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189112
189208
  ],
189113
- "array-map-function-reference": [{ code: "BF101", severity: "error" }],
189114
- "dangerous-inner-html": [{ code: "BF101", severity: "error" }],
189115
- "string-replaceall": [{ code: "BF101", severity: "error" }]
189209
+ "dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
189116
189210
  };
189117
189211
  // src/render-divergences.ts
189118
- var renderDivergences = {
189119
- "html-entity-text": "`&copy;` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
189120
- "math-methods": "Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)",
189121
- "static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
189122
- "object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations",
189123
- "nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
189124
- "jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
189125
- "string-slice": "`.slice()` on a STRING renders empty (array-slice helper misfires on strings)",
189126
- "string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering)"
189127
- };
189212
+ var renderDivergences = {};
189128
189213
  export {
189129
189214
  renderDivergences,
189130
189215
  jinjaAdapter,
@@ -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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/jinja",
3
- "version": "0.18.4",
3
+ "version": "0.18.7",
4
4
  "description": "Jinja2 adapter for BarefootJS — compiles IR to .jinja templates and ships the Python BarefootJS rendering runtime; runs under any Python web framework (Flask, etc.)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,14 +53,14 @@
53
53
  "directory": "packages/adapter-jinja"
54
54
  },
55
55
  "dependencies": {
56
- "@barefootjs/shared": "0.18.4"
56
+ "@barefootjs/shared": "0.18.7"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@barefootjs/jsx": ">=0.2.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@barefootjs/adapter-tests": "0.1.0",
63
- "@barefootjs/jsx": "0.18.4",
63
+ "@barefootjs/jsx": "0.18.7",
64
64
  "typescript": "^5.0.0"
65
65
  }
66
66
  }
@@ -819,6 +819,29 @@ class BarefootJS:
819
819
  # is -1, not -2). `floor(n + 0.5)` reproduces that for both signs.
820
820
  return float(math.floor(n + 0.5))
821
821
 
822
+ def min(self, a: Any, b: Any) -> float:
823
+ """`Math.min(a, b)` -- two-arg form only (#2168 math-methods).
824
+ JS returns NaN if either operand is NaN."""
825
+ x, y = js_number(a), js_number(b)
826
+ if _is_nan(x):
827
+ return x
828
+ if _is_nan(y):
829
+ return y
830
+ return x if x < y else y
831
+
832
+ def max(self, a: Any, b: Any) -> float:
833
+ x, y = js_number(a), js_number(b)
834
+ if _is_nan(x):
835
+ return x
836
+ if _is_nan(y):
837
+ return y
838
+ return x if x > y else y
839
+
840
+ def abs(self, value: Any) -> float:
841
+ """`Math.abs()` (#2168 math-methods)."""
842
+ n = js_number(value)
843
+ return n if _is_nan(n) else abs(n)
844
+
822
845
  # -----------------------------------------------------------------
823
846
  # Array / String method helpers (#1448 Tier A)
824
847
  # -----------------------------------------------------------------
@@ -933,12 +956,20 @@ class BarefootJS:
933
956
  out.extend(b)
934
957
  return out
935
958
 
936
- def slice(self, recv: Any, start: Any, end: Any) -> list:
937
- if not isinstance(recv, list):
959
+ def slice(self, recv: Any, start: Any, end: Any) -> Any:
960
+ # `Array.prototype.slice(start, end?)` AND `String.prototype.slice`
961
+ # (the `string-slice` divergence, #2182) -- the adapter emits the
962
+ # same `bf.slice(recv, start, end)` call for both receiver shapes
963
+ # (it can't disambiguate string vs. array at compile time), so
964
+ # this dispatches on the Python type, mirroring `includes` above.
965
+ # `str` and `list` share slicing semantics (`recv[s:e]`) and
966
+ # `len()`, so one clamp computation serves both; `str` indexing
967
+ # is by Unicode code point already, matching JS except for
968
+ # astral-plane input (the same divergence boundary every other
969
+ # adapter's pad/trim helpers already accept).
970
+ if not isinstance(recv, (list, str)):
938
971
  return []
939
972
  length = len(recv)
940
- if length == 0:
941
- return []
942
973
  s = start if start is not None else 0
943
974
  if s < 0:
944
975
  s = length + s
@@ -950,8 +981,8 @@ class BarefootJS:
950
981
  e = max(e, 0)
951
982
  e = min(e, length)
952
983
  if s >= e:
953
- return []
954
- return list(recv[s:e])
984
+ return recv[0:0]
985
+ return recv[s:e] if isinstance(recv, str) else list(recv[s:e])
955
986
 
956
987
  def reverse(self, recv: Any) -> list:
957
988
  if not isinstance(recv, list):
@@ -1014,6 +1045,20 @@ class BarefootJS:
1014
1045
  return ""
1015
1046
  return js_string(recv).strip()
1016
1047
 
1048
+ def trim_start(self, recv: Any) -> str:
1049
+ """`String.prototype.trimStart()` -- the one-sided sibling of `trim`
1050
+ above (#2183 follow-up)."""
1051
+ if recv is None or isinstance(recv, (list, dict)):
1052
+ return ""
1053
+ return js_string(recv).lstrip()
1054
+
1055
+ def trim_end(self, recv: Any) -> str:
1056
+ """`String.prototype.trimEnd()` -- the one-sided sibling of `trim`
1057
+ above (#2183 follow-up)."""
1058
+ if recv is None or isinstance(recv, (list, dict)):
1059
+ return ""
1060
+ return js_string(recv).rstrip()
1061
+
1017
1062
  def to_fixed(self, value: Any, digits: int = 0) -> str:
1018
1063
  n = self.number(value)
1019
1064
  if _is_nan(n):
@@ -1076,6 +1121,19 @@ class BarefootJS:
1076
1121
  return s
1077
1122
  return s[:i] + n + s[i + len(o) :]
1078
1123
 
1124
+ def replace_all(self, recv: Any, pattern: Any, replacement: Any) -> str:
1125
+ """`String.prototype.replaceAll(pattern, replacement)`, string-pattern
1126
+ form only (#2182) -- every occurrence, the all-occurrences sibling of
1127
+ `replace` above. Python's own `str.replace(old, new)` (no count arg)
1128
+ is already global by default, including the empty-pattern-inserts-
1129
+ at-every-boundary edge case (`"abc".replace("", "X")` -> "XaXbXcX"),
1130
+ so it needs no hand-rolled loop the way the other runtimes' first-
1131
+ occurrence-only native replace does."""
1132
+ s = _scalar_or_empty(recv)
1133
+ o = js_string(pattern)
1134
+ n = js_string(replacement)
1135
+ return s.replace(o, n)
1136
+
1079
1137
  def query(self, base: Any, *triples: Any) -> str:
1080
1138
  """`queryHref(base, {...})` (#2042) -- build `"$base?k=v&..."` from a
1081
1139
  flat list of (guard, key, value) triples. A pair is included iff its
@@ -117,13 +117,19 @@ BINDINGS = {
117
117
  "floor": bf.floor,
118
118
  "ceil": bf.ceil,
119
119
  "round": bf.round,
120
+ "min": bf.min,
121
+ "max": bf.max,
122
+ "abs": bf.abs,
120
123
  "to_fixed": lambda *a: bf.to_fixed(*a),
121
124
  "lower": bf.lc,
122
125
  "upper": bf.uc,
123
126
  "trim": bf.trim,
127
+ "trim_start": bf.trim_start,
128
+ "trim_end": bf.trim_end,
124
129
  "starts_with": lambda *a: bf.starts_with(*a),
125
130
  "ends_with": lambda *a: bf.ends_with(*a),
126
131
  "replace": lambda *a: bf.replace(*a),
132
+ "replace_all": lambda *a: bf.replace_all(*a),
127
133
  "repeat": lambda *a: bf.repeat(*a),
128
134
  "pad_start": lambda *a: bf.pad_start(*a),
129
135
  "pad_end": lambda *a: bf.pad_end(*a),
@@ -74,6 +74,27 @@ class TemplatePrimitivesTest(unittest.TestCase):
74
74
  self.assertEqual(self.bf.round(-1.6), -2)
75
75
  self.assertTrue(_is_nan(self.bf.round("not")))
76
76
 
77
+ def test_min_max_abs(self):
78
+ # `Math.min(a, b)` / `Math.max(a, b)` (two-arg forms only) and
79
+ # `Math.abs()` (#2168 math-methods). JS returns NaN if EITHER
80
+ # min/max operand is NaN.
81
+ self.assertEqual(self.bf.min(3, 7), 3)
82
+ self.assertEqual(self.bf.min(7, 3), 3)
83
+ self.assertEqual(self.bf.min(-2, -5), -5)
84
+ self.assertTrue(_is_nan(self.bf.min("not", 5)))
85
+ self.assertTrue(_is_nan(self.bf.min(5, "not")))
86
+
87
+ self.assertEqual(self.bf.max(3, 7), 7)
88
+ self.assertEqual(self.bf.max(7, 3), 7)
89
+ self.assertEqual(self.bf.max(-2, -5), -2)
90
+ self.assertTrue(_is_nan(self.bf.max("not", 5)))
91
+ self.assertTrue(_is_nan(self.bf.max(5, "not")))
92
+
93
+ self.assertEqual(self.bf.abs(-7.6), 7.6)
94
+ self.assertEqual(self.bf.abs(7.6), 7.6)
95
+ self.assertEqual(self.bf.abs(0), 0)
96
+ self.assertTrue(_is_nan(self.bf.abs("not")))
97
+
77
98
  def test_includes_dispatch(self):
78
99
  # `Array.prototype.includes(x)` + `String.prototype.includes(sub)`
79
100
  # lower to the same `bf.includes(recv, elem)` shape -- see #1448
@@ -158,6 +179,18 @@ class TemplatePrimitivesTest(unittest.TestCase):
158
179
  out.append("mutated")
159
180
  self.assertEqual(src, ["a", "b", "c"])
160
181
 
182
+ def test_slice_string_receiver(self):
183
+ # The `string-slice` divergence (#2182): a string receiver used
184
+ # to fall through the array-only branch and return an empty
185
+ # list instead of a substring.
186
+ word = "barefootjs"
187
+ self.assertEqual(self.bf.slice(word, 0, 4), "bare")
188
+ self.assertEqual(self.bf.slice(word, -4, None), "otjs")
189
+ self.assertEqual(self.bf.slice(word, 4, None), "footjs")
190
+ self.assertEqual(self.bf.slice(word, 5, 2), "")
191
+ # Multi-byte: index by character, not byte.
192
+ self.assertEqual(self.bf.slice("héllo", 0, 2), "hé")
193
+
161
194
  def test_reverse_mutation_isolation(self):
162
195
  self.assertEqual(self.bf.reverse(["a", "b", "c"]), ["c", "b", "a"])
163
196
  self.assertEqual(self.bf.reverse([]), [])
@@ -175,6 +208,18 @@ class TemplatePrimitivesTest(unittest.TestCase):
175
208
  self.assertEqual(self.bf.trim({"a": 1}), "")
176
209
  self.assertEqual(self.bf.trim(42), "42")
177
210
 
211
+ def test_trim_start_and_trim_end(self):
212
+ # The one-sided siblings of `trim` above (#2183). Padding BOTH
213
+ # sides so a swapped side fails visibly.
214
+ self.assertEqual(self.bf.trim_start(" padded "), "padded ")
215
+ self.assertEqual(self.bf.trim_end(" padded "), " padded")
216
+ self.assertEqual(self.bf.trim_start(""), "")
217
+ self.assertEqual(self.bf.trim_end(""), "")
218
+ self.assertEqual(self.bf.trim_start(None), "")
219
+ self.assertEqual(self.bf.trim_end(None), "")
220
+ self.assertEqual(self.bf.trim_start({"a": 1}), "")
221
+ self.assertEqual(self.bf.trim_end([1, 2]), "")
222
+
178
223
  def test_split(self):
179
224
  self.assertEqual(self.bf.split("a,b,c", ","), ["a", "b", "c"])
180
225
  self.assertEqual(self.bf.split("a.b.c", "."), ["a", "b", "c"])