@barefootjs/jinja 0.18.4 → 0.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +120 -20
- package/dist/adapter/jinja-adapter.d.ts +49 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -1
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/lib/static-value.d.ts +15 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/build.js +120 -20
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +122 -37
- 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 +141 -0
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +19 -6
- package/src/adapter/jinja-adapter.ts +164 -13
- package/src/adapter/lib/constants.ts +3 -0
- package/src/adapter/lib/static-value.ts +41 -0
- package/src/conformance-pins.ts +24 -30
- package/src/render-divergences.ts +4 -16
- package/src/test-render.ts +13 -142
package/dist/index.js
CHANGED
|
@@ -187308,7 +187308,13 @@ import {
|
|
|
187308
187308
|
prepareLoweringMatchers,
|
|
187309
187309
|
queryHrefArgs,
|
|
187310
187310
|
isValidHelperId,
|
|
187311
|
-
sortComparatorFromArrow as sortComparatorFromArrow2
|
|
187311
|
+
sortComparatorFromArrow as sortComparatorFromArrow2,
|
|
187312
|
+
isDangerousInnerHtmlAttr,
|
|
187313
|
+
resolveDangerousInnerHtml,
|
|
187314
|
+
dangerousInnerHtmlMetacharViolation,
|
|
187315
|
+
dangerousInnerHtmlDiagnostic,
|
|
187316
|
+
resolveStaticLoopSource,
|
|
187317
|
+
collectLoopBoundNames
|
|
187312
187318
|
} from "@barefootjs/jsx";
|
|
187313
187319
|
|
|
187314
187320
|
// src/adapter/boolean-result.ts
|
|
@@ -187369,7 +187375,7 @@ function isAriaBooleanAttr(name) {
|
|
|
187369
187375
|
}
|
|
187370
187376
|
|
|
187371
187377
|
// src/adapter/jinja-adapter.ts
|
|
187372
|
-
import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
|
|
187378
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from "@barefootjs/shared";
|
|
187373
187379
|
|
|
187374
187380
|
// src/adapter/lib/constants.ts
|
|
187375
187381
|
var JINJA_TEMPLATE_PRIMITIVES = {
|
|
@@ -187378,7 +187384,10 @@ var JINJA_TEMPLATE_PRIMITIVES = {
|
|
|
187378
187384
|
Number: { arity: 1, emit: (args) => `bf.number(${args[0]})` },
|
|
187379
187385
|
"Math.floor": { arity: 1, emit: (args) => `bf.floor(${args[0]})` },
|
|
187380
187386
|
"Math.ceil": { arity: 1, emit: (args) => `bf.ceil(${args[0]})` },
|
|
187381
|
-
"Math.round": { arity: 1, emit: (args) => `bf.round(${args[0]})` }
|
|
187387
|
+
"Math.round": { arity: 1, emit: (args) => `bf.round(${args[0]})` },
|
|
187388
|
+
"Math.min": { arity: 2, emit: (args) => `bf.min(${args[0]}, ${args[1]})` },
|
|
187389
|
+
"Math.max": { arity: 2, emit: (args) => `bf.max(${args[0]}, ${args[1]})` },
|
|
187390
|
+
"Math.abs": { arity: 1, emit: (args) => `bf.abs(${args[0]})` }
|
|
187382
187391
|
};
|
|
187383
187392
|
var JINJA_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(JINJA_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
|
|
187384
187393
|
|
|
@@ -187557,6 +187566,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187557
187566
|
const recv = emit(object);
|
|
187558
187567
|
return `bf.trim(${recv})`;
|
|
187559
187568
|
}
|
|
187569
|
+
case "trimStart":
|
|
187570
|
+
case "trimEnd": {
|
|
187571
|
+
const fn = method === "trimStart" ? "trim_start" : "trim_end";
|
|
187572
|
+
const recv = emit(object);
|
|
187573
|
+
return `bf.${fn}(${recv})`;
|
|
187574
|
+
}
|
|
187560
187575
|
case "toFixed": {
|
|
187561
187576
|
const recv = emit(object);
|
|
187562
187577
|
const digits = args.length >= 1 ? emit(args[0]) : "0";
|
|
@@ -187590,6 +187605,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187590
187605
|
const newS = emit(args[1]);
|
|
187591
187606
|
return `bf.replace(${recv}, ${oldS}, ${newS})`;
|
|
187592
187607
|
}
|
|
187608
|
+
case "replaceAll": {
|
|
187609
|
+
const recv = emit(object);
|
|
187610
|
+
const oldS = emit(args[0]);
|
|
187611
|
+
const newS = emit(args[1]);
|
|
187612
|
+
return `bf.replace_all(${recv}, ${oldS}, ${newS})`;
|
|
187613
|
+
}
|
|
187593
187614
|
case "repeat": {
|
|
187594
187615
|
const recv = emit(object);
|
|
187595
187616
|
const count = args.length === 0 ? "0" : emit(args[0]);
|
|
@@ -187684,6 +187705,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187684
187705
|
return `bf.flat(${recv}, ${d})`;
|
|
187685
187706
|
}
|
|
187686
187707
|
|
|
187708
|
+
// src/adapter/lib/static-value.ts
|
|
187709
|
+
function staticValueToJinja(value) {
|
|
187710
|
+
if (value === null || value === undefined)
|
|
187711
|
+
return "none";
|
|
187712
|
+
if (typeof value === "boolean")
|
|
187713
|
+
return value ? "true" : "false";
|
|
187714
|
+
if (typeof value === "number")
|
|
187715
|
+
return String(value);
|
|
187716
|
+
if (typeof value === "string")
|
|
187717
|
+
return `'${escapeJinjaSingleQuoted(value)}'`;
|
|
187718
|
+
if (Array.isArray(value)) {
|
|
187719
|
+
const items = [];
|
|
187720
|
+
for (const el of value) {
|
|
187721
|
+
const serialized = staticValueToJinja(el);
|
|
187722
|
+
if (serialized === null)
|
|
187723
|
+
return null;
|
|
187724
|
+
items.push(serialized);
|
|
187725
|
+
}
|
|
187726
|
+
return `[${items.join(", ")}]`;
|
|
187727
|
+
}
|
|
187728
|
+
if (typeof value === "object") {
|
|
187729
|
+
const entries = [];
|
|
187730
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187731
|
+
const serialized = staticValueToJinja(val);
|
|
187732
|
+
if (serialized === null)
|
|
187733
|
+
return null;
|
|
187734
|
+
entries.push(`${jinjaHashKey(key)}: ${serialized}`);
|
|
187735
|
+
}
|
|
187736
|
+
return `{${entries.join(", ")}}`;
|
|
187737
|
+
}
|
|
187738
|
+
return null;
|
|
187739
|
+
}
|
|
187740
|
+
|
|
187687
187741
|
// src/adapter/expr/emitters.ts
|
|
187688
187742
|
import {
|
|
187689
187743
|
groupBinaryOperand,
|
|
@@ -187732,11 +187786,11 @@ class JinjaFilterEmitter {
|
|
|
187732
187786
|
return "none";
|
|
187733
187787
|
return String(value);
|
|
187734
187788
|
}
|
|
187735
|
-
member(object, property, _computed, emit) {
|
|
187789
|
+
member(object, property, _computed, _optional, emit) {
|
|
187736
187790
|
if (property === "length") {
|
|
187737
187791
|
return `bf.length(${emit(object)})`;
|
|
187738
187792
|
}
|
|
187739
|
-
return `${emit(object)}
|
|
187793
|
+
return `${emit(object)}['${escapeJinjaSingleQuoted(property)}']`;
|
|
187740
187794
|
}
|
|
187741
187795
|
indexAccess(object, index, emit) {
|
|
187742
187796
|
return `${emit(object)}[${emit(index)}]`;
|
|
@@ -187838,7 +187892,7 @@ class JinjaTopLevelEmitter {
|
|
|
187838
187892
|
return "none";
|
|
187839
187893
|
return String(value);
|
|
187840
187894
|
}
|
|
187841
|
-
member(object, property, _computed, emit) {
|
|
187895
|
+
member(object, property, _computed, _optional, emit) {
|
|
187842
187896
|
if (object.kind === "identifier" && object.name === "props") {
|
|
187843
187897
|
return jinjaIdent(property);
|
|
187844
187898
|
}
|
|
@@ -187850,7 +187904,7 @@ class JinjaTopLevelEmitter {
|
|
|
187850
187904
|
const obj = emit(object);
|
|
187851
187905
|
if (property === "length")
|
|
187852
187906
|
return `bf.length(${obj})`;
|
|
187853
|
-
return `${obj}
|
|
187907
|
+
return `${obj}['${escapeJinjaSingleQuoted(property)}']`;
|
|
187854
187908
|
}
|
|
187855
187909
|
indexAccess(object, index, emit) {
|
|
187856
187910
|
return `${emit(object)}[${emit(index)}]`;
|
|
@@ -188257,6 +188311,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188257
188311
|
options;
|
|
188258
188312
|
errors = [];
|
|
188259
188313
|
inLoop = false;
|
|
188314
|
+
currentLoopKeyDepth = 0;
|
|
188260
188315
|
propsObjectName = null;
|
|
188261
188316
|
propsParams = [];
|
|
188262
188317
|
booleanTypedProps = new Set;
|
|
@@ -188265,6 +188320,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188265
188320
|
_searchParamsLocals = new Set;
|
|
188266
188321
|
_loweringMatchers = [];
|
|
188267
188322
|
localConstants = [];
|
|
188323
|
+
staticLoopSourceBoundNames = new Set;
|
|
188268
188324
|
nullableOptionalProps = new Set;
|
|
188269
188325
|
constructor(options = {}) {
|
|
188270
188326
|
super();
|
|
@@ -188280,6 +188336,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188280
188336
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
188281
188337
|
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
188282
188338
|
this.localConstants = ir.metadata.localConstants ?? [];
|
|
188339
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames(ir);
|
|
188283
188340
|
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
188284
188341
|
this.stringValueNames = collectStringValueNames(ir);
|
|
188285
188342
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
@@ -188333,7 +188390,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188333
188390
|
return this.renderElement(node);
|
|
188334
188391
|
}
|
|
188335
188392
|
emitText(node) {
|
|
188336
|
-
return node.value;
|
|
188393
|
+
return escapeHtml(node.value);
|
|
188337
188394
|
}
|
|
188338
188395
|
emitExpression(node) {
|
|
188339
188396
|
return this.renderExpression(node);
|
|
@@ -188401,7 +188458,8 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188401
188458
|
renderElement(element) {
|
|
188402
188459
|
const tag = element.tag;
|
|
188403
188460
|
const attrs = this.renderAttributes(element);
|
|
188404
|
-
const
|
|
188461
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188462
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188405
188463
|
let hydrationAttrs = "";
|
|
188406
188464
|
if (element.needsScope) {
|
|
188407
188465
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188436,6 +188494,22 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188436
188494
|
}
|
|
188437
188495
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188438
188496
|
}
|
|
188497
|
+
renderDangerousInnerHtml(element) {
|
|
188498
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188499
|
+
if (!resolution)
|
|
188500
|
+
return null;
|
|
188501
|
+
if (resolution.kind === "dynamic") {
|
|
188502
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188503
|
+
return "";
|
|
188504
|
+
}
|
|
188505
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188506
|
+
if (violation) {
|
|
188507
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188508
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188509
|
+
return "";
|
|
188510
|
+
}
|
|
188511
|
+
return resolution.html;
|
|
188512
|
+
}
|
|
188439
188513
|
renderExpression(expr) {
|
|
188440
188514
|
if (expr.clientOnly) {
|
|
188441
188515
|
if (expr.slotId) {
|
|
@@ -188443,7 +188517,7 @@ class JinjaAdapter extends BaseAdapter {
|
|
|
188443
188517
|
}
|
|
188444
188518
|
return "";
|
|
188445
188519
|
}
|
|
188446
|
-
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr)})`;
|
|
188520
|
+
const jinjaExpr = `bf.string(${this.convertExpressionToJinja(expr.expr, expr.parsed)})`;
|
|
188447
188521
|
if (expr.slotId) {
|
|
188448
188522
|
return `{{ bf.text_start("${expr.slotId}") | safe }}{{ ${jinjaExpr} }}{{ bf.text_end() | safe }}`;
|
|
188449
188523
|
}
|
|
@@ -188535,8 +188609,12 @@ ${whenTrue}
|
|
|
188535
188609
|
}
|
|
188536
188610
|
});
|
|
188537
188611
|
}
|
|
188612
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188613
|
+
isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
|
|
188614
|
+
});
|
|
188615
|
+
const staticArray = staticItems !== null ? staticValueToJinja(staticItems) : null;
|
|
188538
188616
|
const arrayName = loop.array.trim();
|
|
188539
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188617
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188540
188618
|
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
188541
188619
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
188542
188620
|
this.errors.push({
|
|
@@ -188550,7 +188628,7 @@ ${whenTrue}
|
|
|
188550
188628
|
});
|
|
188551
188629
|
}
|
|
188552
188630
|
}
|
|
188553
|
-
const rawArray = this.convertExpressionToJinja(loop.array);
|
|
188631
|
+
const rawArray = staticArray ?? this.convertExpressionToJinja(loop.array);
|
|
188554
188632
|
let array = rawArray;
|
|
188555
188633
|
if (loop.sortComparator) {
|
|
188556
188634
|
const sort = loop.sortComparator;
|
|
@@ -188564,7 +188642,7 @@ ${whenTrue}
|
|
|
188564
188642
|
const renderedChildren = this.renderChildren(loop.children);
|
|
188565
188643
|
const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
|
|
188566
188644
|
const indexLocalLines = [];
|
|
188567
|
-
if (loop.iterationShape === "keys") {
|
|
188645
|
+
if (loop.objectIteration) {} else if (loop.iterationShape === "keys") {
|
|
188568
188646
|
indexLocalLines.push(`{% set ${jinjaIdent(param)} = loop.index0 %}`);
|
|
188569
188647
|
} else if (loop.index) {
|
|
188570
188648
|
indexLocalLines.push(`{% set ${jinjaIdent(loop.index)} = loop.index0 %}`);
|
|
@@ -188584,13 +188662,17 @@ ${whenTrue}
|
|
|
188584
188662
|
}
|
|
188585
188663
|
const prevInLoop = this.inLoop;
|
|
188586
188664
|
this.inLoop = true;
|
|
188665
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth;
|
|
188666
|
+
this.currentLoopKeyDepth = loop.depth;
|
|
188587
188667
|
const childrenUnderLoop = this.renderChildren(loop.children);
|
|
188668
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth;
|
|
188588
188669
|
this.inLoop = prevInLoop;
|
|
188589
188670
|
const bodyChildren = loop.bodyIsItemConditional && loop.key ? `{{ bf.comment("loop-i:" ~ bf.string(${this.convertExpressionToJinja(loop.key)})) | safe }}
|
|
188590
188671
|
${childrenUnderLoop}` : childrenUnderLoop;
|
|
188591
188672
|
const lines = [];
|
|
188592
188673
|
lines.push(`{{ bf.comment("loop:${loop.markerId}") | safe }}`);
|
|
188593
|
-
|
|
188674
|
+
const forHeader = loop.objectIteration === "entries" ? `{% for ${jinjaIdent(loop.index ?? param)}, ${jinjaIdent(param)} in ${array}.items() %}` : loop.objectIteration === "keys" ? `{% for ${jinjaIdent(param)} in ${array}.keys() %}` : loop.objectIteration === "values" ? `{% for ${jinjaIdent(param)} in ${array}.values() %}` : `{% for ${jinjaIdent(loopVar)} in ${array} %}`;
|
|
188675
|
+
lines.push(forHeader);
|
|
188594
188676
|
for (const il of indexLocalLines)
|
|
188595
188677
|
lines.push(il);
|
|
188596
188678
|
if (loop.filterPredicate) {
|
|
@@ -188662,9 +188744,20 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188662
188744
|
renderComponent(comp) {
|
|
188663
188745
|
const segments = [{ kind: "entries", parts: [] }];
|
|
188664
188746
|
const currentEntries = () => this.componentPropSegmentEntries(segments);
|
|
188747
|
+
const namedSlotSetBlocks = [];
|
|
188665
188748
|
for (const p of comp.props) {
|
|
188666
188749
|
if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
|
|
188667
188750
|
continue;
|
|
188751
|
+
if (p.value.kind === "jsx-children" && p.name !== "children") {
|
|
188752
|
+
const prevInLoop = this.inLoop;
|
|
188753
|
+
this.inLoop = false;
|
|
188754
|
+
const slotBody = this.renderChildren(p.value.children);
|
|
188755
|
+
this.inLoop = prevInLoop;
|
|
188756
|
+
const captureName = `bf_prop_${this.childrenCaptureCounter++}`;
|
|
188757
|
+
namedSlotSetBlocks.push(`{% set ${captureName} %}${slotBody}{% endset %}`);
|
|
188758
|
+
currentEntries().push(`${jinjaHashKey(p.name)}: ${captureName}`);
|
|
188759
|
+
continue;
|
|
188760
|
+
}
|
|
188668
188761
|
if (p.value.kind === "spread") {
|
|
188669
188762
|
const trimmed = p.value.expr.trim();
|
|
188670
188763
|
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
@@ -188693,11 +188786,11 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188693
188786
|
const captureName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
188694
188787
|
currentEntries().push(`${jinjaHashKey("children")}: ${captureName}`);
|
|
188695
188788
|
const dict = this.combineComponentPropSegments(segments);
|
|
188696
|
-
return
|
|
188789
|
+
return `${namedSlotSetBlocks.join("")}{% set ${captureName} %}${childrenBody}{% endset %}{{ bf.render_child('${tplName}', ${dict}) | safe }}`;
|
|
188697
188790
|
}
|
|
188698
188791
|
const isEmpty = segments.every((s) => s.kind === "entries" && s.parts.length === 0);
|
|
188699
188792
|
const dictEntries = isEmpty ? "" : `, ${this.combineComponentPropSegments(segments)}`;
|
|
188700
|
-
return
|
|
188793
|
+
return `${namedSlotSetBlocks.join("")}{{ bf.render_child('${tplName}'${dictEntries}) | safe }}`;
|
|
188701
188794
|
}
|
|
188702
188795
|
childrenCaptureCounter = 0;
|
|
188703
188796
|
presenceVarCounter = 0;
|
|
@@ -188742,7 +188835,7 @@ ${alternate}
|
|
|
188742
188835
|
${children}`;
|
|
188743
188836
|
}
|
|
188744
188837
|
elementAttrEmitter = {
|
|
188745
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
188838
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
188746
188839
|
emitExpression: (value, name) => {
|
|
188747
188840
|
if (name === "style") {
|
|
188748
188841
|
const css = this.tryLowerStyleObject(value.expr);
|
|
@@ -188847,12 +188940,15 @@ ${name}="{{ bf.string(${val}) }}"
|
|
|
188847
188940
|
for (const attr of element.attrs) {
|
|
188848
188941
|
if (attr.clientOnly)
|
|
188849
188942
|
continue;
|
|
188943
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
188944
|
+
continue;
|
|
188850
188945
|
let attrName;
|
|
188851
188946
|
if (attr.name === "className")
|
|
188852
188947
|
attrName = "class";
|
|
188853
|
-
else if (attr.name === "key")
|
|
188854
|
-
|
|
188855
|
-
|
|
188948
|
+
else if (attr.name === "key") {
|
|
188949
|
+
const depth = this.currentLoopKeyDepth;
|
|
188950
|
+
attrName = depth > 0 ? `data-key-${depth}` : "data-key";
|
|
188951
|
+
} else
|
|
188856
188952
|
attrName = attr.name;
|
|
188857
188953
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
188858
188954
|
if (lowered)
|
|
@@ -189045,6 +189141,8 @@ Options:
|
|
|
189045
189141
|
return isBooleanResultExpr(expr) || isAriaBooleanAttr(name) || this.isBooleanTypedPropRef(expr);
|
|
189046
189142
|
}
|
|
189047
189143
|
_resolveLiteralConst(name) {
|
|
189144
|
+
if (this.staticLoopSourceBoundNames.has(name))
|
|
189145
|
+
return null;
|
|
189048
189146
|
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
189049
189147
|
if (c?.value === undefined)
|
|
189050
189148
|
return null;
|
|
@@ -189057,6 +189155,8 @@ Options:
|
|
|
189057
189155
|
return null;
|
|
189058
189156
|
}
|
|
189059
189157
|
_resolveStaticRecordLiteral(objectName, key) {
|
|
189158
|
+
if (this.staticLoopSourceBoundNames.has(objectName))
|
|
189159
|
+
return null;
|
|
189060
189160
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
189061
189161
|
if (!hit)
|
|
189062
189162
|
return null;
|
|
@@ -189094,14 +189194,10 @@ Options:
|
|
|
189094
189194
|
var jinjaAdapter = new JinjaAdapter;
|
|
189095
189195
|
// src/conformance-pins.ts
|
|
189096
189196
|
var conformancePins = {
|
|
189097
|
-
"static-array-children": [{ code: "BF103", severity: "error" }],
|
|
189098
|
-
"todo-app": [{ code: "BF103", severity: "error" }],
|
|
189099
|
-
"todo-app-ssr": [{ code: "BF103", severity: "error" }],
|
|
189100
189197
|
"static-array-from-props": [
|
|
189101
189198
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
|
|
189102
189199
|
],
|
|
189103
189200
|
"static-array-from-props-with-component": [
|
|
189104
|
-
{ code: "BF103", severity: "error" },
|
|
189105
189201
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
|
|
189106
189202
|
],
|
|
189107
189203
|
"filter-nested-callback-predicate": [
|
|
@@ -189110,21 +189206,10 @@ var conformancePins = {
|
|
|
189110
189206
|
"filter-nested-find-predicate": [
|
|
189111
189207
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189112
189208
|
],
|
|
189113
|
-
"
|
|
189114
|
-
"dangerous-inner-html": [{ code: "BF101", severity: "error" }],
|
|
189115
|
-
"string-replaceall": [{ code: "BF101", severity: "error" }]
|
|
189209
|
+
"dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
|
|
189116
189210
|
};
|
|
189117
189211
|
// src/render-divergences.ts
|
|
189118
|
-
var renderDivergences = {
|
|
189119
|
-
"html-entity-text": "`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
|
|
189120
|
-
"math-methods": "Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)",
|
|
189121
|
-
"static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
189122
|
-
"object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations",
|
|
189123
|
-
"nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
|
|
189124
|
-
"jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
|
|
189125
|
-
"string-slice": "`.slice()` on a STRING renders empty (array-slice helper misfires on strings)",
|
|
189126
|
-
"string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering)"
|
|
189127
|
-
};
|
|
189212
|
+
var renderDivergences = {};
|
|
189128
189213
|
export {
|
|
189129
189214
|
renderDivergences,
|
|
189130
189215
|
jinjaAdapter,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,
|
|
1
|
+
{"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,iBAK/B,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/jinja",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.7",
|
|
4
4
|
"description": "Jinja2 adapter for BarefootJS — compiles IR to .jinja templates and ships the Python BarefootJS rendering runtime; runs under any Python web framework (Flask, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
"directory": "packages/adapter-jinja"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@barefootjs/shared": "0.18.
|
|
56
|
+
"@barefootjs/shared": "0.18.7"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@barefootjs/jsx": ">=0.2.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
63
|
-
"@barefootjs/jsx": "0.18.
|
|
63
|
+
"@barefootjs/jsx": "0.18.7",
|
|
64
64
|
"typescript": "^5.0.0"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -819,6 +819,29 @@ class BarefootJS:
|
|
|
819
819
|
# is -1, not -2). `floor(n + 0.5)` reproduces that for both signs.
|
|
820
820
|
return float(math.floor(n + 0.5))
|
|
821
821
|
|
|
822
|
+
def min(self, a: Any, b: Any) -> float:
|
|
823
|
+
"""`Math.min(a, b)` -- two-arg form only (#2168 math-methods).
|
|
824
|
+
JS returns NaN if either operand is NaN."""
|
|
825
|
+
x, y = js_number(a), js_number(b)
|
|
826
|
+
if _is_nan(x):
|
|
827
|
+
return x
|
|
828
|
+
if _is_nan(y):
|
|
829
|
+
return y
|
|
830
|
+
return x if x < y else y
|
|
831
|
+
|
|
832
|
+
def max(self, a: Any, b: Any) -> float:
|
|
833
|
+
x, y = js_number(a), js_number(b)
|
|
834
|
+
if _is_nan(x):
|
|
835
|
+
return x
|
|
836
|
+
if _is_nan(y):
|
|
837
|
+
return y
|
|
838
|
+
return x if x > y else y
|
|
839
|
+
|
|
840
|
+
def abs(self, value: Any) -> float:
|
|
841
|
+
"""`Math.abs()` (#2168 math-methods)."""
|
|
842
|
+
n = js_number(value)
|
|
843
|
+
return n if _is_nan(n) else abs(n)
|
|
844
|
+
|
|
822
845
|
# -----------------------------------------------------------------
|
|
823
846
|
# Array / String method helpers (#1448 Tier A)
|
|
824
847
|
# -----------------------------------------------------------------
|
|
@@ -933,12 +956,20 @@ class BarefootJS:
|
|
|
933
956
|
out.extend(b)
|
|
934
957
|
return out
|
|
935
958
|
|
|
936
|
-
def slice(self, recv: Any, start: Any, end: Any) ->
|
|
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"])
|