@barefootjs/erb 0.18.5 → 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/erb-adapter.d.ts +7 -0
- package/dist/adapter/erb-adapter.d.ts.map +1 -1
- package/dist/adapter/expr/emitters.d.ts +2 -1
- package/dist/adapter/expr/emitters.d.ts.map +1 -1
- package/dist/adapter/index.js +75 -10
- package/dist/adapter/lib/static-value.d.ts +17 -0
- package/dist/adapter/lib/static-value.d.ts.map +1 -0
- package/dist/build.js +75 -10
- package/dist/index.js +76 -16
- package/dist/render-divergences.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/erb-adapter.test.ts +121 -0
- package/src/adapter/erb-adapter.ts +94 -12
- package/src/adapter/expr/emitters.ts +14 -1
- package/src/adapter/lib/static-value.ts +43 -0
- package/src/conformance-pins.ts +23 -23
- package/src/render-divergences.ts +6 -1
- package/src/test-render.ts +13 -136
|
@@ -251,6 +251,13 @@ export declare class ErbAdapter extends BaseAdapter implements IRNodeEmitter<Erb
|
|
|
251
251
|
private isClientOnlyContextIdentifier;
|
|
252
252
|
emitAsync(node: IRAsync, _ctx: ErbRenderCtx, _emit: EmitIRNode<ErbRenderCtx>): string;
|
|
253
253
|
renderElement(element: IRElement): string;
|
|
254
|
+
/**
|
|
255
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
256
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
257
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
258
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
259
|
+
*/
|
|
260
|
+
private renderDangerousInnerHtml;
|
|
254
261
|
renderExpression(expr: IRExpression): string;
|
|
255
262
|
/**
|
|
256
263
|
* True when `expr` is a structural read of the `children` prop: a bare
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"erb-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/erb-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EACV,WAAW,EAEX,MAAM,EACN,SAAS,EACT,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,EACN,WAAW,EACX,UAAU,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,OAAO,EAMP,yBAAyB,EAC1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAE3B,KAAK,aAAa,EAClB,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"erb-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/erb-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -39,7 +39,8 @@ export declare class ErbFilterEmitter implements ParsedExprEmitter {
|
|
|
39
39
|
private readonly isLoopBoundOuter;
|
|
40
40
|
private readonly isStringName;
|
|
41
41
|
private readonly onUnsupported?;
|
|
42
|
-
|
|
42
|
+
private readonly renderParamAs;
|
|
43
|
+
constructor(param: string, localVarMap: Map<string, string>, isLoopBoundOuter?: (n: string) => boolean, isStringName?: (n: string) => boolean, onUnsupported?: ((message: string, reason?: string) => void) | undefined, renderParamAs?: string);
|
|
43
44
|
identifier(name: string): string;
|
|
44
45
|
literal(value: string | number | boolean | null, literalType: LiteralType): string;
|
|
45
46
|
member(object: ParsedExpr, property: string, _computed: boolean, optional: boolean, emit: (e: ParsedExpr) => string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emitters.d.ts","sourceRoot":"","sources":["../../../src/adapter/expr/emitters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,qBAAqB,EAC1B,KAAK,SAAS,EACd,KAAK,YAAY,EAKlB,MAAM,iBAAiB,CAAA;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAwCxD,qBAAa,gBAAiB,YAAW,iBAAiB;IAEtD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAM5B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAIjC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAK7B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"emitters.d.ts","sourceRoot":"","sources":["../../../src/adapter/expr/emitters.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,qBAAqB,EAC1B,KAAK,SAAS,EACd,KAAK,YAAY,EAKlB,MAAM,iBAAiB,CAAA;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAwCxD,qBAAa,gBAAiB,YAAW,iBAAiB;IAEtD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAM5B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAIjC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAK7B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAa/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IA9BhC,YACmB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAMhC,gBAAgB,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAqB,EAItD,YAAY,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAA4B,EAKzD,aAAa,CAAC,GAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,aAAA,EAa1D,aAAa,GAAE,MAAyB,EACvD;IAEJ,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/B;IAED,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,EAAE,WAAW,EAAE,WAAW,GAAG,MAAM,CAKjF;IAED,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAc3H;IAED,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAE1F;IAED,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAMpF;IAED,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAM/E;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAa/F;IAED,OAAO,CAAC,EAAE,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAU5G;IAED,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,OAAO,CAAC,UAAU,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,EAC7C,SAAS,EAAE,UAAU,EAAE,EACvB,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAC9B,MAAM,CA0CR;IAED,YAAY,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAE5E;IAED,WAAW,CACT,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,EAAE,EAClB,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAC9B,MAAM,CAER;IAED,UAAU,CACR,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,SAAS,GAAG;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,EACvC,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAC9B,MAAM,CAER;IAED,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CAEtF;IAED,eAAe,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,CAE9C;IAED,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAIpF;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1B;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;IAED,aAAa,CAAC,WAAW,EAAE,qBAAqB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAM1G;CACF;AAED;;;;;;;;GAQG;AACH,qBAAa,kBAAmB,YAAW,iBAAiB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAhC,YAA6B,GAAG,EAAE,cAAc,EAAI;IAEpD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAsB/B;IAED,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,EAAE,WAAW,EAAE,WAAW,GAAG,MAAM,CAKjF;IAED,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAsB3H;IAED,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAE1F;IAED,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CA0CpF;IAED,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAK/E;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAQ/F;IAED,OAAO,CAAC,EAAE,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAM5G;IAED,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,OAAO,CAAC,UAAU,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,EAC7C,QAAQ,EAAE,UAAU,EAAE,EACtB,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAC9B,MAAM,CAqER;IAED,OAAO,CAAC,eAAe;IAiDvB,YAAY,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAK5E;IAED,WAAW,CACT,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,EAAE,EAClB,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAC9B,MAAM,CAER;IAED,UAAU,CACR,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,SAAS,GAAG;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,EACvC,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAC9B,MAAM,CAER;IAED,WAAW,CACT,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,UAAU,EACtB,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAC9B,MAAM,CAGR;IAED,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAiB9E;IAED,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAOpF;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1B;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKjD;IAED,aAAa,CAAC,UAAU,EAAE,qBAAqB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,MAAM,GAAG,MAAM,CAWzG;CACF"}
|
package/dist/adapter/index.js
CHANGED
|
@@ -187308,7 +187308,12 @@ 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
|
|
187312
187317
|
} from "@barefootjs/jsx";
|
|
187313
187318
|
|
|
187314
187319
|
// src/adapter/boolean-result.ts
|
|
@@ -187693,6 +187698,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187693
187698
|
return `bf.flat(${recv}, ${d})`;
|
|
187694
187699
|
}
|
|
187695
187700
|
|
|
187701
|
+
// src/adapter/lib/static-value.ts
|
|
187702
|
+
function staticValueToRuby(value) {
|
|
187703
|
+
if (value === null || value === undefined)
|
|
187704
|
+
return "nil";
|
|
187705
|
+
if (typeof value === "boolean")
|
|
187706
|
+
return value ? "true" : "false";
|
|
187707
|
+
if (typeof value === "number")
|
|
187708
|
+
return String(value);
|
|
187709
|
+
if (typeof value === "string")
|
|
187710
|
+
return rubyStringLiteral(value);
|
|
187711
|
+
if (Array.isArray(value)) {
|
|
187712
|
+
const items = [];
|
|
187713
|
+
for (const el of value) {
|
|
187714
|
+
const serialized = staticValueToRuby(el);
|
|
187715
|
+
if (serialized === null)
|
|
187716
|
+
return null;
|
|
187717
|
+
items.push(serialized);
|
|
187718
|
+
}
|
|
187719
|
+
return `[${items.join(", ")}]`;
|
|
187720
|
+
}
|
|
187721
|
+
if (typeof value === "object") {
|
|
187722
|
+
const entries = [];
|
|
187723
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187724
|
+
const serialized = staticValueToRuby(val);
|
|
187725
|
+
if (serialized === null)
|
|
187726
|
+
return null;
|
|
187727
|
+
entries.push(`${rubySymbolKey(key)} ${serialized}`);
|
|
187728
|
+
}
|
|
187729
|
+
return `{ ${entries.join(", ")} }`;
|
|
187730
|
+
}
|
|
187731
|
+
return null;
|
|
187732
|
+
}
|
|
187733
|
+
|
|
187696
187734
|
// src/adapter/expr/emitters.ts
|
|
187697
187735
|
import {
|
|
187698
187736
|
emitParsedExpr,
|
|
@@ -187738,16 +187776,18 @@ class ErbFilterEmitter {
|
|
|
187738
187776
|
isLoopBoundOuter;
|
|
187739
187777
|
isStringName;
|
|
187740
187778
|
onUnsupported;
|
|
187741
|
-
|
|
187779
|
+
renderParamAs;
|
|
187780
|
+
constructor(param, localVarMap, isLoopBoundOuter = () => false, isStringName = unusedIsStringName, onUnsupported, renderParamAs = rubyLocal(param)) {
|
|
187742
187781
|
this.param = param;
|
|
187743
187782
|
this.localVarMap = localVarMap;
|
|
187744
187783
|
this.isLoopBoundOuter = isLoopBoundOuter;
|
|
187745
187784
|
this.isStringName = isStringName;
|
|
187746
187785
|
this.onUnsupported = onUnsupported;
|
|
187786
|
+
this.renderParamAs = renderParamAs;
|
|
187747
187787
|
}
|
|
187748
187788
|
identifier(name) {
|
|
187749
187789
|
if (name === this.param)
|
|
187750
|
-
return
|
|
187790
|
+
return this.renderParamAs;
|
|
187751
187791
|
const signal = this.localVarMap.get(name);
|
|
187752
187792
|
if (signal)
|
|
187753
187793
|
return `v[${rubySymbolLiteral(signal)}]`;
|
|
@@ -188547,7 +188587,8 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188547
188587
|
renderElement(element) {
|
|
188548
188588
|
const tag = element.tag;
|
|
188549
188589
|
const attrs = this.renderAttributes(element);
|
|
188550
|
-
const
|
|
188590
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188591
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188551
188592
|
let hydrationAttrs = "";
|
|
188552
188593
|
if (element.needsScope) {
|
|
188553
188594
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188582,6 +188623,22 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188582
188623
|
}
|
|
188583
188624
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188584
188625
|
}
|
|
188626
|
+
renderDangerousInnerHtml(element) {
|
|
188627
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188628
|
+
if (!resolution)
|
|
188629
|
+
return null;
|
|
188630
|
+
if (resolution.kind === "dynamic") {
|
|
188631
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188632
|
+
return "";
|
|
188633
|
+
}
|
|
188634
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188635
|
+
if (violation) {
|
|
188636
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188637
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188638
|
+
return "";
|
|
188639
|
+
}
|
|
188640
|
+
return resolution.html;
|
|
188641
|
+
}
|
|
188585
188642
|
renderExpression(expr) {
|
|
188586
188643
|
if (expr.clientOnly) {
|
|
188587
188644
|
if (expr.slotId) {
|
|
@@ -188589,7 +188646,7 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188589
188646
|
}
|
|
188590
188647
|
return "";
|
|
188591
188648
|
}
|
|
188592
|
-
const rubyExpr = this.convertExpressionToRuby(expr.expr);
|
|
188649
|
+
const rubyExpr = this.convertExpressionToRuby(expr.expr, expr.parsed);
|
|
188593
188650
|
const wrapped = this.isChildrenValueExpr(expr) ? rubyExpr : `bf.h(${rubyExpr})`;
|
|
188594
188651
|
if (expr.slotId) {
|
|
188595
188652
|
return `<%= bf.text_start("${expr.slotId}") %><%= ${wrapped} %><%= bf.text_end %>`;
|
|
@@ -188698,7 +188755,11 @@ ${whenTrue}
|
|
|
188698
188755
|
}
|
|
188699
188756
|
});
|
|
188700
188757
|
}
|
|
188701
|
-
|
|
188758
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188759
|
+
isNameShadowed: (name) => this.loopBoundNames.has(name)
|
|
188760
|
+
});
|
|
188761
|
+
const staticArray = staticItems !== null ? staticValueToRuby(staticItems) : null;
|
|
188762
|
+
if (staticArray === null && loop.arrayParsed?.kind === "identifier") {
|
|
188702
188763
|
const arrayName = loop.arrayParsed.name;
|
|
188703
188764
|
const isUnresolvableLocalConst = !this.loopBoundNames.has(arrayName) && this.resolveModuleStringConst(arrayName) === null && this.resolveLiteralConst(arrayName) === null && this.localConstants.some((c) => c.name === arrayName && !c.isModule);
|
|
188704
188765
|
if (isUnresolvableLocalConst) {
|
|
@@ -188708,7 +188769,7 @@ ${whenTrue}
|
|
|
188708
188769
|
3. Precompute the value server-side and pass it in as a prop.`);
|
|
188709
188770
|
}
|
|
188710
188771
|
}
|
|
188711
|
-
const rawArray = this.convertExpressionToRuby(loop.array);
|
|
188772
|
+
const rawArray = staticArray ?? this.convertExpressionToRuby(loop.array);
|
|
188712
188773
|
let sortedHoist = null;
|
|
188713
188774
|
let array = rawArray;
|
|
188714
188775
|
if (loop.sortComparator) {
|
|
@@ -188788,7 +188849,9 @@ ${renderedChildren}` : renderedChildren;
|
|
|
188788
188849
|
if (loop.filterPredicate) {
|
|
188789
188850
|
let filterCond;
|
|
188790
188851
|
if (loop.filterPredicate.predicate) {
|
|
188791
|
-
|
|
188852
|
+
const filterOwnParam = loop.filterPredicate.param;
|
|
188853
|
+
const matchParam = filterOwnParam && !filterOwnParam.startsWith("[") && !filterOwnParam.startsWith("{") ? filterOwnParam : param;
|
|
188854
|
+
filterCond = this.renderRubyFilterExpr(loop.filterPredicate.predicate, matchParam, undefined, rubyLocal(param));
|
|
188792
188855
|
} else {
|
|
188793
188856
|
filterCond = "true";
|
|
188794
188857
|
}
|
|
@@ -189009,6 +189072,8 @@ ${children}`;
|
|
|
189009
189072
|
for (const attr of element.attrs) {
|
|
189010
189073
|
if (attr.clientOnly)
|
|
189011
189074
|
continue;
|
|
189075
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
189076
|
+
continue;
|
|
189012
189077
|
let attrName;
|
|
189013
189078
|
if (attr.name === "className")
|
|
189014
189079
|
attrName = "class";
|
|
@@ -189032,8 +189097,8 @@ ${children}`;
|
|
|
189032
189097
|
renderCondMarker(condId) {
|
|
189033
189098
|
return `${BF_COND}="${condId}"`;
|
|
189034
189099
|
}
|
|
189035
|
-
renderRubyFilterExpr(expr, param, localVarMap = new Map) {
|
|
189036
|
-
return emitParsedExpr2(expr, new ErbFilterEmitter(param, localVarMap, (n) => this.isLoopBoundName(n), (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason)));
|
|
189100
|
+
renderRubyFilterExpr(expr, param, localVarMap = new Map, renderParamAs) {
|
|
189101
|
+
return emitParsedExpr2(expr, new ErbFilterEmitter(param, localVarMap, (n) => this.isLoopBoundName(n), (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason), renderParamAs));
|
|
189037
189102
|
}
|
|
189038
189103
|
convertTemplateLiteralPartsToRuby(literalParts) {
|
|
189039
189104
|
const parts = [];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
|
|
3
|
+
* `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
|
|
4
|
+
* Ruby literal. Used to inline a fully-static loop source (an inline array
|
|
5
|
+
* literal, or a function-scope local const with a static initializer)
|
|
6
|
+
* directly in the loop-bound expression, rather than requiring a bound
|
|
7
|
+
* template variable.
|
|
8
|
+
*
|
|
9
|
+
* Hash keys render as symbols (`label: 'Alpha'`) to match `item[:label]`,
|
|
10
|
+
* this adapter's existing member-access convention (`rubyLocal`'s
|
|
11
|
+
* companion, `rubySymbolKey`/`rubySymbolLiteral`).
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` for a value this adapter can't represent as a literal —
|
|
14
|
+
* the caller falls back to its existing BF101 refusal instead of guessing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function staticValueToRuby(value: unknown): string | null;
|
|
17
|
+
//# sourceMappingURL=static-value.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-value.d.ts","sourceRoot":"","sources":["../../../src/adapter/lib/static-value.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwB/D"}
|
package/dist/build.js
CHANGED
|
@@ -187308,7 +187308,12 @@ 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
|
|
187312
187317
|
} from "@barefootjs/jsx";
|
|
187313
187318
|
|
|
187314
187319
|
// src/adapter/boolean-result.ts
|
|
@@ -187693,6 +187698,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187693
187698
|
return `bf.flat(${recv}, ${d})`;
|
|
187694
187699
|
}
|
|
187695
187700
|
|
|
187701
|
+
// src/adapter/lib/static-value.ts
|
|
187702
|
+
function staticValueToRuby(value) {
|
|
187703
|
+
if (value === null || value === undefined)
|
|
187704
|
+
return "nil";
|
|
187705
|
+
if (typeof value === "boolean")
|
|
187706
|
+
return value ? "true" : "false";
|
|
187707
|
+
if (typeof value === "number")
|
|
187708
|
+
return String(value);
|
|
187709
|
+
if (typeof value === "string")
|
|
187710
|
+
return rubyStringLiteral(value);
|
|
187711
|
+
if (Array.isArray(value)) {
|
|
187712
|
+
const items = [];
|
|
187713
|
+
for (const el of value) {
|
|
187714
|
+
const serialized = staticValueToRuby(el);
|
|
187715
|
+
if (serialized === null)
|
|
187716
|
+
return null;
|
|
187717
|
+
items.push(serialized);
|
|
187718
|
+
}
|
|
187719
|
+
return `[${items.join(", ")}]`;
|
|
187720
|
+
}
|
|
187721
|
+
if (typeof value === "object") {
|
|
187722
|
+
const entries = [];
|
|
187723
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187724
|
+
const serialized = staticValueToRuby(val);
|
|
187725
|
+
if (serialized === null)
|
|
187726
|
+
return null;
|
|
187727
|
+
entries.push(`${rubySymbolKey(key)} ${serialized}`);
|
|
187728
|
+
}
|
|
187729
|
+
return `{ ${entries.join(", ")} }`;
|
|
187730
|
+
}
|
|
187731
|
+
return null;
|
|
187732
|
+
}
|
|
187733
|
+
|
|
187696
187734
|
// src/adapter/expr/emitters.ts
|
|
187697
187735
|
import {
|
|
187698
187736
|
emitParsedExpr,
|
|
@@ -187738,16 +187776,18 @@ class ErbFilterEmitter {
|
|
|
187738
187776
|
isLoopBoundOuter;
|
|
187739
187777
|
isStringName;
|
|
187740
187778
|
onUnsupported;
|
|
187741
|
-
|
|
187779
|
+
renderParamAs;
|
|
187780
|
+
constructor(param, localVarMap, isLoopBoundOuter = () => false, isStringName = unusedIsStringName, onUnsupported, renderParamAs = rubyLocal(param)) {
|
|
187742
187781
|
this.param = param;
|
|
187743
187782
|
this.localVarMap = localVarMap;
|
|
187744
187783
|
this.isLoopBoundOuter = isLoopBoundOuter;
|
|
187745
187784
|
this.isStringName = isStringName;
|
|
187746
187785
|
this.onUnsupported = onUnsupported;
|
|
187786
|
+
this.renderParamAs = renderParamAs;
|
|
187747
187787
|
}
|
|
187748
187788
|
identifier(name) {
|
|
187749
187789
|
if (name === this.param)
|
|
187750
|
-
return
|
|
187790
|
+
return this.renderParamAs;
|
|
187751
187791
|
const signal = this.localVarMap.get(name);
|
|
187752
187792
|
if (signal)
|
|
187753
187793
|
return `v[${rubySymbolLiteral(signal)}]`;
|
|
@@ -188547,7 +188587,8 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188547
188587
|
renderElement(element) {
|
|
188548
188588
|
const tag = element.tag;
|
|
188549
188589
|
const attrs = this.renderAttributes(element);
|
|
188550
|
-
const
|
|
188590
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188591
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188551
188592
|
let hydrationAttrs = "";
|
|
188552
188593
|
if (element.needsScope) {
|
|
188553
188594
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188582,6 +188623,22 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188582
188623
|
}
|
|
188583
188624
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188584
188625
|
}
|
|
188626
|
+
renderDangerousInnerHtml(element) {
|
|
188627
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188628
|
+
if (!resolution)
|
|
188629
|
+
return null;
|
|
188630
|
+
if (resolution.kind === "dynamic") {
|
|
188631
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188632
|
+
return "";
|
|
188633
|
+
}
|
|
188634
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188635
|
+
if (violation) {
|
|
188636
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188637
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188638
|
+
return "";
|
|
188639
|
+
}
|
|
188640
|
+
return resolution.html;
|
|
188641
|
+
}
|
|
188585
188642
|
renderExpression(expr) {
|
|
188586
188643
|
if (expr.clientOnly) {
|
|
188587
188644
|
if (expr.slotId) {
|
|
@@ -188589,7 +188646,7 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188589
188646
|
}
|
|
188590
188647
|
return "";
|
|
188591
188648
|
}
|
|
188592
|
-
const rubyExpr = this.convertExpressionToRuby(expr.expr);
|
|
188649
|
+
const rubyExpr = this.convertExpressionToRuby(expr.expr, expr.parsed);
|
|
188593
188650
|
const wrapped = this.isChildrenValueExpr(expr) ? rubyExpr : `bf.h(${rubyExpr})`;
|
|
188594
188651
|
if (expr.slotId) {
|
|
188595
188652
|
return `<%= bf.text_start("${expr.slotId}") %><%= ${wrapped} %><%= bf.text_end %>`;
|
|
@@ -188698,7 +188755,11 @@ ${whenTrue}
|
|
|
188698
188755
|
}
|
|
188699
188756
|
});
|
|
188700
188757
|
}
|
|
188701
|
-
|
|
188758
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188759
|
+
isNameShadowed: (name) => this.loopBoundNames.has(name)
|
|
188760
|
+
});
|
|
188761
|
+
const staticArray = staticItems !== null ? staticValueToRuby(staticItems) : null;
|
|
188762
|
+
if (staticArray === null && loop.arrayParsed?.kind === "identifier") {
|
|
188702
188763
|
const arrayName = loop.arrayParsed.name;
|
|
188703
188764
|
const isUnresolvableLocalConst = !this.loopBoundNames.has(arrayName) && this.resolveModuleStringConst(arrayName) === null && this.resolveLiteralConst(arrayName) === null && this.localConstants.some((c) => c.name === arrayName && !c.isModule);
|
|
188704
188765
|
if (isUnresolvableLocalConst) {
|
|
@@ -188708,7 +188769,7 @@ ${whenTrue}
|
|
|
188708
188769
|
3. Precompute the value server-side and pass it in as a prop.`);
|
|
188709
188770
|
}
|
|
188710
188771
|
}
|
|
188711
|
-
const rawArray = this.convertExpressionToRuby(loop.array);
|
|
188772
|
+
const rawArray = staticArray ?? this.convertExpressionToRuby(loop.array);
|
|
188712
188773
|
let sortedHoist = null;
|
|
188713
188774
|
let array = rawArray;
|
|
188714
188775
|
if (loop.sortComparator) {
|
|
@@ -188788,7 +188849,9 @@ ${renderedChildren}` : renderedChildren;
|
|
|
188788
188849
|
if (loop.filterPredicate) {
|
|
188789
188850
|
let filterCond;
|
|
188790
188851
|
if (loop.filterPredicate.predicate) {
|
|
188791
|
-
|
|
188852
|
+
const filterOwnParam = loop.filterPredicate.param;
|
|
188853
|
+
const matchParam = filterOwnParam && !filterOwnParam.startsWith("[") && !filterOwnParam.startsWith("{") ? filterOwnParam : param;
|
|
188854
|
+
filterCond = this.renderRubyFilterExpr(loop.filterPredicate.predicate, matchParam, undefined, rubyLocal(param));
|
|
188792
188855
|
} else {
|
|
188793
188856
|
filterCond = "true";
|
|
188794
188857
|
}
|
|
@@ -189009,6 +189072,8 @@ ${children}`;
|
|
|
189009
189072
|
for (const attr of element.attrs) {
|
|
189010
189073
|
if (attr.clientOnly)
|
|
189011
189074
|
continue;
|
|
189075
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
189076
|
+
continue;
|
|
189012
189077
|
let attrName;
|
|
189013
189078
|
if (attr.name === "className")
|
|
189014
189079
|
attrName = "class";
|
|
@@ -189032,8 +189097,8 @@ ${children}`;
|
|
|
189032
189097
|
renderCondMarker(condId) {
|
|
189033
189098
|
return `${BF_COND}="${condId}"`;
|
|
189034
189099
|
}
|
|
189035
|
-
renderRubyFilterExpr(expr, param, localVarMap = new Map) {
|
|
189036
|
-
return emitParsedExpr2(expr, new ErbFilterEmitter(param, localVarMap, (n) => this.isLoopBoundName(n), (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason)));
|
|
189100
|
+
renderRubyFilterExpr(expr, param, localVarMap = new Map, renderParamAs) {
|
|
189101
|
+
return emitParsedExpr2(expr, new ErbFilterEmitter(param, localVarMap, (n) => this.isLoopBoundName(n), (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason), renderParamAs));
|
|
189037
189102
|
}
|
|
189038
189103
|
convertTemplateLiteralPartsToRuby(literalParts) {
|
|
189039
189104
|
const parts = [];
|
package/dist/index.js
CHANGED
|
@@ -187308,7 +187308,12 @@ 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
|
|
187312
187317
|
} from "@barefootjs/jsx";
|
|
187313
187318
|
|
|
187314
187319
|
// src/adapter/boolean-result.ts
|
|
@@ -187693,6 +187698,39 @@ function renderFlatMethod(recv, depth, emit) {
|
|
|
187693
187698
|
return `bf.flat(${recv}, ${d})`;
|
|
187694
187699
|
}
|
|
187695
187700
|
|
|
187701
|
+
// src/adapter/lib/static-value.ts
|
|
187702
|
+
function staticValueToRuby(value) {
|
|
187703
|
+
if (value === null || value === undefined)
|
|
187704
|
+
return "nil";
|
|
187705
|
+
if (typeof value === "boolean")
|
|
187706
|
+
return value ? "true" : "false";
|
|
187707
|
+
if (typeof value === "number")
|
|
187708
|
+
return String(value);
|
|
187709
|
+
if (typeof value === "string")
|
|
187710
|
+
return rubyStringLiteral(value);
|
|
187711
|
+
if (Array.isArray(value)) {
|
|
187712
|
+
const items = [];
|
|
187713
|
+
for (const el of value) {
|
|
187714
|
+
const serialized = staticValueToRuby(el);
|
|
187715
|
+
if (serialized === null)
|
|
187716
|
+
return null;
|
|
187717
|
+
items.push(serialized);
|
|
187718
|
+
}
|
|
187719
|
+
return `[${items.join(", ")}]`;
|
|
187720
|
+
}
|
|
187721
|
+
if (typeof value === "object") {
|
|
187722
|
+
const entries = [];
|
|
187723
|
+
for (const [key, val] of Object.entries(value)) {
|
|
187724
|
+
const serialized = staticValueToRuby(val);
|
|
187725
|
+
if (serialized === null)
|
|
187726
|
+
return null;
|
|
187727
|
+
entries.push(`${rubySymbolKey(key)} ${serialized}`);
|
|
187728
|
+
}
|
|
187729
|
+
return `{ ${entries.join(", ")} }`;
|
|
187730
|
+
}
|
|
187731
|
+
return null;
|
|
187732
|
+
}
|
|
187733
|
+
|
|
187696
187734
|
// src/adapter/expr/emitters.ts
|
|
187697
187735
|
import {
|
|
187698
187736
|
emitParsedExpr,
|
|
@@ -187738,16 +187776,18 @@ class ErbFilterEmitter {
|
|
|
187738
187776
|
isLoopBoundOuter;
|
|
187739
187777
|
isStringName;
|
|
187740
187778
|
onUnsupported;
|
|
187741
|
-
|
|
187779
|
+
renderParamAs;
|
|
187780
|
+
constructor(param, localVarMap, isLoopBoundOuter = () => false, isStringName = unusedIsStringName, onUnsupported, renderParamAs = rubyLocal(param)) {
|
|
187742
187781
|
this.param = param;
|
|
187743
187782
|
this.localVarMap = localVarMap;
|
|
187744
187783
|
this.isLoopBoundOuter = isLoopBoundOuter;
|
|
187745
187784
|
this.isStringName = isStringName;
|
|
187746
187785
|
this.onUnsupported = onUnsupported;
|
|
187786
|
+
this.renderParamAs = renderParamAs;
|
|
187747
187787
|
}
|
|
187748
187788
|
identifier(name) {
|
|
187749
187789
|
if (name === this.param)
|
|
187750
|
-
return
|
|
187790
|
+
return this.renderParamAs;
|
|
187751
187791
|
const signal = this.localVarMap.get(name);
|
|
187752
187792
|
if (signal)
|
|
187753
187793
|
return `v[${rubySymbolLiteral(signal)}]`;
|
|
@@ -188547,7 +188587,8 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188547
188587
|
renderElement(element) {
|
|
188548
188588
|
const tag = element.tag;
|
|
188549
188589
|
const attrs = this.renderAttributes(element);
|
|
188550
|
-
const
|
|
188590
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element);
|
|
188591
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children);
|
|
188551
188592
|
let hydrationAttrs = "";
|
|
188552
188593
|
if (element.needsScope) {
|
|
188553
188594
|
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
@@ -188582,6 +188623,22 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188582
188623
|
}
|
|
188583
188624
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
188584
188625
|
}
|
|
188626
|
+
renderDangerousInnerHtml(element) {
|
|
188627
|
+
const resolution = resolveDangerousInnerHtml(element);
|
|
188628
|
+
if (!resolution)
|
|
188629
|
+
return null;
|
|
188630
|
+
if (resolution.kind === "dynamic") {
|
|
188631
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc));
|
|
188632
|
+
return "";
|
|
188633
|
+
}
|
|
188634
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name);
|
|
188635
|
+
if (violation) {
|
|
188636
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr);
|
|
188637
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation));
|
|
188638
|
+
return "";
|
|
188639
|
+
}
|
|
188640
|
+
return resolution.html;
|
|
188641
|
+
}
|
|
188585
188642
|
renderExpression(expr) {
|
|
188586
188643
|
if (expr.clientOnly) {
|
|
188587
188644
|
if (expr.slotId) {
|
|
@@ -188589,7 +188646,7 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188589
188646
|
}
|
|
188590
188647
|
return "";
|
|
188591
188648
|
}
|
|
188592
|
-
const rubyExpr = this.convertExpressionToRuby(expr.expr);
|
|
188649
|
+
const rubyExpr = this.convertExpressionToRuby(expr.expr, expr.parsed);
|
|
188593
188650
|
const wrapped = this.isChildrenValueExpr(expr) ? rubyExpr : `bf.h(${rubyExpr})`;
|
|
188594
188651
|
if (expr.slotId) {
|
|
188595
188652
|
return `<%= bf.text_start("${expr.slotId}") %><%= ${wrapped} %><%= bf.text_end %>`;
|
|
@@ -188698,7 +188755,11 @@ ${whenTrue}
|
|
|
188698
188755
|
}
|
|
188699
188756
|
});
|
|
188700
188757
|
}
|
|
188701
|
-
|
|
188758
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
188759
|
+
isNameShadowed: (name) => this.loopBoundNames.has(name)
|
|
188760
|
+
});
|
|
188761
|
+
const staticArray = staticItems !== null ? staticValueToRuby(staticItems) : null;
|
|
188762
|
+
if (staticArray === null && loop.arrayParsed?.kind === "identifier") {
|
|
188702
188763
|
const arrayName = loop.arrayParsed.name;
|
|
188703
188764
|
const isUnresolvableLocalConst = !this.loopBoundNames.has(arrayName) && this.resolveModuleStringConst(arrayName) === null && this.resolveLiteralConst(arrayName) === null && this.localConstants.some((c) => c.name === arrayName && !c.isModule);
|
|
188704
188765
|
if (isUnresolvableLocalConst) {
|
|
@@ -188708,7 +188769,7 @@ ${whenTrue}
|
|
|
188708
188769
|
3. Precompute the value server-side and pass it in as a prop.`);
|
|
188709
188770
|
}
|
|
188710
188771
|
}
|
|
188711
|
-
const rawArray = this.convertExpressionToRuby(loop.array);
|
|
188772
|
+
const rawArray = staticArray ?? this.convertExpressionToRuby(loop.array);
|
|
188712
188773
|
let sortedHoist = null;
|
|
188713
188774
|
let array = rawArray;
|
|
188714
188775
|
if (loop.sortComparator) {
|
|
@@ -188788,7 +188849,9 @@ ${renderedChildren}` : renderedChildren;
|
|
|
188788
188849
|
if (loop.filterPredicate) {
|
|
188789
188850
|
let filterCond;
|
|
188790
188851
|
if (loop.filterPredicate.predicate) {
|
|
188791
|
-
|
|
188852
|
+
const filterOwnParam = loop.filterPredicate.param;
|
|
188853
|
+
const matchParam = filterOwnParam && !filterOwnParam.startsWith("[") && !filterOwnParam.startsWith("{") ? filterOwnParam : param;
|
|
188854
|
+
filterCond = this.renderRubyFilterExpr(loop.filterPredicate.predicate, matchParam, undefined, rubyLocal(param));
|
|
188792
188855
|
} else {
|
|
188793
188856
|
filterCond = "true";
|
|
188794
188857
|
}
|
|
@@ -189009,6 +189072,8 @@ ${children}`;
|
|
|
189009
189072
|
for (const attr of element.attrs) {
|
|
189010
189073
|
if (attr.clientOnly)
|
|
189011
189074
|
continue;
|
|
189075
|
+
if (isDangerousInnerHtmlAttr(attr))
|
|
189076
|
+
continue;
|
|
189012
189077
|
let attrName;
|
|
189013
189078
|
if (attr.name === "className")
|
|
189014
189079
|
attrName = "class";
|
|
@@ -189032,8 +189097,8 @@ ${children}`;
|
|
|
189032
189097
|
renderCondMarker(condId) {
|
|
189033
189098
|
return `${BF_COND}="${condId}"`;
|
|
189034
189099
|
}
|
|
189035
|
-
renderRubyFilterExpr(expr, param, localVarMap = new Map) {
|
|
189036
|
-
return emitParsedExpr2(expr, new ErbFilterEmitter(param, localVarMap, (n) => this.isLoopBoundName(n), (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason)));
|
|
189100
|
+
renderRubyFilterExpr(expr, param, localVarMap = new Map, renderParamAs) {
|
|
189101
|
+
return emitParsedExpr2(expr, new ErbFilterEmitter(param, localVarMap, (n) => this.isLoopBoundName(n), (n) => this._isStringValueName(n), (message, reason) => this._recordExprBF101(message, reason), renderParamAs));
|
|
189037
189102
|
}
|
|
189038
189103
|
convertTemplateLiteralPartsToRuby(literalParts) {
|
|
189039
189104
|
const parts = [];
|
|
@@ -189194,21 +189259,16 @@ Options:
|
|
|
189194
189259
|
var erbAdapter = new ErbAdapter;
|
|
189195
189260
|
// src/conformance-pins.ts
|
|
189196
189261
|
var conformancePins = {
|
|
189197
|
-
"static-array-children": [{ code: "BF103", severity: "error" }],
|
|
189198
|
-
"todo-app": [{ code: "BF103", severity: "error" }],
|
|
189199
|
-
"todo-app-ssr": [{ code: "BF103", severity: "error" }],
|
|
189200
189262
|
"static-array-from-props": [
|
|
189201
189263
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
|
|
189202
189264
|
],
|
|
189203
189265
|
"static-array-from-props-with-component": [
|
|
189204
|
-
{ code: "BF103", severity: "error" },
|
|
189205
189266
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2087" }
|
|
189206
189267
|
],
|
|
189207
189268
|
"filter-nested-find-predicate": [
|
|
189208
189269
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189209
189270
|
],
|
|
189210
|
-
"
|
|
189211
|
-
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189271
|
+
"dangerous-inner-html-dynamic": [{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2215" }]
|
|
189212
189272
|
};
|
|
189213
189273
|
// src/render-divergences.ts
|
|
189214
189274
|
var 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/erb",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.7",
|
|
4
4
|
"description": "ERB (Embedded Ruby) adapter for BarefootJS — compiles IR to .erb templates and ships the Ruby rendering backend; runs under any Rack app (Sinatra, Rails)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
"directory": "packages/adapter-erb"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@barefootjs/shared": "0.18.
|
|
57
|
+
"@barefootjs/shared": "0.18.7"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@barefootjs/jsx": ">=0.2.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
64
|
-
"@barefootjs/jsx": "0.18.
|
|
64
|
+
"@barefootjs/jsx": "0.18.7",
|
|
65
65
|
"typescript": "^5.0.0"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -305,6 +305,127 @@ export { Slot }
|
|
|
305
305
|
})
|
|
306
306
|
})
|
|
307
307
|
|
|
308
|
+
describe('ErbAdapter - filter().map() predicate matches the FILTER param, not the loop param (#2245)', () => {
|
|
309
|
+
// `todos.filter(t => t.done).map(todo => ...)`: the predicate's own `t`
|
|
310
|
+
// used to be matched against the LOOP's (map's) param `todo` inside
|
|
311
|
+
// `ErbFilterEmitter.identifier()`, so every reference to `t` inside the
|
|
312
|
+
// predicate fell to the `v[:t]` vars-Hash fallback instead of resolving
|
|
313
|
+
// to the loop-bound `todo` local — `v[:t]` is never seeded, and real
|
|
314
|
+
// Ruby raises `NoMethodError: undefined method '[]' for nil` on
|
|
315
|
+
// `v[:t][:done]` at render time (masked in the shipped `todo-app-ssr`
|
|
316
|
+
// corpus by its `'all'`-default filter short-circuiting the buggy
|
|
317
|
+
// branch away — see `filter-wrapper-props-reachable`'s docstring).
|
|
318
|
+
const DIFFERENTLY_NAMED_SOURCE = `
|
|
319
|
+
'use client'
|
|
320
|
+
import { createSignal } from '@barefootjs/client'
|
|
321
|
+
|
|
322
|
+
type Todo = { id: number; text: string; done: boolean }
|
|
323
|
+
|
|
324
|
+
export function TodoList(props: { initialTodos?: Todo[] }) {
|
|
325
|
+
const [todos] = createSignal<Todo[]>(props.initialTodos ?? [])
|
|
326
|
+
return (
|
|
327
|
+
<ul>
|
|
328
|
+
{todos().filter(t => !t.done).map(todo => (
|
|
329
|
+
<li key={todo.id}>{todo.text}</li>
|
|
330
|
+
))}
|
|
331
|
+
</ul>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
`
|
|
335
|
+
|
|
336
|
+
function compileToIR(source: string): ComponentIR {
|
|
337
|
+
const result = compileJSX(source.trimStart(), 'test.tsx', {
|
|
338
|
+
adapter: new ErbAdapter(),
|
|
339
|
+
outputIR: true,
|
|
340
|
+
})
|
|
341
|
+
const irFile = result.files.find(f => f.type === 'ir')
|
|
342
|
+
if (!irFile) throw new Error('No IR output')
|
|
343
|
+
return JSON.parse(irFile.content) as ComponentIR
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
test('predicate reference lowers through the loop-bound local, never the filter-param vars-Hash fallback', () => {
|
|
347
|
+
const ir = compileToIR(DIFFERENTLY_NAMED_SOURCE)
|
|
348
|
+
const { template } = new ErbAdapter().generate(ir)
|
|
349
|
+
// The loop-gating `<if>` must reference the loop's actual bound Ruby
|
|
350
|
+
// local (`todo[:done]`, from the MAP callback's param)...
|
|
351
|
+
expect(template).toContain('todo[:done]')
|
|
352
|
+
// ...never the filter callback's own param name resolved as an
|
|
353
|
+
// (unseeded) vars-Hash key — the literal pre-fix bug.
|
|
354
|
+
expect(template).not.toContain('v[:t]')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('same-named filter/map params render byte-identically to the pre-#2245 form (regression pin)', () => {
|
|
358
|
+
const sameNamedSource = DIFFERENTLY_NAMED_SOURCE.replace(
|
|
359
|
+
'filter(t => !t.done)',
|
|
360
|
+
'filter(todo => !todo.done)',
|
|
361
|
+
)
|
|
362
|
+
const ir = compileToIR(sameNamedSource)
|
|
363
|
+
const { template } = new ErbAdapter().generate(ir)
|
|
364
|
+
expect(template).toContain('<%- if bf.truthy?(!bf.truthy?(todo[:done])) -%>')
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
test('real Ruby render: reachable predicate on differently-named params renders correctly (pre-fix NoMethodError pin)', async () => {
|
|
368
|
+
// `filter` defaults to `'active'` (never `'all'`) so the predicate
|
|
369
|
+
// branch referencing `t.done` is actually REACHABLE at render time —
|
|
370
|
+
// an `'all'`-style short-circuiting default is exactly what hid this
|
|
371
|
+
// bug in the shipped `todo-app-ssr` fixture. Block-body predicate
|
|
372
|
+
// (folded to one expression by #2040's `foldBlockToExpr` +
|
|
373
|
+
// `predicateTernaryToLogical`) matches the real `TodoAppSSR.tsx` shape.
|
|
374
|
+
const source = `
|
|
375
|
+
'use client'
|
|
376
|
+
import { createSignal } from '@barefootjs/client'
|
|
377
|
+
|
|
378
|
+
type Todo = { id: number; text: string; done: boolean }
|
|
379
|
+
type Filter = 'all' | 'active'
|
|
380
|
+
|
|
381
|
+
export function TodoList(props: { initialTodos?: Todo[] }) {
|
|
382
|
+
const [todos] = createSignal<Todo[]>(props.initialTodos ?? [])
|
|
383
|
+
const [filter] = createSignal<Filter>('active')
|
|
384
|
+
return (
|
|
385
|
+
<ul>
|
|
386
|
+
{todos().filter(t => {
|
|
387
|
+
const f = filter()
|
|
388
|
+
if (f === 'active') return !t.done
|
|
389
|
+
return true
|
|
390
|
+
}).map(todo => (
|
|
391
|
+
<li key={todo.id}>{todo.text}</li>
|
|
392
|
+
))}
|
|
393
|
+
</ul>
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
`
|
|
397
|
+
let html: string
|
|
398
|
+
try {
|
|
399
|
+
html = await renderErbComponent({
|
|
400
|
+
source: source.trimStart(),
|
|
401
|
+
adapter: new ErbAdapter(),
|
|
402
|
+
props: {
|
|
403
|
+
initialTodos: [
|
|
404
|
+
{ id: 1, text: 'Eat breakfast', done: true },
|
|
405
|
+
{ id: 2, text: 'Write tests', done: false },
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (err instanceof ErbNotAvailableError) {
|
|
411
|
+
console.log('Skipping #2245 filter-param e2e: ruby/erb not available')
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
throw err
|
|
415
|
+
}
|
|
416
|
+
// Pre-fix: real Ruby raises `NoMethodError: undefined method '[]' for
|
|
417
|
+
// nil` evaluating `v[:t][:done]` — `renderErbComponent` surfaces that
|
|
418
|
+
// as a thrown "ruby render failed" error, so a `NoMethodError` string
|
|
419
|
+
// anywhere in a caught error would fail this test outright rather than
|
|
420
|
+
// reaching these assertions. Post-fix: only the not-done todo (id 2)
|
|
421
|
+
// survives the 'active' filter.
|
|
422
|
+
expect(html).not.toContain('Eat breakfast')
|
|
423
|
+
expect(html).toContain('Write tests')
|
|
424
|
+
expect(html).toContain('data-key="2"')
|
|
425
|
+
expect(html).not.toContain('data-key="1"')
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
308
429
|
describe('ErbAdapter - named-slot capture identifier safety (#2168 jsx-element-prop)', () => {
|
|
309
430
|
// A JSX-valued prop under a hyphenated name (`data-slot`, a valid JSX
|
|
310
431
|
// attribute name) must not leak into the buffer-slice capture's local
|
|
@@ -88,6 +88,11 @@ import {
|
|
|
88
88
|
queryHrefArgs,
|
|
89
89
|
isValidHelperId,
|
|
90
90
|
sortComparatorFromArrow,
|
|
91
|
+
isDangerousInnerHtmlAttr,
|
|
92
|
+
resolveDangerousInnerHtml,
|
|
93
|
+
dangerousInnerHtmlMetacharViolation,
|
|
94
|
+
dangerousInnerHtmlDiagnostic,
|
|
95
|
+
resolveStaticLoopSource,
|
|
91
96
|
} from '@barefootjs/jsx'
|
|
92
97
|
import { isAriaBooleanAttr, isBooleanResultExpr, isExplicitStringCall } from './boolean-result.ts'
|
|
93
98
|
import type { ParsedExpr, LoweringMatcher, LoopBindingPathSegment } from '@barefootjs/jsx'
|
|
@@ -107,6 +112,7 @@ import {
|
|
|
107
112
|
collectRootScopeNodes,
|
|
108
113
|
} from './lib/ir-scope.ts'
|
|
109
114
|
import { renderSortMethod, renderSortEval } from './expr/array-method.ts'
|
|
115
|
+
import { staticValueToRuby } from './lib/static-value.ts'
|
|
110
116
|
import { ErbFilterEmitter, ErbTopLevelEmitter } from './expr/emitters.ts'
|
|
111
117
|
import type { ErbEmitContext, ErbSpreadContext, ErbMemoContext } from './emit-context.ts'
|
|
112
118
|
import {
|
|
@@ -636,7 +642,8 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
636
642
|
renderElement(element: IRElement): string {
|
|
637
643
|
const tag = element.tag
|
|
638
644
|
const attrs = this.renderAttributes(element)
|
|
639
|
-
const
|
|
645
|
+
const dangerousHtml = this.renderDangerousInnerHtml(element)
|
|
646
|
+
const children = dangerousHtml !== null ? dangerousHtml : this.renderChildren(element.children)
|
|
640
647
|
|
|
641
648
|
let hydrationAttrs = ''
|
|
642
649
|
if (element.needsScope) {
|
|
@@ -673,6 +680,28 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
673
680
|
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`
|
|
674
681
|
}
|
|
675
682
|
|
|
683
|
+
/**
|
|
684
|
+
* `dangerouslySetInnerHTML={{ __html: '...' }}` (#2207) — see the Blade
|
|
685
|
+
* adapter's identical helper for the full rationale. `null` means the
|
|
686
|
+
* attribute is absent (caller falls through to normal `renderChildren`);
|
|
687
|
+
* a non-`null` string (possibly `''`) replaces the children outright.
|
|
688
|
+
*/
|
|
689
|
+
private renderDangerousInnerHtml(element: IRElement): string | null {
|
|
690
|
+
const resolution = resolveDangerousInnerHtml(element)
|
|
691
|
+
if (!resolution) return null
|
|
692
|
+
if (resolution.kind === 'dynamic') {
|
|
693
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(resolution.expr, resolution.loc))
|
|
694
|
+
return ''
|
|
695
|
+
}
|
|
696
|
+
const violation = dangerousInnerHtmlMetacharViolation(resolution.html, this.name)
|
|
697
|
+
if (violation) {
|
|
698
|
+
const attr = element.attrs.find(isDangerousInnerHtmlAttr)!
|
|
699
|
+
this.errors.push(dangerousInnerHtmlDiagnostic(`{ __html: ${JSON.stringify(resolution.html)} }`, attr.loc, violation))
|
|
700
|
+
return ''
|
|
701
|
+
}
|
|
702
|
+
return resolution.html
|
|
703
|
+
}
|
|
704
|
+
|
|
676
705
|
// ===========================================================================
|
|
677
706
|
// Expression Rendering
|
|
678
707
|
// ===========================================================================
|
|
@@ -685,7 +714,12 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
685
714
|
return ''
|
|
686
715
|
}
|
|
687
716
|
|
|
688
|
-
|
|
717
|
+
// Thread the IR-carried `.parsed` tree through (mirrors go-template's
|
|
718
|
+
// `convertExpressionToGo(expr.expr, classify, expr.parsed)`) so a
|
|
719
|
+
// resolved bare-identifier `.map`/`.filter`/… callback
|
|
720
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206) isn't lost to a
|
|
721
|
+
// fresh, unresolved re-parse of the raw string.
|
|
722
|
+
const rubyExpr = this.convertExpressionToRuby(expr.expr, expr.parsed)
|
|
689
723
|
|
|
690
724
|
// A bare read of the `children` prop (`{children}` / `{props.children}`,
|
|
691
725
|
// optionally `?? fallback`) is pre-rendered HTML — captured via the
|
|
@@ -869,7 +903,23 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
869
903
|
// reproduces identically with a non-destructured param, so it is NOT a
|
|
870
904
|
// destructure-lowering limitation. Surface BF101 honestly instead of
|
|
871
905
|
// emitting a loop bound that silently crashes / renders empty.
|
|
872
|
-
|
|
906
|
+
// #2208: a loop source that is a fully-static array literal — either
|
|
907
|
+
// inline (`[{ label: 'Alpha' }, ...].map(...)`) or a bare identifier
|
|
908
|
+
// bound to a FUNCTION-scope local const whose initializer has no
|
|
909
|
+
// prop/signal/function-call dependency — inlines as a native Ruby
|
|
910
|
+
// array/hash literal below, the same way a module-scope const's value
|
|
911
|
+
// is already seeded. Previously the INLINE shape wasn't gated here at
|
|
912
|
+
// all (this check only ever inspected an `identifier` array source) —
|
|
913
|
+
// it still ended up refusing via `convertExpressionToRuby`'s generic
|
|
914
|
+
// `unsupported` object-literal path (BF101, "Expression not
|
|
915
|
+
// supported"), which is what this loop-specific check now also does
|
|
916
|
+
// deliberately, up front, for both shapes.
|
|
917
|
+
const staticItems = resolveStaticLoopSource(loop.arrayParsed, this.localConstants, {
|
|
918
|
+
isNameShadowed: name => this.loopBoundNames.has(name),
|
|
919
|
+
})
|
|
920
|
+
const staticArray = staticItems !== null ? staticValueToRuby(staticItems) : null
|
|
921
|
+
|
|
922
|
+
if (staticArray === null && loop.arrayParsed?.kind === 'identifier') {
|
|
873
923
|
const arrayName = loop.arrayParsed.name
|
|
874
924
|
const isUnresolvableLocalConst =
|
|
875
925
|
!this.loopBoundNames.has(arrayName) &&
|
|
@@ -884,7 +934,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
884
934
|
}
|
|
885
935
|
}
|
|
886
936
|
|
|
887
|
-
const rawArray = this.convertExpressionToRuby(loop.array)
|
|
937
|
+
const rawArray = staticArray ?? this.convertExpressionToRuby(loop.array)
|
|
888
938
|
// Apply sort if present: hoist the (possibly sorted) array into a Ruby
|
|
889
939
|
// local BEFORE the index loop, so both the loop bound and the per-item
|
|
890
940
|
// lookup reference the same materialised array — otherwise a sort
|
|
@@ -1040,14 +1090,31 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
1040
1090
|
if (loop.filterPredicate) {
|
|
1041
1091
|
let filterCond: string
|
|
1042
1092
|
if (loop.filterPredicate.predicate) {
|
|
1043
|
-
// The
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
|
|
1093
|
+
// The filter predicate's identifiers were parsed against the
|
|
1094
|
+
// FILTER callback's own param (`loop.filterPredicate.param`), which
|
|
1095
|
+
// can differ from the loop's rendered Ruby local (`param`, the MAP
|
|
1096
|
+
// callback's param) whenever the two are named differently —
|
|
1097
|
+
// `todos.filter(t => t.done).map(todo => ...)` (#2245). Ruby's
|
|
1098
|
+
// named-block-param model means there's no Mojo-style regex
|
|
1099
|
+
// `$filterParam → $loopParam` TEXT rewrite to do, but the
|
|
1100
|
+
// DISTINCTION it encoded still matters: match identifiers against
|
|
1101
|
+
// the filter's own param, while EMITTING the loop's actual bound
|
|
1102
|
+
// local — `ErbFilterEmitter`'s `renderParamAs` carries that split.
|
|
1103
|
+
// `filterPredicate.param` is only ever a bare identifier in
|
|
1104
|
+
// practice (`extractFilterPredicate` in jsx-to-ir.ts refuses a
|
|
1105
|
+
// destructured filter param outright, leaving `filterPredicate`
|
|
1106
|
+
// unset entirely rather than populating it with pattern text — see
|
|
1107
|
+
// its docstring), but guard defensively instead of relying on that
|
|
1108
|
+
// invariant: a pattern-text param (leading `[`/`{` — the same
|
|
1109
|
+
// prefix check `destructureLoopParam`/#2238 use, NOT an identifier
|
|
1110
|
+
// regex, which would misclassify a Unicode param name) falls back
|
|
1111
|
+
// to `param`, matching pre-#2245 behavior byte-for-byte.
|
|
1112
|
+
const filterOwnParam = loop.filterPredicate.param
|
|
1113
|
+
const matchParam =
|
|
1114
|
+
filterOwnParam && !filterOwnParam.startsWith('[') && !filterOwnParam.startsWith('{')
|
|
1115
|
+
? filterOwnParam
|
|
1116
|
+
: param
|
|
1117
|
+
filterCond = this.renderRubyFilterExpr(loop.filterPredicate.predicate, matchParam, undefined, rubyLocal(param))
|
|
1051
1118
|
} else {
|
|
1052
1119
|
filterCond = 'true'
|
|
1053
1120
|
}
|
|
@@ -1509,6 +1576,12 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
1509
1576
|
// the unsupported-expression lowering is never reached for a
|
|
1510
1577
|
// deferred predicate (no BF101 / BF102).
|
|
1511
1578
|
if (attr.clientOnly) continue
|
|
1579
|
+
// `dangerouslySetInnerHTML` never renders as an HTML attribute — it's
|
|
1580
|
+
// handled by `renderDangerousInnerHtml` instead, which replaces the
|
|
1581
|
+
// element's children. Skip it here so its `{ __html: ... }` object
|
|
1582
|
+
// literal never reaches the generic object-literal BF101 refusal
|
|
1583
|
+
// (which would double-report alongside the purpose-built one).
|
|
1584
|
+
if (isDangerousInnerHtmlAttr(attr)) continue
|
|
1512
1585
|
// Rewrite JSX special-prop names to their HTML-attribute
|
|
1513
1586
|
// counterparts. `className` → `class`; `key` → `data-key` matches
|
|
1514
1587
|
// the canonical Hono attribute name the client runtime reconciles
|
|
@@ -1563,6 +1636,14 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
1563
1636
|
expr: ParsedExpr,
|
|
1564
1637
|
param: string,
|
|
1565
1638
|
localVarMap: Map<string, string> = new Map(),
|
|
1639
|
+
// See `ErbFilterEmitter`'s constructor docstring (#2245): the Ruby
|
|
1640
|
+
// local to EMIT for a reference to `param`, when it differs from
|
|
1641
|
+
// `param` itself (the loop-gating call site passes the filter
|
|
1642
|
+
// callback's own param as `param` — the name to MATCH — and this as
|
|
1643
|
+
// the loop's actual bound local). Every other caller omits it, so
|
|
1644
|
+
// `ErbFilterEmitter`'s own default (`rubyLocal(param)`) applies and
|
|
1645
|
+
// match/render stay the same value, unchanged from before #2245.
|
|
1646
|
+
renderParamAs?: string,
|
|
1566
1647
|
): string {
|
|
1567
1648
|
return emitParsedExpr(
|
|
1568
1649
|
expr,
|
|
@@ -1572,6 +1653,7 @@ export class ErbAdapter extends BaseAdapter implements IRNodeEmitter<ErbRenderCt
|
|
|
1572
1653
|
n => this.isLoopBoundName(n),
|
|
1573
1654
|
n => this._isStringValueName(n),
|
|
1574
1655
|
(message, reason) => this._recordExprBF101(message, reason),
|
|
1656
|
+
renderParamAs,
|
|
1575
1657
|
),
|
|
1576
1658
|
)
|
|
1577
1659
|
}
|
|
@@ -106,10 +106,23 @@ export class ErbFilterEmitter implements ParsedExprEmitter {
|
|
|
106
106
|
// construction stays possible without an adapter; a missing hook keeps
|
|
107
107
|
// the old silent-degrade emit.
|
|
108
108
|
private readonly onUnsupported?: (message: string, reason?: string) => void,
|
|
109
|
+
// The Ruby local to EMIT for a reference matching `this.param` — as
|
|
110
|
+
// opposed to `this.param` itself, which is only the name to MATCH.
|
|
111
|
+
// These two are the SAME value everywhere in this file except the
|
|
112
|
+
// `filter().map()` loop-gating `<if>` (erb-adapter.ts's `renderLoop`,
|
|
113
|
+
// #2245): `todos.filter(t => t.done).map(todo => ...)` parses the
|
|
114
|
+
// predicate against the filter callback's OWN param (`t`), but the
|
|
115
|
+
// Ruby local actually bound by the loop is the MAP callback's param
|
|
116
|
+
// (`todo`) — Ruby has no per-callback block scope there (unlike the
|
|
117
|
+
// real nested `.select { |t| ... }` block `callbackMethod` below
|
|
118
|
+
// builds, where match and render are naturally the same param).
|
|
119
|
+
// Defaults to `rubyLocal(this.param)`, i.e. every other construction
|
|
120
|
+
// site is unaffected.
|
|
121
|
+
private readonly renderParamAs: string = rubyLocal(param),
|
|
109
122
|
) {}
|
|
110
123
|
|
|
111
124
|
identifier(name: string): string {
|
|
112
|
-
if (name === this.param) return
|
|
125
|
+
if (name === this.param) return this.renderParamAs
|
|
113
126
|
const signal = this.localVarMap.get(name)
|
|
114
127
|
if (signal) return `v[${rubySymbolLiteral(signal)}]`
|
|
115
128
|
if (this.isLoopBoundOuter(name)) return rubyLocal(name)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compile-time-evaluated JS value (`@barefootjs/jsx`'s
|
|
3
|
+
* `evaluateStaticLiteral`/`resolveStaticLoopSource`, #2208) into a native
|
|
4
|
+
* Ruby literal. Used to inline a fully-static loop source (an inline array
|
|
5
|
+
* literal, or a function-scope local const with a static initializer)
|
|
6
|
+
* directly in the loop-bound expression, rather than requiring a bound
|
|
7
|
+
* template variable.
|
|
8
|
+
*
|
|
9
|
+
* Hash keys render as symbols (`label: 'Alpha'`) to match `item[:label]`,
|
|
10
|
+
* this adapter's existing member-access convention (`rubyLocal`'s
|
|
11
|
+
* companion, `rubySymbolKey`/`rubySymbolLiteral`).
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` for a value this adapter can't represent as a literal —
|
|
14
|
+
* the caller falls back to its existing BF101 refusal instead of guessing.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { rubyStringLiteral, rubySymbolKey } from './ruby-naming.ts'
|
|
18
|
+
|
|
19
|
+
export function staticValueToRuby(value: unknown): string | null {
|
|
20
|
+
if (value === null || value === undefined) return 'nil'
|
|
21
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
|
22
|
+
if (typeof value === 'number') return String(value)
|
|
23
|
+
if (typeof value === 'string') return rubyStringLiteral(value)
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
const items: string[] = []
|
|
26
|
+
for (const el of value) {
|
|
27
|
+
const serialized = staticValueToRuby(el)
|
|
28
|
+
if (serialized === null) return null
|
|
29
|
+
items.push(serialized)
|
|
30
|
+
}
|
|
31
|
+
return `[${items.join(', ')}]`
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'object') {
|
|
34
|
+
const entries: string[] = []
|
|
35
|
+
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
36
|
+
const serialized = staticValueToRuby(val)
|
|
37
|
+
if (serialized === null) return null
|
|
38
|
+
entries.push(`${rubySymbolKey(key)} ${serialized}`)
|
|
39
|
+
}
|
|
40
|
+
return `{ ${entries.join(', ')} }`
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
package/src/conformance-pins.ts
CHANGED
|
@@ -12,16 +12,17 @@
|
|
|
12
12
|
import type { ConformancePins } from '@barefootjs/jsx'
|
|
13
13
|
|
|
14
14
|
export const conformancePins: ConformancePins = {
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
// `todo-app` / `todo-app-ssr` no longer pinned (#2205) — the conformance
|
|
16
|
+
// harness now passes `siblingTemplatesRegistered: true` for fixtures with
|
|
17
|
+
// sibling `components`, matching `bf build`'s real semantics, so the
|
|
18
|
+
// BF103 loop-body cross-template check no longer fires spuriously. (Both
|
|
19
|
+
// fixtures are still skipped on this adapter via `render-divergences.ts`
|
|
20
|
+
// — #2209 — for an unrelated signal-seeding gap.)
|
|
21
|
+
// `static-array-children` no longer pinned (#2208) — `items`'s
|
|
22
|
+
// array-literal initializer is now recognized as fully-static
|
|
23
|
+
// (`resolveStaticLoopSource`) and inlined as a native Ruby array/hash
|
|
24
|
+
// literal in the loop-bound expression, the same way a module-scope
|
|
25
|
+
// const's value is already seeded.
|
|
25
26
|
// `static-array-from-props` / `static-array-from-props-with-component`:
|
|
26
27
|
// the `.map(([emoji, users]) => …)` / `.map(([id, t]) => …)` callback is
|
|
27
28
|
// a plain array-index destructure (the `.filter(...)` runs on a
|
|
@@ -45,8 +46,9 @@ export const conformancePins: ConformancePins = {
|
|
|
45
46
|
'static-array-from-props': [
|
|
46
47
|
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
|
|
47
48
|
],
|
|
49
|
+
// BF103 (imported child in the loop body) no longer fires now that the
|
|
50
|
+
// conformance harness passes `siblingTemplatesRegistered: true` (#2205).
|
|
48
51
|
'static-array-from-props-with-component': [
|
|
49
|
-
{ code: 'BF103', severity: 'error' },
|
|
50
52
|
{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2087' },
|
|
51
53
|
],
|
|
52
54
|
// #2087 Phase B: `isLowerableLoopDestructure` now admits every fixed-
|
|
@@ -91,16 +93,14 @@ export const conformancePins: ConformancePins = {
|
|
|
91
93
|
// `/* @client */` keyed-map slot-id elision contract only (same as
|
|
92
94
|
// `todo-app`), not a render or BF101 gap.
|
|
93
95
|
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
// silently renders tags as text.
|
|
105
|
-
'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
|
|
96
|
+
// `array-map-function-reference` no longer pinned — a bare-identifier
|
|
97
|
+
// `.map(format)` callback now resolves one hop to its declaration
|
|
98
|
+
// (`resolveCallbackMethodFunctionReferences`, #2206), the same mechanism
|
|
99
|
+
// #2090 established for `.sort(fnref)`.
|
|
100
|
+
// `dangerous-inner-html` no longer pinned — a compile-time string-literal
|
|
101
|
+
// `dangerouslySetInnerHTML={{ __html: '...' }}` is spliced directly into
|
|
102
|
+
// the template as trusted raw text (`resolveDangerousInnerHtml`, #2207).
|
|
103
|
+
// A dynamic/signal-derived value still refuses with BF101 — see the
|
|
104
|
+
// `dangerous-inner-html-dynamic` fixture/pin below (tracked: #2215).
|
|
105
|
+
'dangerous-inner-html-dynamic': [{ code: 'BF101', severity: 'error', issue: 'https://github.com/piconic-ai/barefootjs/issues/2215' }],
|
|
106
106
|
}
|
|
@@ -14,4 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import type { RenderDivergences } from '@barefootjs/jsx'
|
|
16
16
|
|
|
17
|
-
export const renderDivergences: RenderDivergences = {
|
|
17
|
+
export const renderDivergences: RenderDivergences = {
|
|
18
|
+
// `todo-app` / `todo-app-ssr` no longer diverge (#2209) — the shared
|
|
19
|
+
// `evaluateSignalInit` (`@barefootjs/jsx`, sandboxed real-JS evaluation
|
|
20
|
+
// instead of a fixed regex-shape catalogue) now correctly seeds `todos`
|
|
21
|
+
// from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
|
|
22
|
+
}
|
package/src/test-render.ts
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* regression test.
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
|
|
39
|
+
import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit, tryEvaluateSignalInit } from '@barefootjs/jsx'
|
|
40
40
|
import type { ComponentIR, SsrDefault } from '@barefootjs/jsx'
|
|
41
41
|
import { mkdir, rm } from 'node:fs/promises'
|
|
42
42
|
import { resolve } from 'node:path'
|
|
@@ -151,8 +151,15 @@ export async function renderErbComponent(options: RenderOptions): Promise<string
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// Compile parent source.
|
|
155
|
-
|
|
154
|
+
// Compile parent source. `siblingTemplatesRegistered: Boolean(components)`
|
|
155
|
+
// matches this harness's real behavior — every sibling child template is registered
|
|
156
|
+
// alongside the parent before rendering, so a loop-body cross-template
|
|
157
|
+
// call resolves at render time (#2205).
|
|
158
|
+
const result = compileJSX(source, 'component.tsx', {
|
|
159
|
+
adapter,
|
|
160
|
+
outputIR: true,
|
|
161
|
+
siblingTemplatesRegistered: Boolean(components),
|
|
162
|
+
})
|
|
156
163
|
|
|
157
164
|
const errors = result.errors.filter(e => e.severity === 'error')
|
|
158
165
|
if (errors.length > 0) {
|
|
@@ -495,9 +502,9 @@ function buildRubyProps(
|
|
|
495
502
|
for (const param of ir.metadata.propsParams) {
|
|
496
503
|
if (props && param.name in props) continue
|
|
497
504
|
if (param.defaultValue) {
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
500
|
-
obj[param.name] = value
|
|
505
|
+
const result = tryEvaluateSignalInit(param.defaultValue.trim(), props)
|
|
506
|
+
if (result.ok) {
|
|
507
|
+
obj[param.name] = result.value
|
|
501
508
|
continue
|
|
502
509
|
}
|
|
503
510
|
}
|
|
@@ -556,133 +563,3 @@ function buildRubyProps(
|
|
|
556
563
|
return { obj, needsSearchParams }
|
|
557
564
|
}
|
|
558
565
|
|
|
559
|
-
/**
|
|
560
|
-
* Best-effort literal evaluation of a prop-destructure default's source
|
|
561
|
-
* text (`{ size = 'md' }` → `'md'`), including a `props.x ?? default`
|
|
562
|
-
* nullish fallback (handled generically, though destructure defaults
|
|
563
|
-
* rarely reference `props`) and delegating to `parseLiteral` for
|
|
564
|
-
* everything else. Returns `undefined` for a non-literal (computed)
|
|
565
|
-
* default, matching the Perl harnesses' "fall through to undef" behaviour.
|
|
566
|
-
*/
|
|
567
|
-
function jsDefaultLiteral(expr: string): unknown {
|
|
568
|
-
const t = expr.trim()
|
|
569
|
-
const nullishMatch = t.match(/\?\?\s*(.+)$/)
|
|
570
|
-
if (nullishMatch) return jsDefaultLiteral(nullishMatch[1])
|
|
571
|
-
if (t.startsWith('props.')) return undefined
|
|
572
|
-
const parsed = parseLiteral(t)
|
|
573
|
-
return parsed === null && t !== 'null' ? undefined : parsed
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Evaluate a signal initializer expression using provided props.
|
|
578
|
-
* Handles: props.initial ?? 0, props.value, literal values.
|
|
579
|
-
*/
|
|
580
|
-
export function evaluateSignalInit(
|
|
581
|
-
expr: string,
|
|
582
|
-
props?: Record<string, unknown>,
|
|
583
|
-
): unknown {
|
|
584
|
-
const nullishMatch = expr.match(/^props\.(\w+)\s*\?\?\s*(.+)$/)
|
|
585
|
-
if (nullishMatch) {
|
|
586
|
-
const propName = nullishMatch[1]
|
|
587
|
-
const defaultExpr = nullishMatch[2].trim()
|
|
588
|
-
if (props && propName in props) return props[propName]
|
|
589
|
-
return parseLiteral(defaultExpr)
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
const propsMatch = expr.match(/^props\.(\w+)$/)
|
|
593
|
-
if (propsMatch) {
|
|
594
|
-
if (props && propsMatch[1] in props) return props[propsMatch[1]]
|
|
595
|
-
return null
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return parseLiteral(expr)
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function parseLiteral(expr: string): unknown {
|
|
602
|
-
if (/^-?\d+(\.\d+)?$/.test(expr)) return Number(expr)
|
|
603
|
-
if (expr === 'true') return true
|
|
604
|
-
if (expr === 'false') return false
|
|
605
|
-
if (expr === '[]') return []
|
|
606
|
-
|
|
607
|
-
{
|
|
608
|
-
const t = expr.trim()
|
|
609
|
-
if (t.startsWith('[') && t.endsWith(']')) {
|
|
610
|
-
const inner = t.slice(1, -1).trim()
|
|
611
|
-
if (!inner) return []
|
|
612
|
-
const out: unknown[] = []
|
|
613
|
-
for (const seg of splitTopLevelCommas(inner)) {
|
|
614
|
-
if (!seg.trim()) continue
|
|
615
|
-
const parsed = parseLiteral(seg.trim())
|
|
616
|
-
if (parsed === null && seg.trim() !== 'null') return null
|
|
617
|
-
out.push(parsed)
|
|
618
|
-
}
|
|
619
|
-
return out
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const stringMatch = expr.match(/^(['"])(.*)\1$/s)
|
|
624
|
-
if (stringMatch) return unescapeJsString(stringMatch[2])
|
|
625
|
-
|
|
626
|
-
const trimmed = expr.trim()
|
|
627
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
628
|
-
const inner = trimmed.slice(1, -1).trim()
|
|
629
|
-
if (!inner) return {}
|
|
630
|
-
const obj: Record<string, unknown> = {}
|
|
631
|
-
for (const pair of splitTopLevelCommas(inner)) {
|
|
632
|
-
if (!pair.trim()) continue
|
|
633
|
-
const colonIdx = pair.indexOf(':')
|
|
634
|
-
if (colonIdx < 0) return null
|
|
635
|
-
let key = pair.slice(0, colonIdx).trim()
|
|
636
|
-
const val = pair.slice(colonIdx + 1).trim()
|
|
637
|
-
const keyMatch = key.match(/^(['"])(.*)\1$/s)
|
|
638
|
-
if (keyMatch) key = unescapeJsString(keyMatch[2])
|
|
639
|
-
const parsedVal = parseLiteral(val)
|
|
640
|
-
if (parsedVal === null && val !== 'null') return null
|
|
641
|
-
obj[key] = parsedVal
|
|
642
|
-
}
|
|
643
|
-
return obj
|
|
644
|
-
}
|
|
645
|
-
return null
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function splitTopLevelCommas(inner: string): string[] {
|
|
649
|
-
const segments: string[] = []
|
|
650
|
-
let depth = 0
|
|
651
|
-
let start = 0
|
|
652
|
-
let quote: string | null = null
|
|
653
|
-
for (let i = 0; i < inner.length; i++) {
|
|
654
|
-
const c = inner[i]
|
|
655
|
-
if (quote) {
|
|
656
|
-
if (c === quote) {
|
|
657
|
-
let backslashes = 0
|
|
658
|
-
for (let j = i - 1; j >= 0 && inner[j] === '\\'; j--) backslashes++
|
|
659
|
-
if (backslashes % 2 === 0) quote = null
|
|
660
|
-
}
|
|
661
|
-
continue
|
|
662
|
-
}
|
|
663
|
-
if (c === '"' || c === "'") {
|
|
664
|
-
quote = c
|
|
665
|
-
continue
|
|
666
|
-
}
|
|
667
|
-
if (c === '{' || c === '[') depth++
|
|
668
|
-
else if (c === '}' || c === ']') depth--
|
|
669
|
-
else if (c === ',' && depth === 0) {
|
|
670
|
-
segments.push(inner.slice(start, i))
|
|
671
|
-
start = i + 1
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
segments.push(inner.slice(start))
|
|
675
|
-
return segments
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function unescapeJsString(s: string): string {
|
|
679
|
-
return s.replace(/\\(.)/g, (_, c) => {
|
|
680
|
-
switch (c) {
|
|
681
|
-
case 'n': return '\n'
|
|
682
|
-
case 'r': return '\r'
|
|
683
|
-
case 't': return '\t'
|
|
684
|
-
case '0': return '\0'
|
|
685
|
-
default: return c
|
|
686
|
-
}
|
|
687
|
-
})
|
|
688
|
-
}
|