@barefootjs/xslate 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 +131 -19
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/lib/static-value.d.ts +18 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +25 -7
- package/dist/adapter/props/prop-classes.d.ts.map +1 -1
- package/dist/adapter/xslate-adapter.d.ts +49 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +131 -19
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +134 -39
- package/dist/render-divergences.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Xslate.pm +1 -1
- package/package.json +3 -3
- package/src/__tests__/xslate-adapter.test.ts +139 -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 +44 -0
- package/src/adapter/props/prop-classes.ts +29 -7
- package/src/adapter/xslate-adapter.ts +178 -15
- package/src/conformance-pins.ts +26 -33
- package/src/render-divergences.ts +4 -16
- package/src/test-render.ts +13 -139
package/dist/index.js
CHANGED
|
@@ -187308,7 +187308,13 @@ import {
|
|
|
187308
187308
|
queryHrefArgs,
|
|
187309
187309
|
isValidHelperId,
|
|
187310
187310
|
sortComparatorFromArrow as sortComparatorFromArrow2,
|
|
187311
|
-
isLowerableLoopDestructure
|
|
187311
|
+
isLowerableLoopDestructure,
|
|
187312
|
+
isDangerousInnerHtmlAttr,
|
|
187313
|
+
resolveDangerousInnerHtml,
|
|
187314
|
+
dangerousInnerHtmlMetacharViolation,
|
|
187315
|
+
dangerousInnerHtmlDiagnostic,
|
|
187316
|
+
resolveStaticLoopSource,
|
|
187317
|
+
collectLoopBoundNames as collectLoopBoundNames2
|
|
187312
187318
|
} from "@barefootjs/jsx";
|
|
187313
187319
|
|
|
187314
187320
|
// src/adapter/boolean-result.ts
|
|
@@ -187365,7 +187371,7 @@ function isAriaBooleanAttr(name) {
|
|
|
187365
187371
|
}
|
|
187366
187372
|
|
|
187367
187373
|
// src/adapter/xslate-adapter.ts
|
|
187368
|
-
import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
|
|
187374
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from "@barefootjs/shared";
|
|
187369
187375
|
|
|
187370
187376
|
// src/adapter/lib/constants.ts
|
|
187371
187377
|
var XSLATE_TEMPLATE_PRIMITIVES = {
|
|
@@ -187374,7 +187380,10 @@ var XSLATE_TEMPLATE_PRIMITIVES = {
|
|
|
187374
187380
|
Number: { arity: 1, emit: (args) => `$bf.number(${args[0]})` },
|
|
187375
187381
|
"Math.floor": { arity: 1, emit: (args) => `$bf.floor(${args[0]})` },
|
|
187376
187382
|
"Math.ceil": { arity: 1, emit: (args) => `$bf.ceil(${args[0]})` },
|
|
187377
|
-
"Math.round": { arity: 1, emit: (args) => `$bf.round(${args[0]})` }
|
|
187383
|
+
"Math.round": { arity: 1, emit: (args) => `$bf.round(${args[0]})` },
|
|
187384
|
+
"Math.min": { arity: 2, emit: (args) => `$bf.min(${args[0]}, ${args[1]})` },
|
|
187385
|
+
"Math.max": { arity: 2, emit: (args) => `$bf.max(${args[0]}, ${args[1]})` },
|
|
187386
|
+
"Math.abs": { arity: 1, emit: (args) => `$bf.abs(${args[0]})` }
|
|
187378
187387
|
};
|
|
187379
187388
|
var XSLATE_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(XSLATE_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
|
|
187380
187389
|
|
|
@@ -187479,6 +187488,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187479
187488
|
const recv = emit(object);
|
|
187480
187489
|
return `$bf.trim(${recv})`;
|
|
187481
187490
|
}
|
|
187491
|
+
case "trimStart":
|
|
187492
|
+
case "trimEnd": {
|
|
187493
|
+
const fn = method === "trimStart" ? "trim_start" : "trim_end";
|
|
187494
|
+
const recv = emit(object);
|
|
187495
|
+
return `$bf.${fn}(${recv})`;
|
|
187496
|
+
}
|
|
187482
187497
|
case "toFixed": {
|
|
187483
187498
|
const recv = emit(object);
|
|
187484
187499
|
const digits = args.length >= 1 ? emit(args[0]) : "0";
|
|
@@ -187512,6 +187527,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187512
187527
|
const newS = emit(args[1]);
|
|
187513
187528
|
return `$bf.replace(${recv}, ${oldS}, ${newS})`;
|
|
187514
187529
|
}
|
|
187530
|
+
case "replaceAll": {
|
|
187531
|
+
const recv = emit(object);
|
|
187532
|
+
const oldS = emit(args[0]);
|
|
187533
|
+
const newS = emit(args[1]);
|
|
187534
|
+
return `$bf.replace_all(${recv}, ${oldS}, ${newS})`;
|
|
187535
|
+
}
|
|
187515
187536
|
case "repeat": {
|
|
187516
187537
|
const recv = emit(object);
|
|
187517
187538
|
const count = args.length === 0 ? "0" : emit(args[0]);
|
|
@@ -187609,6 +187630,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187609
187630
|
return `$bf.flat(${recv}, ${d})`;
|
|
187610
187631
|
}
|
|
187611
187632
|
|
|
187633
|
+
// src/adapter/lib/static-value.ts
|
|
187634
|
+
function staticValueToKolon(value) {
|
|
187635
|
+
if (value === null || value === undefined)
|
|
187636
|
+
return "nil";
|
|
187637
|
+
if (typeof value === "boolean")
|
|
187638
|
+
return null;
|
|
187639
|
+
if (typeof value === "number")
|
|
187640
|
+
return String(value);
|
|
187641
|
+
if (typeof value === "string")
|
|
187642
|
+
return `'${escapeKolonSingleQuoted(value)}'`;
|
|
187643
|
+
if (Array.isArray(value)) {
|
|
187644
|
+
const items = [];
|
|
187645
|
+
for (const el of value) {
|
|
187646
|
+
const serialized = staticValueToKolon(el);
|
|
187647
|
+
if (serialized === null)
|
|
187648
|
+
return null;
|
|
187649
|
+
items.push(serialized);
|
|
187650
|
+
}
|
|
187651
|
+
return `[${items.join(", ")}]`;
|
|
187652
|
+
}
|
|
187653
|
+
if (typeof value === "object") {
|
|
187654
|
+
const entries = [];
|
|
187655
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187656
|
+
const serialized = staticValueToKolon(val);
|
|
187657
|
+
if (serialized === null)
|
|
187658
|
+
return null;
|
|
187659
|
+
entries.push(`${kolonHashKey(key)} => ${serialized}`);
|
|
187660
|
+
}
|
|
187661
|
+
return `{ ${entries.join(", ")} }`;
|
|
187662
|
+
}
|
|
187663
|
+
return null;
|
|
187664
|
+
}
|
|
187665
|
+
|
|
187612
187666
|
// src/adapter/expr/emitters.ts
|
|
187613
187667
|
import {
|
|
187614
187668
|
groupBinaryOperand,
|
|
@@ -187655,7 +187709,7 @@ class XslateFilterEmitter {
|
|
|
187655
187709
|
return "nil";
|
|
187656
187710
|
return String(value);
|
|
187657
187711
|
}
|
|
187658
|
-
member(object, property, _computed, emit) {
|
|
187712
|
+
member(object, property, _computed, _optional, emit) {
|
|
187659
187713
|
if (property === "length") {
|
|
187660
187714
|
return `$bf.length(${emit(object)})`;
|
|
187661
187715
|
}
|
|
@@ -187767,7 +187821,7 @@ class XslateTopLevelEmitter {
|
|
|
187767
187821
|
return "nil";
|
|
187768
187822
|
return String(value);
|
|
187769
187823
|
}
|
|
187770
|
-
member(object, property, _computed, emit) {
|
|
187824
|
+
member(object, property, _computed, _optional, emit) {
|
|
187771
187825
|
if (object.kind === "identifier" && object.name === "props") {
|
|
187772
187826
|
return `$${property}`;
|
|
187773
187827
|
}
|
|
@@ -188152,6 +188206,9 @@ function generateDerivedMemoSeed(ctx, ir) {
|
|
|
188152
188206
|
` : "";
|
|
188153
188207
|
}
|
|
188154
188208
|
|
|
188209
|
+
// src/adapter/props/prop-classes.ts
|
|
188210
|
+
import { collectLoopBoundNames } from "@barefootjs/jsx";
|
|
188211
|
+
|
|
188155
188212
|
// src/adapter/value/parsed-literal.ts
|
|
188156
188213
|
import { evalStringArrayJoin } from "@barefootjs/jsx";
|
|
188157
188214
|
function isStringTypeInfo(type2) {
|
|
@@ -188182,6 +188239,12 @@ function collectStringValueNames(ir) {
|
|
|
188182
188239
|
if (isStringTypeInfo(p.type))
|
|
188183
188240
|
names.add(p.name);
|
|
188184
188241
|
}
|
|
188242
|
+
for (const c of ir.metadata.localConstants) {
|
|
188243
|
+
if (isStringTypeInfo(c.type ?? undefined) || isBareStringLiteral(c.value))
|
|
188244
|
+
names.add(c.name);
|
|
188245
|
+
}
|
|
188246
|
+
for (const bound of collectLoopBoundNames(ir))
|
|
188247
|
+
names.delete(bound);
|
|
188185
188248
|
return names;
|
|
188186
188249
|
}
|
|
188187
188250
|
|
|
@@ -188208,6 +188271,7 @@ class XslateAdapter extends BaseAdapter {
|
|
|
188208
188271
|
options;
|
|
188209
188272
|
errors = [];
|
|
188210
188273
|
inLoop = false;
|
|
188274
|
+
currentLoopKeyDepth = 0;
|
|
188211
188275
|
propsObjectName = null;
|
|
188212
188276
|
propsParams = [];
|
|
188213
188277
|
booleanTypedProps = new Set;
|
|
@@ -188216,6 +188280,7 @@ class XslateAdapter extends BaseAdapter {
|
|
|
188216
188280
|
_searchParamsLocals = new Set;
|
|
188217
188281
|
_loweringMatchers = [];
|
|
188218
188282
|
localConstants = [];
|
|
188283
|
+
staticLoopSourceBoundNames = new Set;
|
|
188219
188284
|
nullableOptionalProps = new Set;
|
|
188220
188285
|
constructor(options = {}) {
|
|
188221
188286
|
super();
|
|
@@ -188231,6 +188296,7 @@ class XslateAdapter extends BaseAdapter {
|
|
|
188231
188296
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
188232
188297
|
this.booleanTypedProps = collectBooleanTypedProps(ir);
|
|
188233
188298
|
this.localConstants = ir.metadata.localConstants ?? [];
|
|
188299
|
+
this.staticLoopSourceBoundNames = collectLoopBoundNames2(ir);
|
|
188234
188300
|
this.nullableOptionalProps = collectNullableOptionalProps(ir);
|
|
188235
188301
|
this.stringValueNames = collectStringValueNames(ir);
|
|
188236
188302
|
this.moduleStringConsts = collectModuleStringConsts(ir.metadata.localConstants);
|
|
@@ -188284,7 +188350,7 @@ class XslateAdapter extends BaseAdapter {
|
|
|
188284
188350
|
return this.renderElement(node);
|
|
188285
188351
|
}
|
|
188286
188352
|
emitText(node) {
|
|
188287
|
-
return node.value;
|
|
188353
|
+
return escapeHtml(node.value);
|
|
188288
188354
|
}
|
|
188289
188355
|
emitExpression(node) {
|
|
188290
188356
|
return this.renderExpression(node);
|
|
@@ -188347,7 +188413,8 @@ class XslateAdapter extends BaseAdapter {
|
|
|
188347
188413
|
renderElement(element) {
|
|
188348
188414
|
const tag = element.tag;
|
|
188349
188415
|
const attrs = this.renderAttributes(element);
|
|
188350
|
-
const
|
|
188416
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188417
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188351
188418
|
let hydrationAttrs = "";
|
|
188352
188419
|
if (element.needsScope) {
|
|
188353
188420
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188382,6 +188449,22 @@ class XslateAdapter extends BaseAdapter {
|
|
|
188382
188449
|
}
|
|
188383
188450
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188384
188451
|
}
|
|
188452
|
+
renderDangerousInnerHtml(element) {
|
|
188453
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188454
|
+
if (!resolution)
|
|
188455
|
+
return null;
|
|
188456
|
+
if (resolution.kind === "dynamic") {
|
|
188457
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188458
|
+
return "";
|
|
188459
|
+
}
|
|
188460
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188461
|
+
if (violation) {
|
|
188462
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188463
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188464
|
+
return "";
|
|
188465
|
+
}
|
|
188466
|
+
return resolution.html;
|
|
188467
|
+
}
|
|
188385
188468
|
renderExpression(expr) {
|
|
188386
188469
|
if (expr.clientOnly) {
|
|
188387
188470
|
if (expr.slotId) {
|
|
@@ -188389,7 +188472,7 @@ class XslateAdapter extends BaseAdapter {
|
|
|
188389
188472
|
}
|
|
188390
188473
|
return "";
|
|
188391
188474
|
}
|
|
188392
|
-
const perlExpr = this.convertExpressionToKolon(expr.expr);
|
|
188475
|
+
const perlExpr = this.convertExpressionToKolon(expr.expr, expr.parsed);
|
|
188393
188476
|
if (expr.slotId) {
|
|
188394
188477
|
return `<: $bf.text_start("${expr.slotId}") | mark_raw :><: ${perlExpr} :><: $bf.text_end() | mark_raw :>`;
|
|
188395
188478
|
}
|
|
@@ -188481,8 +188564,12 @@ ${whenTrue}
|
|
|
188481
188564
|
}
|
|
188482
188565
|
});
|
|
188483
188566
|
}
|
|
188567
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188568
|
+
isNameShadowed: (name) => this.staticLoopSourceBoundNames.has(name)
|
|
188569
|
+
});
|
|
188570
|
+
const staticArray = staticItems !== null ? staticValueToKolon(staticItems) : null;
|
|
188484
188571
|
const arrayName = loop.array.trim();
|
|
188485
|
-
if (/^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188572
|
+
if (staticArray === null && /^[A-Za-z_$][\w$]*$/.test(arrayName)) {
|
|
188486
188573
|
const arrayConst = (this.localConstants ?? []).find((c) => c.name === arrayName);
|
|
188487
188574
|
if (arrayConst && !arrayConst.isModule && this._resolveLiteralConst(arrayName) === null) {
|
|
188488
188575
|
this.errors.push({
|
|
@@ -188496,7 +188583,7 @@ ${whenTrue}
|
|
|
188496
188583
|
});
|
|
188497
188584
|
}
|
|
188498
188585
|
}
|
|
188499
|
-
const rawArray = this.convertExpressionToKolon(loop.array);
|
|
188586
|
+
const rawArray = staticArray ?? this.convertExpressionToKolon(loop.array);
|
|
188500
188587
|
let array = rawArray;
|
|
188501
188588
|
if (loop.sortComparator) {
|
|
188502
188589
|
const sort = loop.sortComparator;
|
|
@@ -188508,9 +188595,12 @@ ${whenTrue}
|
|
|
188508
188595
|
}
|
|
188509
188596
|
const param = loop.param;
|
|
188510
188597
|
const renderedChildren = this.renderChildren(loop.children);
|
|
188511
|
-
const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
|
|
188598
|
+
const loopVar = loop.objectIteration === "entries" ? "__bf_pair" : loop.objectIteration ? param : loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
|
|
188512
188599
|
const indexLocalLines = [];
|
|
188513
|
-
if (loop.
|
|
188600
|
+
if (loop.objectIteration === "entries") {
|
|
188601
|
+
indexLocalLines.push(`: my $${loop.index ?? param} = $${loopVar}.key;`);
|
|
188602
|
+
indexLocalLines.push(`: my $${param} = $${loopVar}.value;`);
|
|
188603
|
+
} else if (loop.objectIteration) {} else if (loop.iterationShape === "keys") {
|
|
188514
188604
|
indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`);
|
|
188515
188605
|
} else if (loop.index) {
|
|
188516
188606
|
indexLocalLines.push(`: my $${loop.index} = $~${loopVar}.index;`);
|
|
@@ -188530,13 +188620,17 @@ ${whenTrue}
|
|
|
188530
188620
|
}
|
|
188531
188621
|
const prevInLoop = this.inLoop;
|
|
188532
188622
|
this.inLoop = true;
|
|
188623
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth;
|
|
188624
|
+
this.currentLoopKeyDepth = loop.depth;
|
|
188533
188625
|
const childrenUnderLoop = this.renderChildren(loop.children);
|
|
188626
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth;
|
|
188534
188627
|
this.inLoop = prevInLoop;
|
|
188535
188628
|
const bodyChildren = loop.bodyIsItemConditional && loop.key ? `<: $bf.comment("loop-i:" ~ ${this.convertExpressionToKolon(loop.key)}) | mark_raw :>
|
|
188536
188629
|
${childrenUnderLoop}` : childrenUnderLoop;
|
|
188537
188630
|
const lines = [];
|
|
188538
188631
|
lines.push(`<: $bf.comment("loop:${loop.markerId}") | mark_raw :>`);
|
|
188539
|
-
|
|
188632
|
+
const forHeader = loop.objectIteration === "entries" ? `: for ${array}.kv() -> $${loopVar} {` : loop.objectIteration === "keys" ? `: for ${array}.keys() -> $${loopVar} {` : loop.objectIteration === "values" ? `: for ${array}.values() -> $${loopVar} {` : `: for ${array} -> $${loopVar} {`;
|
|
188633
|
+
lines.push(forHeader);
|
|
188540
188634
|
for (const il of indexLocalLines)
|
|
188541
188635
|
lines.push(il);
|
|
188542
188636
|
if (loop.filterPredicate) {
|
|
@@ -188607,9 +188701,20 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188607
188701
|
renderComponent(comp) {
|
|
188608
188702
|
const segments = [{ kind: "entries", parts: [] }];
|
|
188609
188703
|
const currentEntries = () => this.componentPropSegmentEntries(segments);
|
|
188704
|
+
const namedSlotMacros = [];
|
|
188610
188705
|
for (const p of comp.props) {
|
|
188611
188706
|
if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
|
|
188612
188707
|
continue;
|
|
188708
|
+
if (p.value.kind === "jsx-children" && p.name !== "children") {
|
|
188709
|
+
const prevInLoop = this.inLoop;
|
|
188710
|
+
this.inLoop = false;
|
|
188711
|
+
const slotBody = this.renderChildren(p.value.children);
|
|
188712
|
+
this.inLoop = prevInLoop;
|
|
188713
|
+
const macroName = `bf_prop_${this.childrenCaptureCounter++}`;
|
|
188714
|
+
namedSlotMacros.push(`<: macro ${macroName} -> () { :>${slotBody}<: } :>`);
|
|
188715
|
+
currentEntries().push(`${kolonHashKey(p.name)} => ${macroName}()`);
|
|
188716
|
+
continue;
|
|
188717
|
+
}
|
|
188613
188718
|
if (p.value.kind === "spread") {
|
|
188614
188719
|
const trimmed = p.value.expr.trim();
|
|
188615
188720
|
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
@@ -188638,11 +188743,11 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188638
188743
|
const macroName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
188639
188744
|
currentEntries().push(`children => ${macroName}()`);
|
|
188640
188745
|
const dict = this.combineComponentPropSegments(segments);
|
|
188641
|
-
return
|
|
188746
|
+
return `${namedSlotMacros.join("")}<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', ${dict}) | mark_raw :>`;
|
|
188642
188747
|
}
|
|
188643
188748
|
const isEmpty = segments.every((s) => s.kind === "entries" && s.parts.length === 0);
|
|
188644
188749
|
const hashEntries = isEmpty ? "" : `, ${this.combineComponentPropSegments(segments)}`;
|
|
188645
|
-
return
|
|
188750
|
+
return `${namedSlotMacros.join("")}<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`;
|
|
188646
188751
|
}
|
|
188647
188752
|
childrenCaptureCounter = 0;
|
|
188648
188753
|
presenceVarCounter = 0;
|
|
@@ -188687,7 +188792,7 @@ ${alternate}
|
|
|
188687
188792
|
${children}`;
|
|
188688
188793
|
}
|
|
188689
188794
|
elementAttrEmitter = {
|
|
188690
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
188795
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
188691
188796
|
emitExpression: (value, name) => {
|
|
188692
188797
|
if (name === "style") {
|
|
188693
188798
|
const css = this.tryLowerStyleObject(value.expr);
|
|
@@ -188791,12 +188896,15 @@ ${name}="<: ${val} :>"
|
|
|
188791
188896
|
for (const attr of element.attrs) {
|
|
188792
188897
|
if (attr.clientOnly)
|
|
188793
188898
|
continue;
|
|
188899
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
188900
|
+
continue;
|
|
188794
188901
|
let attrName;
|
|
188795
188902
|
if (attr.name === "className")
|
|
188796
188903
|
attrName = "class";
|
|
188797
|
-
else if (attr.name === "key")
|
|
188798
|
-
|
|
188799
|
-
|
|
188904
|
+
else if (attr.name === "key") {
|
|
188905
|
+
const depth = this.currentLoopKeyDepth;
|
|
188906
|
+
attrName = depth > 0 ? `data-key-${depth}` : "data-key";
|
|
188907
|
+
} else
|
|
188800
188908
|
attrName = attr.name;
|
|
188801
188909
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
188802
188910
|
if (lowered)
|
|
@@ -188973,6 +189081,8 @@ Options:
|
|
|
188973
189081
|
return this.booleanTypedProps.has(bare);
|
|
188974
189082
|
}
|
|
188975
189083
|
_resolveLiteralConst(name) {
|
|
189084
|
+
if (this.staticLoopSourceBoundNames.has(name))
|
|
189085
|
+
return null;
|
|
188976
189086
|
const c = (this.localConstants ?? []).find((lc) => lc.name === name);
|
|
188977
189087
|
if (c?.value === undefined)
|
|
188978
189088
|
return null;
|
|
@@ -188985,6 +189095,8 @@ Options:
|
|
|
188985
189095
|
return null;
|
|
188986
189096
|
}
|
|
188987
189097
|
_resolveStaticRecordLiteral(objectName, key) {
|
|
189098
|
+
if (this.staticLoopSourceBoundNames.has(objectName))
|
|
189099
|
+
return null;
|
|
188988
189100
|
const hit = lookupStaticRecordLiteral(objectName, key, this.localConstants);
|
|
188989
189101
|
if (!hit)
|
|
188990
189102
|
return null;
|
|
@@ -189022,35 +189134,18 @@ Options:
|
|
|
189022
189134
|
var xslateAdapter = new XslateAdapter;
|
|
189023
189135
|
// src/conformance-pins.ts
|
|
189024
189136
|
var conformancePins = {
|
|
189025
|
-
"static-array-children": [{ code: "BF103", severity: "error" }],
|
|
189026
|
-
"todo-app": [{ code: "BF103", severity: "error" }],
|
|
189027
|
-
"todo-app-ssr": [{ code: "BF103", severity: "error" }],
|
|
189028
189137
|
"static-array-from-props": [{ code: "BF101", severity: "error" }],
|
|
189029
|
-
"static-array-from-props-with-component": [
|
|
189030
|
-
{ code: "BF103", severity: "error" },
|
|
189031
|
-
{ code: "BF101", severity: "error" }
|
|
189032
|
-
],
|
|
189138
|
+
"static-array-from-props-with-component": [{ code: "BF101", severity: "error" }],
|
|
189033
189139
|
"filter-nested-callback-predicate": [
|
|
189034
189140
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189035
189141
|
],
|
|
189036
189142
|
"filter-nested-find-predicate": [
|
|
189037
189143
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189038
189144
|
],
|
|
189039
|
-
"
|
|
189040
|
-
"dangerous-inner-html": [{ code: "BF101", severity: "error" }],
|
|
189041
|
-
"string-replaceall": [{ code: "BF101", severity: "error" }]
|
|
189145
|
+
"dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
|
|
189042
189146
|
};
|
|
189043
189147
|
// src/render-divergences.ts
|
|
189044
|
-
var renderDivergences = {
|
|
189045
|
-
"html-entity-text": "`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
|
|
189046
|
-
"math-methods": "Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)",
|
|
189047
|
-
"static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
189048
|
-
"object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations",
|
|
189049
|
-
"nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
|
|
189050
|
-
"jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
|
|
189051
|
-
"string-slice": "`.slice()` on a STRING misfires through the array slice helper",
|
|
189052
|
-
"string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering)"
|
|
189053
|
-
};
|
|
189148
|
+
var renderDivergences = {};
|
|
189054
189149
|
export {
|
|
189055
189150
|
xslateAdapter,
|
|
189056
189151
|
renderDivergences,
|
|
@@ -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/xslate",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.7",
|
|
4
4
|
"description": "Text::Xslate (Kolon) adapter for BarefootJS — compiles IR to .tx templates and ships the Xslate rendering backend; runs under any PSGI/Plack app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,14 +55,14 @@
|
|
|
55
55
|
"directory": "packages/adapter-xslate"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@barefootjs/shared": "0.18.
|
|
58
|
+
"@barefootjs/shared": "0.18.7"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@barefootjs/jsx": ">=0.2.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
65
|
-
"@barefootjs/jsx": "0.18.
|
|
65
|
+
"@barefootjs/jsx": "0.18.7",
|
|
66
66
|
"typescript": "^5.0.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -396,8 +396,147 @@ export { A }
|
|
|
396
396
|
})
|
|
397
397
|
})
|
|
398
398
|
|
|
399
|
+
describe('XslateAdapter - named-slot capture identifier safety (#2168 jsx-element-prop)', () => {
|
|
400
|
+
// A JSX-valued prop under a hyphenated name (`data-slot`, a valid JSX
|
|
401
|
+
// attribute name) must not leak into the Kolon macro's identifier — Kolon
|
|
402
|
+
// macro names can't contain `-`. The macro name is purely counter-based
|
|
403
|
+
// (never derived from the prop name); the hash KEY passed to
|
|
404
|
+
// `render_child` still carries the real name, quoted via `kolonHashKey`.
|
|
405
|
+
test('a hyphenated prop name does not appear in the macro name', () => {
|
|
406
|
+
const { template } = compileAndGenerate(`
|
|
407
|
+
function Card(props) { return null }
|
|
408
|
+
export function Parent() {
|
|
409
|
+
return <Card data-slot={<strong>Title</strong>}>text</Card>
|
|
410
|
+
}
|
|
411
|
+
`)
|
|
412
|
+
expect(template).toContain('<: macro bf_prop_0 -> ()')
|
|
413
|
+
expect(template).toContain("'data-slot' => bf_prop_0()")
|
|
414
|
+
expect(template).not.toContain('data-slot -> ()')
|
|
415
|
+
expect(template).not.toContain('data-slot_')
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
|
|
399
419
|
// #2038 nested-callback-predicate loudness is pinned at the shared
|
|
400
420
|
// conformance layer: `filter-nested-callback-predicate` /
|
|
401
421
|
// `filter-nested-find-predicate` (BF101 via `expectedDiagnostics` above) and
|
|
402
422
|
// `filter-nested-callback-predicate-client` (the `/* @client */` suppression
|
|
403
423
|
// twin, which must render clean).
|
|
424
|
+
|
|
425
|
+
// #2221: `_resolveLiteralConst` is a flat name lookup against
|
|
426
|
+
// `ir.metadata.localConstants` with no notion of AST scope — it used to
|
|
427
|
+
// substitute an outer const's literal value even at an occurrence that is
|
|
428
|
+
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
429
|
+
// iteration rendered the same hard-coded literal. Guarded with the same
|
|
430
|
+
// coarse `collectLoopBoundNames` exclusion as #2212: any name a loop binds
|
|
431
|
+
// anywhere in the component never inlines, falling back to the bare
|
|
432
|
+
// identifier.
|
|
433
|
+
describe('XslateAdapter - const inlining vs loop-param shadowing (#2221)', () => {
|
|
434
|
+
test('a loop param shadowing an outer literal const emits the identifier, not the const value', () => {
|
|
435
|
+
const { template } = compileAndGenerate(`
|
|
436
|
+
function Widget() {
|
|
437
|
+
const label: string = 'x'
|
|
438
|
+
return <ul>{[2, 5].map((label) => <li key={label}>{1 + label}</li>)}</ul>
|
|
439
|
+
}
|
|
440
|
+
`)
|
|
441
|
+
// The loop body must reference the per-iteration loop var...
|
|
442
|
+
expect(template).toContain('1 + $label')
|
|
443
|
+
// ...never the outer const's hard-coded value.
|
|
444
|
+
expect(template).not.toContain("1 + 'x'")
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('a numeric const shadowed by a loop param emits the identifier too', () => {
|
|
448
|
+
const { template } = compileAndGenerate(`
|
|
449
|
+
function Widget() {
|
|
450
|
+
const count = 7
|
|
451
|
+
return <ul>{[2, 5].map((count) => <li key={count}>{1 + count}</li>)}</ul>
|
|
452
|
+
}
|
|
453
|
+
`)
|
|
454
|
+
expect(template).toContain('1 + $count')
|
|
455
|
+
expect(template).not.toContain('1 + 7')
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test('a literal const NOT shadowed by any loop still inlines (#1897 pin)', () => {
|
|
459
|
+
const { template } = compileAndGenerate(`
|
|
460
|
+
function Widget({ values }: { values: number[] }) {
|
|
461
|
+
const totalPages = 5
|
|
462
|
+
return <div>
|
|
463
|
+
<p>Page 1 of {1 + totalPages}</p>
|
|
464
|
+
<ul>{values.map((v) => <li key={v}>{v}</li>)}</ul>
|
|
465
|
+
</div>
|
|
466
|
+
}
|
|
467
|
+
`)
|
|
468
|
+
expect(template).toContain('1 + 5')
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
// The accepted coarse-exclusion trade-off (same as #2212): a name that is
|
|
472
|
+
// loop-bound ANYWHERE in the component never inlines, even at a genuinely
|
|
473
|
+
// non-shadowed occurrence outside the loop — the bare identifier is
|
|
474
|
+
// emitted instead of the value.
|
|
475
|
+
test('a const referenced outside the loop whose name is loop-bound elsewhere falls back to the identifier (accepted trade-off)', () => {
|
|
476
|
+
const { template } = compileAndGenerate(`
|
|
477
|
+
function Widget({ values }: { values: number[] }) {
|
|
478
|
+
const label: string = 'x'
|
|
479
|
+
return <div>
|
|
480
|
+
<p>{1 + label}</p>
|
|
481
|
+
<ul>{values.map((label) => <li key={label}>{2 + label}</li>)}</ul>
|
|
482
|
+
</div>
|
|
483
|
+
}
|
|
484
|
+
`)
|
|
485
|
+
expect(template).not.toContain("1 + 'x'")
|
|
486
|
+
expect(template).toContain('2 + $label')
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
// #2237: `_resolveStaticRecordLiteral` (`IDENT.key` on a module-scope
|
|
491
|
+
// object-literal const, e.g. `variantClasses.ghost` — #1896/#1897) is a
|
|
492
|
+
// flat name lookup on `objectName` with no notion of AST scope, the
|
|
493
|
+
// record-literal sibling of #2221's `_resolveLiteralConst` bug. It used to
|
|
494
|
+
// substitute the outer const's member value even at an occurrence that is
|
|
495
|
+
// actually an enclosing loop callback's own (shadowing) parameter, so every
|
|
496
|
+
// iteration rendered the same hard-coded literal instead of the per-item
|
|
497
|
+
// value. Guarded with the same coarse `staticLoopSourceBoundNames`
|
|
498
|
+
// exclusion as #2221: any name a loop binds anywhere in the component
|
|
499
|
+
// never inlines, falling back to the bare `$cfg.x` member expression.
|
|
500
|
+
describe('XslateAdapter - record-literal member lookup vs loop-param shadowing (#2237)', () => {
|
|
501
|
+
test('a loop param shadowing an outer module object const emits the member access, not the outer literal', () => {
|
|
502
|
+
const { template } = compileAndGenerate(`
|
|
503
|
+
const cfg = { x: 'outer-lit' }
|
|
504
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
505
|
+
return <ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
506
|
+
}
|
|
507
|
+
`)
|
|
508
|
+
// The loop body must reference the per-iteration member access...
|
|
509
|
+
expect(template).toContain('<: $cfg.x :>')
|
|
510
|
+
// ...never the outer const's hard-coded value.
|
|
511
|
+
expect(template).not.toContain("<: 'outer-lit' :>")
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
test('a module object const NOT shadowed by any loop still inlines (variantClasses.ghost shape, #1896/#1897 pin)', () => {
|
|
515
|
+
const { template } = compileAndGenerate(`
|
|
516
|
+
const variantClasses = { solid: 'bg-solid', ghost: 'bg-ghost' }
|
|
517
|
+
function Widget({ variant }: { variant: 'solid' | 'ghost' }) {
|
|
518
|
+
return <div>{variantClasses.ghost}</div>
|
|
519
|
+
}
|
|
520
|
+
`)
|
|
521
|
+
expect(template).toContain("<: 'bg-ghost' :>")
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
// The accepted coarse-exclusion trade-off (same as #2221/#2212): an
|
|
525
|
+
// object name that is loop-bound ANYWHERE in the component never
|
|
526
|
+
// inlines its member lookups, even at a genuinely non-shadowed
|
|
527
|
+
// occurrence outside the loop — the bare member expression is emitted
|
|
528
|
+
// instead of the value.
|
|
529
|
+
test('a record member referenced outside the loop whose object name is loop-bound elsewhere falls back to the member expression (accepted trade-off)', () => {
|
|
530
|
+
const { template } = compileAndGenerate(`
|
|
531
|
+
const cfg = { x: 'outer-lit' }
|
|
532
|
+
function Widget({ rows }: { rows: { x: string }[] }) {
|
|
533
|
+
return <div>
|
|
534
|
+
<p>{cfg.x}</p>
|
|
535
|
+
<ul>{rows.map((cfg) => <li key={cfg.x}>{cfg.x}</li>)}</ul>
|
|
536
|
+
</div>
|
|
537
|
+
}
|
|
538
|
+
`)
|
|
539
|
+
expect(template).not.toContain("<: 'outer-lit' :>")
|
|
540
|
+
expect(template).toContain('<: $cfg.x :>')
|
|
541
|
+
})
|
|
542
|
+
})
|
|
@@ -89,6 +89,15 @@ export function renderArrayMethod(
|
|
|
89
89
|
const recv = emit(object)
|
|
90
90
|
return `$bf.trim(${recv})`
|
|
91
91
|
}
|
|
92
|
+
case 'trimStart':
|
|
93
|
+
case 'trimEnd': {
|
|
94
|
+
// `.trimStart()` / `.trimEnd()` — the one-sided siblings of
|
|
95
|
+
// `.trim()` (#2183 follow-up). Dedicated `$bf.trim_start` /
|
|
96
|
+
// `$bf.trim_end` helpers, not `$bf.trim` with a flag.
|
|
97
|
+
const fn = method === 'trimStart' ? 'trim_start' : 'trim_end'
|
|
98
|
+
const recv = emit(object)
|
|
99
|
+
return `$bf.${fn}(${recv})`
|
|
100
|
+
}
|
|
92
101
|
case 'toFixed': {
|
|
93
102
|
// `.toFixed(digits?)` — `$bf.to_fixed` mirrors JS rounding +
|
|
94
103
|
// zero-padding (default 0 digits). #1897.
|
|
@@ -124,6 +133,16 @@ export function renderArrayMethod(
|
|
|
124
133
|
const newS = emit(args[1])
|
|
125
134
|
return `$bf.replace(${recv}, ${oldS}, ${newS})`
|
|
126
135
|
}
|
|
136
|
+
case 'replaceAll': {
|
|
137
|
+
// `.replaceAll(old, new)` — string-pattern form, EVERY occurrence,
|
|
138
|
+
// via the dedicated `$bf.replace_all` helper (not `$bf.replace`
|
|
139
|
+
// with a flag) — the regex-pattern form is refused upstream at
|
|
140
|
+
// the parser, same as `.replace`. See #2182.
|
|
141
|
+
const recv = emit(object)
|
|
142
|
+
const oldS = emit(args[0])
|
|
143
|
+
const newS = emit(args[1])
|
|
144
|
+
return `$bf.replace_all(${recv}, ${oldS}, ${newS})`
|
|
145
|
+
}
|
|
127
146
|
case 'repeat': {
|
|
128
147
|
const recv = emit(object)
|
|
129
148
|
const count = args.length === 0 ? '0' : emit(args[0])
|
|
@@ -94,7 +94,7 @@ export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
|
94
94
|
return String(value)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
97
|
+
member(object: ParsedExpr, property: string, _computed: boolean, _optional: boolean, emit: (e: ParsedExpr) => string): string {
|
|
98
98
|
// `.length` — route through `$bf.length` (handles both array element
|
|
99
99
|
// count and string char count, JS-compatibly). Kolon's builtin `.size()`
|
|
100
100
|
// is array-only and faults on a string.
|
|
@@ -263,7 +263,7 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
|
263
263
|
return String(value)
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
member(object: ParsedExpr, property: string, _computed: boolean, emit: (e: ParsedExpr) => string): string {
|
|
266
|
+
member(object: ParsedExpr, property: string, _computed: boolean, _optional: boolean, emit: (e: ParsedExpr) => string): string {
|
|
267
267
|
// `props.x` flattens to the bare `$x` the SSR caller binds each prop to
|
|
268
268
|
// (props arrive as individual top-level vars, not a `$props` hashref).
|
|
269
269
|
if (object.kind === 'identifier' && object.name === 'props') {
|
|
@@ -23,6 +23,9 @@ export const XSLATE_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
|
|
|
23
23
|
'Math.floor': { arity: 1, emit: (args) => `$bf.floor(${args[0]})` },
|
|
24
24
|
'Math.ceil': { arity: 1, emit: (args) => `$bf.ceil(${args[0]})` },
|
|
25
25
|
'Math.round': { arity: 1, emit: (args) => `$bf.round(${args[0]})` },
|
|
26
|
+
'Math.min': { arity: 2, emit: (args) => `$bf.min(${args[0]}, ${args[1]})` },
|
|
27
|
+
'Math.max': { arity: 2, emit: (args) => `$bf.max(${args[0]}, ${args[1]})` },
|
|
28
|
+
'Math.abs': { arity: 1, emit: (args) => `$bf.abs(${args[0]})` },
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|