@barefootjs/jinja 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 +52 -19
- package/dist/adapter/jinja-adapter.d.ts +8 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -1
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/build.js +52 -19
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +54 -35
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/python/barefootjs/runtime.py +64 -6
- package/python/tests/test_helper_vectors.py +6 -0
- package/python/tests/test_template_primitives.py +45 -0
- package/src/__tests__/jinja-adapter-unit.test.ts +21 -0
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +30 -11
- package/src/adapter/jinja-adapter.ts +62 -8
- package/src/adapter/lib/constants.ts +3 -0
- 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/jinja-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
|
|
|
@@ -187557,6 +187560,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187557
187560
|
const recv = emit(object);
|
|
187558
187561
|
return `bf.trim(${recv})`;
|
|
187559
187562
|
}
|
|
187563
|
+
case "trimStart":
|
|
187564
|
+
case "trimEnd": {
|
|
187565
|
+
const fn = method === "trimStart" ? "trim_start" : "trim_end";
|
|
187566
|
+
const recv = emit(object);
|
|
187567
|
+
return `bf.${fn}(${recv})`;
|
|
187568
|
+
}
|
|
187560
187569
|
case "toFixed": {
|
|
187561
187570
|
const recv = emit(object);
|
|
187562
187571
|
const digits = args.length >= 1 ? emit(args[0]) : "0";
|
|
@@ -187590,6 +187599,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187590
187599
|
const newS = emit(args[1]);
|
|
187591
187600
|
return `bf.replace(${recv}, ${oldS}, ${newS})`;
|
|
187592
187601
|
}
|
|
187602
|
+
case "replaceAll": {
|
|
187603
|
+
const recv = emit(object);
|
|
187604
|
+
const oldS = emit(args[0]);
|
|
187605
|
+
const newS = emit(args[1]);
|
|
187606
|
+
return `bf.replace_all(${recv}, ${oldS}, ${newS})`;
|
|
187607
|
+
}
|
|
187593
187608
|
case "repeat": {
|
|
187594
187609
|
const recv = emit(object);
|
|
187595
187610
|
const count = args.length === 0 ? "0" : emit(args[0]);
|
|
@@ -187686,6 +187701,7 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187686
187701
|
|
|
187687
187702
|
// src/adapter/expr/emitters.ts
|
|
187688
187703
|
import {
|
|
187704
|
+
groupBinaryOperand,
|
|
187689
187705
|
identifierPath,
|
|
187690
187706
|
matchSearchParamsMethodCall,
|
|
187691
187707
|
sortComparatorFromArrow
|
|
@@ -187731,11 +187747,11 @@ class JinjaFilterEmitter {
|
|
|
187731
187747
|
return "none";
|
|
187732
187748
|
return String(value);
|
|
187733
187749
|
}
|
|
187734
|
-
member(object, property, _computed, emit) {
|
|
187750
|
+
member(object, property, _computed, _optional, emit) {
|
|
187735
187751
|
if (property === "length") {
|
|
187736
187752
|
return `bf.length(${emit(object)})`;
|
|
187737
187753
|
}
|
|
187738
|
-
return `${emit(object)}
|
|
187754
|
+
return `${emit(object)}['${escapeJinjaSingleQuoted(property)}']`;
|
|
187739
187755
|
}
|
|
187740
187756
|
indexAccess(object, index, emit) {
|
|
187741
187757
|
return `${emit(object)}[${emit(index)}]`;
|
|
@@ -187754,8 +187770,8 @@ class JinjaFilterEmitter {
|
|
|
187754
187770
|
return emit(argument);
|
|
187755
187771
|
}
|
|
187756
187772
|
binary(op, left, right, emit) {
|
|
187757
|
-
const l = emit(left);
|
|
187758
|
-
const r = emit(right);
|
|
187773
|
+
const l = groupBinaryOperand(left, emit(left));
|
|
187774
|
+
const r = groupBinaryOperand(right, emit(right));
|
|
187759
187775
|
const opMap = {
|
|
187760
187776
|
"===": "==",
|
|
187761
187777
|
"!==": "!=",
|
|
@@ -187837,7 +187853,7 @@ class JinjaTopLevelEmitter {
|
|
|
187837
187853
|
return "none";
|
|
187838
187854
|
return String(value);
|
|
187839
187855
|
}
|
|
187840
|
-
member(object, property, _computed, emit) {
|
|
187856
|
+
member(object, property, _computed, _optional, emit) {
|
|
187841
187857
|
if (object.kind === "identifier" && object.name === "props") {
|
|
187842
187858
|
return jinjaIdent(property);
|
|
187843
187859
|
}
|
|
@@ -187849,7 +187865,7 @@ class JinjaTopLevelEmitter {
|
|
|
187849
187865
|
const obj = emit(object);
|
|
187850
187866
|
if (property === "length")
|
|
187851
187867
|
return `bf.length(${obj})`;
|
|
187852
|
-
return `${obj}
|
|
187868
|
+
return `${obj}['${escapeJinjaSingleQuoted(property)}']`;
|
|
187853
187869
|
}
|
|
187854
187870
|
indexAccess(object, index, emit) {
|
|
187855
187871
|
return `${emit(object)}[${emit(index)}]`;
|
|
@@ -187883,8 +187899,8 @@ class JinjaTopLevelEmitter {
|
|
|
187883
187899
|
return emit(argument);
|
|
187884
187900
|
}
|
|
187885
187901
|
binary(op, left, right, emit) {
|
|
187886
|
-
const l = emit(left);
|
|
187887
|
-
const r = emit(right);
|
|
187902
|
+
const l = groupBinaryOperand(left, emit(left));
|
|
187903
|
+
const r = groupBinaryOperand(right, emit(right));
|
|
187888
187904
|
const opMap = {
|
|
187889
187905
|
"===": "==",
|
|
187890
187906
|
"!==": "!=",
|
|
@@ -188256,6 +188272,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188256
188272
|
options;
|
|
188257
188273
|
errors = [];
|
|
188258
188274
|
inLoop = false;
|
|
188275
|
+
currentLoopKeyDepth = 0;
|
|
188259
188276
|
propsObjectName = null;
|
|
188260
188277
|
propsParams = [];
|
|
188261
188278
|
booleanTypedProps = new Set;
|
|
@@ -188332,7 +188349,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188332
188349
|
return this.renderElement(node);
|
|
188333
188350
|
}
|
|
188334
188351
|
emitText(node) {
|
|
188335
|
-
return node.value;
|
|
188352
|
+
return escapeHtml(node.value);
|
|
188336
188353
|
}
|
|
188337
188354
|
emitExpression(node) {
|
|
188338
188355
|
return this.renderExpression(node);
|
|
@@ -188563,7 +188580,7 @@ ${whenTrue}
|
|
|
188563
188580
|
const renderedChildren = this.renderChildren(loop.children);
|
|
188564
188581
|
const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
|
|
188565
188582
|
const indexLocalLines = [];
|
|
188566
|
-
if (loop.iterationShape === "keys") {
|
|
188583
|
+
if (loop.objectIteration) {} else if (loop.iterationShape === "keys") {
|
|
188567
188584
|
indexLocalLines.push(`{% set ${jinjaIdent(param)} = loop.index0 %}`);
|
|
188568
188585
|
} else if (loop.index) {
|
|
188569
188586
|
indexLocalLines.push(`{% set ${jinjaIdent(loop.index)} = loop.index0 %}`);
|
|
@@ -188583,13 +188600,17 @@ ${whenTrue}
|
|
|
188583
188600
|
}
|
|
188584
188601
|
const prevInLoop = this.inLoop;
|
|
188585
188602
|
this.inLoop = true;
|
|
188603
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth;
|
|
188604
|
+
this.currentLoopKeyDepth = loop.depth;
|
|
188586
188605
|
const childrenUnderLoop = this.renderChildren(loop.children);
|
|
188606
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth;
|
|
188587
188607
|
this.inLoop = prevInLoop;
|
|
188588
188608
|
const bodyChildren = loop.bodyIsItemConditional && loop.key ? `{{ bf.comment("loop-i:" ~ bf.string(${this.convertExpressionToJinja(loop.key)})) | safe }}
|
|
188589
188609
|
${childrenUnderLoop}` : childrenUnderLoop;
|
|
188590
188610
|
const lines = [];
|
|
188591
188611
|
lines.push(`{{ bf.comment("loop:${loop.markerId}") | safe }}`);
|
|
188592
|
-
|
|
188612
|
+
const forHeader = loop.objectIteration === "entries" ? `{% for ${jinjaIdent(loop.index ?? param)}, ${jinjaIdent(param)} in ${array}.items() %}` : loop.objectIteration === "keys" ? `{% for ${jinjaIdent(param)} in ${array}.keys() %}` : loop.objectIteration === "values" ? `{% for ${jinjaIdent(param)} in ${array}.values() %}` : `{% for ${jinjaIdent(loopVar)} in ${array} %}`;
|
|
188613
|
+
lines.push(forHeader);
|
|
188593
188614
|
for (const il of indexLocalLines)
|
|
188594
188615
|
lines.push(il);
|
|
188595
188616
|
if (loop.filterPredicate) {
|
|
@@ -188661,9 +188682,20 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188661
188682
|
renderComponent(comp) {
|
|
188662
188683
|
const segments = [{ kind: "entries", parts: [] }];
|
|
188663
188684
|
const currentEntries = () => this.componentPropSegmentEntries(segments);
|
|
188685
|
+
const namedSlotSetBlocks = [];
|
|
188664
188686
|
for (const p of comp.props) {
|
|
188665
188687
|
if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
|
|
188666
188688
|
continue;
|
|
188689
|
+
if (p.value.kind === "jsx-children" && p.name !== "children") {
|
|
188690
|
+
const prevInLoop = this.inLoop;
|
|
188691
|
+
this.inLoop = false;
|
|
188692
|
+
const slotBody = this.renderChildren(p.value.children);
|
|
188693
|
+
this.inLoop = prevInLoop;
|
|
188694
|
+
const captureName = `bf_prop_${this.childrenCaptureCounter++}`;
|
|
188695
|
+
namedSlotSetBlocks.push(`{% set ${captureName} %}${slotBody}{% endset %}`);
|
|
188696
|
+
currentEntries().push(`${jinjaHashKey(p.name)}: ${captureName}`);
|
|
188697
|
+
continue;
|
|
188698
|
+
}
|
|
188667
188699
|
if (p.value.kind === "spread") {
|
|
188668
188700
|
const trimmed = p.value.expr.trim();
|
|
188669
188701
|
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
@@ -188692,11 +188724,11 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188692
188724
|
const captureName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
188693
188725
|
currentEntries().push(`${jinjaHashKey("children")}: ${captureName}`);
|
|
188694
188726
|
const dict = this.combineComponentPropSegments(segments);
|
|
188695
|
-
return
|
|
188727
|
+
return `${namedSlotSetBlocks.join("")}{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`;
|
|
188696
188728
|
}
|
|
188697
188729
|
const isEmpty = segments.every((s) => s.kind === "entries" && s.parts.length === 0);
|
|
188698
188730
|
const dictEntries = isEmpty ? "" : `, ${this.combineComponentPropSegments(segments)}`;
|
|
188699
|
-
return
|
|
188731
|
+
return `${namedSlotSetBlocks.join("")}{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`;
|
|
188700
188732
|
}
|
|
188701
188733
|
childrenCaptureCounter = 0;
|
|
188702
188734
|
presenceVarCounter = 0;
|
|
@@ -188741,7 +188773,7 @@ ${alternate}
|
|
|
188741
188773
|
${children}`;
|
|
188742
188774
|
}
|
|
188743
188775
|
elementAttrEmitter = {
|
|
188744
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
188776
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
188745
188777
|
emitExpression: (value, name) => {
|
|
188746
188778
|
if (name === "style") {
|
|
188747
188779
|
const css = this.tryLowerStyleObject(value.expr);
|
|
@@ -188849,9 +188881,10 @@ ${name}="{{ bf.string(${val}) }}"
|
|
|
188849
188881
|
let attrName;
|
|
188850
188882
|
if (attr.name === "className")
|
|
188851
188883
|
attrName = "class";
|
|
188852
|
-
else if (attr.name === "key")
|
|
188853
|
-
|
|
188854
|
-
|
|
188884
|
+
else if (attr.name === "key") {
|
|
188885
|
+
const depth = this.currentLoopKeyDepth;
|
|
188886
|
+
attrName = depth > 0 ? `data-key-${depth}` : "data-key";
|
|
188887
|
+
} else
|
|
188855
188888
|
attrName = attr.name;
|
|
188856
188889
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
188857
188890
|
if (lowered)
|
|
@@ -189110,24 +189143,10 @@ var conformancePins = {
|
|
|
189110
189143
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189111
189144
|
],
|
|
189112
189145
|
"array-map-function-reference": [{ code: "BF101", severity: "error" }],
|
|
189113
|
-
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189114
|
-
"string-replaceall": [{ code: "BF101", severity: "error" }]
|
|
189146
|
+
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189115
189147
|
};
|
|
189116
189148
|
// src/render-divergences.ts
|
|
189117
|
-
var renderDivergences = {
|
|
189118
|
-
"arithmetic-text": "`(count() + 2) * 3` renders 10 instead of 18 — the parenthesised sub-expression loses its grouping (silent wrong arithmetic)",
|
|
189119
|
-
"html-entity-text": "`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
|
|
189120
|
-
"math-methods": "Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)",
|
|
189121
|
-
"boolean-attr-literals": 'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
|
|
189122
|
-
"camelcase-attributes": "`htmlFor` is not lowered to `for` (Hono maps it)",
|
|
189123
|
-
"static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
189124
|
-
"svg-icon": "SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case",
|
|
189125
|
-
"object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations",
|
|
189126
|
-
"nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
|
|
189127
|
-
"jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
|
|
189128
|
-
"string-slice": "`.slice()` on a STRING renders empty (array-slice helper misfires on strings)",
|
|
189129
|
-
"string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering)"
|
|
189130
|
-
};
|
|
189149
|
+
var renderDivergences = {};
|
|
189131
189150
|
export {
|
|
189132
189151
|
renderDivergences,
|
|
189133
189152
|
jinjaAdapter,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,iBAAsB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/jinja",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.5",
|
|
4
4
|
"description": "Jinja2 adapter for BarefootJS — compiles IR to .jinja templates and ships the Python BarefootJS rendering runtime; runs under any Python web framework (Flask, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
"directory": "packages/adapter-jinja"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@barefootjs/shared": "0.18.
|
|
56
|
+
"@barefootjs/shared": "0.18.5"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@barefootjs/jsx": ">=0.2.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
63
|
-
"@barefootjs/jsx": "0.18.
|
|
63
|
+
"@barefootjs/jsx": "0.18.5",
|
|
64
64
|
"typescript": "^5.0.0"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -819,6 +819,29 @@ class BarefootJS:
|
|
|
819
819
|
# is -1, not -2). `floor(n + 0.5)` reproduces that for both signs.
|
|
820
820
|
return float(math.floor(n + 0.5))
|
|
821
821
|
|
|
822
|
+
def min(self, a: Any, b: Any) -> float:
|
|
823
|
+
"""`Math.min(a, b)` -- two-arg form only (#2168 math-methods).
|
|
824
|
+
JS returns NaN if either operand is NaN."""
|
|
825
|
+
x, y = js_number(a), js_number(b)
|
|
826
|
+
if _is_nan(x):
|
|
827
|
+
return x
|
|
828
|
+
if _is_nan(y):
|
|
829
|
+
return y
|
|
830
|
+
return x if x < y else y
|
|
831
|
+
|
|
832
|
+
def max(self, a: Any, b: Any) -> float:
|
|
833
|
+
x, y = js_number(a), js_number(b)
|
|
834
|
+
if _is_nan(x):
|
|
835
|
+
return x
|
|
836
|
+
if _is_nan(y):
|
|
837
|
+
return y
|
|
838
|
+
return x if x > y else y
|
|
839
|
+
|
|
840
|
+
def abs(self, value: Any) -> float:
|
|
841
|
+
"""`Math.abs()` (#2168 math-methods)."""
|
|
842
|
+
n = js_number(value)
|
|
843
|
+
return n if _is_nan(n) else abs(n)
|
|
844
|
+
|
|
822
845
|
# -----------------------------------------------------------------
|
|
823
846
|
# Array / String method helpers (#1448 Tier A)
|
|
824
847
|
# -----------------------------------------------------------------
|
|
@@ -933,12 +956,20 @@ class BarefootJS:
|
|
|
933
956
|
out.extend(b)
|
|
934
957
|
return out
|
|
935
958
|
|
|
936
|
-
def slice(self, recv: Any, start: Any, end: Any) ->
|
|
937
|
-
|
|
959
|
+
def slice(self, recv: Any, start: Any, end: Any) -> Any:
|
|
960
|
+
# `Array.prototype.slice(start, end?)` AND `String.prototype.slice`
|
|
961
|
+
# (the `string-slice` divergence, #2182) -- the adapter emits the
|
|
962
|
+
# same `bf.slice(recv, start, end)` call for both receiver shapes
|
|
963
|
+
# (it can't disambiguate string vs. array at compile time), so
|
|
964
|
+
# this dispatches on the Python type, mirroring `includes` above.
|
|
965
|
+
# `str` and `list` share slicing semantics (`recv[s:e]`) and
|
|
966
|
+
# `len()`, so one clamp computation serves both; `str` indexing
|
|
967
|
+
# is by Unicode code point already, matching JS except for
|
|
968
|
+
# astral-plane input (the same divergence boundary every other
|
|
969
|
+
# adapter's pad/trim helpers already accept).
|
|
970
|
+
if not isinstance(recv, (list, str)):
|
|
938
971
|
return []
|
|
939
972
|
length = len(recv)
|
|
940
|
-
if length == 0:
|
|
941
|
-
return []
|
|
942
973
|
s = start if start is not None else 0
|
|
943
974
|
if s < 0:
|
|
944
975
|
s = length + s
|
|
@@ -950,8 +981,8 @@ class BarefootJS:
|
|
|
950
981
|
e = max(e, 0)
|
|
951
982
|
e = min(e, length)
|
|
952
983
|
if s >= e:
|
|
953
|
-
return []
|
|
954
|
-
return list(recv[s:e])
|
|
984
|
+
return recv[0:0]
|
|
985
|
+
return recv[s:e] if isinstance(recv, str) else list(recv[s:e])
|
|
955
986
|
|
|
956
987
|
def reverse(self, recv: Any) -> list:
|
|
957
988
|
if not isinstance(recv, list):
|
|
@@ -1014,6 +1045,20 @@ class BarefootJS:
|
|
|
1014
1045
|
return ""
|
|
1015
1046
|
return js_string(recv).strip()
|
|
1016
1047
|
|
|
1048
|
+
def trim_start(self, recv: Any) -> str:
|
|
1049
|
+
"""`String.prototype.trimStart()` -- the one-sided sibling of `trim`
|
|
1050
|
+
above (#2183 follow-up)."""
|
|
1051
|
+
if recv is None or isinstance(recv, (list, dict)):
|
|
1052
|
+
return ""
|
|
1053
|
+
return js_string(recv).lstrip()
|
|
1054
|
+
|
|
1055
|
+
def trim_end(self, recv: Any) -> str:
|
|
1056
|
+
"""`String.prototype.trimEnd()` -- the one-sided sibling of `trim`
|
|
1057
|
+
above (#2183 follow-up)."""
|
|
1058
|
+
if recv is None or isinstance(recv, (list, dict)):
|
|
1059
|
+
return ""
|
|
1060
|
+
return js_string(recv).rstrip()
|
|
1061
|
+
|
|
1017
1062
|
def to_fixed(self, value: Any, digits: int = 0) -> str:
|
|
1018
1063
|
n = self.number(value)
|
|
1019
1064
|
if _is_nan(n):
|
|
@@ -1076,6 +1121,19 @@ class BarefootJS:
|
|
|
1076
1121
|
return s
|
|
1077
1122
|
return s[:i] + n + s[i + len(o) :]
|
|
1078
1123
|
|
|
1124
|
+
def replace_all(self, recv: Any, pattern: Any, replacement: Any) -> str:
|
|
1125
|
+
"""`String.prototype.replaceAll(pattern, replacement)`, string-pattern
|
|
1126
|
+
form only (#2182) -- every occurrence, the all-occurrences sibling of
|
|
1127
|
+
`replace` above. Python's own `str.replace(old, new)` (no count arg)
|
|
1128
|
+
is already global by default, including the empty-pattern-inserts-
|
|
1129
|
+
at-every-boundary edge case (`"abc".replace("", "X")` -> "XaXbXcX"),
|
|
1130
|
+
so it needs no hand-rolled loop the way the other runtimes' first-
|
|
1131
|
+
occurrence-only native replace does."""
|
|
1132
|
+
s = _scalar_or_empty(recv)
|
|
1133
|
+
o = js_string(pattern)
|
|
1134
|
+
n = js_string(replacement)
|
|
1135
|
+
return s.replace(o, n)
|
|
1136
|
+
|
|
1079
1137
|
def query(self, base: Any, *triples: Any) -> str:
|
|
1080
1138
|
"""`queryHref(base, {...})` (#2042) -- build `"$base?k=v&..."` from a
|
|
1081
1139
|
flat list of (guard, key, value) triples. A pair is included iff its
|
|
@@ -117,13 +117,19 @@ BINDINGS = {
|
|
|
117
117
|
"floor": bf.floor,
|
|
118
118
|
"ceil": bf.ceil,
|
|
119
119
|
"round": bf.round,
|
|
120
|
+
"min": bf.min,
|
|
121
|
+
"max": bf.max,
|
|
122
|
+
"abs": bf.abs,
|
|
120
123
|
"to_fixed": lambda *a: bf.to_fixed(*a),
|
|
121
124
|
"lower": bf.lc,
|
|
122
125
|
"upper": bf.uc,
|
|
123
126
|
"trim": bf.trim,
|
|
127
|
+
"trim_start": bf.trim_start,
|
|
128
|
+
"trim_end": bf.trim_end,
|
|
124
129
|
"starts_with": lambda *a: bf.starts_with(*a),
|
|
125
130
|
"ends_with": lambda *a: bf.ends_with(*a),
|
|
126
131
|
"replace": lambda *a: bf.replace(*a),
|
|
132
|
+
"replace_all": lambda *a: bf.replace_all(*a),
|
|
127
133
|
"repeat": lambda *a: bf.repeat(*a),
|
|
128
134
|
"pad_start": lambda *a: bf.pad_start(*a),
|
|
129
135
|
"pad_end": lambda *a: bf.pad_end(*a),
|
|
@@ -74,6 +74,27 @@ class TemplatePrimitivesTest(unittest.TestCase):
|
|
|
74
74
|
self.assertEqual(self.bf.round(-1.6), -2)
|
|
75
75
|
self.assertTrue(_is_nan(self.bf.round("not")))
|
|
76
76
|
|
|
77
|
+
def test_min_max_abs(self):
|
|
78
|
+
# `Math.min(a, b)` / `Math.max(a, b)` (two-arg forms only) and
|
|
79
|
+
# `Math.abs()` (#2168 math-methods). JS returns NaN if EITHER
|
|
80
|
+
# min/max operand is NaN.
|
|
81
|
+
self.assertEqual(self.bf.min(3, 7), 3)
|
|
82
|
+
self.assertEqual(self.bf.min(7, 3), 3)
|
|
83
|
+
self.assertEqual(self.bf.min(-2, -5), -5)
|
|
84
|
+
self.assertTrue(_is_nan(self.bf.min("not", 5)))
|
|
85
|
+
self.assertTrue(_is_nan(self.bf.min(5, "not")))
|
|
86
|
+
|
|
87
|
+
self.assertEqual(self.bf.max(3, 7), 7)
|
|
88
|
+
self.assertEqual(self.bf.max(7, 3), 7)
|
|
89
|
+
self.assertEqual(self.bf.max(-2, -5), -2)
|
|
90
|
+
self.assertTrue(_is_nan(self.bf.max("not", 5)))
|
|
91
|
+
self.assertTrue(_is_nan(self.bf.max(5, "not")))
|
|
92
|
+
|
|
93
|
+
self.assertEqual(self.bf.abs(-7.6), 7.6)
|
|
94
|
+
self.assertEqual(self.bf.abs(7.6), 7.6)
|
|
95
|
+
self.assertEqual(self.bf.abs(0), 0)
|
|
96
|
+
self.assertTrue(_is_nan(self.bf.abs("not")))
|
|
97
|
+
|
|
77
98
|
def test_includes_dispatch(self):
|
|
78
99
|
# `Array.prototype.includes(x)` + `String.prototype.includes(sub)`
|
|
79
100
|
# lower to the same `bf.includes(recv, elem)` shape -- see #1448
|
|
@@ -158,6 +179,18 @@ class TemplatePrimitivesTest(unittest.TestCase):
|
|
|
158
179
|
out.append("mutated")
|
|
159
180
|
self.assertEqual(src, ["a", "b", "c"])
|
|
160
181
|
|
|
182
|
+
def test_slice_string_receiver(self):
|
|
183
|
+
# The `string-slice` divergence (#2182): a string receiver used
|
|
184
|
+
# to fall through the array-only branch and return an empty
|
|
185
|
+
# list instead of a substring.
|
|
186
|
+
word = "barefootjs"
|
|
187
|
+
self.assertEqual(self.bf.slice(word, 0, 4), "bare")
|
|
188
|
+
self.assertEqual(self.bf.slice(word, -4, None), "otjs")
|
|
189
|
+
self.assertEqual(self.bf.slice(word, 4, None), "footjs")
|
|
190
|
+
self.assertEqual(self.bf.slice(word, 5, 2), "")
|
|
191
|
+
# Multi-byte: index by character, not byte.
|
|
192
|
+
self.assertEqual(self.bf.slice("héllo", 0, 2), "hé")
|
|
193
|
+
|
|
161
194
|
def test_reverse_mutation_isolation(self):
|
|
162
195
|
self.assertEqual(self.bf.reverse(["a", "b", "c"]), ["c", "b", "a"])
|
|
163
196
|
self.assertEqual(self.bf.reverse([]), [])
|
|
@@ -175,6 +208,18 @@ class TemplatePrimitivesTest(unittest.TestCase):
|
|
|
175
208
|
self.assertEqual(self.bf.trim({"a": 1}), "")
|
|
176
209
|
self.assertEqual(self.bf.trim(42), "42")
|
|
177
210
|
|
|
211
|
+
def test_trim_start_and_trim_end(self):
|
|
212
|
+
# The one-sided siblings of `trim` above (#2183). Padding BOTH
|
|
213
|
+
# sides so a swapped side fails visibly.
|
|
214
|
+
self.assertEqual(self.bf.trim_start(" padded "), "padded ")
|
|
215
|
+
self.assertEqual(self.bf.trim_end(" padded "), " padded")
|
|
216
|
+
self.assertEqual(self.bf.trim_start(""), "")
|
|
217
|
+
self.assertEqual(self.bf.trim_end(""), "")
|
|
218
|
+
self.assertEqual(self.bf.trim_start(None), "")
|
|
219
|
+
self.assertEqual(self.bf.trim_end(None), "")
|
|
220
|
+
self.assertEqual(self.bf.trim_start({"a": 1}), "")
|
|
221
|
+
self.assertEqual(self.bf.trim_end([1, 2]), "")
|
|
222
|
+
|
|
178
223
|
def test_split(self):
|
|
179
224
|
self.assertEqual(self.bf.split("a,b,c", ","), ["a", "b", "c"])
|
|
180
225
|
self.assertEqual(self.bf.split("a.b.c", "."), ["a", "b", "c"])
|
|
@@ -383,6 +383,27 @@ export function C(props: { count: number }) {
|
|
|
383
383
|
})
|
|
384
384
|
})
|
|
385
385
|
|
|
386
|
+
describe('JinjaAdapter - named-slot capture identifier safety (#2168 jsx-element-prop)', () => {
|
|
387
|
+
// A JSX-valued prop under a hyphenated name (`data-slot`, a valid JSX
|
|
388
|
+
// attribute name) must not leak into the `{% set %}` capture variable's
|
|
389
|
+
// identifier — Jinja variable names can't contain `-`. The capture
|
|
390
|
+
// identifier is purely counter-based (never derived from the prop name);
|
|
391
|
+
// the hash KEY passed to `render_child` still carries the real name,
|
|
392
|
+
// quoted via `jinjaHashKey`.
|
|
393
|
+
test('a hyphenated prop name does not appear in the capture variable', () => {
|
|
394
|
+
const { template } = compileAndGenerate(`
|
|
395
|
+
function Card(props) { return null }
|
|
396
|
+
export function Parent() {
|
|
397
|
+
return <Card data-slot={<strong>Title</strong>}>text</Card>
|
|
398
|
+
}
|
|
399
|
+
`)
|
|
400
|
+
expect(template).toContain('{% set bf_prop_0 %}')
|
|
401
|
+
expect(template).toContain("'data-slot': bf_prop_0")
|
|
402
|
+
expect(template).not.toContain('data-slot %}')
|
|
403
|
+
expect(template).not.toContain('data-slot_')
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
386
407
|
// #2038 nested-callback-predicate loudness is pinned at the shared
|
|
387
408
|
// conformance layer (workstream C): `filter-nested-callback-predicate` /
|
|
388
409
|
// `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` with
|
|
141
|
+
// a flag) — the regex-pattern form is refused upstream at the
|
|
142
|
+
// 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
|
|
@@ -149,8 +149,16 @@ export class JinjaFilterEmitter implements ParsedExprEmitter {
|
|
|
149
149
|
if (property === 'length') {
|
|
150
150
|
return `bf.length(${emit(object)})`
|
|
151
151
|
}
|
|
152
|
-
//
|
|
153
|
-
|
|
152
|
+
// Bracket/item access, NOT `.` attribute access: Jinja's default
|
|
153
|
+
// `getattr` semantics try a Python ATTRIBUTE first, falling back to a
|
|
154
|
+
// dict key only if no such attribute exists — so `group.items` (a
|
|
155
|
+
// dict key from the JS object) instead resolves to the built-in bound
|
|
156
|
+
// method `dict.items`, raising "not iterable" downstream instead of
|
|
157
|
+
// returning the key's value. `[...]` compiles through Jinja's
|
|
158
|
+
// `getitem`, which tries the KEY first, sidestepping any built-in
|
|
159
|
+
// dict method name (items/keys/values/get/pop/update/...) that would
|
|
160
|
+
// otherwise shadow a same-named JS object field.
|
|
161
|
+
return `${emit(object)}['${escapeJinjaSingleQuoted(property)}']`
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
@@ -175,8 +183,11 @@ export class JinjaFilterEmitter implements ParsedExprEmitter {
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
// Preserve source grouping: a compound operand re-emitted as infix
|
|
187
|
+
// text is otherwise re-parsed under THIS language's precedence —
|
|
188
|
+
// `(count() + 2) * 3` would silently become `count + 2 * 3` (#2173).
|
|
189
|
+
const l = groupBinaryOperand(left, emit(left))
|
|
190
|
+
const r = groupBinaryOperand(right, emit(right))
|
|
180
191
|
// Jinja's `==` / `!=` are value-equality operators that compare strings
|
|
181
192
|
// and numbers correctly — unlike Perl's numeric `==` (which the Mojo
|
|
182
193
|
// adapter must steer around with `eq`/`ne`). Same reasoning as Kolon.
|
|
@@ -310,7 +321,7 @@ export class JinjaTopLevelEmitter implements ParsedExprEmitter {
|
|
|
310
321
|
return String(value)
|
|
311
322
|
}
|
|
312
323
|
|
|
313
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
324
|
+
member(object: ParsedExpr, property: string, _computed: boolean, _optional: boolean, emit: (e: ParsedExpr) => string): string {
|
|
314
325
|
// `props.x` flattens to the bare context var the SSR caller binds each
|
|
315
326
|
// prop to (props arrive as individual top-level context entries, not a
|
|
316
327
|
// nested `props` dict).
|
|
@@ -328,8 +339,13 @@ export class JinjaTopLevelEmitter implements ParsedExprEmitter {
|
|
|
328
339
|
const obj = emit(object)
|
|
329
340
|
// `.length` → `bf.length` (array count or string char count, JS-compat).
|
|
330
341
|
if (property === 'length') return `bf.length(${obj})`
|
|
331
|
-
//
|
|
332
|
-
|
|
342
|
+
// Bracket/item access, NOT `.` attribute access — see the sibling
|
|
343
|
+
// `JinjaFilterEmitter.member()` for why: Jinja's `.` tries a Python
|
|
344
|
+
// ATTRIBUTE first, so a dict key that happens to share a name with a
|
|
345
|
+
// built-in dict method (`items`, `keys`, `values`, `get`, ...) resolves
|
|
346
|
+
// to the bound method instead of the value. `[...]` (Jinja `getitem`)
|
|
347
|
+
// tries the key first.
|
|
348
|
+
return `${obj}['${escapeJinjaSingleQuoted(property)}']`
|
|
333
349
|
}
|
|
334
350
|
|
|
335
351
|
indexAccess(object: ParsedExpr, index: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
@@ -378,8 +394,11 @@ export class JinjaTopLevelEmitter implements ParsedExprEmitter {
|
|
|
378
394
|
}
|
|
379
395
|
|
|
380
396
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
// Preserve source grouping: a compound operand re-emitted as infix
|
|
398
|
+
// text is otherwise re-parsed under THIS language's precedence —
|
|
399
|
+
// `(count() + 2) * 3` would silently become `count + 2 * 3` (#2173).
|
|
400
|
+
const l = groupBinaryOperand(left, emit(left))
|
|
401
|
+
const r = groupBinaryOperand(right, emit(right))
|
|
383
402
|
// Jinja's `==` / `!=` handle both strings and numbers (unlike Perl's
|
|
384
403
|
// numeric `==`), so all equality comparisons stay on `==` / `!=`.
|
|
385
404
|
const opMap: Record<string, string> = {
|