@barefootjs/xslate 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/emit-context.d.ts +2 -0
- package/dist/adapter/emit-context.d.ts.map +1 -1
- 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 +62 -18
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/adapter/xslate-adapter.d.ts +8 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -1
- package/dist/build.js +62 -18
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +64 -35
- 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 +20 -0
- package/src/adapter/emit-context.ts +3 -0
- package/src/adapter/expr/array-method.ts +19 -0
- package/src/adapter/expr/emitters.ts +28 -7
- package/src/adapter/lib/constants.ts +3 -0
- package/src/adapter/xslate-adapter.ts +77 -11
- package/src/conformance-pins.ts +0 -5
- package/src/render-divergences.ts +1 -28
- package/dist/adapter/expr/operand.d.ts +0 -25
- package/dist/adapter/expr/operand.d.ts.map +0 -1
- package/src/adapter/expr/operand.ts +0 -35
package/dist/index.js
CHANGED
|
@@ -187365,7 +187365,7 @@ function isAriaBooleanAttr(name) {
|
|
|
187365
187365
|
}
|
|
187366
187366
|
|
|
187367
187367
|
// src/adapter/xslate-adapter.ts
|
|
187368
|
-
import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
|
|
187368
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from "@barefootjs/shared";
|
|
187369
187369
|
|
|
187370
187370
|
// src/adapter/lib/constants.ts
|
|
187371
187371
|
var XSLATE_TEMPLATE_PRIMITIVES = {
|
|
@@ -187374,7 +187374,10 @@ var XSLATE_TEMPLATE_PRIMITIVES = {
|
|
|
187374
187374
|
Number: { arity: 1, emit: (args) => `$bf.number(${args[0]})` },
|
|
187375
187375
|
"Math.floor": { arity: 1, emit: (args) => `$bf.floor(${args[0]})` },
|
|
187376
187376
|
"Math.ceil": { arity: 1, emit: (args) => `$bf.ceil(${args[0]})` },
|
|
187377
|
-
"Math.round": { arity: 1, emit: (args) => `$bf.round(${args[0]})` }
|
|
187377
|
+
"Math.round": { arity: 1, emit: (args) => `$bf.round(${args[0]})` },
|
|
187378
|
+
"Math.min": { arity: 2, emit: (args) => `$bf.min(${args[0]}, ${args[1]})` },
|
|
187379
|
+
"Math.max": { arity: 2, emit: (args) => `$bf.max(${args[0]}, ${args[1]})` },
|
|
187380
|
+
"Math.abs": { arity: 1, emit: (args) => `$bf.abs(${args[0]})` }
|
|
187378
187381
|
};
|
|
187379
187382
|
var XSLATE_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(XSLATE_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
|
|
187380
187383
|
|
|
@@ -187479,6 +187482,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187479
187482
|
const recv = emit(object);
|
|
187480
187483
|
return `$bf.trim(${recv})`;
|
|
187481
187484
|
}
|
|
187485
|
+
case "trimStart":
|
|
187486
|
+
case "trimEnd": {
|
|
187487
|
+
const fn = method === "trimStart" ? "trim_start" : "trim_end";
|
|
187488
|
+
const recv = emit(object);
|
|
187489
|
+
return `$bf.${fn}(${recv})`;
|
|
187490
|
+
}
|
|
187482
187491
|
case "toFixed": {
|
|
187483
187492
|
const recv = emit(object);
|
|
187484
187493
|
const digits = args.length >= 1 ? emit(args[0]) : "0";
|
|
@@ -187512,6 +187521,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187512
187521
|
const newS = emit(args[1]);
|
|
187513
187522
|
return `$bf.replace(${recv}, ${oldS}, ${newS})`;
|
|
187514
187523
|
}
|
|
187524
|
+
case "replaceAll": {
|
|
187525
|
+
const recv = emit(object);
|
|
187526
|
+
const oldS = emit(args[0]);
|
|
187527
|
+
const newS = emit(args[1]);
|
|
187528
|
+
return `$bf.replace_all(${recv}, ${oldS}, ${newS})`;
|
|
187529
|
+
}
|
|
187515
187530
|
case "repeat": {
|
|
187516
187531
|
const recv = emit(object);
|
|
187517
187532
|
const count = args.length === 0 ? "0" : emit(args[0]);
|
|
@@ -187611,6 +187626,8 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187611
187626
|
|
|
187612
187627
|
// src/adapter/expr/emitters.ts
|
|
187613
187628
|
import {
|
|
187629
|
+
groupBinaryOperand,
|
|
187630
|
+
isStringConcatBinary,
|
|
187614
187631
|
identifierPath,
|
|
187615
187632
|
matchSearchParamsMethodCall,
|
|
187616
187633
|
sortComparatorFromArrow
|
|
@@ -187653,7 +187670,7 @@ class XslateFilterEmitter {
|
|
|
187653
187670
|
return "nil";
|
|
187654
187671
|
return String(value);
|
|
187655
187672
|
}
|
|
187656
|
-
member(object, property, _computed, emit) {
|
|
187673
|
+
member(object, property, _computed, _optional, emit) {
|
|
187657
187674
|
if (property === "length") {
|
|
187658
187675
|
return `$bf.length(${emit(object)})`;
|
|
187659
187676
|
}
|
|
@@ -187679,8 +187696,11 @@ class XslateFilterEmitter {
|
|
|
187679
187696
|
return arg;
|
|
187680
187697
|
}
|
|
187681
187698
|
binary(op, left, right, emit) {
|
|
187682
|
-
const l = emit(left);
|
|
187683
|
-
const r = emit(right);
|
|
187699
|
+
const l = groupBinaryOperand(left, emit(left));
|
|
187700
|
+
const r = groupBinaryOperand(right, emit(right));
|
|
187701
|
+
if (isStringConcatBinary(op, left, right, this.isStringName)) {
|
|
187702
|
+
return `${l} ~ ${r}`;
|
|
187703
|
+
}
|
|
187684
187704
|
const opMap = {
|
|
187685
187705
|
"===": "==",
|
|
187686
187706
|
"!==": "!=",
|
|
@@ -187762,7 +187782,7 @@ class XslateTopLevelEmitter {
|
|
|
187762
187782
|
return "nil";
|
|
187763
187783
|
return String(value);
|
|
187764
187784
|
}
|
|
187765
|
-
member(object, property, _computed, emit) {
|
|
187785
|
+
member(object, property, _computed, _optional, emit) {
|
|
187766
187786
|
if (object.kind === "identifier" && object.name === "props") {
|
|
187767
187787
|
return `$${property}`;
|
|
187768
187788
|
}
|
|
@@ -187809,8 +187829,11 @@ class XslateTopLevelEmitter {
|
|
|
187809
187829
|
return arg;
|
|
187810
187830
|
}
|
|
187811
187831
|
binary(op, left, right, emit) {
|
|
187812
|
-
const l = emit(left);
|
|
187813
|
-
const r = emit(right);
|
|
187832
|
+
const l = groupBinaryOperand(left, emit(left));
|
|
187833
|
+
const r = groupBinaryOperand(right, emit(right));
|
|
187834
|
+
if (isStringConcatBinary(op, left, right, (n) => this.ctx._isStringValueName(n))) {
|
|
187835
|
+
return `${l} ~ ${r}`;
|
|
187836
|
+
}
|
|
187814
187837
|
const opMap = {
|
|
187815
187838
|
"===": "==",
|
|
187816
187839
|
"!==": "!=",
|
|
@@ -188200,6 +188223,7 @@ class XslateAdapter extends BaseAdapter {
|
|
|
188200
188223
|
options;
|
|
188201
188224
|
errors = [];
|
|
188202
188225
|
inLoop = false;
|
|
188226
|
+
currentLoopKeyDepth = 0;
|
|
188203
188227
|
propsObjectName = null;
|
|
188204
188228
|
propsParams = [];
|
|
188205
188229
|
booleanTypedProps = new Set;
|
|
@@ -188276,7 +188300,7 @@ class XslateAdapter extends BaseAdapter {
|
|
|
188276
188300
|
return this.renderElement(node);
|
|
188277
188301
|
}
|
|
188278
188302
|
emitText(node) {
|
|
188279
|
-
return node.value;
|
|
188303
|
+
return escapeHtml(node.value);
|
|
188280
188304
|
}
|
|
188281
188305
|
emitExpression(node) {
|
|
188282
188306
|
return this.renderExpression(node);
|
|
@@ -188500,9 +188524,12 @@ ${whenTrue}
|
|
|
188500
188524
|
}
|
|
188501
188525
|
const param = loop.param;
|
|
188502
188526
|
const renderedChildren = this.renderChildren(loop.children);
|
|
188503
|
-
const loopVar = loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
|
|
188527
|
+
const loopVar = loop.objectIteration === "entries" ? "__bf_pair" : loop.objectIteration ? param : loop.iterationShape === "keys" ? "__bf_item" : supportableDestructure ? "__bf_item" : param;
|
|
188504
188528
|
const indexLocalLines = [];
|
|
188505
|
-
if (loop.
|
|
188529
|
+
if (loop.objectIteration === "entries") {
|
|
188530
|
+
indexLocalLines.push(`: my $${loop.index ?? param} = $${loopVar}.key;`);
|
|
188531
|
+
indexLocalLines.push(`: my $${param} = $${loopVar}.value;`);
|
|
188532
|
+
} else if (loop.objectIteration) {} else if (loop.iterationShape === "keys") {
|
|
188506
188533
|
indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`);
|
|
188507
188534
|
} else if (loop.index) {
|
|
188508
188535
|
indexLocalLines.push(`: my $${loop.index} = $~${loopVar}.index;`);
|
|
@@ -188522,13 +188549,17 @@ ${whenTrue}
|
|
|
188522
188549
|
}
|
|
188523
188550
|
const prevInLoop = this.inLoop;
|
|
188524
188551
|
this.inLoop = true;
|
|
188552
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth;
|
|
188553
|
+
this.currentLoopKeyDepth = loop.depth;
|
|
188525
188554
|
const childrenUnderLoop = this.renderChildren(loop.children);
|
|
188555
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth;
|
|
188526
188556
|
this.inLoop = prevInLoop;
|
|
188527
188557
|
const bodyChildren = loop.bodyIsItemConditional && loop.key ? `<: $bf.comment("loop-i:" ~ ${this.convertExpressionToKolon(loop.key)}) | mark_raw :>
|
|
188528
188558
|
${childrenUnderLoop}` : childrenUnderLoop;
|
|
188529
188559
|
const lines = [];
|
|
188530
188560
|
lines.push(`<: $bf.comment("loop:${loop.markerId}") | mark_raw :>`);
|
|
188531
|
-
|
|
188561
|
+
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} {`;
|
|
188562
|
+
lines.push(forHeader);
|
|
188532
188563
|
for (const il of indexLocalLines)
|
|
188533
188564
|
lines.push(il);
|
|
188534
188565
|
if (loop.filterPredicate) {
|
|
@@ -188599,9 +188630,20 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188599
188630
|
renderComponent(comp) {
|
|
188600
188631
|
const segments = [{ kind: "entries", parts: [] }];
|
|
188601
188632
|
const currentEntries = () => this.componentPropSegmentEntries(segments);
|
|
188633
|
+
const namedSlotMacros = [];
|
|
188602
188634
|
for (const p of comp.props) {
|
|
188603
188635
|
if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
|
|
188604
188636
|
continue;
|
|
188637
|
+
if (p.value.kind === "jsx-children" && p.name !== "children") {
|
|
188638
|
+
const prevInLoop = this.inLoop;
|
|
188639
|
+
this.inLoop = false;
|
|
188640
|
+
const slotBody = this.renderChildren(p.value.children);
|
|
188641
|
+
this.inLoop = prevInLoop;
|
|
188642
|
+
const macroName = `bf_prop_${this.childrenCaptureCounter++}`;
|
|
188643
|
+
namedSlotMacros.push(`<: macro ${macroName} -> () { :>${slotBody}<: } :>`);
|
|
188644
|
+
currentEntries().push(`${kolonHashKey(p.name)} => ${macroName}()`);
|
|
188645
|
+
continue;
|
|
188646
|
+
}
|
|
188605
188647
|
if (p.value.kind === "spread") {
|
|
188606
188648
|
const trimmed = p.value.expr.trim();
|
|
188607
188649
|
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
@@ -188630,11 +188672,11 @@ ${childrenUnderLoop}` : childrenUnderLoop;
|
|
|
188630
188672
|
const macroName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
188631
188673
|
currentEntries().push(`children => ${macroName}()`);
|
|
188632
188674
|
const dict = this.combineComponentPropSegments(segments);
|
|
188633
|
-
return
|
|
188675
|
+
return `${namedSlotMacros.join("")}<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', ${dict}) | mark_raw :>`;
|
|
188634
188676
|
}
|
|
188635
188677
|
const isEmpty = segments.every((s) => s.kind === "entries" && s.parts.length === 0);
|
|
188636
188678
|
const hashEntries = isEmpty ? "" : `, ${this.combineComponentPropSegments(segments)}`;
|
|
188637
|
-
return
|
|
188679
|
+
return `${namedSlotMacros.join("")}<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`;
|
|
188638
188680
|
}
|
|
188639
188681
|
childrenCaptureCounter = 0;
|
|
188640
188682
|
presenceVarCounter = 0;
|
|
@@ -188679,7 +188721,7 @@ ${alternate}
|
|
|
188679
188721
|
${children}`;
|
|
188680
188722
|
}
|
|
188681
188723
|
elementAttrEmitter = {
|
|
188682
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
188724
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
188683
188725
|
emitExpression: (value, name) => {
|
|
188684
188726
|
if (name === "style") {
|
|
188685
188727
|
const css = this.tryLowerStyleObject(value.expr);
|
|
@@ -188786,9 +188828,10 @@ ${name}="<: ${val} :>"
|
|
|
188786
188828
|
let attrName;
|
|
188787
188829
|
if (attr.name === "className")
|
|
188788
188830
|
attrName = "class";
|
|
188789
|
-
else if (attr.name === "key")
|
|
188790
|
-
|
|
188791
|
-
|
|
188831
|
+
else if (attr.name === "key") {
|
|
188832
|
+
const depth = this.currentLoopKeyDepth;
|
|
188833
|
+
attrName = depth > 0 ? `data-key-${depth}` : "data-key";
|
|
188834
|
+
} else
|
|
188792
188835
|
attrName = attr.name;
|
|
188793
188836
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
188794
188837
|
if (lowered)
|
|
@@ -188875,6 +188918,7 @@ ${reason}` : "";
|
|
|
188875
188918
|
_resolveModuleStringConst: (name) => this._resolveModuleStringConst(name),
|
|
188876
188919
|
_resolveLiteralConst: (name) => this._resolveLiteralConst(name),
|
|
188877
188920
|
_resolveStaticRecordLiteral: (o, k) => this._resolveStaticRecordLiteral(o, k),
|
|
188921
|
+
_isStringValueName: (name) => this._isStringValueName(name),
|
|
188878
188922
|
_recordExprBF101: (message, reason) => this._recordExprBF101(message, reason),
|
|
188879
188923
|
_renderKolonFilterExprPublic: (e, p) => this._renderKolonFilterExprPublic(e, p)
|
|
188880
188924
|
};
|
|
@@ -189028,25 +189072,10 @@ var conformancePins = {
|
|
|
189028
189072
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189029
189073
|
],
|
|
189030
189074
|
"array-map-function-reference": [{ code: "BF101", severity: "error" }],
|
|
189031
|
-
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189032
|
-
"string-replaceall": [{ code: "BF101", severity: "error" }]
|
|
189075
|
+
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189033
189076
|
};
|
|
189034
189077
|
// src/render-divergences.ts
|
|
189035
|
-
var renderDivergences = {
|
|
189036
|
-
"arithmetic-text": "`(count() + 2) * 3` renders 10 instead of 18 — the parenthesised sub-expression loses its grouping (silent wrong arithmetic)",
|
|
189037
|
-
"string-concat-plus": "`'Hello, ' + name` numeric-coerces through Perl's `+` (needs `.`/`~` concat)",
|
|
189038
|
-
"html-entity-text": "`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
|
|
189039
|
-
"math-methods": "Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)",
|
|
189040
|
-
"boolean-attr-literals": 'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
|
|
189041
|
-
"camelcase-attributes": "`htmlFor` is not lowered to `for` (Hono maps it)",
|
|
189042
|
-
"static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
189043
|
-
"svg-icon": "SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case",
|
|
189044
|
-
"object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders an EMPTY list — the object-shaped prop silently produces zero iterations",
|
|
189045
|
-
"nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
|
|
189046
|
-
"jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
|
|
189047
|
-
"string-slice": "`.slice()` on a STRING misfires through the array slice helper",
|
|
189048
|
-
"string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering)"
|
|
189049
|
-
};
|
|
189078
|
+
var renderDivergences = {};
|
|
189050
189079
|
export {
|
|
189051
189080
|
xslateAdapter,
|
|
189052
189081
|
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,iBAAsB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/xslate",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.5",
|
|
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.5"
|
|
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.5",
|
|
66
66
|
"typescript": "^5.0.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -396,6 +396,26 @@ 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
|
|
@@ -42,6 +42,9 @@ export interface XslateEmitContext {
|
|
|
42
42
|
*/
|
|
43
43
|
_resolveStaticRecordLiteral(objectName: string, key: string): string | null
|
|
44
44
|
|
|
45
|
+
/** Whether a getter/prop name resolves to a string-typed SSR value. */
|
|
46
|
+
_isStringValueName(name: string): boolean
|
|
47
|
+
|
|
45
48
|
/** Record a BF101 unsupported-expression diagnostic. */
|
|
46
49
|
_recordExprBF101(message: string, reason?: string): void
|
|
47
50
|
|
|
@@ -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])
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* the adapter only through the narrow `XslateEmitContext` seam.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { groupBinaryOperand,
|
|
15
|
+
isStringConcatBinary,
|
|
15
16
|
type ParsedExprEmitter,
|
|
16
17
|
type HigherOrderMethod,
|
|
17
18
|
type ArrayMethod,
|
|
@@ -93,7 +94,7 @@ export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
|
93
94
|
return String(value)
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
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 {
|
|
97
98
|
// `.length` — route through `$bf.length` (handles both array element
|
|
98
99
|
// count and string char count, JS-compatibly). Kolon's builtin `.size()`
|
|
99
100
|
// is array-only and faults on a string.
|
|
@@ -130,8 +131,17 @@ export class XslateFilterEmitter implements ParsedExprEmitter {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
// Preserve source grouping: a compound operand re-emitted as infix
|
|
135
|
+
// text is otherwise re-parsed under THIS language's precedence —
|
|
136
|
+
// `(count() + 2) * 3` would silently become `count + 2 * 3` (#2173).
|
|
137
|
+
const l = groupBinaryOperand(left, emit(left))
|
|
138
|
+
const r = groupBinaryOperand(right, emit(right))
|
|
139
|
+
// JS `+` with a string-typed operand is CONCATENATION, not addition —
|
|
140
|
+
// Kolon's `+` is numeric-only and coerces `'Hello, ' + $name` to 0
|
|
141
|
+
// (#2176). Lower to Kolon's `~` concat operator.
|
|
142
|
+
if (isStringConcatBinary(op, left, right, this.isStringName)) {
|
|
143
|
+
return `${l} ~ ${r}`
|
|
144
|
+
}
|
|
135
145
|
// Kolon's `==` / `!=` are value-equality operators that compare strings
|
|
136
146
|
// and numbers correctly — unlike Perl's numeric `==` (which the Mojo
|
|
137
147
|
// adapter must steer around with `eq`/`ne`). Kolon has no `eq`/`ne`
|
|
@@ -253,7 +263,7 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
|
253
263
|
return String(value)
|
|
254
264
|
}
|
|
255
265
|
|
|
256
|
-
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 {
|
|
257
267
|
// `props.x` flattens to the bare `$x` the SSR caller binds each prop to
|
|
258
268
|
// (props arrive as individual top-level vars, not a `$props` hashref).
|
|
259
269
|
if (object.kind === 'identifier' && object.name === 'props') {
|
|
@@ -322,8 +332,19 @@ export class XslateTopLevelEmitter implements ParsedExprEmitter {
|
|
|
322
332
|
}
|
|
323
333
|
|
|
324
334
|
binary(op: string, left: ParsedExpr, right: ParsedExpr, emit: (e: ParsedExpr) => string): string {
|
|
325
|
-
|
|
326
|
-
|
|
335
|
+
// Preserve source grouping: a compound operand re-emitted as infix
|
|
336
|
+
// text is otherwise re-parsed under THIS language's precedence —
|
|
337
|
+
// `(count() + 2) * 3` would silently become `count + 2 * 3` (#2173).
|
|
338
|
+
const l = groupBinaryOperand(left, emit(left))
|
|
339
|
+
const r = groupBinaryOperand(right, emit(right))
|
|
340
|
+
// JS `+` with a string-typed operand is CONCATENATION, not addition —
|
|
341
|
+
// Kolon's `+` is numeric-only and coerces `'Hello, ' + $name` to 0
|
|
342
|
+
// (#2176). Lower to Kolon's `~` concat operator. The adapter's
|
|
343
|
+
// string-value registry catches getter/prop operands with no literal
|
|
344
|
+
// present (`firstName() + lastName()`).
|
|
345
|
+
if (isStringConcatBinary(op, left, right, n => this.ctx._isStringValueName(n))) {
|
|
346
|
+
return `${l} ~ ${r}`
|
|
347
|
+
}
|
|
327
348
|
// Kolon's `==` / `!=` are value-equality operators handling both strings
|
|
328
349
|
// and numbers (unlike Perl's numeric `==`, which the Mojo adapter must
|
|
329
350
|
// route around with `eq`/`ne`). Kolon has no `eq`/`ne` operator, so all
|
|
@@ -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
|
/**
|
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result.ts'
|
|
78
78
|
import ts from 'typescript'
|
|
79
79
|
import type { ParsedExpr, LoweringMatcher } from '@barefootjs/jsx'
|
|
80
|
-
import { BF_SLOT, BF_COND, BF_REGION } from '@barefootjs/shared'
|
|
80
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from '@barefootjs/shared'
|
|
81
81
|
|
|
82
82
|
import type { XslateRenderCtx } from './lib/types.ts'
|
|
83
83
|
import { XSLATE_PRIMITIVE_EMIT_MAP } from './lib/constants.ts'
|
|
@@ -176,6 +176,14 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
176
176
|
private options: Required<XslateAdapterOptions>
|
|
177
177
|
private errors: CompilerError[] = []
|
|
178
178
|
private inLoop: boolean = false
|
|
179
|
+
/**
|
|
180
|
+
* `IRLoop.depth` of the loop currently being rendered (save/restore
|
|
181
|
+
* around `renderChildren(loop.children)`, mirroring `inLoop` above).
|
|
182
|
+
* `renderAttributes` reads this to derive the `key` → `data-key`/
|
|
183
|
+
* `data-key-N` suffix — the depth is IR-computed (jsx-to-ir.ts), not
|
|
184
|
+
* re-derived here (#2168 nested-loop-outer-binding).
|
|
185
|
+
*/
|
|
186
|
+
private currentLoopKeyDepth = 0
|
|
179
187
|
/**
|
|
180
188
|
* SolidJS-style props identifier (`function(props: P)`) and the
|
|
181
189
|
* analyzer-extracted prop names. Stashed at `generate()` entry so the
|
|
@@ -375,7 +383,9 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
375
383
|
}
|
|
376
384
|
|
|
377
385
|
emitText(node: IRText): string {
|
|
378
|
-
|
|
386
|
+
// IRText carries the entity-DECODED value (Phase 1 decodes JSX
|
|
387
|
+
// character references); re-escape for direct HTML emission.
|
|
388
|
+
return escapeHtml(node.value)
|
|
379
389
|
}
|
|
380
390
|
|
|
381
391
|
emitExpression(node: IRExpression): string {
|
|
@@ -724,9 +734,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
724
734
|
// For `keys`-shape iterations the callback param IS the index. We iterate
|
|
725
735
|
// the array but bind the loop var to a throwaway and expose the index as
|
|
726
736
|
// `$param`. Kolon's `$~loopvar.index` provides the 0-based index.
|
|
727
|
-
const loopVar = loop.
|
|
728
|
-
? '
|
|
729
|
-
:
|
|
737
|
+
const loopVar = loop.objectIteration === 'entries'
|
|
738
|
+
? '__bf_pair'
|
|
739
|
+
: loop.objectIteration
|
|
740
|
+
? param
|
|
741
|
+
: loop.iterationShape === 'keys'
|
|
742
|
+
? '__bf_item'
|
|
743
|
+
: supportableDestructure ? '__bf_item' : param
|
|
730
744
|
|
|
731
745
|
// Index alias: when an explicit `index` param is present (`.map((x, i) =>
|
|
732
746
|
// ...)`) or the iteration is `keys`-shaped, expose it via a `: my` Kolon
|
|
@@ -746,7 +760,16 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
746
760
|
// `segments` (empty at the loop root, per the `LoopParamBinding` jsdoc) —
|
|
747
761
|
// NOT the same as a fixed binding's full-accessor segments.
|
|
748
762
|
const indexLocalLines: string[] = []
|
|
749
|
-
if (loop.
|
|
763
|
+
if (loop.objectIteration === 'entries') {
|
|
764
|
+
// `key`/`value` bind off the `.kv()` pair (see the for-header below)
|
|
765
|
+
// — no derived `.index` local needed, unlike the array
|
|
766
|
+
// `iterationShape` cases.
|
|
767
|
+
indexLocalLines.push(`: my $${loop.index ?? param} = $${loopVar}.key;`)
|
|
768
|
+
indexLocalLines.push(`: my $${param} = $${loopVar}.value;`)
|
|
769
|
+
} else if (loop.objectIteration) {
|
|
770
|
+
// 'keys'/'values': `.keys()`/`.values()` already yield the bound
|
|
771
|
+
// value directly — no derived local needed either.
|
|
772
|
+
} else if (loop.iterationShape === 'keys') {
|
|
750
773
|
indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`)
|
|
751
774
|
} else if (loop.index) {
|
|
752
775
|
indexLocalLines.push(`: my $${loop.index} = $~${loopVar}.index;`)
|
|
@@ -769,10 +792,13 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
769
792
|
|
|
770
793
|
const prevInLoop = this.inLoop
|
|
771
794
|
this.inLoop = true
|
|
795
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth
|
|
796
|
+
this.currentLoopKeyDepth = loop.depth
|
|
772
797
|
// Re-render children now that inLoop is set (so nested components use the
|
|
773
798
|
// loop-child naming convention). renderedChildren above was computed with
|
|
774
799
|
// the previous flag; recompute under the loop flag.
|
|
775
800
|
const childrenUnderLoop = this.renderChildren(loop.children)
|
|
801
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth
|
|
776
802
|
this.inLoop = prevInLoop
|
|
777
803
|
void renderedChildren
|
|
778
804
|
|
|
@@ -789,7 +815,21 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
789
815
|
// Scoped per-call-site marker so sibling `.map()`s under the same parent
|
|
790
816
|
// each get their own reconciliation range.
|
|
791
817
|
lines.push(`<: $bf.comment("loop:${loop.markerId}") | mark_raw :>`)
|
|
792
|
-
|
|
818
|
+
// `objectIteration` (#2168 object-entries-map): Kolon has no built-in
|
|
819
|
+
// hash-destructure `for` target, so `'entries'` iterates `.kv()`
|
|
820
|
+
// (yielding `{key, value}` pair objects, unpacked via the `: my`
|
|
821
|
+
// locals above) while `'keys'`/`'values'` iterate `.keys()`/`.values()`
|
|
822
|
+
// directly. All three are alphabetically sorted by Text::Xslate itself
|
|
823
|
+
// (verified empirically) — not JS insertion order, a documented known
|
|
824
|
+
// limitation for out-of-alphabetical-order data, same as Go/Rust.
|
|
825
|
+
const forHeader = loop.objectIteration === 'entries'
|
|
826
|
+
? `: for ${array}.kv() -> $${loopVar} {`
|
|
827
|
+
: loop.objectIteration === 'keys'
|
|
828
|
+
? `: for ${array}.keys() -> $${loopVar} {`
|
|
829
|
+
: loop.objectIteration === 'values'
|
|
830
|
+
? `: for ${array}.values() -> $${loopVar} {`
|
|
831
|
+
: `: for ${array} -> $${loopVar} {`
|
|
832
|
+
lines.push(forHeader)
|
|
793
833
|
for (const il of indexLocalLines) lines.push(il)
|
|
794
834
|
|
|
795
835
|
// Handle filter().map() pattern by wrapping children in if-condition
|
|
@@ -932,11 +972,33 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
932
972
|
type Segment = { kind: 'entries'; parts: string[] } | { kind: 'spread'; expr: string }
|
|
933
973
|
const segments: Segment[] = [{ kind: 'entries', parts: [] }]
|
|
934
974
|
const currentEntries = () => this.componentPropSegmentEntries(segments)
|
|
975
|
+
// Named JSX-valued props OTHER than the reserved `children`
|
|
976
|
+
// (`header={<strong>Title</strong>}`, #2168 jsx-element-prop) each get
|
|
977
|
+
// their own macro, prepended to the final returned string below —
|
|
978
|
+
// same mechanism as the reserved children macro, just keyed by the
|
|
979
|
+
// prop's own name instead of `children`.
|
|
980
|
+
const namedSlotMacros: string[] = []
|
|
935
981
|
|
|
936
982
|
for (const p of comp.props) {
|
|
937
983
|
// Skip callback props (onXxx) and `ref` — both are client-only for
|
|
938
984
|
// SSR (Hono renders neither; the client JS wires them at hydration).
|
|
939
985
|
if ((p.name.match(/^on[A-Z]/) || p.name === 'ref') && p.value.kind === 'expression') continue
|
|
986
|
+
if (p.value.kind === 'jsx-children' && p.name !== 'children') {
|
|
987
|
+
const prevInLoop = this.inLoop
|
|
988
|
+
this.inLoop = false
|
|
989
|
+
const slotBody = this.renderChildren(p.value.children)
|
|
990
|
+
this.inLoop = prevInLoop
|
|
991
|
+
// Purely counter-based — NOT derived from `p.name` or `comp.slotId`.
|
|
992
|
+
// A JSX prop name can contain characters (`data-slot`) that aren't a
|
|
993
|
+
// valid Kolon macro identifier, and `comp.slotId` alone would
|
|
994
|
+
// collide across two named-slot props on the same component
|
|
995
|
+
// invocation (unlike the reserved children slot, there's only ever
|
|
996
|
+
// one of those per invocation).
|
|
997
|
+
const macroName = `bf_prop_${this.childrenCaptureCounter++}`
|
|
998
|
+
namedSlotMacros.push(`<: macro ${macroName} -> () { :>${slotBody}<: } :>`)
|
|
999
|
+
currentEntries().push(`${kolonHashKey(p.name)} => ${macroName}()`)
|
|
1000
|
+
continue
|
|
1001
|
+
}
|
|
940
1002
|
if (p.value.kind === 'spread') {
|
|
941
1003
|
const trimmed = p.value.expr.trim()
|
|
942
1004
|
// SolidJS-style props identifier (`function(props: P)`) has no
|
|
@@ -996,12 +1058,12 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
996
1058
|
const macroName = `bf_children_${comp.slotId ?? 'c' + this.childrenCaptureCounter++}`
|
|
997
1059
|
currentEntries().push(`children => ${macroName}()`)
|
|
998
1060
|
const dict = this.combineComponentPropSegments(segments)
|
|
999
|
-
return
|
|
1061
|
+
return `${namedSlotMacros.join('')}<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', ${dict}) | mark_raw :>`
|
|
1000
1062
|
}
|
|
1001
1063
|
|
|
1002
1064
|
const isEmpty = segments.every(s => s.kind === 'entries' && s.parts.length === 0)
|
|
1003
1065
|
const hashEntries = isEmpty ? '' : `, ${this.combineComponentPropSegments(segments)}`
|
|
1004
|
-
return
|
|
1066
|
+
return `${namedSlotMacros.join('')}<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`
|
|
1005
1067
|
}
|
|
1006
1068
|
|
|
1007
1069
|
private childrenCaptureCounter = 0
|
|
@@ -1085,7 +1147,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1085
1147
|
* AttrValue lowering for intrinsic-element attributes (Kolon).
|
|
1086
1148
|
*/
|
|
1087
1149
|
private readonly elementAttrEmitter: AttrValueEmitter = {
|
|
1088
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
1150
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
1089
1151
|
emitExpression: (value, name) => {
|
|
1090
1152
|
// `style={{ … }}` object literal → a CSS string with dynamic values
|
|
1091
1153
|
// interpolated, instead of refusing the bare object with BF101 (#1322).
|
|
@@ -1285,7 +1347,10 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1285
1347
|
// Rewrite JSX special-prop names to their HTML-attribute counterparts.
|
|
1286
1348
|
let attrName: string
|
|
1287
1349
|
if (attr.name === 'className') attrName = 'class'
|
|
1288
|
-
else if (attr.name === 'key')
|
|
1350
|
+
else if (attr.name === 'key') {
|
|
1351
|
+
const depth = this.currentLoopKeyDepth
|
|
1352
|
+
attrName = depth > 0 ? `data-key-${depth}` : 'data-key'
|
|
1353
|
+
}
|
|
1289
1354
|
else attrName = attr.name
|
|
1290
1355
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName)
|
|
1291
1356
|
if (lowered) parts.push(lowered)
|
|
@@ -1436,6 +1501,7 @@ export class XslateAdapter extends BaseAdapter implements IRNodeEmitter<XslateRe
|
|
|
1436
1501
|
_resolveModuleStringConst: (name) => this._resolveModuleStringConst(name),
|
|
1437
1502
|
_resolveLiteralConst: (name) => this._resolveLiteralConst(name),
|
|
1438
1503
|
_resolveStaticRecordLiteral: (o, k) => this._resolveStaticRecordLiteral(o, k),
|
|
1504
|
+
_isStringValueName: (name) => this._isStringValueName(name),
|
|
1439
1505
|
_recordExprBF101: (message, reason) => this._recordExprBF101(message, reason),
|
|
1440
1506
|
_renderKolonFilterExprPublic: (e, p) => this._renderKolonFilterExprPublic(e, p),
|
|
1441
1507
|
}
|
package/src/conformance-pins.ts
CHANGED
|
@@ -115,9 +115,4 @@ export const conformancePins: ConformancePins = {
|
|
|
115
115
|
// the shape loudly instead of emitting entity-escaped markup that
|
|
116
116
|
// silently renders tags as text.
|
|
117
117
|
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
118
|
-
// Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
|
|
119
|
-
// only first-occurrence `.replace` is wired to the runtime helpers.
|
|
120
|
-
// Refused with BF101 rather than reusing the first-only lowering,
|
|
121
|
-
// which would silently change semantics.
|
|
122
|
-
'string-replaceall': [{ code: 'BF101', severity: 'error' }],
|
|
123
118
|
}
|