@barefootjs/erb 0.18.4 → 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/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
- lines.push(`<%- (0...${array}.length).each do |${indexVar}| -%>`);
188741
- if (loop.iterationShape !== "keys") {
188742
- if (supportableDestructure) {
188743
- lines.push(`<%- __bf_item = ${array}[${indexVar}] -%>`);
188744
- for (const b of loop.paramBindings ?? []) {
188745
- const accessor = rubyAccessorFromSegments("__bf_item", b.segments ?? []);
188746
- if (b.rest?.kind === "array") {
188747
- lines.push(`<%- ${rubyLocal(b.name)} = bf.slice(${accessor}, ${b.rest.from}) -%>`);
188748
- } else if (b.rest?.kind === "object") {
188749
- const excludeSyms = b.rest.exclude.map((k) => rubySymbolLiteral(k.key)).join(", ");
188750
- lines.push(`<%- ${rubyLocal(b.name)} = ${accessor}.except(${excludeSyms}) -%>`);
188751
- } else {
188752
- lines.push(`<%- ${rubyLocal(b.name)} = ${accessor} -%>`);
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 `<% ${lenVar} = _erbout.length %>${childrenBody}<% ${capVar} = _erbout.slice!(${lenVar}..) %><%= bf.render_child('${tplName}', ${propsHash2}) %>`;
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 `<%= bf.render_child('${tplName}', ${propsHash}) %>`;
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
- attrName = "data-key";
188974
- else
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,21 +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": "`&copy;` 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
- "static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
189176
- "object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders but its loop item keys diverge from the reference serialisation",
189177
- "nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
189178
- "jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
189179
- "string-slice": '`.slice()` on a STRING lowers through the array slice helper and renders "[]" instead of the substring',
189180
- "string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering; only both-sides `.trim` is wired)"
189181
- };
189214
+ var renderDivergences = {};
189182
189215
  export {
189183
189216
  renderDivergences,
189184
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,iBAmB/B,CAAA"}
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) -> identity (ERB has no "safe string"
17
- # wrapper type -- ERB's own `<%=` is
18
- # NOT auto-escaping, so the compiled
19
- # templates that need escaping call
20
- # `bf.h(...)` explicitly; `mark_raw`
21
- # only exists so runtime helpers that
22
- # already produce finished HTML --
23
- # e.g. spread_attrs -- share one
24
- # interface with the Kolon/EP ports)
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
- # ERB has no "already-safe" string wrapper the way Kolon's `mark_raw`
62
- # or Mojo::ByteStream do -- stdlib ERB's `<%=` never auto-escapes, so
63
- # there is nothing to opt out of. Identity, kept only so runtime
64
- # helpers (`spread_attrs`) share one `backend.mark_raw(...)` call
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
- return Float(t) if t =~ NUMERIC_STRING_RE
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
@@ -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?)`. Mirrors the Go/Perl `bf_slice` /
567
- # `slice` arithmetic so adapter output stays symmetric.
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 [] if len.zero?
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 [] if s >= e
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('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&#34;').gsub("'", '&#39;')
983
1088
  end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/erb",
3
- "version": "0.18.4",
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.4"
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.4",
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
+ })