@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.
- package/lib/BarefootJS/DevReload.pm +1 -1
- package/lib/BarefootJS/SearchParams.pm +71 -0
- package/lib/BarefootJS.pm +68 -5
- package/package.json +1 -1
- package/t/helper_vectors.t +6 -0
- package/t/render_child.t +59 -0
- package/t/search_params.t +58 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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 ? $
|
|
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($
|
|
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.
|
|
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": [
|
package/t/helper_vectors.t
CHANGED
|
@@ -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`,
|
package/t/render_child.t
ADDED
|
@@ -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;
|