@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/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 +118 -18
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/lib/static-value.d.ts +13 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/adapter/minijinja-adapter.d.ts +49 -0
- package/dist/adapter/minijinja-adapter.d.ts.map +1 -1
- package/dist/build.js +118 -18
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +120 -35
- 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 +140 -0
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +2 -2
- package/src/adapter/lib/constants.ts +3 -0
- package/src/adapter/lib/static-value.ts +39 -0
- package/src/adapter/minijinja-adapter.ts +170 -13
- package/src/conformance-pins.ts +24 -30
- package/src/render-divergences.ts +4 -16
- package/src/test-render.ts +11 -144
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
188849
|
-
|
|
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
|
-
"
|
|
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": "`©` 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,
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
+
"@barefootjs/jsx": "0.18.7",
|
|
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")]));
|