@barefootjs/rust 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/minijinja-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
 
@@ -187544,6 +187553,12 @@ function renderArrayMethod(method, object, args, emit) {
187544
187553
  const recv = emit(object);
187545
187554
  return `bf.trim(${recv})`;
187546
187555
  }
187556
+ case "trimStart":
187557
+ case "trimEnd": {
187558
+ const fn = method === "trimStart" ? "trim_start" : "trim_end";
187559
+ const recv = emit(object);
187560
+ return `bf.${fn}(${recv})`;
187561
+ }
187547
187562
  case "toFixed": {
187548
187563
  const recv = emit(object);
187549
187564
  const digits = args.length >= 1 ? emit(args[0]) : "0";
@@ -187577,6 +187592,12 @@ function renderArrayMethod(method, object, args, emit) {
187577
187592
  const newS = emit(args[1]);
187578
187593
  return `bf.replace(${recv}, ${oldS}, ${newS})`;
187579
187594
  }
187595
+ case "replaceAll": {
187596
+ const recv = emit(object);
187597
+ const oldS = emit(args[0]);
187598
+ const newS = emit(args[1]);
187599
+ return `bf.replace_all(${recv}, ${oldS}, ${newS})`;
187600
+ }
187580
187601
  case "repeat": {
187581
187602
  const recv = emit(object);
187582
187603
  const count = args.length === 0 ? "0" : emit(args[0]);
@@ -187671,6 +187692,39 @@ function renderFlatMethod(recv, depth, emit) {
187671
187692
  return `bf.flat(${recv}, ${d})`;
187672
187693
  }
187673
187694
 
187695
+ // src/adapter/lib/static-value.ts
187696
+ function staticValueToMinijinja(value) {
187697
+ if (value === null || value === undefined)
187698
+ return "none";
187699
+ if (typeof value === "boolean")
187700
+ return value ? "true" : "false";
187701
+ if (typeof value === "number")
187702
+ return String(value);
187703
+ if (typeof value === "string")
187704
+ return `'${escapeMinijinjaSingleQuoted(value)}'`;
187705
+ if (Array.isArray(value)) {
187706
+ const items = [];
187707
+ for (const el of value) {
187708
+ const serialized = staticValueToMinijinja(el);
187709
+ if (serialized === null)
187710
+ return null;
187711
+ items.push(serialized);
187712
+ }
187713
+ return `[${items.join(", ")}]`;
187714
+ }
187715
+ if (typeof value === "object") {
187716
+ const entries = [];
187717
+ for (const [key, val] of Object.entries(value)) {
187718
+ const serialized = staticValueToMinijinja(val);
187719
+ if (serialized === null)
187720
+ return null;
187721
+ entries.push(`${minijinjaHashKey(key)}: ${serialized}`);
187722
+ }
187723
+ return `{${entries.join(", ")}}`;
187724
+ }
187725
+ return null;
187726
+ }
187727
+
187674
187728
  // src/adapter/expr/emitters.ts
187675
187729
  import {
187676
187730
  groupBinaryOperand,
@@ -187719,7 +187773,7 @@ class JinjaFilterEmitter {
187719
187773
  return "none";
187720
187774
  return String(value);
187721
187775
  }
187722
- member(object, property, _computed, emit) {
187776
+ member(object, property, _computed, _optional, emit) {
187723
187777
  if (property === "length") {
187724
187778
  return `bf.length(${emit(object)})`;
187725
187779
  }
@@ -187825,7 +187879,7 @@ class JinjaTopLevelEmitter {
187825
187879
  return "none";
187826
187880
  return String(value);
187827
187881
  }
187828
- member(object, property, _computed, emit) {
187882
+ member(object, property, _computed, _optional, emit) {
187829
187883
  if (object.kind === "identifier" && object.name === "props") {
187830
187884
  return minijinjaIdent(property);
187831
187885
  }
@@ -188252,6 +188306,7 @@ class MinijinjaAdapter extends BaseAdapter {
188252
188306
  options;
188253
188307
  errors = [];
188254
188308
  inLoop = false;
188309
+ currentLoopKeyDepth = 0;
188255
188310
  propsObjectName = null;
188256
188311
  propsParams = [];
188257
188312
  booleanTypedProps = new Set;
@@ -188260,6 +188315,7 @@ class MinijinjaAdapter extends BaseAdapter {
188260
188315
  _searchParamsLocals = new Set;
188261
188316
  _loweringMatchers = [];
188262
188317
  localConstants = [];
188318
+ staticLoopSourceBoundNames = new Set;
188263
188319
  nullableOptionalProps = new Set;
188264
188320
  constructor(options = {}) {
188265
188321
  super();
@@ -188275,6 +188331,7 @@ class MinijinjaAdapter extends BaseAdapter {
188275
188331
  this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
188276
188332
  this.booleanTypedProps = collectBooleanTypedProps(ir);
188277
188333
  this.localConstants = ir.metadata.localConstants ?? [];
188334
+ this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
188278
188335
  this.nullableOptionalProps = collectNullableOptionalProps(ir);
188279
188336
  this.stringValueNames = collectStringValueNames(ir);
188280
188337
  this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
@@ -188328,7 +188385,7 @@ class MinijinjaAdapter extends BaseAdapter {
188328
188385
  return this.renderElement(node);
188329
188386
  }
188330
188387
  emitText(node) {
188331
- return node.value;
188388
+ return escapeHtml(node.value);
188332
188389
  }
188333
188390
  emitExpression(node) {
188334
188391
  return this.renderExpression(node);
@@ -188396,7 +188453,8 @@ class MinijinjaAdapter extends BaseAdapter {
188396
188453
  renderElement(element) {
188397
188454
  const tag = element.tag;
188398
188455
  const attrs = this.renderAttributes(element);
188399
- const children = this.renderChildren(element.children);
188456
+ const dangerousHtml = this.renderDangerousInnerHtml(element);
188457
+ const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
188400
188458
  let hydrationAttrs = "";
188401
188459
  if (element.needsScope) {
188402
188460
  hydrationAttrs += ` ${this.renderScopeMarker("")}`;
@@ -188431,6 +188489,22 @@ class MinijinjaAdapter extends BaseAdapter {
188431
188489
  }
188432
188490
  return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
188433
188491
  }
188492
+ renderDangerousInnerHtml(element) {
188493
+ const resolution = resolveDangerousInnerHtml(element);
188494
+ if (!resolution)
188495
+ return null;
188496
+ if (resolution.kind === "dynamic") {
188497
+ this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
188498
+ return "";
188499
+ }
188500
+ const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
188501
+ if (violation) {
188502
+ const attr = element.attrs.find(isDangerousInnerHtmlAttr);
188503
+ this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
188504
+ return "";
188505
+ }
188506
+ return resolution.html;
188507
+ }
188434
188508
  renderExpression(expr) {
188435
188509
  if (expr.clientOnly) {
188436
188510
  if (expr.slotId) {
@@ -188438,7 +188512,7 @@ class MinijinjaAdapter extends BaseAdapter {
188438
188512
  }
188439
188513
  return "";
188440
188514
  }
188441
- const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
188515
+ const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
188442
188516
  if (expr.slotId) {
188443
188517
  return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
188444
188518
  }
@@ -188529,8 +188603,12 @@ ${whenTrue}
188529
188603
  }
188530
188604
  });
188531
188605
  }
188606
+ const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
188607
+ isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
188608
+ });
188609
+ const staticArray = staticItems !== null ? staticValueToMinijinja(staticItems) : null;
188532
188610
  const arrayName = loop.array.trim();
188533
- if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188611
+ if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
188534
188612
  const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
188535
188613
  if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
188536
188614
  this.errors.push({
@@ -188544,7 +188622,7 @@ ${whenTrue}
188544
188622
  });
188545
188623
  }
188546
188624
  }
188547
- const rawArray = this.convertExpressionToJinja(loop.array);
188625
+ const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
188548
188626
  let array = rawArray;
188549
188627
  if (loop.sortComparator) {
188550
188628
  const sort = loop.sortComparator;
@@ -188558,7 +188636,7 @@ ${whenTrue}
188558
188636
  const renderedChildren = this.renderChildren(loop.children);
188559
188637
  const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
188560
188638
  const indexLocalLines = [];
188561
- if (loop.iterationShape === "keys") {
188639
+ if (loop.objectIteration) {} else if (loop.iterationShape === "keys") {
188562
188640
  indexLocalLines.push(`{% set ${minijinjaIdent(param)} = loop.index0 %}`);
188563
188641
  } else if (loop.index) {
188564
188642
  indexLocalLines.push(`{% set ${minijinjaIdent(loop.index)} = loop.index0 %}`);
@@ -188578,13 +188656,17 @@ ${whenTrue}
188578
188656
  }
188579
188657
  const prevInLoop = this.inLoop;
188580
188658
  this.inLoop = true;
188659
+ const prevLoopKeyDepth = this.currentLoopKeyDepth;
188660
+ this.currentLoopKeyDepth = loop.depth;
188581
188661
  const childrenUnderLoop = this.renderChildren(loop.children);
188662
+ this.currentLoopKeyDepth = prevLoopKeyDepth;
188582
188663
  this.inLoop = prevInLoop;
188583
188664
  const bodyChildren = loop.bodyIsItemConditional && loop.key ? `{{ bf.comment("loop-i:" ~ bf.string(${this.convertExpressionToJinja(loop.key)})) | safe }}
188584
188665
  ${childrenUnderLoop}` : childrenUnderLoop;
188585
188666
  const lines = [];
188586
188667
  lines.push(`{{ bf.comment("loop:${loop.markerId}") | safe }}`);
188587
- lines.push(`{% for ${minijinjaIdent(loopVar)} in ${array} %}`);
188668
+ const forHeader = loop.objectIteration === "entries" ? `{% for ${minijinjaIdent(loop.index ?? param)}, ${minijinjaIdent(param)} in ${array}|items %}` : loop.objectIteration === "keys" ? `{% for ${minijinjaIdent(param)}, __bf_v in ${array}|items %}` : loop.objectIteration === "values" ? `{% for __bf_k, ${minijinjaIdent(param)} in ${array}|items %}` : `{% for ${minijinjaIdent(loopVar)} in ${array} %}`;
188669
+ lines.push(forHeader);
188588
188670
  for (const il of indexLocalLines)
188589
188671
  lines.push(il);
188590
188672
  if (loop.filterPredicate) {
@@ -188656,9 +188738,20 @@ ${childrenUnderLoop}` : childrenUnderLoop;
188656
188738
  renderComponent(comp) {
188657
188739
  const segments = [{ kind: "entries", parts: [] }];
188658
188740
  const currentEntries = () => this.componentPropSegmentEntries(segments);
188741
+ const namedSlotSetBlocks = [];
188659
188742
  for (const p of comp.props) {
188660
188743
  if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
188661
188744
  continue;
188745
+ if (p.value.kind === "jsx-children" && p.name !== "children") {
188746
+ const prevInLoop = this.inLoop;
188747
+ this.inLoop = false;
188748
+ const slotBody = this.renderChildren(p.value.children);
188749
+ this.inLoop = prevInLoop;
188750
+ const captureName = `bf_prop_${this.childrenCaptureCounter++}`;
188751
+ namedSlotSetBlocks.push(`{% set ${captureName} %}${slotBody}{% endset %}`);
188752
+ currentEntries().push(`${minijinjaHashKey(p.name)}: ${captureName}`);
188753
+ continue;
188754
+ }
188662
188755
  if (p.value.kind === "spread") {
188663
188756
  const trimmed = p.value.expr.trim();
188664
188757
  if (this.propsObjectName && this.propsObjectName === trimmed) {
@@ -188687,11 +188780,11 @@ ${childrenUnderLoop}` : childrenUnderLoop;
188687
188780
  const captureName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
188688
188781
  currentEntries().push(`${minijinjaHashKey("children")}: ${captureName}`);
188689
188782
  const dict = this.combineComponentPropSegments(segments);
188690
- return `{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`;
188783
+ return `${namedSlotSetBlocks.join("")}{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`;
188691
188784
  }
188692
188785
  const isEmpty = segments.every((s) => s.kind === "entries" && s.parts.length === 0);
188693
188786
  const dictEntries = isEmpty ? "" : `, ${this.combineComponentPropSegments(segments)}`;
188694
- return `{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`;
188787
+ return `${namedSlotSetBlocks.join("")}{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`;
188695
188788
  }
188696
188789
  childrenCaptureCounter = 0;
188697
188790
  presenceVarCounter = 0;
@@ -188736,7 +188829,7 @@ ${alternate}
188736
188829
  ${children}`;
188737
188830
  }
188738
188831
  elementAttrEmitter = {
188739
- emitLiteral: (value, name) => `${name}="${value.value}"`,
188832
+ emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
188740
188833
  emitExpression: (value, name) => {
188741
188834
  if (name === "style") {
188742
188835
  const css = this.tryLowerStyleObject(value.expr);
@@ -188841,12 +188934,15 @@ ${name}="{{ bf.string(${val}) }}"
188841
188934
  for (const attr of element.attrs) {
188842
188935
  if (attr.clientOnly)
188843
188936
  continue;
188937
+ if (isDangerousInnerHtmlAttr(attr))
188938
+ continue;
188844
188939
  let attrName;
188845
188940
  if (attr.name === "className")
188846
188941
  attrName = "class";
188847
- else if (attr.name === "key")
188848
- attrName = "data-key";
188849
- else
188942
+ else if (attr.name === "key") {
188943
+ const depth = this.currentLoopKeyDepth;
188944
+ attrName = depth > 0 ? `data-key-${depth}` : "data-key";
188945
+ } else
188850
188946
  attrName = attr.name;
188851
188947
  const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
188852
188948
  if (lowered)
@@ -189039,6 +189135,8 @@ Options:
189039
189135
  return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
189040
189136
  }
189041
189137
  _resolveLiteralConst(name) {
189138
+ if (this.staticLoopSourceBoundNames.has(name))
189139
+ return null;
189042
189140
  const c = (this.localConstants ?? []).find((lc) => lc.name === name);
189043
189141
  if (c?.value === undefined)
189044
189142
  return null;
@@ -189051,6 +189149,8 @@ Options:
189051
189149
  return null;
189052
189150
  }
189053
189151
  _resolveStaticRecordLiteral(objectName, key) {
189152
+ if (this.staticLoopSourceBoundNames.has(objectName))
189153
+ return null;
189054
189154
  const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
189055
189155
  if (!hit)
189056
189156
  return null;
@@ -189088,14 +189188,10 @@ Options:
189088
189188
  var minijinjaAdapter = new MinijinjaAdapter;
189089
189189
  // src/conformance-pins.ts
189090
189190
  var conformancePins = {
189091
- "static-array-children": [{ code: "BF103", severity: "error" }],
189092
- "todo-app": [{ code: "BF103", severity: "error" }],
189093
- "todo-app-ssr": [{ code: "BF103", severity: "error" }],
189094
189191
  "static-array-from-props": [
189095
189192
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189096
189193
  ],
189097
189194
  "static-array-from-props-with-component": [
189098
- { code: "BF103", severity: "error" },
189099
189195
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
189100
189196
  ],
189101
189197
  "filter-nested-callback-predicate": [
@@ -189104,21 +189200,10 @@ var conformancePins = {
189104
189200
  "filter-nested-find-predicate": [
189105
189201
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189106
189202
  ],
189107
- "array-map-function-reference": [{ code: "BF101", severity: "error" }],
189108
- "dangerous-inner-html": [{ code: "BF101", severity: "error" }],
189109
- "string-replaceall": [{ code: "BF101", severity: "error" }]
189203
+ "dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
189110
189204
  };
189111
189205
  // src/render-divergences.ts
189112
- var renderDivergences = {
189113
- "html-entity-text": "`&copy;` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
189114
- "math-methods": "Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)",
189115
- "static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
189116
- "object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations",
189117
- "nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
189118
- "jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
189119
- "string-slice": "`.slice()` on a STRING renders empty (array-slice helper misfires on strings)",
189120
- "string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering)"
189121
- };
189206
+ var renderDivergences = {};
189122
189207
  export {
189123
189208
  renderDivergences,
189124
189209
  minijinjaAdapter,
@@ -1 +1 @@
1
- {"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;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;;;;;;;;;;;;;;GAcG;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/rust",
3
- "version": "0.18.4",
3
+ "version": "0.18.7",
4
4
  "description": "minijinja (Rust) adapter for BarefootJS — compiles IR to .j2 templates and ships a Rust rendering runtime (packages/adapter-rust/runtime/); runs under any Rust web framework (axum, etc.)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,14 +54,14 @@
54
54
  "directory": "packages/adapter-rust"
55
55
  },
56
56
  "dependencies": {
57
- "@barefootjs/shared": "0.18.4"
57
+ "@barefootjs/shared": "0.18.7"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@barefootjs/jsx": ">=0.2.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@barefootjs/adapter-tests": "0.1.0",
64
- "@barefootjs/jsx": "0.18.4",
64
+ "@barefootjs/jsx": "0.18.7",
65
65
  "typescript": "^5.0.0"
66
66
  }
67
67
  }
@@ -373,6 +373,36 @@ pub fn js_round(n: f64) -> f64 {
373
373
  (n + 0.5).floor()
374
374
  }
375
375
 
376
+ /// `Math.min(a, b)` / `Math.max(a, b)` -- two-arg forms only (#2168
377
+ /// math-methods). NOT `f64::min`/`f64::max`: those follow IEEE-754
378
+ /// `minNum`/`maxNum` semantics and return the non-NaN operand when only
379
+ /// one side is NaN, whereas JS returns NaN if EITHER operand is NaN.
380
+ pub fn js_min(a: f64, b: f64) -> f64 {
381
+ if a.is_nan() {
382
+ return a;
383
+ }
384
+ if b.is_nan() {
385
+ return b;
386
+ }
387
+ if a < b { a } else { b }
388
+ }
389
+
390
+ pub fn js_max(a: f64, b: f64) -> f64 {
391
+ if a.is_nan() {
392
+ return a;
393
+ }
394
+ if b.is_nan() {
395
+ return b;
396
+ }
397
+ if a > b { a } else { b }
398
+ }
399
+
400
+ /// `Math.abs()` (#2168 math-methods). `f64::abs` already matches JS for
401
+ /// NaN (NaN.abs() is NaN) and both infinities, no guard needed.
402
+ pub fn js_abs(n: f64) -> f64 {
403
+ n.abs()
404
+ }
405
+
376
406
  /// JS `%`: remainder with the dividend's sign. Rust's `%` on `f64` already
377
407
  /// implements C `fmod` semantics (IEEE-754 remainder), so this is a
378
408
  /// documentation-only wrapper -- see the module docstring for why no
@@ -166,6 +166,34 @@ fn char_slice_to(s: &str, n: usize) -> String {
166
166
  s.chars().take(n).collect()
167
167
  }
168
168
 
169
+ /// `[start, end)` range slice by Unicode scalar value (`char`), not
170
+ /// byte offset -- shared by `slice`'s string branch. Matches JS except
171
+ /// for astral-plane input, the same divergence boundary `char_len` /
172
+ /// `char_slice_from` / `char_slice_to` already accept.
173
+ fn char_slice_range(s: &str, start: usize, end: usize) -> String {
174
+ if start >= end {
175
+ return String::new();
176
+ }
177
+ s.chars().skip(start).take(end - start).collect()
178
+ }
179
+
180
+ /// Clamp JS `.slice(start, end?)` bounds against a receiver of
181
+ /// `length` elements -- shared by `slice`'s array and string branches
182
+ /// below.
183
+ fn clamp_slice_range(length: i64, start: &JsValue, end: &JsValue) -> (i64, i64) {
184
+ let mut s = if matches!(start, JsValue::Null) { 0 } else { num::to_f64(start) as i64 };
185
+ if s < 0 {
186
+ s += length;
187
+ }
188
+ s = s.clamp(0, length);
189
+ let mut e = if matches!(end, JsValue::Null) { length } else { num::to_f64(end) as i64 };
190
+ if e < 0 {
191
+ e += length;
192
+ }
193
+ e = e.clamp(0, length);
194
+ (s, e)
195
+ }
196
+
169
197
  // ---------------------------------------------------------------------------
170
198
  // spread_attrs support (JSX intrinsic-element spread, #1407).
171
199
  // ---------------------------------------------------------------------------
@@ -1155,6 +1183,9 @@ impl Object for BfInstance {
1155
1183
  "floor" => Ok(MjValue::from(num::js_floor(js_number(a(0))))),
1156
1184
  "ceil" => Ok(MjValue::from(num::js_ceil(js_number(a(0))))),
1157
1185
  "round" => Ok(MjValue::from(num::js_round(js_number(a(0))))),
1186
+ "min" => Ok(MjValue::from(num::js_min(js_number(a(0)), js_number(a(1))))),
1187
+ "max" => Ok(MjValue::from(num::js_max(js_number(a(0)), js_number(a(1))))),
1188
+ "abs" => Ok(MjValue::from(num::js_abs(js_number(a(0))))),
1158
1189
  "to_fixed" => Ok(MjValue::from(num::to_fixed(js_number(a(0)), num::to_f64(a(1)) as i32))),
1159
1190
 
1160
1191
  // -- Array / string method helpers (#1448 Tier A) ------------------
@@ -1191,6 +1222,8 @@ impl Object for BfInstance {
1191
1222
  Ok(js_to_mj(&flat_map_tuple(a(0), &specs)))
1192
1223
  }
1193
1224
  "trim" => Ok(MjValue::from(trim(a(0)))),
1225
+ "trim_start" => Ok(MjValue::from(trim_start(a(0)))),
1226
+ "trim_end" => Ok(MjValue::from(trim_end(a(0)))),
1194
1227
  "split" => {
1195
1228
  let sep = if args.len() > 1 { Some(a(1)) } else { None };
1196
1229
  let limit = if args.len() > 2 && !matches!(a(2), JsValue::Null) { Some(num::to_f64(a(2)) as i64) } else { None };
@@ -1199,6 +1232,7 @@ impl Object for BfInstance {
1199
1232
  "starts_with" => Ok(MjValue::from(starts_with(a(0), a(1), a(2)))),
1200
1233
  "ends_with" => Ok(MjValue::from(ends_with(a(0), a(1), a(2)))),
1201
1234
  "replace" => Ok(MjValue::from(replace(a(0), a(1), a(2)))),
1235
+ "replace_all" => Ok(MjValue::from(replace_all(a(0), a(1), a(2)))),
1202
1236
  "query" => Ok(MjValue::from(query(a(0), &js_args[1..]))),
1203
1237
  "repeat" => Ok(MjValue::from(repeat(a(0), a(1)))),
1204
1238
  "pad_start" => Ok(MjValue::from(pad(&scalar_or_empty(a(0)), a(1), a(2), true))),
@@ -1351,7 +1385,18 @@ pub fn concat(a: &JsValue, b: &JsValue) -> JsValue {
1351
1385
  JsValue::Array(out)
1352
1386
  }
1353
1387
 
1388
+ /// `Array.prototype.slice(start, end?)` AND `String.prototype.slice`
1389
+ /// (the `string-slice` divergence) -- the adapter emits the same
1390
+ /// `bf.slice(recv, start, end)` call for both receiver shapes (it
1391
+ /// can't disambiguate string vs. array at compile time), so this
1392
+ /// dispatches on `recv`'s `JsValue` variant, mirroring `includes` /
1393
+ /// `length` above.
1354
1394
  pub fn slice(recv: &JsValue, start: &JsValue, end: &JsValue) -> JsValue {
1395
+ if let JsValue::String(s) = recv {
1396
+ let length = char_len(s) as i64;
1397
+ let (s_idx, e_idx) = clamp_slice_range(length, start, end);
1398
+ return JsValue::String(char_slice_range(s, s_idx as usize, e_idx as usize));
1399
+ }
1355
1400
  let items = match recv.as_array() {
1356
1401
  Some(a) => a,
1357
1402
  None => return JsValue::Array(Vec::new()),
@@ -1360,16 +1405,7 @@ pub fn slice(recv: &JsValue, start: &JsValue, end: &JsValue) -> JsValue {
1360
1405
  if length == 0 {
1361
1406
  return JsValue::Array(Vec::new());
1362
1407
  }
1363
- let mut s = if matches!(start, JsValue::Null) { 0 } else { num::to_f64(start) as i64 };
1364
- if s < 0 {
1365
- s += length;
1366
- }
1367
- s = s.clamp(0, length);
1368
- let mut e = if matches!(end, JsValue::Null) { length } else { num::to_f64(end) as i64 };
1369
- if e < 0 {
1370
- e += length;
1371
- }
1372
- e = e.clamp(0, length);
1408
+ let (s, e) = clamp_slice_range(length, start, end);
1373
1409
  if s >= e {
1374
1410
  return JsValue::Array(Vec::new());
1375
1411
  }
@@ -1420,6 +1456,24 @@ pub fn trim(recv: &JsValue) -> String {
1420
1456
  }
1421
1457
  }
1422
1458
 
1459
+ /// `String.prototype.trimStart()` -- the one-sided sibling of `trim`
1460
+ /// above (#2183 follow-up), via Rust's native `str::trim_start`.
1461
+ pub fn trim_start(recv: &JsValue) -> String {
1462
+ match recv {
1463
+ JsValue::Null | JsValue::Array(_) | JsValue::Object(_) => String::new(),
1464
+ other => js_string(other).trim_start().to_string(),
1465
+ }
1466
+ }
1467
+
1468
+ /// `String.prototype.trimEnd()` -- the one-sided sibling of `trim`
1469
+ /// above (#2183 follow-up), via Rust's native `str::trim_end`.
1470
+ pub fn trim_end(recv: &JsValue) -> String {
1471
+ match recv {
1472
+ JsValue::Null | JsValue::Array(_) | JsValue::Object(_) => String::new(),
1473
+ other => js_string(other).trim_end().to_string(),
1474
+ }
1475
+ }
1476
+
1423
1477
  pub fn split(recv: &JsValue, sep: Option<&JsValue>, limit: Option<i64>) -> JsValue {
1424
1478
  let s = scalar_or_empty(recv);
1425
1479
  let mut parts: Vec<String> = match sep {
@@ -1488,6 +1542,20 @@ pub fn replace(recv: &JsValue, pattern: &JsValue, replacement: &JsValue) -> Stri
1488
1542
  }
1489
1543
  }
1490
1544
 
1545
+ /// `String.prototype.replaceAll(pattern, replacement)`, string-pattern
1546
+ /// form only (#2182) -- every occurrence, the all-occurrences sibling
1547
+ /// of `replace` above. Rust's own `str::replace` (no count arg) is
1548
+ /// already global by default, including the empty-pattern-inserts-at-
1549
+ /// every-boundary edge case (`"abc".replace("", "X")` -> "XaXbXcX"),
1550
+ /// so it needs no hand-rolled loop the way `replace_all` on the
1551
+ /// backends whose native replace is first-occurrence-only does.
1552
+ pub fn replace_all(recv: &JsValue, pattern: &JsValue, replacement: &JsValue) -> String {
1553
+ let s = scalar_or_empty(recv);
1554
+ let o = js_string(pattern);
1555
+ let n = js_string(replacement);
1556
+ s.replace(&o, &n)
1557
+ }
1558
+
1491
1559
  /// `queryHref(base, {...})` (#2042) -- build `"$base?k=v&..."` from a flat
1492
1560
  /// list of (guard, key, value) triples. A pair is included iff its guard is
1493
1561
  /// JS-truthy AND its value is a non-empty string. A value may also be a
@@ -116,6 +116,9 @@ fn call_binding(fn_name: &str, args: &[JsValue]) -> Option<JsValue> {
116
116
  "floor" => JsValue::Number(num::js_floor(runtime::js_number(&a(0)))),
117
117
  "ceil" => JsValue::Number(num::js_ceil(runtime::js_number(&a(0)))),
118
118
  "round" => JsValue::Number(num::js_round(runtime::js_number(&a(0)))),
119
+ "min" => JsValue::Number(num::js_min(runtime::js_number(&a(0)), runtime::js_number(&a(1)))),
120
+ "max" => JsValue::Number(num::js_max(runtime::js_number(&a(0)), runtime::js_number(&a(1)))),
121
+ "abs" => JsValue::Number(num::js_abs(runtime::js_number(&a(0)))),
119
122
  "to_fixed" => {
120
123
  let digits = if args.len() > 1 { num::to_f64(&a(1)) as i32 } else { 0 };
121
124
  JsValue::String(num::to_fixed(runtime::js_number(&a(0)), digits))
@@ -123,9 +126,12 @@ fn call_binding(fn_name: &str, args: &[JsValue]) -> Option<JsValue> {
123
126
  "lower" => JsValue::String(runtime::js_string(&a(0)).to_lowercase()),
124
127
  "upper" => JsValue::String(runtime::js_string(&a(0)).to_uppercase()),
125
128
  "trim" => JsValue::String(runtime::trim(&a(0))),
129
+ "trim_start" => JsValue::String(runtime::trim_start(&a(0))),
130
+ "trim_end" => JsValue::String(runtime::trim_end(&a(0))),
126
131
  "starts_with" => JsValue::Bool(runtime::starts_with(&a(0), &a(1), &a(2))),
127
132
  "ends_with" => JsValue::Bool(runtime::ends_with(&a(0), &a(1), &a(2))),
128
133
  "replace" => JsValue::String(runtime::replace(&a(0), &a(1), &a(2))),
134
+ "replace_all" => JsValue::String(runtime::replace_all(&a(0), &a(1), &a(2))),
129
135
  "repeat" => JsValue::String(runtime::repeat(&a(0), &a(1))),
130
136
  "pad_start" => JsValue::String(runtime::pad(&runtime::js_string(&a(0)), &a(1), &a(2), true)),
131
137
  "pad_end" => JsValue::String(runtime::pad(&runtime::js_string(&a(0)), &a(1), &a(2), false)),
@@ -169,6 +169,20 @@ fn slice_mutation_isolation_and_clamping() {
169
169
  assert_eq!(src, arr(vec![s("a"), s("b"), s("c")]));
170
170
  }
171
171
 
172
+ /// The `string-slice` divergence (#2182): a string receiver used to
173
+ /// fall through the array-only branch and return an empty array
174
+ /// instead of a substring.
175
+ #[test]
176
+ fn slice_string_receiver() {
177
+ let word = s("barefootjs");
178
+ assert_eq!(runtime::slice(&word, &n(0.0), &n(4.0)), s("bare"));
179
+ assert_eq!(runtime::slice(&word, &n(-4.0), &JsValue::Null), s("otjs"));
180
+ assert_eq!(runtime::slice(&word, &n(4.0), &JsValue::Null), s("footjs"));
181
+ assert_eq!(runtime::slice(&word, &n(5.0), &n(2.0)), s(""));
182
+ // Multi-byte: index by character, not byte.
183
+ assert_eq!(runtime::slice(&s("héllo"), &n(0.0), &n(2.0)), s("hé"));
184
+ }
185
+
172
186
  #[test]
173
187
  fn reverse_mutation_isolation() {
174
188
  assert_eq!(runtime::reverse(&arr(vec![s("a"), s("b"), s("c")])), arr(vec![s("c"), s("b"), s("a")]));
@@ -192,6 +206,34 @@ fn trim_helper() {
192
206
  assert_eq!(runtime::trim(&n(42.0)), "42");
193
207
  }
194
208
 
209
+ #[test]
210
+ fn trim_start_and_trim_end_helpers() {
211
+ // The one-sided siblings of `trim` above (#2183 follow-up). Padding
212
+ // BOTH sides of the flagship input so a swapped side (or a
213
+ // routed-through-both-sides regression) fails visibly.
214
+ assert_eq!(runtime::trim_start(&s(" padded ")), "padded ");
215
+ assert_eq!(runtime::trim_end(&s(" padded ")), " padded");
216
+
217
+ assert_eq!(runtime::trim_start(&s("\t\nleading")), "leading");
218
+ assert_eq!(runtime::trim_end(&s("trailing ")), "trailing");
219
+
220
+ assert_eq!(runtime::trim_start(&s("no-pad")), "no-pad");
221
+ assert_eq!(runtime::trim_end(&s("no-pad")), "no-pad");
222
+
223
+ assert_eq!(runtime::trim_start(&s(" ")), "");
224
+ assert_eq!(runtime::trim_end(&s(" ")), "");
225
+
226
+ assert_eq!(runtime::trim_start(&s("")), "");
227
+ assert_eq!(runtime::trim_end(&s("")), "");
228
+
229
+ assert_eq!(runtime::trim_start(&JsValue::Null), "");
230
+ assert_eq!(runtime::trim_end(&JsValue::Null), "");
231
+ assert_eq!(runtime::trim_start(&obj(&[("a", n(1.0))])), "");
232
+ assert_eq!(runtime::trim_end(&obj(&[("a", n(1.0))])), "");
233
+ assert_eq!(runtime::trim_start(&n(42.0)), "42");
234
+ assert_eq!(runtime::trim_end(&n(42.0)), "42");
235
+ }
236
+
195
237
  #[test]
196
238
  fn split_helper() {
197
239
  assert_eq!(runtime::split(&s("a,b,c"), Some(&s(",")), None), arr(vec![s("a"), s("b"), s("c")]));