@barefootjs/rust 0.18.3 → 0.18.5
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/adapter/expr/array-method.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +2 -2
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +50 -17
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/minijinja-adapter.d.ts +8 -0
- package/dist/adapter/minijinja-adapter.d.ts.map +1 -1
- package/dist/build.js +50 -17
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +52 -33
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/runtime/src/num.rs +30 -0
- package/runtime/src/runtime.rs +78 -10
- package/runtime/tests/helper_vectors.rs +6 -0
- package/runtime/tests/template_primitives.rs +42 -0
- package/src/__tests__/minijinja-adapter-unit.test.ts +21 -0
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +13 -7
- package/src/adapter/lib/constants.ts +3 -0
- package/src/adapter/minijinja-adapter.ts +68 -8
- package/src/conformance-pins.ts +0 -5
- package/src/render-divergences.ts +1 -26
package/dist/index.js
CHANGED
|
@@ -187369,7 +187369,7 @@ function isAriaBooleanAttr(name) {
|
|
|
187369
187369
|
}
|
|
187370
187370
|
|
|
187371
187371
|
// src/adapter/minijinja-adapter.ts
|
|
187372
|
-
import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
|
|
187372
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from "@barefootjs/shared";
|
|
187373
187373
|
|
|
187374
187374
|
// src/adapter/lib/constants.ts
|
|
187375
187375
|
var JINJA_TEMPLATE_PRIMITIVES = {
|
|
@@ -187378,7 +187378,10 @@ var JINJA_TEMPLATE_PRIMITIVES = {
|
|
|
187378
187378
|
Number: { arity: 1, emit: (args) => `bf.number(${args[0]})` },
|
|
187379
187379
|
"Math.floor": { arity: 1, emit: (args) => `bf.floor(${args[0]})` },
|
|
187380
187380
|
"Math.ceil": { arity: 1, emit: (args) => `bf.ceil(${args[0]})` },
|
|
187381
|
-
"Math.round": { arity: 1, emit: (args) => `bf.round(${args[0]})` }
|
|
187381
|
+
"Math.round": { arity: 1, emit: (args) => `bf.round(${args[0]})` },
|
|
187382
|
+
"Math.min": { arity: 2, emit: (args) => `bf.min(${args[0]}, ${args[1]})` },
|
|
187383
|
+
"Math.max": { arity: 2, emit: (args) => `bf.max(${args[0]}, ${args[1]})` },
|
|
187384
|
+
"Math.abs": { arity: 1, emit: (args) => `bf.abs(${args[0]})` }
|
|
187382
187385
|
};
|
|
187383
187386
|
var JINJA_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(JINJA_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
|
|
187384
187387
|
|
|
@@ -187544,6 +187547,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187544
187547
|
const recv = emit(object);
|
|
187545
187548
|
return `bf.trim(${recv})`;
|
|
187546
187549
|
}
|
|
187550
|
+
case "trimStart":
|
|
187551
|
+
case "trimEnd": {
|
|
187552
|
+
const fn = method === "trimStart" ? "trim_start" : "trim_end";
|
|
187553
|
+
const recv = emit(object);
|
|
187554
|
+
return `bf.${fn}(${recv})`;
|
|
187555
|
+
}
|
|
187547
187556
|
case "toFixed": {
|
|
187548
187557
|
const recv = emit(object);
|
|
187549
187558
|
const digits = args.length >= 1 ? emit(args[0]) : "0";
|
|
@@ -187577,6 +187586,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187577
187586
|
const newS = emit(args[1]);
|
|
187578
187587
|
return `bf.replace(${recv}, ${oldS}, ${newS})`;
|
|
187579
187588
|
}
|
|
187589
|
+
case "replaceAll": {
|
|
187590
|
+
const recv = emit(object);
|
|
187591
|
+
const oldS = emit(args[0]);
|
|
187592
|
+
const newS = emit(args[1]);
|
|
187593
|
+
return `bf.replace_all(${recv}, ${oldS}, ${newS})`;
|
|
187594
|
+
}
|
|
187580
187595
|
case "repeat": {
|
|
187581
187596
|
const recv = emit(object);
|
|
187582
187597
|
const count = args.length === 0 ? "0" : emit(args[0]);
|
|
@@ -187673,6 +187688,7 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187673
187688
|
|
|
187674
187689
|
// src/adapter/expr/emitters.ts
|
|
187675
187690
|
import {
|
|
187691
|
+
groupBinaryOperand,
|
|
187676
187692
|
identifierPath,
|
|
187677
187693
|
matchSearchParamsMethodCall,
|
|
187678
187694
|
sortComparatorFromArrow
|
|
@@ -187718,7 +187734,7 @@ class JinjaFilterEmitter {
|
|
|
187718
187734
|
return "none";
|
|
187719
187735
|
return String(value);
|
|
187720
187736
|
}
|
|
187721
|
-
member(object, property, _computed, emit) {
|
|
187737
|
+
member(object, property, _computed, _optional, emit) {
|
|
187722
187738
|
if (property === "length") {
|
|
187723
187739
|
return `bf.length(${emit(object)})`;
|
|
187724
187740
|
}
|
|
@@ -187741,8 +187757,8 @@ class JinjaFilterEmitter {
|
|
|
187741
187757
|
return emit(argument);
|
|
187742
187758
|
}
|
|
187743
187759
|
binary(op, left, right, emit) {
|
|
187744
|
-
const l = emit(left);
|
|
187745
|
-
const r = emit(right);
|
|
187760
|
+
const l = groupBinaryOperand(left, emit(left));
|
|
187761
|
+
const r = groupBinaryOperand(right, emit(right));
|
|
187746
187762
|
const opMap = {
|
|
187747
187763
|
"===": "==",
|
|
187748
187764
|
"!==": "!=",
|
|
@@ -187824,7 +187840,7 @@ class JinjaTopLevelEmitter {
|
|
|
187824
187840
|
return "none";
|
|
187825
187841
|
return String(value);
|
|
187826
187842
|
}
|
|
187827
|
-
member(object, property, _computed, emit) {
|
|
187843
|
+
member(object, property, _computed, _optional, emit) {
|
|
187828
187844
|
if (object.kind === "identifier" && object.name === "props") {
|
|
187829
187845
|
return minijinjaIdent(property);
|
|
187830
187846
|
}
|
|
@@ -187870,8 +187886,8 @@ class JinjaTopLevelEmitter {
|
|
|
187870
187886
|
return emit(argument);
|
|
187871
187887
|
}
|
|
187872
187888
|
binary(op, left, right, emit) {
|
|
187873
|
-
const l = emit(left);
|
|
187874
|
-
const r = emit(right);
|
|
187889
|
+
const l = groupBinaryOperand(left, emit(left));
|
|
187890
|
+
const r = groupBinaryOperand(right, emit(right));
|
|
187875
187891
|
const opMap = {
|
|
187876
187892
|
"===": "==",
|
|
187877
187893
|
"!==": "!=",
|
|
@@ -188251,6 +188267,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188251
188267
|
options;
|
|
188252
188268
|
errors = [];
|
|
188253
188269
|
inLoop = false;
|
|
188270
|
+
currentLoopKeyDepth = 0;
|
|
188254
188271
|
propsObjectName = null;
|
|
188255
188272
|
propsParams = [];
|
|
188256
188273
|
booleanTypedProps = new Set;
|
|
@@ -188327,7 +188344,7 @@ class MinijinjaAdapter extends BaseAdapter {
|
|
|
188327
188344
|
return this.renderElement(node);
|
|
188328
188345
|
}
|
|
188329
188346
|
emitText(node) {
|
|
188330
|
-
return node.value;
|
|
188347
|
+
return escapeHtml(node.value);
|
|
188331
188348
|
}
|
|
188332
188349
|
emitExpression(node) {
|
|
188333
188350
|
return this.renderExpression(node);
|
|
@@ -188557,7 +188574,7 @@ ${whenTrue}
|
|
|
188557
188574
|
const renderedChildren = this.renderChildren(loop.children);
|
|
188558
188575
|
const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
|
|
188559
188576
|
const indexLocalLines = [];
|
|
188560
|
-
if (loop.iterationShape === "keys") {
|
|
188577
|
+
if (loop.objectIteration) {} else if (loop.iterationShape === "keys") {
|
|
188561
188578
|
indexLocalLines.push(`{% set ${minijinjaIdent(param)} = loop.index0 %}`);
|
|
188562
188579
|
} else if (loop.index) {
|
|
188563
188580
|
indexLocalLines.push(`{% set ${minijinjaIdent(loop.index)} = loop.index0 %}`);
|
|
@@ -188577,13 +188594,17 @@ ${whenTrue}
|
|
|
188577
188594
|
}
|
|
188578
188595
|
const prevInLoop = this.inLoop;
|
|
188579
188596
|
this.inLoop = true;
|
|
188597
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth;
|
|
188598
|
+
this.currentLoopKeyDepth = loop.depth;
|
|
188580
188599
|
const childrenUnderLoop = this.renderChildren(loop.children);
|
|
188600
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth;
|
|
188581
188601
|
this.inLoop = prevInLoop;
|
|
188582
188602
|
const bodyChildren = loop.bodyIsItemConditional && loop.key ? `{{ bf.comment("loop-i:" ~ bf.string(${this.convertExpressionToJinja(loop.key)})) | safe }}
|
|
188583
188603
|
${childrenUnderLoop}` : childrenUnderLoop;
|
|
188584
188604
|
const lines = [];
|
|
188585
188605
|
lines.push(`{{ bf.comment("loop:${loop.markerId}") | safe }}`);
|
|
188586
|
-
|
|
188606
|
+
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} %}`;
|
|
188607
|
+
lines.push(forHeader);
|
|
188587
188608
|
for (const il of indexLocalLines)
|
|
188588
188609
|
lines.push(il);
|
|
188589
188610
|
if (loop.filterPredicate) {
|
|
@@ -188655,9 +188676,20 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188655
188676
|
renderComponent(comp) {
|
|
188656
188677
|
const segments = [{ kind: "entries", parts: [] }];
|
|
188657
188678
|
const currentEntries = () => this.componentPropSegmentEntries(segments);
|
|
188679
|
+
const namedSlotSetBlocks = [];
|
|
188658
188680
|
for (const p of comp.props) {
|
|
188659
188681
|
if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
|
|
188660
188682
|
continue;
|
|
188683
|
+
if (p.value.kind === "jsx-children" && p.name !== "children") {
|
|
188684
|
+
const prevInLoop = this.inLoop;
|
|
188685
|
+
this.inLoop = false;
|
|
188686
|
+
const slotBody = this.renderChildren(p.value.children);
|
|
188687
|
+
this.inLoop = prevInLoop;
|
|
188688
|
+
const captureName = `bf_prop_${this.childrenCaptureCounter++}`;
|
|
188689
|
+
namedSlotSetBlocks.push(`{% set ${captureName} %}${slotBody}{% endset %}`);
|
|
188690
|
+
currentEntries().push(`${minijinjaHashKey(p.name)}: ${captureName}`);
|
|
188691
|
+
continue;
|
|
188692
|
+
}
|
|
188661
188693
|
if (p.value.kind === "spread") {
|
|
188662
188694
|
const trimmed = p.value.expr.trim();
|
|
188663
188695
|
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
@@ -188686,11 +188718,11 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188686
188718
|
const captureName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
188687
188719
|
currentEntries().push(`${minijinjaHashKey("children")}: ${captureName}`);
|
|
188688
188720
|
const dict = this.combineComponentPropSegments(segments);
|
|
188689
|
-
return
|
|
188721
|
+
return `${namedSlotSetBlocks.join("")}{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`;
|
|
188690
188722
|
}
|
|
188691
188723
|
const isEmpty = segments.every((s) => s.kind === "entries" && s.parts.length === 0);
|
|
188692
188724
|
const dictEntries = isEmpty ? "" : `, ${this.combineComponentPropSegments(segments)}`;
|
|
188693
|
-
return
|
|
188725
|
+
return `${namedSlotSetBlocks.join("")}{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`;
|
|
188694
188726
|
}
|
|
188695
188727
|
childrenCaptureCounter = 0;
|
|
188696
188728
|
presenceVarCounter = 0;
|
|
@@ -188735,7 +188767,7 @@ ${alternate}
|
|
|
188735
188767
|
${children}`;
|
|
188736
188768
|
}
|
|
188737
188769
|
elementAttrEmitter = {
|
|
188738
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
188770
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
188739
188771
|
emitExpression: (value, name) => {
|
|
188740
188772
|
if (name === "style") {
|
|
188741
188773
|
const css = this.tryLowerStyleObject(value.expr);
|
|
@@ -188843,9 +188875,10 @@ ${name}="{{ bf.string(${val}) }}"
|
|
|
188843
188875
|
let attrName;
|
|
188844
188876
|
if (attr.name === "className")
|
|
188845
188877
|
attrName = "class";
|
|
188846
|
-
else if (attr.name === "key")
|
|
188847
|
-
|
|
188848
|
-
|
|
188878
|
+
else if (attr.name === "key") {
|
|
188879
|
+
const depth = this.currentLoopKeyDepth;
|
|
188880
|
+
attrName = depth > 0 ? `data-key-${depth}` : "data-key";
|
|
188881
|
+
} else
|
|
188849
188882
|
attrName = attr.name;
|
|
188850
188883
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
188851
188884
|
if (lowered)
|
|
@@ -189104,24 +189137,10 @@ var conformancePins = {
|
|
|
189104
189137
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189105
189138
|
],
|
|
189106
189139
|
"array-map-function-reference": [{ code: "BF101", severity: "error" }],
|
|
189107
|
-
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189108
|
-
"string-replaceall": [{ code: "BF101", severity: "error" }]
|
|
189140
|
+
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189109
189141
|
};
|
|
189110
189142
|
// src/render-divergences.ts
|
|
189111
|
-
var renderDivergences = {
|
|
189112
|
-
"arithmetic-text": "`(count() + 2) * 3` renders 10 instead of 18 — the parenthesised sub-expression loses its grouping (silent wrong arithmetic)",
|
|
189113
|
-
"html-entity-text": "`©` 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
|
-
"boolean-attr-literals": 'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
|
|
189116
|
-
"camelcase-attributes": "`htmlFor` is not lowered to `for` (Hono maps it)",
|
|
189117
|
-
"static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
189118
|
-
"svg-icon": "SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case",
|
|
189119
|
-
"object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations",
|
|
189120
|
-
"nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
|
|
189121
|
-
"jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
|
|
189122
|
-
"string-slice": "`.slice()` on a STRING renders empty (array-slice helper misfires on strings)",
|
|
189123
|
-
"string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering)"
|
|
189124
|
-
};
|
|
189143
|
+
var renderDivergences = {};
|
|
189125
189144
|
export {
|
|
189126
189145
|
renderDivergences,
|
|
189127
189146
|
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,
|
|
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,iBAAsB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/rust",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.5",
|
|
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.
|
|
57
|
+
"@barefootjs/shared": "0.18.5"
|
|
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.
|
|
64
|
+
"@barefootjs/jsx": "0.18.5",
|
|
65
65
|
"typescript": "^5.0.0"
|
|
66
66
|
}
|
|
67
67
|
}
|
package/runtime/src/num.rs
CHANGED
|
@@ -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
|
package/runtime/src/runtime.rs
CHANGED
|
@@ -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
|
|
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")]));
|
|
@@ -385,6 +385,27 @@ export function C(props: { count: number }) {
|
|
|
385
385
|
})
|
|
386
386
|
})
|
|
387
387
|
|
|
388
|
+
describe('MinijinjaAdapter - named-slot capture identifier safety (#2168 jsx-element-prop)', () => {
|
|
389
|
+
// A JSX-valued prop under a hyphenated name (`data-slot`, a valid JSX
|
|
390
|
+
// attribute name) must not leak into the `{% set %}` capture variable's
|
|
391
|
+
// identifier — minijinja variable names can't contain `-`. The capture
|
|
392
|
+
// identifier is purely counter-based (never derived from the prop name);
|
|
393
|
+
// the hash KEY passed to `render_child` still carries the real name,
|
|
394
|
+
// quoted via `minijinjaHashKey`.
|
|
395
|
+
test('a hyphenated prop name does not appear in the capture variable', () => {
|
|
396
|
+
const { template } = compileAndGenerate(`
|
|
397
|
+
function Card(props) { return null }
|
|
398
|
+
export function Parent() {
|
|
399
|
+
return <Card data-slot={<strong>Title</strong>}>text</Card>
|
|
400
|
+
}
|
|
401
|
+
`)
|
|
402
|
+
expect(template).toContain('{% set bf_prop_0 %}')
|
|
403
|
+
expect(template).toContain("'data-slot': bf_prop_0")
|
|
404
|
+
expect(template).not.toContain('data-slot %}')
|
|
405
|
+
expect(template).not.toContain('data-slot_')
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
388
409
|
// #2038 nested-callback-predicate loudness is pinned at the shared
|
|
389
410
|
// conformance layer (workstream C): `filter-nested-callback-predicate` /
|
|
390
411
|
// `filter-nested-find-predicate` (BF101 via `expectedDiagnostics`) and
|
|
@@ -91,6 +91,15 @@ export function renderArrayMethod(
|
|
|
91
91
|
const recv = emit(object)
|
|
92
92
|
return `bf.trim(${recv})`
|
|
93
93
|
}
|
|
94
|
+
case 'trimStart':
|
|
95
|
+
case 'trimEnd': {
|
|
96
|
+
// `.trimStart()` / `.trimEnd()` — the one-sided siblings of
|
|
97
|
+
// `.trim()` (#2183 follow-up). Dedicated `bf.trim_start` /
|
|
98
|
+
// `bf.trim_end` helpers, not `bf.trim` with a flag.
|
|
99
|
+
const fn = method === 'trimStart' ? 'trim_start' : 'trim_end'
|
|
100
|
+
const recv = emit(object)
|
|
101
|
+
return `bf.${fn}(${recv})`
|
|
102
|
+
}
|
|
94
103
|
case 'toFixed': {
|
|
95
104
|
// `.toFixed(digits?)` — `bf.to_fixed` mirrors JS rounding +
|
|
96
105
|
// zero-padding (default 0 digits). #1897.
|
|
@@ -126,6 +135,16 @@ export function renderArrayMethod(
|
|
|
126
135
|
const newS = emit(args[1])
|
|
127
136
|
return `bf.replace(${recv}, ${oldS}, ${newS})`
|
|
128
137
|
}
|
|
138
|
+
case 'replaceAll': {
|
|
139
|
+
// `.replaceAll(old, new)` — string-pattern form, EVERY occurrence,
|
|
140
|
+
// via the dedicated `bf.replace_all` helper (not `bf.replace`
|
|
141
|
+
// with a flag) — the regex-pattern form is refused upstream at
|
|
142
|
+
// the parser, same as `.replace`. See #2182.
|
|
143
|
+
const recv = emit(object)
|
|
144
|
+
const oldS = emit(args[0])
|
|
145
|
+
const newS = emit(args[1])
|
|
146
|
+
return `bf.replace_all(${recv}, ${oldS}, ${newS})`
|
|
147
|
+
}
|
|
129
148
|
case 'repeat': {
|
|
130
149
|
const recv = emit(object)
|
|
131
150
|
const count = args.length === 0 ? '0' : emit(args[0])
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* entry point `_renderJinjaFilterExprPublic`.
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
-
import {
|
|
51
|
+
import { groupBinaryOperand,
|
|
52
52
|
type ParsedExprEmitter,
|
|
53
53
|
type HigherOrderMethod,
|
|
54
54
|
type ArrayMethod,
|
|
@@ -141,7 +141,7 @@ export class JinjaFilterEmitter implements ParsedExprEmitter {
|
|
|
141
141
|
return String(value)
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
144
|
+
member(object: ParsedExpr, property: string, _computed: boolean, _optional: boolean, emit: (e: ParsedExpr) => string): string {
|
|
145
145
|
// `.length` — route through `bf.length` (handles both array element
|
|
146
146
|
// count and string char count, JS-compatibly). Jinja's builtin
|
|
147
147
|
// `|length` filter also faults trying to match JS semantics for every
|
|
@@ -175,8 +175,11 @@ export class JinjaFilterEmitter implements ParsedExprEmitter {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
178
|
-
|
|
179
|
-
|
|
178
|
+
// Preserve source grouping: a compound operand re-emitted as infix
|
|
179
|
+
// text is otherwise re-parsed under THIS language's precedence —
|
|
180
|
+
// `(count() + 2) * 3` would silently become `count + 2 * 3` (#2173).
|
|
181
|
+
const l = groupBinaryOperand(left, emit(left))
|
|
182
|
+
const r = groupBinaryOperand(right, emit(right))
|
|
180
183
|
// Jinja's `==` / `!=` are value-equality operators that compare strings
|
|
181
184
|
// and numbers correctly — unlike Perl's numeric `==` (which the Mojo
|
|
182
185
|
// adapter must steer around with `eq`/`ne`). Same reasoning as Kolon.
|
|
@@ -310,7 +313,7 @@ export class JinjaTopLevelEmitter implements ParsedExprEmitter {
|
|
|
310
313
|
return String(value)
|
|
311
314
|
}
|
|
312
315
|
|
|
313
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
316
|
+
member(object: ParsedExpr, property: string, _computed: boolean, _optional: boolean, emit: (e: ParsedExpr) => string): string {
|
|
314
317
|
// `props.x` flattens to the bare context var the SSR caller binds each
|
|
315
318
|
// prop to (props arrive as individual top-level context entries, not a
|
|
316
319
|
// nested `props` dict).
|
|
@@ -378,8 +381,11 @@ export class JinjaTopLevelEmitter implements ParsedExprEmitter {
|
|
|
378
381
|
}
|
|
379
382
|
|
|
380
383
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
381
|
-
|
|
382
|
-
|
|
384
|
+
// Preserve source grouping: a compound operand re-emitted as infix
|
|
385
|
+
// text is otherwise re-parsed under THIS language's precedence —
|
|
386
|
+
// `(count() + 2) * 3` would silently become `count + 2 * 3` (#2173).
|
|
387
|
+
const l = groupBinaryOperand(left, emit(left))
|
|
388
|
+
const r = groupBinaryOperand(right, emit(right))
|
|
383
389
|
// Jinja's `==` / `!=` handle both strings and numbers (unlike Perl's
|
|
384
390
|
// numeric `==`), so all equality comparisons stay on `==` / `!=`.
|
|
385
391
|
const opMap: Record<string, string> = {
|
|
@@ -26,6 +26,9 @@ export const JINJA_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
|
|
|
26
26
|
'Math.floor': { arity: 1, emit: (args) => `bf.floor(${args[0]})` },
|
|
27
27
|
'Math.ceil': { arity: 1, emit: (args) => `bf.ceil(${args[0]})` },
|
|
28
28
|
'Math.round': { arity: 1, emit: (args) => `bf.round(${args[0]})` },
|
|
29
|
+
'Math.min': { arity: 2, emit: (args) => `bf.min(${args[0]}, ${args[1]})` },
|
|
30
|
+
'Math.max': { arity: 2, emit: (args) => `bf.max(${args[0]}, ${args[1]})` },
|
|
31
|
+
'Math.abs': { arity: 1, emit: (args) => `bf.abs(${args[0]})` },
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/**
|