@barefootjs/erb 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/erb-adapter.d.ts +8 -0
- package/dist/adapter/erb-adapter.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 +71 -27
- package/dist/adapter/lib/constants.d.ts.map +1 -1
- package/dist/build.js +71 -27
- package/dist/conformance-pins.d.ts.map +1 -1
- package/dist/index.js +73 -43
- package/dist/render-divergences.d.ts.map +1 -1
- package/lib/barefoot_js/backend/erb.rb +24 -15
- package/lib/barefoot_js/evaluator.rb +14 -1
- package/lib/barefoot_js.rb +111 -6
- package/package.json +3 -3
- package/src/__tests__/erb-adapter.test.ts +21 -0
- package/src/adapter/erb-adapter.ts +81 -11
- package/src/adapter/expr/array-method.ts +22 -0
- package/src/adapter/expr/emitters.ts +15 -2
- package/src/adapter/lib/constants.ts +3 -0
- package/src/conformance-pins.ts +0 -5
- package/src/render-divergences.ts +1 -26
package/dist/index.js
CHANGED
|
@@ -187371,7 +187371,7 @@ function isExplicitStringCall(expr) {
|
|
|
187371
187371
|
}
|
|
187372
187372
|
|
|
187373
187373
|
// src/adapter/erb-adapter.ts
|
|
187374
|
-
import { BF_SLOT, BF_COND, BF_REGION } from "@barefootjs/shared";
|
|
187374
|
+
import { BF_SLOT, BF_COND, BF_REGION, escapeHtml } from "@barefootjs/shared";
|
|
187375
187375
|
|
|
187376
187376
|
// src/adapter/lib/constants.ts
|
|
187377
187377
|
var ERB_TEMPLATE_PRIMITIVES = {
|
|
@@ -187380,7 +187380,10 @@ var ERB_TEMPLATE_PRIMITIVES = {
|
|
|
187380
187380
|
Number: { arity: 1, emit: (args) => `bf.number(${args[0]})` },
|
|
187381
187381
|
"Math.floor": { arity: 1, emit: (args) => `bf.floor(${args[0]})` },
|
|
187382
187382
|
"Math.ceil": { arity: 1, emit: (args) => `bf.ceil(${args[0]})` },
|
|
187383
|
-
"Math.round": { arity: 1, emit: (args) => `bf.round(${args[0]})` }
|
|
187383
|
+
"Math.round": { arity: 1, emit: (args) => `bf.round(${args[0]})` },
|
|
187384
|
+
"Math.min": { arity: 2, emit: (args) => `bf.min(${args[0]}, ${args[1]})` },
|
|
187385
|
+
"Math.max": { arity: 2, emit: (args) => `bf.max(${args[0]}, ${args[1]})` },
|
|
187386
|
+
"Math.abs": { arity: 1, emit: (args) => `bf.abs(${args[0]})` }
|
|
187384
187387
|
};
|
|
187385
187388
|
var ERB_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(ERB_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
|
|
187386
187389
|
|
|
@@ -187548,6 +187551,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187548
187551
|
const recv = emit(object);
|
|
187549
187552
|
return `bf.trim(${recv})`;
|
|
187550
187553
|
}
|
|
187554
|
+
case "trimStart":
|
|
187555
|
+
case "trimEnd": {
|
|
187556
|
+
const fn = method === "trimStart" ? "trim_start" : "trim_end";
|
|
187557
|
+
const recv = emit(object);
|
|
187558
|
+
return `bf.${fn}(${recv})`;
|
|
187559
|
+
}
|
|
187551
187560
|
case "toFixed": {
|
|
187552
187561
|
const recv = emit(object);
|
|
187553
187562
|
const digits = args.length >= 1 ? emit(args[0]) : "0";
|
|
@@ -187581,6 +187590,12 @@ function renderArrayMethod(method, object, args, emit) {
|
|
|
187581
187590
|
const newS = emit(args[1]);
|
|
187582
187591
|
return `bf.replace(${recv}, ${oldS}, ${newS})`;
|
|
187583
187592
|
}
|
|
187593
|
+
case "replaceAll": {
|
|
187594
|
+
const recv = emit(object);
|
|
187595
|
+
const oldS = emit(args[0]);
|
|
187596
|
+
const newS = emit(args[1]);
|
|
187597
|
+
return `bf.replace_all(${recv}, ${oldS}, ${newS})`;
|
|
187598
|
+
}
|
|
187584
187599
|
case "repeat": {
|
|
187585
187600
|
const recv = emit(object);
|
|
187586
187601
|
const count = args.length === 0 ? "0" : emit(args[0]);
|
|
@@ -187749,9 +187764,11 @@ class ErbFilterEmitter {
|
|
|
187749
187764
|
return "nil";
|
|
187750
187765
|
return String(value);
|
|
187751
187766
|
}
|
|
187752
|
-
member(object, property, _computed, emit) {
|
|
187767
|
+
member(object, property, _computed, optional, emit) {
|
|
187753
187768
|
if (property === "length")
|
|
187754
187769
|
return `${emit(object)}.length`;
|
|
187770
|
+
if (optional)
|
|
187771
|
+
return `${emit(object)}&.[](${rubySymbolLiteral(property)})`;
|
|
187755
187772
|
return `${emit(object)}[${rubySymbolLiteral(property)}]`;
|
|
187756
187773
|
}
|
|
187757
187774
|
indexAccess(object, index, emit) {
|
|
@@ -187873,7 +187890,7 @@ class ErbTopLevelEmitter {
|
|
|
187873
187890
|
return "nil";
|
|
187874
187891
|
return String(value);
|
|
187875
187892
|
}
|
|
187876
|
-
member(object, property, _computed, emit) {
|
|
187893
|
+
member(object, property, _computed, optional, emit) {
|
|
187877
187894
|
if (object.kind === "identifier" && object.name === "props") {
|
|
187878
187895
|
return `v[${rubySymbolLiteral(property)}]`;
|
|
187879
187896
|
}
|
|
@@ -187885,6 +187902,8 @@ class ErbTopLevelEmitter {
|
|
|
187885
187902
|
const obj = emit(object);
|
|
187886
187903
|
if (property === "length")
|
|
187887
187904
|
return `${obj}.length`;
|
|
187905
|
+
if (optional)
|
|
187906
|
+
return `${obj}&.[](${rubySymbolLiteral(property)})`;
|
|
187888
187907
|
return `${obj}[${rubySymbolLiteral(property)}]`;
|
|
187889
187908
|
}
|
|
187890
187909
|
indexAccess(object, index, emit) {
|
|
@@ -188316,6 +188335,7 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188316
188335
|
options;
|
|
188317
188336
|
errors = [];
|
|
188318
188337
|
inLoop = false;
|
|
188338
|
+
currentLoopKeyDepth = 0;
|
|
188319
188339
|
propsObjectName = null;
|
|
188320
188340
|
propsParams = [];
|
|
188321
188341
|
booleanTypedProps = new Set;
|
|
@@ -188456,7 +188476,7 @@ class ErbAdapter extends BaseAdapter {
|
|
|
188456
188476
|
return this.renderElement(node);
|
|
188457
188477
|
}
|
|
188458
188478
|
emitText(node) {
|
|
188459
|
-
return node.value;
|
|
188479
|
+
return escapeHtml(node.value);
|
|
188460
188480
|
}
|
|
188461
188481
|
emitExpression(node) {
|
|
188462
188482
|
return this.renderExpression(node);
|
|
@@ -188697,13 +188717,16 @@ ${whenTrue}
|
|
|
188697
188717
|
}
|
|
188698
188718
|
const param = loop.param;
|
|
188699
188719
|
const indexVar = loop.iterationShape === "keys" ? rubyLocal(param) : rubyLocal(loop.index ?? "_i");
|
|
188700
|
-
const loopBound = loop.iterationShape === "keys" ? [param] : supportableDestructure ? ["__bf_item", ...(loop.paramBindings ?? []).map((b) => b.name), loop.index ?? "_i"] : [param, loop.index ?? "_i"];
|
|
188720
|
+
const loopBound = loop.objectIteration === "entries" ? [param, loop.index ?? "_k"] : loop.objectIteration === "keys" || loop.objectIteration === "values" || loop.iterationShape === "keys" ? [param] : supportableDestructure ? ["__bf_item", ...(loop.paramBindings ?? []).map((b) => b.name), loop.index ?? "_i"] : [param, loop.index ?? "_i"];
|
|
188701
188721
|
for (const n of loopBound) {
|
|
188702
188722
|
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
188703
188723
|
}
|
|
188704
188724
|
const prevInLoop = this.inLoop;
|
|
188705
188725
|
this.inLoop = true;
|
|
188726
|
+
const prevLoopKeyDepth = this.currentLoopKeyDepth;
|
|
188727
|
+
this.currentLoopKeyDepth = loop.depth;
|
|
188706
188728
|
const renderedChildren = this.renderChildren(loop.children);
|
|
188729
|
+
this.currentLoopKeyDepth = prevLoopKeyDepth;
|
|
188707
188730
|
this.inLoop = prevInLoop;
|
|
188708
188731
|
const children = loop.bodyIsItemConditional && loop.key ? `<%= bf.comment("loop-i:" + bf.string(${this.convertExpressionToRuby(loop.key)})) %>
|
|
188709
188732
|
${renderedChildren}` : renderedChildren;
|
|
@@ -188737,23 +188760,29 @@ ${renderedChildren}` : renderedChildren;
|
|
|
188737
188760
|
}
|
|
188738
188761
|
lines.push(`<%- ${sortedHoist} = ${sorted} -%>`);
|
|
188739
188762
|
}
|
|
188740
|
-
|
|
188741
|
-
|
|
188742
|
-
|
|
188743
|
-
|
|
188744
|
-
|
|
188745
|
-
|
|
188746
|
-
|
|
188747
|
-
|
|
188748
|
-
|
|
188749
|
-
|
|
188750
|
-
|
|
188751
|
-
|
|
188752
|
-
|
|
188763
|
+
if (loop.objectIteration) {
|
|
188764
|
+
const method = loop.objectIteration === "entries" ? "each_pair" : loop.objectIteration === "keys" ? "each_key" : "each_value";
|
|
188765
|
+
const blockParams = loop.objectIteration === "entries" ? `${rubyLocal(loop.index ?? param)}, ${rubyLocal(param)}` : rubyLocal(param);
|
|
188766
|
+
lines.push(`<%- ${array}.${method} do |${blockParams}| -%>`);
|
|
188767
|
+
} else {
|
|
188768
|
+
lines.push(`<%- (0...${array}.length).each do |${indexVar}| -%>`);
|
|
188769
|
+
if (loop.iterationShape !== "keys") {
|
|
188770
|
+
if (supportableDestructure) {
|
|
188771
|
+
lines.push(`<%- __bf_item = ${array}[${indexVar}] -%>`);
|
|
188772
|
+
for (const b of loop.paramBindings ?? []) {
|
|
188773
|
+
const accessor = rubyAccessorFromSegments("__bf_item", b.segments ?? []);
|
|
188774
|
+
if (b.rest?.kind === "array") {
|
|
188775
|
+
lines.push(`<%- ${rubyLocal(b.name)} = bf.slice(${accessor}, ${b.rest.from}) -%>`);
|
|
188776
|
+
} else if (b.rest?.kind === "object") {
|
|
188777
|
+
const excludeSyms = b.rest.exclude.map((k) => rubySymbolLiteral(k.key)).join(", ");
|
|
188778
|
+
lines.push(`<%- ${rubyLocal(b.name)} = ${accessor}.except(${excludeSyms}) -%>`);
|
|
188779
|
+
} else {
|
|
188780
|
+
lines.push(`<%- ${rubyLocal(b.name)} = ${accessor} -%>`);
|
|
188781
|
+
}
|
|
188753
188782
|
}
|
|
188783
|
+
} else {
|
|
188784
|
+
lines.push(`<%- ${rubyLocal(param)} = ${array}[${indexVar}] -%>`);
|
|
188754
188785
|
}
|
|
188755
|
-
} else {
|
|
188756
|
-
lines.push(`<%- ${rubyLocal(param)} = ${array}[${indexVar}] -%>`);
|
|
188757
188786
|
}
|
|
188758
188787
|
}
|
|
188759
188788
|
if (loop.filterPredicate) {
|
|
@@ -188805,9 +188834,23 @@ ${renderedChildren}` : renderedChildren;
|
|
|
188805
188834
|
};
|
|
188806
188835
|
renderComponent(comp) {
|
|
188807
188836
|
const propParts = [];
|
|
188837
|
+
const namedSlotCaptures = [];
|
|
188808
188838
|
for (const p of comp.props) {
|
|
188809
188839
|
if ((p.name.match(/^on[A-Z]/) || p.name === "ref") && p.value.kind === "expression")
|
|
188810
188840
|
continue;
|
|
188841
|
+
if (p.value.kind === "jsx-children" && p.name !== "children") {
|
|
188842
|
+
const prevInLoop = this.inLoop;
|
|
188843
|
+
this.inLoop = false;
|
|
188844
|
+
const slotBody = this.renderChildren(p.value.children);
|
|
188845
|
+
this.inLoop = prevInLoop;
|
|
188846
|
+
const suffix = `${this.childrenCaptureCounter++}`;
|
|
188847
|
+
const lenVar = `__bf_len_${suffix}`;
|
|
188848
|
+
const rawVar = `__bf_praw_${suffix}`;
|
|
188849
|
+
const capVar = `__bf_prop_${suffix}`;
|
|
188850
|
+
namedSlotCaptures.push(`<% ${lenVar} = _erbout.length %>${slotBody}<% ${rawVar} = _erbout.slice!(${lenVar}..); ${capVar} = bf.backend.mark_raw(${rawVar}) %>`);
|
|
188851
|
+
propParts.push(`${rubySymbolKey(p.name)} ${capVar}`);
|
|
188852
|
+
continue;
|
|
188853
|
+
}
|
|
188811
188854
|
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
|
|
188812
188855
|
if (lowered)
|
|
188813
188856
|
propParts.push(lowered);
|
|
@@ -188826,10 +188869,10 @@ ${renderedChildren}` : renderedChildren;
|
|
|
188826
188869
|
const lenVar = `__bf_len_${suffix}`;
|
|
188827
188870
|
const capVar = `__bf_children_${suffix}`;
|
|
188828
188871
|
const propsHash2 = `{ ${[...propParts, `children: ${capVar}`].join(", ")} }`;
|
|
188829
|
-
return
|
|
188872
|
+
return `${namedSlotCaptures.join("")}<% ${lenVar} = _erbout.length %>${childrenBody}<% ${capVar} = _erbout.slice!(${lenVar}..) %><%= bf.render_child('${tplName}', ${propsHash2}) %>`;
|
|
188830
188873
|
}
|
|
188831
188874
|
const propsHash = propParts.length > 0 ? `{ ${propParts.join(", ")} }` : "{}";
|
|
188832
|
-
return
|
|
188875
|
+
return `${namedSlotCaptures.join("")}<%= bf.render_child('${tplName}', ${propsHash}) %>`;
|
|
188833
188876
|
}
|
|
188834
188877
|
childrenCaptureCounter = 0;
|
|
188835
188878
|
toTemplateName(componentName) {
|
|
@@ -188874,7 +188917,7 @@ ${alternate}
|
|
|
188874
188917
|
${children}`;
|
|
188875
188918
|
}
|
|
188876
188919
|
elementAttrEmitter = {
|
|
188877
|
-
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
188920
|
+
emitLiteral: (value, name) => `${name}="${escapeHtml(value.value)}"`,
|
|
188878
188921
|
emitExpression: (value, name) => {
|
|
188879
188922
|
if (name === "style") {
|
|
188880
188923
|
const css = this.tryLowerStyleObject(value.expr);
|
|
@@ -188969,9 +189012,10 @@ ${children}`;
|
|
|
188969
189012
|
let attrName;
|
|
188970
189013
|
if (attr.name === "className")
|
|
188971
189014
|
attrName = "class";
|
|
188972
|
-
else if (attr.name === "key")
|
|
188973
|
-
|
|
188974
|
-
|
|
189015
|
+
else if (attr.name === "key") {
|
|
189016
|
+
const depth = this.currentLoopKeyDepth;
|
|
189017
|
+
attrName = depth > 0 ? `data-key-${depth}` : "data-key";
|
|
189018
|
+
} else
|
|
188975
189019
|
attrName = attr.name;
|
|
188976
189020
|
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
188977
189021
|
if (lowered)
|
|
@@ -189164,24 +189208,10 @@ var conformancePins = {
|
|
|
189164
189208
|
{ code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
|
|
189165
189209
|
],
|
|
189166
189210
|
"array-map-function-reference": [{ code: "BF101", severity: "error" }],
|
|
189167
|
-
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189168
|
-
"string-replaceall": [{ code: "BF101", severity: "error" }]
|
|
189211
|
+
"dangerous-inner-html": [{ code: "BF101", severity: "error" }]
|
|
189169
189212
|
};
|
|
189170
189213
|
// src/render-divergences.ts
|
|
189171
|
-
var renderDivergences = {
|
|
189172
|
-
"html-entity-text": "`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
|
|
189173
|
-
"optional-chaining-prop": "`user?.name ?? …` on an object prop: the Ruby render exits 1 (optional chaining into a Hash prop has no lowering)",
|
|
189174
|
-
"math-methods": "Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)",
|
|
189175
|
-
"boolean-attr-literals": 'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
|
|
189176
|
-
"camelcase-attributes": "`htmlFor` is not lowered to `for` (Hono maps it)",
|
|
189177
|
-
"static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
|
|
189178
|
-
"svg-icon": "SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case",
|
|
189179
|
-
"object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders but its loop item keys diverge from the reference serialisation",
|
|
189180
|
-
"nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
|
|
189181
|
-
"jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
|
|
189182
|
-
"string-slice": '`.slice()` on a STRING lowers through the array slice helper and renders "[]" instead of the substring',
|
|
189183
|
-
"string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering; only both-sides `.trim` is wired)"
|
|
189184
|
-
};
|
|
189214
|
+
var renderDivergences = {};
|
|
189185
189215
|
export {
|
|
189186
189216
|
renderDivergences,
|
|
189187
189217
|
erbAdapter,
|
|
@@ -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"}
|
|
@@ -13,15 +13,25 @@ module BarefootJS
|
|
|
13
13
|
# operations the runtime delegates to, targeting Ruby stdlib ERB:
|
|
14
14
|
#
|
|
15
15
|
# encode_json(data) -> JSON string (injectable encoder)
|
|
16
|
-
# mark_raw(str) ->
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
16
|
+
# mark_raw(str) -> wraps in BarefootJS::SafeString --
|
|
17
|
+
# ERB's own `<%=` is NOT auto-escaping,
|
|
18
|
+
# so most compiled templates call
|
|
19
|
+
# `bf.h(...)` explicitly instead of
|
|
20
|
+
# relying on a safe-string bypass; the
|
|
21
|
+
# wrapper exists for the one path that
|
|
22
|
+
# DOES need one -- a named-slot /
|
|
23
|
+
# children value captured in a parent
|
|
24
|
+
# template and forwarded into a
|
|
25
|
+
# child's vars Hash, where the child
|
|
26
|
+
# reads it back through the generic
|
|
27
|
+
# `bf.h(...)` text-expression path
|
|
28
|
+
# (`Context#h` unwraps SafeString to
|
|
29
|
+
# skip re-escaping) -- and so that
|
|
30
|
+
# runtime helpers which already
|
|
31
|
+
# produce finished HTML (e.g.
|
|
32
|
+
# spread_attrs) share one
|
|
33
|
+
# `backend.mark_raw(...)` interface
|
|
34
|
+
# with the Kolon/EP ports
|
|
25
35
|
# materialize(value) -> resolve a captured-children value
|
|
26
36
|
# to a string
|
|
27
37
|
# render_named(name, bf, vars) -> render `<name>.erb` with `bf` and
|
|
@@ -58,13 +68,12 @@ module BarefootJS
|
|
|
58
68
|
@json_encoder.call(data)
|
|
59
69
|
end
|
|
60
70
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
# shape across every BarefootJS backend port.
|
|
71
|
+
# See the class docstring's `mark_raw` entry: wraps in
|
|
72
|
+
# `BarefootJS::SafeString` so `Context#h` recognises and skips
|
|
73
|
+
# re-escaping already-finished HTML forwarded across a
|
|
74
|
+
# parent/child template boundary.
|
|
66
75
|
def mark_raw(str)
|
|
67
|
-
str
|
|
76
|
+
BarefootJS::SafeString.new(str.to_s)
|
|
68
77
|
end
|
|
69
78
|
|
|
70
79
|
# JSX children captured by the adapter's buffer-slice capture
|
|
@@ -188,7 +188,20 @@ module BarefootJS
|
|
|
188
188
|
return Float::INFINITY if t == 'Infinity' || t == '+Infinity'
|
|
189
189
|
return -Float::INFINITY if t == '-Infinity'
|
|
190
190
|
return Integer(t, 16) if t =~ HEX_STRING_RE
|
|
191
|
-
|
|
191
|
+
|
|
192
|
+
if t =~ NUMERIC_STRING_RE
|
|
193
|
+
# Ruby's Float() rejects a trailing decimal point ("5.", "5.e3"),
|
|
194
|
+
# while JS's Number() accepts it (treating the fractional part as
|
|
195
|
+
# empty). Strip a "." that isn't followed by a digit before
|
|
196
|
+
# converting, so the accepted grammar stays identical to JS but the
|
|
197
|
+
# conversion itself never raises.
|
|
198
|
+
normalized = t.sub(/\.(?=\z|[eE])/, '')
|
|
199
|
+
begin
|
|
200
|
+
return Float(normalized)
|
|
201
|
+
rescue ArgumentError, TypeError
|
|
202
|
+
return Float::NAN
|
|
203
|
+
end
|
|
204
|
+
end
|
|
192
205
|
|
|
193
206
|
Float::NAN
|
|
194
207
|
end
|
package/lib/barefoot_js.rb
CHANGED
|
@@ -25,6 +25,18 @@ require 'barefoot_js/search_params'
|
|
|
25
25
|
# runtime carries -- a Ruby `true`/`false` IS a boolean, distinguishable
|
|
26
26
|
# from `0`/`1` for free.
|
|
27
27
|
module BarefootJS
|
|
28
|
+
# Marker wrapper for a string that is ALREADY finished HTML and must not
|
|
29
|
+
# be re-escaped by `Context#h` -- e.g. a named-slot / children capture
|
|
30
|
+
# (`renderComponent`'s output-buffer slice) forwarded from a parent
|
|
31
|
+
# template into a child's vars Hash. Stdlib ERB's `<%=` has no built-in
|
|
32
|
+
# "safe string" concept the way Twig's `Markup` or Kolon's `mark_raw`
|
|
33
|
+
# do, so the escape decision that elsewhere falls out of the template
|
|
34
|
+
# engine has to be carried on the VALUE itself here; see
|
|
35
|
+
# `Backend::Erb#mark_raw`, which is what actually wraps a string in this
|
|
36
|
+
# class.
|
|
37
|
+
class SafeString < String
|
|
38
|
+
end
|
|
39
|
+
|
|
28
40
|
# Context is the `bf` object every compiled `.erb` template receives as a
|
|
29
41
|
# local. One instance per render (root or child); `render_child` /
|
|
30
42
|
# `register_components_from_manifest` construct a fresh child instance
|
|
@@ -391,8 +403,13 @@ module BarefootJS
|
|
|
391
403
|
# HTML-escaping helper for text interpolation (`<%= bf.h(expr) %>` --
|
|
392
404
|
# stdlib ERB does not auto-escape). JS-style stringification via
|
|
393
405
|
# `string` (numbers per JS Number#toString, nil -> "", booleans ->
|
|
394
|
-
# "true"/"false"), then HTML-escaped.
|
|
406
|
+
# "true"/"false"), then HTML-escaped. A `SafeString` (already-finished
|
|
407
|
+
# HTML forwarded from a parent's capture -- see that class's docstring)
|
|
408
|
+
# passes through unescaped, matching Twig/Blade/Kolon's safe-string
|
|
409
|
+
# bypass on their own auto-escaping `{{ }}`/`<: :>` output tags.
|
|
395
410
|
def h(value)
|
|
411
|
+
return value if value.is_a?(SafeString)
|
|
412
|
+
|
|
396
413
|
html_escape(string(value))
|
|
397
414
|
end
|
|
398
415
|
|
|
@@ -430,6 +447,34 @@ module BarefootJS
|
|
|
430
447
|
finite_number?(n) ? (n + 0.5).floor : n
|
|
431
448
|
end
|
|
432
449
|
|
|
450
|
+
# `Math.min(a, b)` / `Math.max(a, b)` -- two-arg forms only (#2168
|
|
451
|
+
# math-methods). JS returns NaN if either operand is NaN. `number()`
|
|
452
|
+
# may return a plain Integer (no `#nan?`), so guard like
|
|
453
|
+
# `finite_number?` above rather than calling `#nan?` unconditionally.
|
|
454
|
+
def min(a, b)
|
|
455
|
+
x = number(a)
|
|
456
|
+
y = number(b)
|
|
457
|
+
return x if nan_number?(x)
|
|
458
|
+
return y if nan_number?(y)
|
|
459
|
+
|
|
460
|
+
x < y ? x : y
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def max(a, b)
|
|
464
|
+
x = number(a)
|
|
465
|
+
y = number(b)
|
|
466
|
+
return x if nan_number?(x)
|
|
467
|
+
return y if nan_number?(y)
|
|
468
|
+
|
|
469
|
+
x > y ? x : y
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# `Math.abs()` (#2168 math-methods).
|
|
473
|
+
def abs(value)
|
|
474
|
+
n = number(value)
|
|
475
|
+
nan_number?(n) ? n : n.abs
|
|
476
|
+
end
|
|
477
|
+
|
|
433
478
|
# -----------------------------------------------------------------
|
|
434
479
|
# Array / String method helpers
|
|
435
480
|
# -----------------------------------------------------------------
|
|
@@ -563,13 +608,22 @@ module BarefootJS
|
|
|
563
608
|
out
|
|
564
609
|
end
|
|
565
610
|
|
|
566
|
-
# `Array.prototype.slice(start, end?)
|
|
567
|
-
# `slice`
|
|
611
|
+
# `Array.prototype.slice(start, end?)` AND `String.prototype.slice`
|
|
612
|
+
# (the `string-slice` divergence) -- the adapter emits the same
|
|
613
|
+
# `bf_slice` call for both receiver shapes (it can't disambiguate
|
|
614
|
+
# string vs. array at compile time), so this dispatches on Ruby
|
|
615
|
+
# class, mirroring `includes` above. Mirrors the Go/Perl `bf_slice`
|
|
616
|
+
# / `slice` arithmetic so adapter output stays symmetric.
|
|
617
|
+
# `String#length` / `#[]` already index by character (not byte) for
|
|
618
|
+
# a UTF-8-encoded string, matching JS except for astral-plane input
|
|
619
|
+
# (the same divergence boundary every other adapter's pad/trim
|
|
620
|
+
# helpers already accept).
|
|
568
621
|
def slice(recv, start, end_ = nil)
|
|
569
|
-
return [] unless recv.is_a?(Array)
|
|
622
|
+
return [] unless recv.is_a?(Array) || recv.is_a?(String)
|
|
570
623
|
|
|
624
|
+
empty = recv.is_a?(String) ? '' : []
|
|
571
625
|
len = recv.length
|
|
572
|
-
return
|
|
626
|
+
return empty if len.zero?
|
|
573
627
|
|
|
574
628
|
s = start.nil? ? 0 : start.to_i
|
|
575
629
|
s = len + s if s.negative?
|
|
@@ -581,7 +635,7 @@ module BarefootJS
|
|
|
581
635
|
e = 0 if e.negative?
|
|
582
636
|
e = len if e > len
|
|
583
637
|
|
|
584
|
-
return
|
|
638
|
+
return empty if s >= e
|
|
585
639
|
|
|
586
640
|
recv[s...e]
|
|
587
641
|
end
|
|
@@ -663,6 +717,21 @@ module BarefootJS
|
|
|
663
717
|
string(recv).gsub(/\A\p{Space}+|\p{Space}+\z/, '')
|
|
664
718
|
end
|
|
665
719
|
|
|
720
|
+
# `String.prototype.trimStart()` / `.trimEnd()` -- the one-sided
|
|
721
|
+
# siblings of `trim` above (#2183 follow-up), same `\p{Space}` regex
|
|
722
|
+
# restricted to one side.
|
|
723
|
+
def trim_start(recv)
|
|
724
|
+
return '' if recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash)
|
|
725
|
+
|
|
726
|
+
string(recv).sub(/\A\p{Space}+/, '')
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def trim_end(recv)
|
|
730
|
+
return '' if recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash)
|
|
731
|
+
|
|
732
|
+
string(recv).sub(/\p{Space}+\z/, '')
|
|
733
|
+
end
|
|
734
|
+
|
|
666
735
|
# `Number.prototype.toFixed(digits)` -- fixed-decimal string with
|
|
667
736
|
# zero-padding, rounding half toward +Infinity (matching `round`).
|
|
668
737
|
def to_fixed(value, digits = 0)
|
|
@@ -741,6 +810,38 @@ module BarefootJS
|
|
|
741
810
|
s[0...idx] + n + s[(idx + o.length)..]
|
|
742
811
|
end
|
|
743
812
|
|
|
813
|
+
# `String.prototype.replaceAll(pattern, replacement)` -- string-pattern
|
|
814
|
+
# form only (#2182), replacing EVERY occurrence (the all-occurrences
|
|
815
|
+
# sibling of `replace` above). Deliberately NOT `String#gsub`: Ruby's
|
|
816
|
+
# `gsub` interprets `\1` / `\&` backreference syntax in the replacement
|
|
817
|
+
# even for a literal string pattern (`"abc".gsub("b", "\\1")` -> "ac",
|
|
818
|
+
# not the literal "\1"), which would diverge from `.replace`'s literal
|
|
819
|
+
# splice above and from the other backends' literal treatment. The
|
|
820
|
+
# index/splice loop keeps the replacement literal, matching `replace`.
|
|
821
|
+
# An empty pattern inserts at every boundary, including before the
|
|
822
|
+
# first and after the last character (`"abc".replaceAll("", "X")` ->
|
|
823
|
+
# "XaXbXcX"), matching JS.
|
|
824
|
+
def replace_all(recv, pattern, replacement)
|
|
825
|
+
s = recv.nil? ? '' : string(recv)
|
|
826
|
+
o = pattern.nil? ? '' : string(pattern)
|
|
827
|
+
n = replacement.nil? ? '' : string(replacement)
|
|
828
|
+
return ([''] + s.chars + ['']).join(n) if o.empty?
|
|
829
|
+
|
|
830
|
+
# `+''` (not the frozen `''` literal under frozen_string_literal)
|
|
831
|
+
# so `<<` can append in place instead of `+=` reallocating a new
|
|
832
|
+
# string each iteration (quadratic for long inputs / many matches).
|
|
833
|
+
out = +''
|
|
834
|
+
pos = 0
|
|
835
|
+
loop do
|
|
836
|
+
idx = s.index(o, pos)
|
|
837
|
+
break if idx.nil?
|
|
838
|
+
|
|
839
|
+
out << s[pos...idx] << n
|
|
840
|
+
pos = idx + o.length
|
|
841
|
+
end
|
|
842
|
+
out << s[pos..]
|
|
843
|
+
end
|
|
844
|
+
|
|
744
845
|
# `queryHref(base, { ... })` (#2042) -- build "base?k=v&..." from a flat
|
|
745
846
|
# list of (guard, key, value) triples. A pair is included iff its guard
|
|
746
847
|
# is truthy AND its value is a non-empty string. A value may instead be
|
|
@@ -978,6 +1079,10 @@ module BarefootJS
|
|
|
978
1079
|
!(n.respond_to?(:nan?) && n.nan?) && !(n.respond_to?(:infinite?) && n.infinite?)
|
|
979
1080
|
end
|
|
980
1081
|
|
|
1082
|
+
def nan_number?(n)
|
|
1083
|
+
n.respond_to?(:nan?) && n.nan?
|
|
1084
|
+
end
|
|
1085
|
+
|
|
981
1086
|
def html_escape(s)
|
|
982
1087
|
s.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"').gsub("'", ''')
|
|
983
1088
|
end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/erb",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.5",
|
|
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.5"
|
|
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.5",
|
|
65
65
|
"typescript": "^5.0.0"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -304,3 +304,24 @@ export { Slot }
|
|
|
304
304
|
expect(template).toMatch(/\*\*v\[:rest\]/)
|
|
305
305
|
})
|
|
306
306
|
})
|
|
307
|
+
|
|
308
|
+
describe('ErbAdapter - named-slot capture identifier safety (#2168 jsx-element-prop)', () => {
|
|
309
|
+
// A JSX-valued prop under a hyphenated name (`data-slot`, a valid JSX
|
|
310
|
+
// attribute name) must not leak into the buffer-slice capture's local
|
|
311
|
+
// variables — Ruby local variable names can't contain `-`. The capture
|
|
312
|
+
// suffix is purely counter-based (never derived from the prop name); the
|
|
313
|
+
// hash KEY passed to `render_child` still carries the real name, quoted
|
|
314
|
+
// via `rubySymbolKey`.
|
|
315
|
+
test('a hyphenated prop name does not appear in the capture variables', () => {
|
|
316
|
+
const { template } = compileAndGenerate(`
|
|
317
|
+
function Card(props) { return null }
|
|
318
|
+
export function Parent() {
|
|
319
|
+
return <Card data-slot={<strong>Title</strong>}>text</Card>
|
|
320
|
+
}
|
|
321
|
+
`)
|
|
322
|
+
expect(template).toContain('__bf_len_0 = _erbout.length')
|
|
323
|
+
expect(template).toContain('__bf_prop_0 = bf.backend.mark_raw(__bf_praw_0)')
|
|
324
|
+
expect(template).toContain('"data-slot": __bf_prop_0')
|
|
325
|
+
expect(template).not.toContain('bf_prop_data')
|
|
326
|
+
})
|
|
327
|
+
})
|