@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.
- package/lib/BarefootJS/DevReload.pm +1 -1
- package/lib/BarefootJS.pm +107 -17
- package/package.json +1 -1
- package/t/helper_vectors.t +6 -0
- package/t/template_primitives.t +67 -2
package/lib/BarefootJS.pm
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
package BarefootJS;
|
|
2
|
-
our $VERSION = "0.18.
|
|
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
|
|
307
|
-
#
|
|
308
|
-
# Mojo
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
#
|
|
313
|
-
#
|
|
314
|
-
|
|
315
|
-
|
|
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?)`
|
|
804
|
-
#
|
|
805
|
-
# adapter
|
|
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
|
-
#
|
|
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
|
|
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.
|
|
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": [
|
package/t/helper_vectors.t
CHANGED
|
@@ -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(@_) },
|
package/t/template_primitives.t
CHANGED
|
@@ -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(
|
|
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.
|