@barefootjs/perl 0.14.0 → 0.15.1

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.13.0";
2
+ our $VERSION = "0.15.0";
3
3
  use strict;
4
4
  use warnings;
5
5
  use feature 'signatures';
@@ -0,0 +1,71 @@
1
+ package BarefootJS::SearchParams;
2
+ our $VERSION = "0.14.0";
3
+ use strict;
4
+ use warnings;
5
+ use utf8;
6
+ use feature 'signatures';
7
+ no warnings 'experimental::signatures';
8
+
9
+ # Request-scoped SSR view of the query string behind the reactive
10
+ # `searchParams()` environment signal (router v0.5, #1922). The framework
11
+ # integration builds one per request from the request URL and threads it into
12
+ # the template scope as `$searchParams` (the camelCase JS name the adapters
13
+ # keep, like every other signal/prop var); the compiled template reads it via
14
+ # `$searchParams->get('key')` (Mojo) / `$searchParams.get('key')` (Xslate).
15
+ #
16
+ # This runtime is template-engine- and framework-agnostic (core Perl only),
17
+ # matching the rest of BarefootJS.pm, so it can ship in the standalone
18
+ # @barefootjs/perl distribution.
19
+ #
20
+ # Semantics mirror the browser's URLSearchParams.get exactly under the
21
+ # adapters' `?? → //` lowering: get() returns the first value for a key, or
22
+ # `undef` when the key is absent. Perl's `//` (defined-or) coalesces only
23
+ # `undef`, so an absent key falls back to the author's default while a
24
+ # present-but-empty value (`?sort=`) keeps the empty string — the same
25
+ # distinction JS `??` draws between `null` and `''`. (This is a closer match
26
+ # than the Go adapter, whose `or` lowering also coalesces the empty string.)
27
+
28
+ # new($class, $query = '')
29
+ #
30
+ # Parse a raw query string into the reader. A leading '?' is tolerated, '+'
31
+ # decodes to a space, and %XX escapes are decoded — mirroring URLSearchParams's
32
+ # application/x-www-form-urlencoded parsing. A malformed pair never dies; it
33
+ # simply contributes nothing, matching the browser's lenient parsing.
34
+ sub new ($class, $query = '') {
35
+ $query //= '';
36
+ $query =~ s/\A\?//;
37
+ my %values;
38
+ for my $pair (split /[&;]/, $query) {
39
+ next if $pair eq '';
40
+ my ($key, $val) = split /=/, $pair, 2;
41
+ $key = _decode($key);
42
+ $val = defined $val ? _decode($val) : '';
43
+ push @{ $values{$key} }, $val;
44
+ }
45
+ return bless { values => \%values }, $class;
46
+ }
47
+
48
+ # get($self, $key)
49
+ #
50
+ # First value for $key, or `undef` when the key is absent (see the package
51
+ # docstring for why `undef` — not '' — is the right "missing" sentinel under
52
+ # the `//` lowering). A present-but-empty value returns ''.
53
+ sub get ($self, $key) {
54
+ my $vals = $self->{values}{$key};
55
+ return undef unless $vals && @$vals;
56
+ return $vals->[0];
57
+ }
58
+
59
+ sub _decode ($s) {
60
+ $s //= '';
61
+ $s =~ tr/+/ /;
62
+ # %XX → raw octet, then interpret the octet stream as UTF-8 (what
63
+ # URLSearchParams does). `utf8::decode` is a core builtin — no CPAN URI /
64
+ # URI::Escape dependency, keeping this runtime core-Perl-only. A byte run
65
+ # that isn't valid UTF-8 is left as-is rather than dying (lenient parsing).
66
+ $s =~ s/%([0-9A-Fa-f]{2})/chr hex $1/ge;
67
+ utf8::decode($s);
68
+ return $s;
69
+ }
70
+
71
+ 1;
package/lib/BarefootJS.pm CHANGED
@@ -1,5 +1,5 @@
1
1
  package BarefootJS;
2
- our $VERSION = "0.13.0";
2
+ our $VERSION = "0.15.0";
3
3
  use strict;
4
4
  use warnings;
5
5
  use utf8;
@@ -103,6 +103,22 @@ sub new ($class, $c, $config = {}) {
103
103
  return $self;
104
104
  }
105
105
 
106
+ # search_params($query = '')
107
+ #
108
+ # Build a request-scoped reader for the reactive searchParams() environment
109
+ # signal (router v0.5, #1922) from a raw query string. Callable as a class or
110
+ # instance method — the invocant is unused.
111
+ #
112
+ # The `require` lives here so consumers (the Mojo plugin, the Xslate host, the
113
+ # test harness, generated render scripts) reach BarefootJS::SearchParams through
114
+ # the BarefootJS object they already hold, never `use`-ing it directly — the
115
+ # same lazy-load seam the Mojo backend uses above. The compiled template reads
116
+ # the returned object via `$searchParams->get('key')`.
117
+ sub search_params ($invocant, $query = '') {
118
+ require BarefootJS::SearchParams;
119
+ return BarefootJS::SearchParams->new($query);
120
+ }
121
+
106
122
  # ---------------------------------------------------------------------------
107
123
  # Scope & Props
108
124
  # ---------------------------------------------------------------------------
@@ -265,6 +281,11 @@ sub register_script ($self, $path) {
265
281
  # ---------------------------------------------------------------------------
266
282
  # (`_child_renderers` accessor is generated by the minimal accessor base above.)
267
283
 
284
+ # Register a renderer for `render_child($name, ...)`. The renderer is
285
+ # invoked as `$renderer->($props_hashref, $invoking_bf)` — unpack `@_`
286
+ # (`my ($props, $caller) = @_;`) instead of declaring a one-argument
287
+ # subroutine signature, which would enforce arity and die on the second
288
+ # argument.
268
289
  sub register_child_renderer ($self, $name, $renderer) {
269
290
  $self->_child_renderers->{$name} = $renderer;
270
291
  }
@@ -287,7 +308,15 @@ sub render_child ($self, $name, @args) {
287
308
  # touch children when present" behaviour.
288
309
  $props{children} = $self->backend->materialize($props{children})
289
310
  if exists $props{children};
290
- return $renderer->(\%props);
311
+ # Renderer contract (#1897): the renderer is invoked with TWO
312
+ # arguments — the props hashref and the INVOKING instance. A renderer
313
+ # registered on the root may be called from a nested child render
314
+ # (AccordionTrigger -> ChevronDownIcon), and the grandchild's scope /
315
+ # slot identity must chain off the CALLER's scope id, not the
316
+ # registrant's. Renderers unpack `@_` (`my ($props, $caller) = @_;`)
317
+ # rather than enforcing arity with a one-arg subroutine signature —
318
+ # see `register_child_renderer`.
319
+ return $renderer->(\%props, $self);
291
320
  }
292
321
 
293
322
  # ---------------------------------------------------------------------------
@@ -345,7 +374,13 @@ sub register_components_from_manifest ($self, $manifest, %opts) {
345
374
  my $signal_init = $signal_inits->{$slot_key};
346
375
  my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults};
347
376
  $self->register_child_renderer($slot_key, sub {
348
- my ($props) = @_;
377
+ # `$caller` is the instance whose template invoked
378
+ # `render_child` (#1897) — for a nested render that is a child
379
+ # instance, and the grandchild's scope/slot identity must chain
380
+ # off ITS scope id (`root_s0_s0`), not the registrant's.
381
+ my ($props, $caller) = @_;
382
+ my $host = $caller // $parent;
383
+ my $host_scope = $host->_scope_id // $parent_scope;
349
384
  # Child shares the parent's backend so nested renders go
350
385
  # through the same engine + controller (and inherit any
351
386
  # injected json_encoder). The controller is fetched via the weak
@@ -358,16 +393,19 @@ sub register_components_from_manifest ($self, $manifest, %opts) {
358
393
  my $data_key = delete $props->{key};
359
394
  $child_bf->_data_key($data_key) if defined $data_key;
360
395
  $child_bf->_scope_id(
361
- $slot_id ? $parent_scope . '_' . $slot_id
396
+ $slot_id ? $host_scope . '_' . $slot_id
362
397
  : $template_name . '_' . substr(rand() =~ s/^0\.//r, 0, 6)
363
398
  );
364
399
  $child_bf->_is_child(1);
365
400
  # (#1249) Slot identity: host scope + slot id. Emitted as
366
401
  # bf-h / bf-m attributes by hydration_attrs.
367
402
  if ($slot_id) {
368
- $child_bf->_bf_parent($parent_scope);
403
+ $child_bf->_bf_parent($host_scope);
369
404
  $child_bf->_bf_mount($slot_id);
370
405
  }
406
+ # Share the root registry so the child's own template can
407
+ # render further imported components (#1897).
408
+ $child_bf->_child_renderers($parent->_child_renderers);
371
409
  $child_bf->_scripts($parent->_scripts);
372
410
  $child_bf->_script_seen($parent->_script_seen);
373
411
 
@@ -508,6 +546,10 @@ sub number ($self, $value) {
508
546
  # portable sentinel check in floor/ceil/round.
509
547
  sub _is_nan { my $n = shift; return $n != $n }
510
548
 
549
+ # True for +/-Infinity. `9**9**9` is Perl's portable infinity literal; a
550
+ # finite number is always strictly less than +Inf in magnitude.
551
+ sub _is_inf { my $n = shift; return $n == 9**9**9 || $n == -9**9**9 }
552
+
511
553
  sub floor ($self, $value) {
512
554
  my $n = $self->number($value);
513
555
  return $n if _is_nan($n);
@@ -826,6 +868,27 @@ sub trim ($self, $recv) {
826
868
  return $s;
827
869
  }
828
870
 
871
+ # `Number.prototype.toFixed(digits)` (#1897) — fixed-decimal string with
872
+ # zero-padding. JS rounds the scaled integer half toward +Infinity (the
873
+ # spec's "pick the larger n" tie-break), so `(2.5).toFixed(0)` is "3";
874
+ # bare `sprintf("%.*f")` would round half-to-even ("2"), diverging. Scale
875
+ # by 10**digits, round with `floor(x + 0.5)` (the same tie-break the
876
+ # `round` helper uses), then format the exact multiple. A negative
877
+ # `digits` clamps to 0, mirroring how the adapters default an omitted
878
+ # argument.
879
+ sub to_fixed ($self, $value, $digits = 0) {
880
+ my $n = $self->number($value);
881
+ # JS toFixed returns the STRINGS "NaN" / "Infinity" / "-Infinity" for
882
+ # non-finite inputs; the numeric values would stringify per-platform
883
+ # ("nan"/"inf"/...) and diverge.
884
+ return 'NaN' if _is_nan($n);
885
+ return $n < 0 ? '-Infinity' : 'Infinity' if _is_inf($n);
886
+ $digits = 0 if !defined $digits || $digits < 0;
887
+ my $factor = 10 ** $digits;
888
+ my $rounded = POSIX::floor($n * $factor + 0.5);
889
+ return sprintf('%.*f', $digits, $rounded / $factor);
890
+ }
891
+
829
892
  # `String.prototype.split(sep)` (#1448 Tier B) — string → ARRAY ref.
830
893
  #
831
894
  # Two JS-parity wrinkles drive the helper (a bare `split` emit would
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/perl",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
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": [
@@ -61,6 +61,7 @@ my %bindings = (
61
61
  floor => sub { $bf->floor($_[0]) },
62
62
  ceil => sub { $bf->ceil($_[0]) },
63
63
  round => sub { $bf->round($_[0]) },
64
+ to_fixed => sub { $bf->to_fixed(@_) },
64
65
 
65
66
  # The Mojo renderer emits native lc()/uc(); Xslate emits $bf.lc /
66
67
  # $bf.uc. The helper methods wrap CORE::lc/uc, so binding them
@@ -93,6 +94,11 @@ my %bindings = (
93
94
  # Mirrors the Mojo inline `[grep { $_ } @{...}]` for filter(Boolean).
94
95
  filter_truthy => sub { [grep { $_ } @{ $_[0] }] },
95
96
 
97
+ # searchParams().get(key) (#1922) via the lazy factory consumers use.
98
+ # No divergence entry: get() returns undef for an absent key (~ JS null)
99
+ # and '' for present-but-empty, so the Perl backend matches JS exactly.
100
+ search_params_get => sub { BarefootJS->search_params($_[0])->get($_[1]) },
101
+
96
102
  # Higher-order entries arrive in the canonical projection form
97
103
  # (spec: items + field [+ value]); the closures below rebuild the
98
104
  # predicate the adapters compile (`i => i.field === value`,
@@ -0,0 +1,59 @@
1
+ use strict;
2
+ use warnings;
3
+ use utf8;
4
+
5
+ use Test2::V0;
6
+
7
+ use lib 'lib';
8
+ use BarefootJS;
9
+
10
+ # A do-nothing backend: render_child only touches the backend to
11
+ # materialize a `children` prop, which these cases never pass — but
12
+ # `new` eagerly builds the default Mojo backend, which isn't a dependency
13
+ # of this dist's test environment.
14
+ {
15
+ package StubBackend;
16
+ sub new { bless {}, shift }
17
+ sub materialize { $_[1] }
18
+ }
19
+ sub new_bf { BarefootJS->new(undef, { backend => StubBackend->new }) }
20
+
21
+ # render_child renderer-invocation contract (#1897): the renderer is
22
+ # invoked with ($props_hashref, $invoking_bf) so nested renders can chain
23
+ # scope/slot identity off the caller. Renderers unpack @_ rather than
24
+ # enforcing arity with a one-arg subroutine signature (see
25
+ # register_child_renderer's doc).
26
+
27
+ subtest 'renderer receives the invoking instance' => sub {
28
+ my $bf = new_bf();
29
+ $bf->_scope_id('Root_test');
30
+
31
+ my ($seen_props, $seen_caller);
32
+ $bf->register_child_renderer('probe', sub {
33
+ my ($props, $caller) = @_;
34
+ ($seen_props, $seen_caller) = ($props, $caller);
35
+ return 'ok';
36
+ });
37
+
38
+ is $bf->render_child('probe', value => 1), 'ok', 'renderer output returned';
39
+ is $seen_props->{value}, 1, 'props forwarded';
40
+ ref_is $seen_caller, $bf, 'second argument is the invoking instance';
41
+
42
+ # A nested invocation from a different instance passes THAT instance.
43
+ my $child = new_bf();
44
+ $child->_scope_id('Root_test_s0');
45
+ $child->_child_renderers($bf->_child_renderers);
46
+ $child->render_child('probe');
47
+ ref_is $seen_caller, $child, 'nested call passes the nested instance';
48
+ };
49
+
50
+ subtest 'renderer exceptions propagate' => sub {
51
+ my $bf = new_bf();
52
+ $bf->register_child_renderer('boom', sub {
53
+ die "renderer exploded\n";
54
+ });
55
+ like dies { $bf->render_child('boom') }, qr/renderer exploded/,
56
+ 'renderer errors propagate to the caller';
57
+ };
58
+
59
+ done_testing;
@@ -0,0 +1,58 @@
1
+ use Test2::V0;
2
+ use utf8;
3
+
4
+ # BarefootJS::SearchParams — Perl-specific concerns of the searchParams()
5
+ # environment-signal reader (router v0.5, #1922).
6
+ #
7
+ # The cross-language VALUE semantics of `get` (absent → null/undef, present →
8
+ # first value, repeated keys, present-but-empty, `+`/`%XX`/encoded-separator
9
+ # decoding, first-`=` split) are owned by the language-independent golden
10
+ # vectors — `search_params_get` in packages/adapter-tests/helper-vectors,
11
+ # asserted here via t/helper_vectors.t and in the Go runtime's vectors_test.go,
12
+ # so Go/Perl/JS parity is mechanical. This file covers only what those value
13
+ # vectors can't: the lazy-load factory seam, lenient parsing (never dies), the
14
+ # `//` composition, and UTF-8 decoding.
15
+
16
+ use FindBin qw($Bin);
17
+ use lib "$Bin/../lib";
18
+
19
+ use BarefootJS;
20
+
21
+ # The lazy factory on the BarefootJS object is how every consumer (Mojo plugin,
22
+ # Xslate host, render harness) reaches the reader — no one `use`s the class
23
+ # directly. Assert it loads + builds a working reader.
24
+ subtest 'BarefootJS->search_params lazy factory' => sub {
25
+ my $sp = BarefootJS->search_params('sort=price');
26
+ is ref($sp), 'BarefootJS::SearchParams', 'factory returns a reader instance';
27
+ is $sp->get('sort'), 'price', 'factory-built reader resolves the query';
28
+ is ref(BarefootJS->search_params), 'BarefootJS::SearchParams', 'default empty query';
29
+ };
30
+
31
+ # The adapters lower `searchParams().get(k) ?? d` to Perl's defined-or
32
+ # (`$searchParams->get(k) // d`), which coalesces only undef — so an absent key
33
+ # falls back to the default while a present-but-empty value keeps ''. This is
34
+ # the Perl-specific divergence from the Go `or` lowering (which coalesces '').
35
+ subtest '// composition (the ?? lowering) coalesces only undef' => sub {
36
+ my $absent = BarefootJS->search_params('other=x');
37
+ is(($absent->get('sort') // 'none'), 'none', 'absent key → author default');
38
+
39
+ my $empty = BarefootJS->search_params('sort=');
40
+ is(($empty->get('sort') // 'none'), '', 'present-but-empty value is kept, NOT defaulted');
41
+ };
42
+
43
+ # Percent-encoded UTF-8 decodes to characters via the core `utf8::decode`
44
+ # builtin (no URI / URI::Escape dependency) — kept here rather than in the
45
+ # ASCII-only shared vectors to avoid cross-harness byte/char encoding skew.
46
+ subtest 'UTF-8 percent-decoding (core utf8::decode, no URI dep)' => sub {
47
+ my $sp = BarefootJS->search_params('q=%E2%9C%93');
48
+ is $sp->get('q'), "\x{2713}", 'percent-encoded UTF-8 → decoded character (✓)';
49
+ };
50
+
51
+ # Malformed input must degrade, never die (SSR survives junk query strings).
52
+ subtest 'lenient parsing never dies' => sub {
53
+ ok lives { BarefootJS->search_params(undef) }, 'undef query';
54
+ ok lives { BarefootJS->search_params('&&&')->get('x') }, 'only separators';
55
+ ok lives { BarefootJS->search_params('=novalue')->get('x') }, 'empty key pair';
56
+ };
57
+
58
+ done_testing;