@barefootjs/perl 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.
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::DevReload;
2
- our $VERSION = "0.18.3";
2
+ our $VERSION = "0.18.4";
3
3
  use strict;
4
4
  use warnings;
5
5
  use feature 'signatures';
package/lib/BarefootJS.pm CHANGED
@@ -1,5 +1,5 @@
1
1
  package BarefootJS;
2
- our $VERSION = "0.18.3";
2
+ our $VERSION = "0.18.4";
3
3
  use strict;
4
4
  use warnings;
5
5
  use utf8;
@@ -303,16 +303,17 @@ sub render_child ($self, $name, @args) {
303
303
  # Template languages whose method calls can't splat a hash into positional
304
304
  # args (Text::Xslate Kolon, Template Toolkit) pass one hashref instead.
305
305
  my %props = (@args == 1 && ref $args[0] eq 'HASH') ? %{ $args[0] } : @args;
306
- # JSX children come in via the engine's children-capture mechanism
307
- # (Mojo's `begin %>...<% end`, which produces a CODE ref returning a
308
- # Mojo::ByteStream). Materialize it through the backend before handing
309
- # the props to the child renderer so the child template sees
310
- # `$children` as already-rendered HTML. Guard on `exists` so a
311
- # childless invocation (`bf->render_child('counter')`) doesn't gain a
312
- # spurious `children => undef` key preserving the historical "only
313
- # touch children when present" behaviour.
314
- $props{children} = $self->backend->materialize($props{children})
315
- if exists $props{children};
306
+ # JSX children AND any other named JSX-valued slot (`header={<strong/>}`,
307
+ # #2168 jsx-element-prop) come in via the engine's children-capture
308
+ # mechanism (Mojo's `begin %>...<% end`, which produces a CODE ref
309
+ # returning a Mojo::ByteStream). Materialize every prop value through
310
+ # the backend before handing the props to the child renderer, so the
311
+ # child template sees each slot as already-rendered HTML rather than a
312
+ # bare CODE ref `materialize` is a no-op for a value that isn't a
313
+ # CODE ref (see e.g. `BarefootJS::Backend::Mojo::materialize`), so this
314
+ # is safe to apply unconditionally rather than naming `children`
315
+ # specifically.
316
+ $props{$_} = $self->backend->materialize($props{$_}) for keys %props;
316
317
  # Renderer contract (#1897): the renderer is invoked with TWO
317
318
  # arguments — the props hashref and the INVOKING instance. A renderer
318
319
  # registered on the root may be called from a nested child render
@@ -627,6 +628,32 @@ sub round ($self, $value) {
627
628
  return POSIX::floor($n + 0.5);
628
629
  }
629
630
 
631
+ # `Math.min(a, b)` / `Math.max(a, b)` -- two-arg forms only (#2168
632
+ # math-methods). JS returns NaN if either operand is NaN.
633
+ sub min ($self, $a, $b) {
634
+ my $x = $self->number($a);
635
+ my $y = $self->number($b);
636
+ return $x if _is_nan($x);
637
+ return $y if _is_nan($y);
638
+ return $x < $y ? $x : $y;
639
+ }
640
+
641
+ sub max ($self, $a, $b) {
642
+ my $x = $self->number($a);
643
+ my $y = $self->number($b);
644
+ return $x if _is_nan($x);
645
+ return $y if _is_nan($y);
646
+ return $x > $y ? $x : $y;
647
+ }
648
+
649
+ # `Math.abs()` (#2168 math-methods). `CORE::abs` avoids Perl's
650
+ # ambiguous-call warning against this package's own `abs` sub.
651
+ sub abs ($self, $value) {
652
+ my $n = $self->number($value);
653
+ return $n if _is_nan($n);
654
+ return CORE::abs($n);
655
+ }
656
+
630
657
  # ---------------------------------------------------------------------------
631
658
  # Array / String method helpers (#1448 Tier A)
632
659
  # ---------------------------------------------------------------------------
@@ -800,22 +827,39 @@ sub concat ($self, $a, $b) {
800
827
  return \@out;
801
828
  }
802
829
 
803
- # `Array.prototype.slice(start, end?)` carves out a sub-range
804
- # into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so
805
- # adapter output stays symmetric:
830
+ # `Array.prototype.slice(start, end?)` AND `String.prototype.slice`
831
+ # (the `string-slice` divergence) carves out a sub-range. The
832
+ # adapter emits the same call for both receiver shapes (it can't
833
+ # disambiguate string vs. array at compile time), so this dispatches
834
+ # at runtime on `ref($recv)`, mirroring `includes` above. Mirrors the
835
+ # Go `bf_slice` arithmetic so adapter output stays symmetric:
806
836
  # - start < 0 → length + start (e.g. -1 = last index)
807
837
  # - end < 0 → length + end
808
838
  # - start < 0 after clamp → 0
809
839
  # - end > length → length
810
840
  # - start >= end → empty
811
841
  # - end undef → "to length"
812
- # Non-array receivers return an empty ARRAY ref.
842
+ # String length/positions are characters (`use utf8` is active), not
843
+ # bytes. Any other receiver returns an empty ARRAY ref.
813
844
 
814
845
  sub slice ($self, $recv, $start, $end) {
846
+ if (!ref($recv) && defined $recv) {
847
+ my $len = CORE::length($recv);
848
+ my ($s, $e) = _clamp_slice_range($len, $start, $end);
849
+ return '' if $s >= $e;
850
+ return substr($recv, $s, $e - $s);
851
+ }
815
852
  return [] unless ref($recv) eq 'ARRAY';
816
853
  my $len = scalar @$recv;
817
854
  return [] if $len == 0;
818
855
 
856
+ my ($s, $e) = _clamp_slice_range($len, $start, $end);
857
+ return [] if $s >= $e;
858
+ return [ @{$recv}[$s .. $e - 1] ];
859
+ }
860
+
861
+ # Shared bounds arithmetic for both `slice` branches above.
862
+ sub _clamp_slice_range ($len, $start, $end) {
819
863
  my $s = $start // 0;
820
864
  $s = $len + $s if $s < 0;
821
865
  $s = 0 if $s < 0;
@@ -826,8 +870,7 @@ sub slice ($self, $recv, $start, $end) {
826
870
  $e = 0 if $e < 0;
827
871
  $e = $len if $e > $len;
828
872
 
829
- return [] if $s >= $e;
830
- return [ @{$recv}[$s .. $e - 1] ];
873
+ return ($s, $e);
831
874
  }
832
875
 
833
876
  # `Array.prototype.reverse()` / `Array.prototype.toReversed()` —
@@ -979,6 +1022,26 @@ sub trim ($self, $recv) {
979
1022
  return $s;
980
1023
  }
981
1024
 
1025
+ # `String.prototype.trimStart()` / `.trimEnd()` — the one-sided
1026
+ # siblings of `trim` above (#2183 follow-up), same `\s` /u regex
1027
+ # semantics restricted to one side.
1028
+
1029
+ sub trim_start ($self, $recv) {
1030
+ return '' unless defined $recv;
1031
+ return '' if ref($recv);
1032
+ my $s = "$recv";
1033
+ $s =~ s/^\s+//u;
1034
+ return $s;
1035
+ }
1036
+
1037
+ sub trim_end ($self, $recv) {
1038
+ return '' unless defined $recv;
1039
+ return '' if ref($recv);
1040
+ my $s = "$recv";
1041
+ $s =~ s/\s+$//u;
1042
+ return $s;
1043
+ }
1044
+
982
1045
  # `Number.prototype.toFixed(digits)` (#1897) — fixed-decimal string with
983
1046
  # zero-padding. JS rounds the scaled integer half toward +Infinity (the
984
1047
  # spec's "pick the larger n" tie-break), so `(2.5).toFixed(0)` is "3";
@@ -1121,6 +1184,33 @@ sub replace ($self, $recv, $pattern, $replacement) {
1121
1184
  return substr($s, 0, $i) . $n . substr($s, $i + CORE::length($o));
1122
1185
  }
1123
1186
 
1187
+ # `String.prototype.replaceAll(pattern, replacement)` — string-pattern
1188
+ # form only (#2182), replacing EVERY occurrence (the all-occurrences
1189
+ # sibling of `replace` above). Same literal-splice approach (no regex
1190
+ # metacharacters, no `$1`/`$&` interpolation) as `replace`, looped
1191
+ # forward from each match's end. An empty pattern inserts the
1192
+ # replacement at every boundary, including before the first and after
1193
+ # the last character (`"abc".replaceAll("", "X")` -> "XaXbXcX"),
1194
+ # matching JS.
1195
+
1196
+ sub replace_all ($self, $recv, $pattern, $replacement) {
1197
+ my $s = defined $recv && !ref($recv) ? "$recv" : '';
1198
+ my $o = defined $pattern ? "$pattern" : '';
1199
+ my $n = defined $replacement ? "$replacement" : '';
1200
+ return CORE::join($n, '', split(//, $s), '') if $o eq '';
1201
+ my $out = '';
1202
+ my $pos = 0;
1203
+ my $olen = CORE::length($o);
1204
+ while (1) {
1205
+ my $i = index($s, $o, $pos);
1206
+ last if $i < 0;
1207
+ $out .= substr($s, $pos, $i - $pos) . $n;
1208
+ $pos = $i + $olen;
1209
+ }
1210
+ $out .= substr($s, $pos);
1211
+ return $out;
1212
+ }
1213
+
1124
1214
  # `queryHref(base, { … })` (#2042) — build `"$base?k=v&…"` from a flat list of
1125
1215
  # (guard, key, value) triples. A pair is included iff its guard is truthy AND
1126
1216
  # its value is a non-empty string, mirroring the client `queryHref`'s `if
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/perl",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "BarefootJS engine-agnostic Perl runtime (BarefootJS.pm) — pluggable rendering backend; the shared basis for Mojolicious and other Perl framework integrations",
5
5
  "type": "module",
6
6
  "files": [
@@ -87,6 +87,9 @@ my %bindings = (
87
87
  floor => sub { $bf->floor($_[0]) },
88
88
  ceil => sub { $bf->ceil($_[0]) },
89
89
  round => sub { $bf->round($_[0]) },
90
+ min => sub { $bf->min($_[0], $_[1]) },
91
+ max => sub { $bf->max($_[0], $_[1]) },
92
+ abs => sub { $bf->abs($_[0]) },
90
93
  to_fixed => sub { $bf->to_fixed(@_) },
91
94
 
92
95
  # The Mojo renderer emits native lc()/uc(); Xslate emits $bf.lc /
@@ -95,9 +98,12 @@ my %bindings = (
95
98
  lower => sub { $bf->lc($_[0]) },
96
99
  upper => sub { $bf->uc($_[0]) },
97
100
  trim => sub { $bf->trim($_[0]) },
101
+ trim_start => sub { $bf->trim_start($_[0]) },
102
+ trim_end => sub { $bf->trim_end($_[0]) },
98
103
  starts_with => sub { $bf->starts_with(@_) },
99
104
  ends_with => sub { $bf->ends_with(@_) },
100
105
  replace => sub { $bf->replace(@_) },
106
+ replace_all => sub { $bf->replace_all(@_) },
101
107
  repeat => sub { $bf->repeat(@_) },
102
108
  pad_start => sub { $bf->pad_start(@_) },
103
109
  pad_end => sub { $bf->pad_end(@_) },
@@ -78,6 +78,28 @@ subtest 'floor / ceil / round — Math.* mirrors; propagate NaN' => sub {
78
78
  ok is_nan($bf->round('not')), 'round: NaN propagates';
79
79
  };
80
80
 
81
+ # `Math.min(a, b)` / `Math.max(a, b)` (two-arg forms only) and
82
+ # `Math.abs()` (#2168 math-methods). JS returns NaN if EITHER min/max
83
+ # operand is NaN.
84
+ subtest 'min / max / abs — Math.* mirrors; propagate NaN' => sub {
85
+ is $bf->min(3, 7), 3, 'min(3, 7) → 3';
86
+ is $bf->min(7, 3), 3, 'min(7, 3) → 3 (order-independent)';
87
+ is $bf->min(-2, -5), -5, 'min(-2, -5) → -5';
88
+ ok is_nan($bf->min('not', 5)), 'min: NaN in first arg propagates';
89
+ ok is_nan($bf->min(5, 'not')), 'min: NaN in second arg propagates';
90
+
91
+ is $bf->max(3, 7), 7, 'max(3, 7) → 7';
92
+ is $bf->max(7, 3), 7, 'max(7, 3) → 7 (order-independent)';
93
+ is $bf->max(-2, -5), -2, 'max(-2, -5) → -2';
94
+ ok is_nan($bf->max('not', 5)), 'max: NaN in first arg propagates';
95
+ ok is_nan($bf->max(5, 'not')), 'max: NaN in second arg propagates';
96
+
97
+ is $bf->abs(-7.6), 7.6, 'abs(-7.6) → 7.6';
98
+ is $bf->abs(7.6), 7.6, 'abs(7.6) → 7.6 (no-op)';
99
+ is $bf->abs(0), 0, 'abs(0) → 0';
100
+ ok is_nan($bf->abs('not')), 'abs: NaN propagates';
101
+ };
102
+
81
103
  # `Array.prototype.includes(x)` + `String.prototype.includes(sub)` lower
82
104
  # to the same `$bf->includes($recv, $elem)` shape — see #1448 Tier A.
83
105
  # The Perl helper dispatches on `ref()`: ARRAY ref scans elements with
@@ -189,7 +211,7 @@ subtest 'concat — merges two arrays into a new array ref' => sub {
189
211
  # into a new ARRAY ref (#1448 Tier A). Mirrors the Go `bf_slice`
190
212
  # JS-compat semantics: negative-index normalisation, out-of-bounds
191
213
  # clamping, `start >= end` returns empty, undef `end` means "to
192
- # length". Non-array receivers return an empty ARRAY ref.
214
+ # length". Non-array, non-string receivers return an empty ARRAY ref.
193
215
  subtest 'slice — array sub-range with negative-index + clamping' => sub {
194
216
  my $arr = ['a', 'b', 'c', 'd', 'e'];
195
217
 
@@ -213,7 +235,7 @@ subtest 'slice — array sub-range with negative-index + clamping' => sub {
213
235
  # Edge cases.
214
236
  is $bf->slice([], 0, undef), [], 'empty array → empty';
215
237
  is $bf->slice(undef, 0, undef), [], 'undef receiver → empty';
216
- is $bf->slice('scalar', 0, undef), [], 'scalar receiver → empty';
238
+ is $bf->slice({foo => 1}, 0, undef), [], 'hashref receiver → empty (not array, not string)';
217
239
 
218
240
  # Mutation isolation.
219
241
  my $src = ['a', 'b', 'c'];
@@ -222,6 +244,22 @@ subtest 'slice — array sub-range with negative-index + clamping' => sub {
222
244
  is $src, ['a', 'b', 'c'], 'source unchanged after mutating slice result';
223
245
  };
224
246
 
247
+ # `String.prototype.slice(start, end?)` — the `string-slice`
248
+ # divergence (#2182): a scalar receiver used to fall through the
249
+ # array-only branch above and return an empty ARRAY ref instead of a
250
+ # substring. Mirrors the array subtest's shape with a string receiver.
251
+ subtest 'slice — string sub-range with negative-index + clamping' => sub {
252
+ my $word = 'barefootjs';
253
+
254
+ is $bf->slice($word, 0, 4), 'bare', 'start+end carves the prefix';
255
+ is $bf->slice($word, -4, undef), 'otjs', 'negative start counts from the end';
256
+ is $bf->slice($word, 4, undef), 'footjs', 'undef end → to length';
257
+ is $bf->slice($word, 5, 2), '', 'start > end → empty string';
258
+
259
+ # Multi-byte: index by character, not byte.
260
+ is $bf->slice('héllo', 0, 2), 'hé', 'multi-byte characters count as one unit each';
261
+ };
262
+
225
263
  # `Array.prototype.reverse()` / `Array.prototype.toReversed()` —
226
264
  # both shapes share the lowering (#1448 Tier A). SSR templates
227
265
  # render a snapshot, so JS's mutate-vs-new distinction has no
@@ -267,6 +305,33 @@ subtest 'trim — strip leading + trailing whitespace' => sub {
267
305
  is $bf->trim(42), '42', 'numeric receiver stringifies';
268
306
  };
269
307
 
308
+ # `String.prototype.trimStart()` / `.trimEnd()` — the one-sided
309
+ # siblings of `trim` above (#2183). Padding BOTH sides of the test
310
+ # input so a swapped side (or a routed-through-both-sides regression)
311
+ # fails visibly.
312
+ subtest 'trim_start / trim_end — strip only their own side' => sub {
313
+ is $bf->trim_start(' padded '), 'padded ', 'trim_start leaves trailing spaces';
314
+ is $bf->trim_end(' padded '), ' padded', 'trim_end leaves leading spaces';
315
+
316
+ is $bf->trim_start("\t\nleading"), 'leading', 'trim_start strips tab + newline';
317
+ is $bf->trim_end('trailing '), 'trailing', 'trim_end strips trailing spaces';
318
+
319
+ is $bf->trim_start('no-pad'), 'no-pad', 'trim_start no-op passthrough';
320
+ is $bf->trim_end('no-pad'), 'no-pad', 'trim_end no-op passthrough';
321
+
322
+ is $bf->trim_start(' '), '', 'trim_start all whitespace → empty';
323
+ is $bf->trim_end(' '), '', 'trim_end all whitespace → empty';
324
+
325
+ is $bf->trim_start(''), '', 'trim_start empty input → empty';
326
+ is $bf->trim_end(''), '', 'trim_end empty input → empty';
327
+
328
+ # Non-string receivers.
329
+ is $bf->trim_start(undef), '', 'trim_start undef receiver → empty';
330
+ is $bf->trim_end(undef), '', 'trim_end undef receiver → empty';
331
+ is $bf->trim_start({a => 1}), '', 'trim_start hash ref receiver → empty';
332
+ is $bf->trim_end(['arr']), '', 'trim_end array ref receiver → empty';
333
+ };
334
+
270
335
  # `String.prototype.split(sep)` — string → ARRAY ref (#1448 Tier B).
271
336
  # Mirrors the Go `bf_split`: literal (quotemeta'd) separator, trailing
272
337
  # empties preserved (the `-1` limit), empty-separator char split.