| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package BarefootJS; |
|
2
|
|
|
|
|
|
|
our $VERSION = "0.16.0"; |
|
3
|
5
|
|
|
5
|
|
772256
|
use strict; |
|
|
5
|
|
|
|
|
8
|
|
|
|
5
|
|
|
|
|
141
|
|
|
4
|
5
|
|
|
5
|
|
26
|
use warnings; |
|
|
5
|
|
|
|
|
14
|
|
|
|
5
|
|
|
|
|
168
|
|
|
5
|
5
|
|
|
5
|
|
420
|
use utf8; |
|
|
5
|
|
|
|
|
195
|
|
|
|
5
|
|
|
|
|
21
|
|
|
6
|
5
|
|
|
5
|
|
1780
|
use feature 'signatures'; |
|
|
5
|
|
|
|
|
7
|
|
|
|
5
|
|
|
|
|
590
|
|
|
7
|
5
|
|
|
5
|
|
18
|
no warnings 'experimental::signatures'; |
|
|
5
|
|
|
|
|
8
|
|
|
|
5
|
|
|
|
|
165
|
|
|
8
|
|
|
|
|
|
|
|
|
9
|
5
|
|
|
5
|
|
2233
|
use POSIX (); |
|
|
5
|
|
|
|
|
29771
|
|
|
|
5
|
|
|
|
|
223
|
|
|
10
|
5
|
|
|
5
|
|
37
|
use Scalar::Util qw(looks_like_number weaken); |
|
|
5
|
|
|
|
|
14
|
|
|
|
5
|
|
|
|
|
759
|
|
|
11
|
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
# NOTE: This runtime is template-engine-agnostic AND framework-agnostic by |
|
13
|
|
|
|
|
|
|
# design, so it can ship as a standalone CPAN distribution. It depends only on |
|
14
|
|
|
|
|
|
|
# core Perl (subroutine signatures + the hand-rolled minimal accessor base |
|
15
|
|
|
|
|
|
|
# below — no Mojo::Base, no Class::Tiny). Every operation that depends on *how* |
|
16
|
|
|
|
|
|
|
# a template is rendered — JSON marshalling, raw-string marking, JSX-children |
|
17
|
|
|
|
|
|
|
# materialisation, and named-template rendering — is delegated to a pluggable |
|
18
|
|
|
|
|
|
|
# `backend` (see BarefootJS::Backend::Mojo for the reference Mojolicious |
|
19
|
|
|
|
|
|
|
# implementation), which is the only component that pulls in the Mojo |
|
20
|
|
|
|
|
|
|
# distribution, and only when it is actually used. |
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
23
|
|
|
|
|
|
|
# Minimal accessor base (no Mojo::Base / Class::Tiny dependency) |
|
24
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
25
|
|
|
|
|
|
|
# |
|
26
|
|
|
|
|
|
|
# Generates read/write accessors with optional lazy defaults so the runtime |
|
27
|
|
|
|
|
|
|
# stays free of any non-core OO base. Semantics mirror the Mojo::Base `has` |
|
28
|
|
|
|
|
|
|
# this class used to inherit: a getter returns the stored value (building it |
|
29
|
|
|
|
|
|
|
# from the default on first access if unset); a setter stores the value and |
|
30
|
|
|
|
|
|
|
# returns $self for chaining. A default is either a plain scalar or a coderef |
|
31
|
|
|
|
|
|
|
# invoked as `$default->($self)` (for per-instance refs like `[]` / `{}` and |
|
32
|
|
|
|
|
|
|
# the lazily-required Mojo backend). |
|
33
|
|
|
|
|
|
|
my %ATTR_DEFAULT = ( |
|
34
|
|
|
|
|
|
|
_scripts => sub { [] }, |
|
35
|
|
|
|
|
|
|
_script_seen => sub { {} }, |
|
36
|
|
|
|
|
|
|
_child_renderers => sub { {} }, |
|
37
|
|
|
|
|
|
|
_is_child => 0, |
|
38
|
|
|
|
|
|
|
# Lazily fall back to the Mojo reference backend so a bare-blessed |
|
39
|
|
|
|
|
|
|
# instance (the pure-function unit tests) and the historical |
|
40
|
|
|
|
|
|
|
# `BarefootJS->new($c, ...)` callers keep working unchanged. A non-Mojo |
|
41
|
|
|
|
|
|
|
# host injects its own backend via `BarefootJS->new($c, { backend => $b })` |
|
42
|
|
|
|
|
|
|
# and never triggers this require — keeping the core load Mojo-free. |
|
43
|
|
|
|
|
|
|
backend => sub { |
|
44
|
|
|
|
|
|
|
require BarefootJS::Backend::Mojo; |
|
45
|
|
|
|
|
|
|
return BarefootJS::Backend::Mojo->new; |
|
46
|
|
|
|
|
|
|
}, |
|
47
|
|
|
|
|
|
|
); |
|
48
|
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
# c — Mojolicious controller (kept for back-compat accessors) |
|
50
|
|
|
|
|
|
|
# config — plugin / instance config |
|
51
|
|
|
|
|
|
|
# backend — the template-engine seam (#engine-abstraction) |
|
52
|
|
|
|
|
|
|
# _scope_id — addressable scope id |
|
53
|
|
|
|
|
|
|
# _bf_parent / _bf_mount — slot identity when this scope is slot-attached |
|
54
|
|
|
|
|
|
|
# _props — props serialised into bf-p / the scope comment |
|
55
|
|
|
|
|
|
|
# _data_key — keyed-loop-item key, emitted as data-key on the scope root |
|
56
|
|
|
|
|
|
|
for my $attr (qw( |
|
57
|
|
|
|
|
|
|
c config backend |
|
58
|
|
|
|
|
|
|
_scripts _script_seen _scope_id _is_child _bf_parent _bf_mount _props |
|
59
|
|
|
|
|
|
|
_data_key _child_renderers |
|
60
|
|
|
|
|
|
|
)) { |
|
61
|
5
|
|
|
5
|
|
25
|
no strict 'refs'; |
|
|
5
|
|
|
|
|
10
|
|
|
|
5
|
|
|
|
|
33413
|
|
|
62
|
|
|
|
|
|
|
*{"BarefootJS::$attr"} = sub { |
|
63
|
29
|
|
|
29
|
|
51
|
my $self = shift; |
|
64
|
29
|
100
|
|
|
|
49
|
if (@_) { $self->{$attr} = shift; return $self; } |
|
|
1
|
|
|
|
|
4
|
|
|
|
1
|
|
|
|
|
3
|
|
|
65
|
28
|
50
|
66
|
|
|
59
|
if (!exists $self->{$attr} && exists $ATTR_DEFAULT{$attr}) { |
|
66
|
1
|
|
|
|
|
2
|
my $d = $ATTR_DEFAULT{$attr}; |
|
67
|
1
|
50
|
|
|
|
11
|
$self->{$attr} = ref($d) eq 'CODE' ? $d->($self) : $d; |
|
68
|
|
|
|
|
|
|
} |
|
69
|
28
|
|
|
|
|
88
|
return $self->{$attr}; |
|
70
|
|
|
|
|
|
|
}; |
|
71
|
|
|
|
|
|
|
} |
|
72
|
|
|
|
|
|
|
|
|
73
|
1
|
|
|
1
|
0
|
145665
|
sub new ($class, $c, $config = {}) { |
|
|
1
|
|
|
|
|
7
|
|
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
3
|
|
|
74
|
|
|
|
|
|
|
# Build (or accept an injected) rendering backend. The default Mojo |
|
75
|
|
|
|
|
|
|
# backend wraps the controller and honours an optional `json_encoder` |
|
76
|
|
|
|
|
|
|
# override so a host can swap in a faster XS JSON implementation |
|
77
|
|
|
|
|
|
|
# without subclassing. A caller targeting another template engine |
|
78
|
|
|
|
|
|
|
# passes its own backend via `$config->{backend}`. |
|
79
|
1
|
|
|
|
|
3
|
my $backend = $config->{backend}; |
|
80
|
1
|
50
|
|
|
|
7
|
unless ($backend) { |
|
81
|
0
|
|
|
|
|
0
|
require BarefootJS::Backend::Mojo; |
|
82
|
|
|
|
|
|
|
$backend = BarefootJS::Backend::Mojo->new( |
|
83
|
|
|
|
|
|
|
c => $c, |
|
84
|
|
|
|
|
|
|
($config->{json_encoder} |
|
85
|
|
|
|
|
|
|
? (json_encoder => $config->{json_encoder}) |
|
86
|
0
|
0
|
|
|
|
0
|
: ()), |
|
87
|
|
|
|
|
|
|
); |
|
88
|
|
|
|
|
|
|
} |
|
89
|
1
|
|
|
|
|
6
|
my $self = bless { |
|
90
|
|
|
|
|
|
|
c => $c, |
|
91
|
|
|
|
|
|
|
config => $config, |
|
92
|
|
|
|
|
|
|
backend => $backend, |
|
93
|
|
|
|
|
|
|
}, $class; |
|
94
|
|
|
|
|
|
|
# Hold the controller weakly. Mojolicious stashes this bf instance under |
|
95
|
|
|
|
|
|
|
# `$c->stash->{'bf.instance'}`, so a strong bf -> controller back-reference |
|
96
|
|
|
|
|
|
|
# closes a per-request cycle ($c -> stash -> bf -> $c) that Perl's |
|
97
|
|
|
|
|
|
|
# refcount GC cannot reclaim, leaking one controller + bf + child-renderer |
|
98
|
|
|
|
|
|
|
# closures per request. The controller owns (outlives) the per-request bf, |
|
99
|
|
|
|
|
|
|
# so the weak ref stays valid for the whole render. Callers that need the |
|
100
|
|
|
|
|
|
|
# controller to outlive the bf instance independently must keep their own |
|
101
|
|
|
|
|
|
|
# strong reference (the normal Mojo request scope already does). |
|
102
|
1
|
50
|
|
|
|
8
|
weaken($self->{c}) if defined $c; |
|
103
|
1
|
|
|
|
|
9
|
return $self; |
|
104
|
|
|
|
|
|
|
} |
|
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
|
8
|
|
|
8
|
0
|
176307
|
sub search_params ($invocant, $query = '') { |
|
|
8
|
|
|
|
|
10
|
|
|
|
8
|
|
|
|
|
9
|
|
|
|
8
|
|
|
|
|
9
|
|
|
118
|
8
|
|
|
|
|
595
|
require BarefootJS::SearchParams; |
|
119
|
8
|
|
|
|
|
21
|
return BarefootJS::SearchParams->new($query); |
|
120
|
|
|
|
|
|
|
} |
|
121
|
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
123
|
|
|
|
|
|
|
# Scope & Props |
|
124
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
125
|
|
|
|
|
|
|
|
|
126
|
0
|
|
|
0
|
0
|
0
|
sub scope_attr ($self) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
127
|
|
|
|
|
|
|
# bf-s is the addressable scope id only (#1249). |
|
128
|
0
|
|
0
|
|
|
0
|
return $self->_scope_id // ''; |
|
129
|
|
|
|
|
|
|
} |
|
130
|
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
# Emits `bf-h="" bf-m="" bf-r=""` conditionally. |
|
132
|
|
|
|
|
|
|
# See spec/compiler.md "Slot identity". |
|
133
|
0
|
|
|
0
|
0
|
0
|
sub hydration_attrs ($self) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
134
|
0
|
|
|
|
|
0
|
my @parts; |
|
135
|
0
|
|
|
|
|
0
|
my $host = $self->_bf_parent; |
|
136
|
0
|
|
|
|
|
0
|
my $mount = $self->_bf_mount; |
|
137
|
0
|
0
|
0
|
|
|
0
|
if (defined $host && length $host) { |
|
138
|
0
|
|
|
|
|
0
|
my $h = $host =~ s/"/"/gr; |
|
139
|
0
|
|
|
|
|
0
|
push @parts, qq{bf-h="$h"}; |
|
140
|
|
|
|
|
|
|
} |
|
141
|
0
|
0
|
0
|
|
|
0
|
if (defined $mount && length $mount) { |
|
142
|
0
|
|
|
|
|
0
|
my $m = $mount =~ s/"/"/gr; |
|
143
|
0
|
|
|
|
|
0
|
push @parts, qq{bf-m="$m"}; |
|
144
|
|
|
|
|
|
|
} |
|
145
|
0
|
0
|
|
|
|
0
|
unless ($self->_is_child) { |
|
146
|
0
|
|
|
|
|
0
|
push @parts, q{bf-r=""}; |
|
147
|
|
|
|
|
|
|
} |
|
148
|
0
|
|
|
|
|
0
|
return join(' ', @parts); |
|
149
|
|
|
|
|
|
|
} |
|
150
|
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
# Emits ` data-key=""` for a keyed loop item, else ''. The client |
|
152
|
|
|
|
|
|
|
# runtime uses data-key for list reconciliation; SSR must match the Hono |
|
153
|
|
|
|
|
|
|
# reference, which stamps it on each loop item's scope root. The value is set |
|
154
|
|
|
|
|
|
|
# on the child instance by the child renderer (`register_child_renderer` / |
|
155
|
|
|
|
|
|
|
# `register_components_from_manifest`) from the JSX `key` prop — a reserved |
|
156
|
|
|
|
|
|
|
# prop, never a real template variable. |
|
157
|
0
|
|
|
0
|
0
|
0
|
sub data_key_attr ($self) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
158
|
0
|
|
|
|
|
0
|
my $k = $self->_data_key; |
|
159
|
0
|
0
|
|
|
|
0
|
return '' unless defined $k; |
|
160
|
0
|
|
|
|
|
0
|
$k =~ s/&/&/g; |
|
161
|
0
|
|
|
|
|
0
|
$k =~ s/"/"/g; |
|
162
|
0
|
|
|
|
|
0
|
return qq{ data-key="$k"}; |
|
163
|
|
|
|
|
|
|
} |
|
164
|
|
|
|
|
|
|
|
|
165
|
0
|
|
|
0
|
0
|
0
|
sub props_attr ($self) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
166
|
0
|
|
|
|
|
0
|
my $props = $self->_props; |
|
167
|
0
|
0
|
0
|
|
|
0
|
return '' unless $props && %$props; |
|
168
|
|
|
|
|
|
|
# encode_json returns a character string (not bytes) for safe embedding |
|
169
|
|
|
|
|
|
|
# in templates (the Mojo backend uses Mojo::JSON::to_json). |
|
170
|
0
|
|
|
|
|
0
|
my $json = $self->backend->encode_json($props); |
|
171
|
0
|
|
|
|
|
0
|
return qq{ bf-p='$json'}; |
|
172
|
|
|
|
|
|
|
} |
|
173
|
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
175
|
|
|
|
|
|
|
# Context (SSR mirror of the client `provideContext` / `useContext`) |
|
176
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
177
|
|
|
|
|
|
|
# |
|
178
|
|
|
|
|
|
|
# A `` seeds a value that descendant `useContext(Ctx)` |
|
179
|
|
|
|
|
|
|
# consumers read during the same render. Dynamic scoping mirrors the client: |
|
180
|
|
|
|
|
|
|
# the provider pushes the value before rendering its children and pops it |
|
181
|
|
|
|
|
|
|
# after, and `use_context` reads the innermost active value (or the |
|
182
|
|
|
|
|
|
|
# `createContext` default when none is active). |
|
183
|
|
|
|
|
|
|
# |
|
184
|
|
|
|
|
|
|
# The value stacks live in a package-level store rather than per-instance or |
|
185
|
|
|
|
|
|
|
# on `$c->stash`: a parent template and the child templates it renders via |
|
186
|
|
|
|
|
|
|
# `render_child` are separate bf instances that don't reliably share a |
|
187
|
|
|
|
|
|
|
# controller (the Xslate backend runs with `c => undef`) nor a backend (the |
|
188
|
|
|
|
|
|
|
# Mojo path lazily builds one per instance). SSR rendering is synchronous — |
|
189
|
|
|
|
|
|
|
# nothing awaits between a provider's push and its matching pop — and the |
|
190
|
|
|
|
|
|
|
# push/pop are perfectly balanced, so the per-name stack always unwinds to |
|
191
|
|
|
|
|
|
|
# empty at the end of each provider subtree, keeping concurrent root renders |
|
192
|
|
|
|
|
|
|
# isolated. provide/revoke return '' so they drop cleanly into an inline |
|
193
|
|
|
|
|
|
|
# `<: … :>` (Kolon) or `% … ;` (EP) emit. |
|
194
|
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
my %CONTEXT_STACKS; |
|
196
|
|
|
|
|
|
|
|
|
197
|
0
|
|
|
0
|
0
|
0
|
sub provide_context ($self, $name, $value) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
198
|
0
|
|
0
|
|
|
0
|
push @{ $CONTEXT_STACKS{$name} //= [] }, $value; |
|
|
0
|
|
|
|
|
0
|
|
|
199
|
0
|
|
|
|
|
0
|
return ''; |
|
200
|
|
|
|
|
|
|
} |
|
201
|
|
|
|
|
|
|
|
|
202
|
0
|
|
|
0
|
0
|
0
|
sub revoke_context ($self, $name) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
203
|
0
|
0
|
0
|
|
|
0
|
pop @{ $CONTEXT_STACKS{$name} } if $CONTEXT_STACKS{$name} && @{ $CONTEXT_STACKS{$name} }; |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
204
|
0
|
|
|
|
|
0
|
return ''; |
|
205
|
|
|
|
|
|
|
} |
|
206
|
|
|
|
|
|
|
|
|
207
|
0
|
|
|
0
|
0
|
0
|
sub use_context ($self, $name, $default = undef) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
208
|
0
|
|
|
|
|
0
|
my $stack = $CONTEXT_STACKS{$name}; |
|
209
|
0
|
0
|
0
|
|
|
0
|
return $default unless $stack && @$stack; |
|
210
|
0
|
|
|
|
|
0
|
return $stack->[-1]; |
|
211
|
|
|
|
|
|
|
} |
|
212
|
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
214
|
|
|
|
|
|
|
# Comment Markers |
|
215
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
216
|
|
|
|
|
|
|
|
|
217
|
0
|
|
|
0
|
0
|
0
|
sub comment ($self, $text) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
218
|
0
|
|
|
|
|
0
|
return ""; |
|
219
|
|
|
|
|
|
|
} |
|
220
|
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
222
|
|
|
|
|
|
|
# JS-equivalent value stringification |
|
223
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
224
|
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
# Map a Perl boolean-shaped value to the JS `String(bool)` form. |
|
226
|
|
|
|
|
|
|
# Used by the Mojo adapter when emitting reactive attribute bindings |
|
227
|
|
|
|
|
|
|
# whose JS source `isBooleanResultExpr` classified as boolean — |
|
228
|
|
|
|
|
|
|
# a comparison (`count() > 0`), a logical negation (`!ok()`), or a |
|
229
|
|
|
|
|
|
|
# literal `true` / `false`. Perl's auto-stringification of those |
|
230
|
|
|
|
|
|
|
# expressions yields `''` / `1`; Hono and Go emit `'false'` / `'true'`. |
|
231
|
|
|
|
|
|
|
# Centralising the bool → string mapping here keeps the contract |
|
232
|
|
|
|
|
|
|
# testable and the template-emit syntax tidy |
|
233
|
|
|
|
|
|
|
# (`<%= bf->bool_str(...) %>` vs an inline ternary). |
|
234
|
|
|
|
|
|
|
# |
|
235
|
|
|
|
|
|
|
# Contract is boolean-only: callers must have classified the |
|
236
|
|
|
|
|
|
|
# expression as boolean-result before routing through this helper. |
|
237
|
|
|
|
|
|
|
# Non-boolean values reaching here will be Perl-truthy-coerced to |
|
238
|
|
|
|
|
|
|
# 'true' / 'false', which is generally wrong — non-boolean attribute |
|
239
|
|
|
|
|
|
|
# bindings stay on the plain `<%= expr %>` emit path and never reach |
|
240
|
|
|
|
|
|
|
# this function. |
|
241
|
0
|
|
|
0
|
0
|
0
|
sub bool_str ($self, $value) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
242
|
0
|
0
|
|
|
|
0
|
return $value ? 'true' : 'false'; |
|
243
|
|
|
|
|
|
|
} |
|
244
|
|
|
|
|
|
|
|
|
245
|
0
|
|
|
0
|
0
|
0
|
sub text_start ($self, $slot_id) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
246
|
0
|
|
|
|
|
0
|
return ""; |
|
247
|
|
|
|
|
|
|
} |
|
248
|
|
|
|
|
|
|
|
|
249
|
0
|
|
|
0
|
0
|
0
|
sub text_end ($self) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
250
|
0
|
|
|
|
|
0
|
return ""; |
|
251
|
|
|
|
|
|
|
} |
|
252
|
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
# See spec/compiler.md "Slot identity" for the comment-scope wire format. |
|
254
|
0
|
|
|
0
|
0
|
0
|
sub scope_comment ($self) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
255
|
0
|
|
0
|
|
|
0
|
my $scope_id = $self->_scope_id // ''; |
|
256
|
0
|
|
|
|
|
0
|
my $host_segment = ''; |
|
257
|
0
|
|
|
|
|
0
|
my $host = $self->_bf_parent; |
|
258
|
0
|
|
|
|
|
0
|
my $mount = $self->_bf_mount; |
|
259
|
0
|
0
|
0
|
|
|
0
|
if (defined $host && length $host) { |
|
260
|
0
|
|
0
|
|
|
0
|
$host_segment = "|h=$host|m=" . ($mount // ''); |
|
261
|
|
|
|
|
|
|
} |
|
262
|
0
|
|
|
|
|
0
|
my $props_json = ''; |
|
263
|
0
|
0
|
0
|
|
|
0
|
if ($self->_props && %{$self->_props}) { |
|
|
0
|
|
|
|
|
0
|
|
|
264
|
0
|
|
|
|
|
0
|
$props_json = '|' . $self->backend->encode_json($self->_props); |
|
265
|
|
|
|
|
|
|
} |
|
266
|
0
|
|
|
|
|
0
|
return ""; |
|
267
|
|
|
|
|
|
|
} |
|
268
|
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
270
|
|
|
|
|
|
|
# Script Registration |
|
271
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
272
|
|
|
|
|
|
|
|
|
273
|
0
|
|
|
0
|
0
|
0
|
sub register_script ($self, $path) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
274
|
0
|
0
|
|
|
|
0
|
return if $self->_script_seen->{$path}; |
|
275
|
0
|
|
|
|
|
0
|
$self->_script_seen->{$path} = 1; |
|
276
|
0
|
|
|
|
|
0
|
push @{$self->_scripts}, $path; |
|
|
0
|
|
|
|
|
0
|
|
|
277
|
|
|
|
|
|
|
} |
|
278
|
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
280
|
|
|
|
|
|
|
# Child Component Rendering |
|
281
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
282
|
|
|
|
|
|
|
# (`_child_renderers` accessor is generated by the minimal accessor base above.) |
|
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. |
|
289
|
1
|
|
|
1
|
0
|
2
|
sub register_child_renderer ($self, $name, $renderer) { |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
0
|
|
|
290
|
1
|
|
|
|
|
3
|
$self->_child_renderers->{$name} = $renderer; |
|
291
|
|
|
|
|
|
|
} |
|
292
|
|
|
|
|
|
|
|
|
293
|
0
|
|
|
0
|
0
|
0
|
sub render_child ($self, $name, @args) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
294
|
0
|
|
|
|
|
0
|
my $renderer = $self->_child_renderers->{$name}; |
|
295
|
0
|
0
|
|
|
|
0
|
die "No renderer registered for child component '$name'" unless $renderer; |
|
296
|
|
|
|
|
|
|
# Accept both the Mojo list form — `bf->render_child($name, k => v, ...)` |
|
297
|
|
|
|
|
|
|
# — and the single-hashref form — `$bf.render_child($name, { k => v })`. |
|
298
|
|
|
|
|
|
|
# Template languages whose method calls can't splat a hash into positional |
|
299
|
|
|
|
|
|
|
# args (Text::Xslate Kolon, Template Toolkit) pass one hashref instead. |
|
300
|
0
|
0
|
0
|
|
|
0
|
my %props = (@args == 1 && ref $args[0] eq 'HASH') ? %{ $args[0] } : @args; |
|
|
0
|
|
|
|
|
0
|
|
|
301
|
|
|
|
|
|
|
# JSX children come in via the engine's children-capture mechanism |
|
302
|
|
|
|
|
|
|
# (Mojo's `begin %>...<% end`, which produces a CODE ref returning a |
|
303
|
|
|
|
|
|
|
# Mojo::ByteStream). Materialize it through the backend before handing |
|
304
|
|
|
|
|
|
|
# the props to the child renderer so the child template sees |
|
305
|
|
|
|
|
|
|
# `$children` as already-rendered HTML. Guard on `exists` so a |
|
306
|
|
|
|
|
|
|
# childless invocation (`bf->render_child('counter')`) doesn't gain a |
|
307
|
|
|
|
|
|
|
# spurious `children => undef` key — preserving the historical "only |
|
308
|
|
|
|
|
|
|
# touch children when present" behaviour. |
|
309
|
|
|
|
|
|
|
$props{children} = $self->backend->materialize($props{children}) |
|
310
|
0
|
0
|
|
|
|
0
|
if exists $props{children}; |
|
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
|
0
|
|
|
|
|
0
|
return $renderer->(\%props, $self); |
|
320
|
|
|
|
|
|
|
} |
|
321
|
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
323
|
|
|
|
|
|
|
# Bulk registration from build manifest |
|
324
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
325
|
|
|
|
|
|
|
# |
|
326
|
|
|
|
|
|
|
# `bf build` emits dist/templates/manifest.json describing every |
|
327
|
|
|
|
|
|
|
# component the page might invoke (Counter, ui/button/index, ...). |
|
328
|
|
|
|
|
|
|
# This helper walks that manifest and registers one child renderer per |
|
329
|
|
|
|
|
|
|
# UI registry entry — the path shape `ui//index` maps to the |
|
330
|
|
|
|
|
|
|
# `` slot key Counter.html.ep and friends use via |
|
331
|
|
|
|
|
|
|
# `<%= bf->render_child('', ...) %>`. |
|
332
|
|
|
|
|
|
|
# |
|
333
|
|
|
|
|
|
|
# Each manifest entry carries an `ssrDefaults` hash derived statically |
|
334
|
|
|
|
|
|
|
# from the component's JSX (prop destructure defaults + signal / |
|
335
|
|
|
|
|
|
|
# memo initial values, see packages/jsx/src/ssr-defaults.ts). The |
|
336
|
|
|
|
|
|
|
# child renderer seeds every template variable from that hash, |
|
337
|
|
|
|
|
|
|
# preferring the caller's matching prop where one exists. This |
|
338
|
|
|
|
|
|
|
# replaces the per-component `signal_init` callback that every |
|
339
|
|
|
|
|
|
|
# scaffold's `app.pl` used to hand-roll for items 1/3 of issue #1416. |
|
340
|
|
|
|
|
|
|
# |
|
341
|
|
|
|
|
|
|
# `signal_init` remains as an opt-in override for cases the static |
|
342
|
|
|
|
|
|
|
# extractor can't see through (e.g. signal initial values that |
|
343
|
|
|
|
|
|
|
# reference imported helpers). When supplied for a given slot key |
|
344
|
|
|
|
|
|
|
# it takes precedence over the manifest's `ssrDefaults` for that |
|
345
|
|
|
|
|
|
|
# child, allowing callers to mix manual overrides with auto-derived |
|
346
|
|
|
|
|
|
|
# defaults for siblings. |
|
347
|
1
|
|
|
1
|
0
|
17
|
sub register_components_from_manifest ($self, $manifest, %opts) { |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
2
|
|
|
348
|
1
|
|
50
|
|
|
19
|
my $signal_inits = $opts{signal_init} // {}; |
|
349
|
1
|
|
|
|
|
2
|
my $parent_scope = $self->_scope_id; |
|
350
|
|
|
|
|
|
|
# Weaken the parent capture so the child-renderer closures stored on |
|
351
|
|
|
|
|
|
|
# `$self->_child_renderers` don't keep `$self` alive (the direct |
|
352
|
|
|
|
|
|
|
# closure <-> parent cycle). The controller is reached through `$parent` |
|
353
|
|
|
|
|
|
|
# at call time rather than captured strongly here, so the closures hold |
|
354
|
|
|
|
|
|
|
# no strong reference to `$c` either — see the controller-cycle note in |
|
355
|
|
|
|
|
|
|
# `new`. `$parent` is always live whenever a closure runs (the closure is |
|
356
|
|
|
|
|
|
|
# stored on `$parent`, so `$parent` outlives every invocation). |
|
357
|
1
|
|
|
|
|
2
|
weaken(my $parent = $self); |
|
358
|
|
|
|
|
|
|
|
|
359
|
1
|
|
|
|
|
3
|
for my $entry_name (keys %$manifest) { |
|
360
|
|
|
|
|
|
|
# `__barefoot__` is the runtime entry, not a component. |
|
361
|
1
|
50
|
|
|
|
3
|
next if $entry_name eq '__barefoot__'; |
|
362
|
|
|
|
|
|
|
# Only UI registry components (path shape `ui//index`) |
|
363
|
|
|
|
|
|
|
# become child renderers; top-level page components are the |
|
364
|
|
|
|
|
|
|
# render target rather than a child. |
|
365
|
1
|
50
|
|
|
|
8
|
next unless $entry_name =~ m{^ui/([^/]+)/index$}; |
|
366
|
1
|
|
|
|
|
3
|
my $slot_key = $1; |
|
367
|
1
|
|
50
|
|
|
3
|
my $marked = $manifest->{$entry_name}{markedTemplate} // ''; |
|
368
|
1
|
50
|
|
|
|
3
|
next unless $marked; |
|
369
|
|
|
|
|
|
|
# `templates/ui/button/index.html.ep` → `ui/button/index` |
|
370
|
1
|
|
|
|
|
2
|
my $template_name = $marked; |
|
371
|
1
|
|
|
|
|
3
|
$template_name =~ s{^templates/}{}; |
|
372
|
1
|
|
|
|
|
3
|
$template_name =~ s{\.html\.ep$}{}; |
|
373
|
|
|
|
|
|
|
|
|
374
|
1
|
|
|
|
|
2
|
my $signal_init = $signal_inits->{$slot_key}; |
|
375
|
1
|
|
|
|
|
2
|
my $manifest_defaults = $manifest->{$entry_name}{ssrDefaults}; |
|
376
|
|
|
|
|
|
|
$self->register_child_renderer($slot_key, sub { |
|
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
|
0
|
|
|
0
|
|
0
|
my ($props, $caller) = @_; |
|
382
|
0
|
|
0
|
|
|
0
|
my $host = $caller // $parent; |
|
383
|
0
|
|
0
|
|
|
0
|
my $host_scope = $host->_scope_id // $parent_scope; |
|
384
|
|
|
|
|
|
|
# Child shares the parent's backend so nested renders go |
|
385
|
|
|
|
|
|
|
# through the same engine + controller (and inherit any |
|
386
|
|
|
|
|
|
|
# injected json_encoder). The controller is fetched via the weak |
|
387
|
|
|
|
|
|
|
# `$parent` at call time — never captured strongly — so the |
|
388
|
|
|
|
|
|
|
# closure adds no edge to the per-request reference cycle. |
|
389
|
0
|
|
|
|
|
0
|
my $child_bf = BarefootJS->new($parent->c, { backend => $parent->backend }); |
|
390
|
0
|
|
|
|
|
0
|
my $slot_id = delete $props->{_bf_slot}; |
|
391
|
|
|
|
|
|
|
# JSX `key` (a reserved prop) → data-key on the child's scope root |
|
392
|
|
|
|
|
|
|
# for keyed-loop reconciliation (see `data_key_attr`). |
|
393
|
0
|
|
|
|
|
0
|
my $data_key = delete $props->{key}; |
|
394
|
0
|
0
|
|
|
|
0
|
$child_bf->_data_key($data_key) if defined $data_key; |
|
395
|
0
|
0
|
|
|
|
0
|
$child_bf->_scope_id( |
|
396
|
|
|
|
|
|
|
$slot_id ? $host_scope . '_' . $slot_id |
|
397
|
|
|
|
|
|
|
: $template_name . '_' . substr(rand() =~ s/^0\.//r, 0, 6) |
|
398
|
|
|
|
|
|
|
); |
|
399
|
0
|
|
|
|
|
0
|
$child_bf->_is_child(1); |
|
400
|
|
|
|
|
|
|
# (#1249) Slot identity: host scope + slot id. Emitted as |
|
401
|
|
|
|
|
|
|
# bf-h / bf-m attributes by hydration_attrs. |
|
402
|
0
|
0
|
|
|
|
0
|
if ($slot_id) { |
|
403
|
0
|
|
|
|
|
0
|
$child_bf->_bf_parent($host_scope); |
|
404
|
0
|
|
|
|
|
0
|
$child_bf->_bf_mount($slot_id); |
|
405
|
|
|
|
|
|
|
} |
|
406
|
|
|
|
|
|
|
# Share the root registry so the child's own template can |
|
407
|
|
|
|
|
|
|
# render further imported components (#1897). |
|
408
|
0
|
|
|
|
|
0
|
$child_bf->_child_renderers($parent->_child_renderers); |
|
409
|
0
|
|
|
|
|
0
|
$child_bf->_scripts($parent->_scripts); |
|
410
|
0
|
|
|
|
|
0
|
$child_bf->_script_seen($parent->_script_seen); |
|
411
|
|
|
|
|
|
|
|
|
412
|
0
|
|
|
|
|
0
|
my %extra; |
|
413
|
0
|
0
|
|
|
|
0
|
if ($signal_init) { |
|
|
|
0
|
|
|
|
|
|
|
414
|
0
|
|
|
|
|
0
|
%extra = $signal_init->($props); |
|
415
|
|
|
|
|
|
|
} elsif ($manifest_defaults) { |
|
416
|
0
|
|
|
|
|
0
|
%extra = _derive_stash_from_defaults($manifest_defaults, $props); |
|
417
|
|
|
|
|
|
|
} |
|
418
|
|
|
|
|
|
|
|
|
419
|
|
|
|
|
|
|
# Render the child template with $child_bf bound as the active |
|
420
|
|
|
|
|
|
|
# instance for the nested render. The backend owns the |
|
421
|
|
|
|
|
|
|
# engine-specific binding + restore (stash juggle for Mojo). |
|
422
|
0
|
|
|
|
|
0
|
my $html = $parent->backend->render_named( |
|
423
|
|
|
|
|
|
|
$template_name, $child_bf, { %$props, %extra }, |
|
424
|
|
|
|
|
|
|
); |
|
425
|
0
|
|
|
|
|
0
|
chomp $html; |
|
426
|
0
|
|
|
|
|
0
|
return $html; |
|
427
|
1
|
|
|
|
|
21
|
}); |
|
428
|
|
|
|
|
|
|
} |
|
429
|
|
|
|
|
|
|
} |
|
430
|
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
# Derive template-stash kvs from a manifest entry's `ssrDefaults` |
|
432
|
|
|
|
|
|
|
# section. Each entry shape: |
|
433
|
|
|
|
|
|
|
# { value => , propName => , isRestProps => bool } |
|
434
|
|
|
|
|
|
|
# For `isRestProps`, the rest bag passes through unchanged (or the |
|
435
|
|
|
|
|
|
|
# static `{}` if the caller didn't supply one). For ordinary entries |
|
436
|
|
|
|
|
|
|
# the caller's `$props->{propName}` wins when defined, otherwise the |
|
437
|
|
|
|
|
|
|
# static `value` does. `propName`-less entries (signal / memo locals) |
|
438
|
|
|
|
|
|
|
# always use the static value — the caller cannot override them. |
|
439
|
0
|
|
|
0
|
|
0
|
sub _derive_stash_from_defaults ($defaults, $props) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
440
|
0
|
|
|
|
|
0
|
my %extra; |
|
441
|
0
|
|
|
|
|
0
|
for my $name (keys %$defaults) { |
|
442
|
0
|
|
|
|
|
0
|
my $d = $defaults->{$name}; |
|
443
|
0
|
0
|
|
|
|
0
|
if (ref($d) ne 'HASH') { |
|
444
|
0
|
|
|
|
|
0
|
$extra{$name} = $d; |
|
445
|
0
|
|
|
|
|
0
|
next; |
|
446
|
|
|
|
|
|
|
} |
|
447
|
0
|
0
|
|
|
|
0
|
if ($d->{isRestProps}) { |
|
448
|
0
|
0
|
|
|
|
0
|
$extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value}; |
|
449
|
0
|
|
|
|
|
0
|
next; |
|
450
|
|
|
|
|
|
|
} |
|
451
|
0
|
|
|
|
|
0
|
my $prop_name = $d->{propName}; |
|
452
|
0
|
0
|
0
|
|
|
0
|
if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) { |
|
|
|
|
0
|
|
|
|
|
|
453
|
0
|
|
|
|
|
0
|
$extra{$name} = $props->{$prop_name}; |
|
454
|
|
|
|
|
|
|
} else { |
|
455
|
0
|
|
|
|
|
0
|
$extra{$name} = $d->{value}; |
|
456
|
|
|
|
|
|
|
} |
|
457
|
|
|
|
|
|
|
} |
|
458
|
0
|
|
|
|
|
0
|
return %extra; |
|
459
|
|
|
|
|
|
|
} |
|
460
|
|
|
|
|
|
|
|
|
461
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
462
|
|
|
|
|
|
|
# Script Output |
|
463
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
464
|
|
|
|
|
|
|
|
|
465
|
0
|
|
|
0
|
0
|
0
|
sub scripts ($self) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
466
|
0
|
|
|
|
|
0
|
my @tags; |
|
467
|
0
|
|
|
|
|
0
|
for my $path (@{$self->_scripts}) { |
|
|
0
|
|
|
|
|
0
|
|
|
468
|
0
|
|
|
|
|
0
|
push @tags, qq{}; |
|
469
|
|
|
|
|
|
|
} |
|
470
|
0
|
|
|
|
|
0
|
return join("\n", @tags); |
|
471
|
|
|
|
|
|
|
} |
|
472
|
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
474
|
|
|
|
|
|
|
# Streaming SSR (Out-of-Order) |
|
475
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
476
|
|
|
|
|
|
|
|
|
477
|
0
|
|
|
0
|
0
|
0
|
sub streaming_bootstrap ($self) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
478
|
0
|
|
|
|
|
0
|
return q{}; |
|
479
|
|
|
|
|
|
|
} |
|
480
|
|
|
|
|
|
|
|
|
481
|
0
|
|
|
0
|
0
|
0
|
sub async_boundary ($self, $id, $fallback_html) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
482
|
|
|
|
|
|
|
# The fallback comes in via Mojo `begin %>...<% end` capture (see |
|
483
|
|
|
|
|
|
|
# MojoAdapter::renderAsync), which produces a CODE ref returning a |
|
484
|
|
|
|
|
|
|
# Mojo::ByteStream. Materialize it through the backend so the rendered |
|
485
|
|
|
|
|
|
|
# HTML embeds in the placeholder rather than the CODE ref's |
|
486
|
|
|
|
|
|
|
# stringification. |
|
487
|
0
|
|
|
|
|
0
|
$fallback_html = $self->backend->materialize($fallback_html); |
|
488
|
0
|
|
|
|
|
0
|
return qq{ $fallback_html }; |
|
489
|
|
|
|
|
|
|
} |
|
490
|
|
|
|
|
|
|
|
|
491
|
0
|
|
|
0
|
0
|
0
|
sub async_resolve ($self, $id, $content_html) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
492
|
0
|
|
|
|
|
0
|
return qq{$content_html}; |
|
493
|
|
|
|
|
|
|
} |
|
494
|
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
496
|
|
|
|
|
|
|
# JS-compat callees (#1189) — invoked from generated Mojo templates as |
|
497
|
|
|
|
|
|
|
# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's |
|
498
|
|
|
|
|
|
|
# `templatePrimitives` registry emits these helper calls in place of the |
|
499
|
|
|
|
|
|
|
# corresponding JS callees (`JSON.stringify`, `Math.floor`, …) so the SSR |
|
500
|
|
|
|
|
|
|
# template can render value-equivalent output without a JS engine. |
|
501
|
|
|
|
|
|
|
# |
|
502
|
|
|
|
|
|
|
# Failure policy mirrors the Go adapter (#1188): user-data marshalling |
|
503
|
|
|
|
|
|
|
# (json) bubbles errors so Mojolicious aborts loudly on cycles / |
|
504
|
|
|
|
|
|
|
# unsupported values rather than silently producing an empty payload. |
|
505
|
|
|
|
|
|
|
# Numeric coercion follows JS semantics (NaN propagates as the special |
|
506
|
|
|
|
|
|
|
# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings |
|
507
|
|
|
|
|
|
|
# always coerce to a string representation. |
|
508
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
509
|
|
|
|
|
|
|
|
|
510
|
4
|
|
|
4
|
0
|
184601
|
sub json ($self, $value) { |
|
|
4
|
|
|
|
|
6
|
|
|
|
4
|
|
|
|
|
6
|
|
|
|
4
|
|
|
|
|
3
|
|
|
511
|
|
|
|
|
|
|
# Mojo::JSON::to_json returns a character string (not bytes), suitable |
|
512
|
|
|
|
|
|
|
# for embedding in HTML output via Mojo::ByteStream / `<%==`. |
|
513
|
|
|
|
|
|
|
# |
|
514
|
|
|
|
|
|
|
# Documented divergence from JS: JS distinguishes `null` (renders as |
|
515
|
|
|
|
|
|
|
# "null") from `undefined` (`JSON.stringify(undefined)` returns the |
|
516
|
|
|
|
|
|
|
# JS value `undefined`, not a string). Perl has no such distinction |
|
517
|
|
|
|
|
|
|
# — both map to `undef`. We choose the `null` rendering for SSR |
|
518
|
|
|
|
|
|
|
# ergonomics: an unset prop becomes the string "null" rather than |
|
519
|
|
|
|
|
|
|
# the literal text "undefined" or an empty attribute. Matches the |
|
520
|
|
|
|
|
|
|
# `null` case of JS exactly; diverges from the `undefined` case. |
|
521
|
4
|
|
|
|
|
9
|
return $self->backend->encode_json($value); |
|
522
|
|
|
|
|
|
|
} |
|
523
|
|
|
|
|
|
|
|
|
524
|
3
|
|
|
3
|
0
|
1775
|
sub string ($self, $value) { |
|
|
3
|
|
|
|
|
11
|
|
|
|
3
|
|
|
|
|
5
|
|
|
|
3
|
|
|
|
|
4
|
|
|
525
|
|
|
|
|
|
|
# JS `String(v)` mirror. `undef` renders as the empty string here so |
|
526
|
|
|
|
|
|
|
# an unset prop doesn't surface as a literal "undefined" / "null" |
|
527
|
|
|
|
|
|
|
# in user-facing HTML — same divergence the Go adapter documents |
|
528
|
|
|
|
|
|
|
# for `bf_string`. |
|
529
|
3
|
100
|
|
|
|
45
|
return defined $value ? "$value" : ''; |
|
530
|
|
|
|
|
|
|
} |
|
531
|
|
|
|
|
|
|
|
|
532
|
15
|
|
|
15
|
0
|
1514
|
sub number ($self, $value) { |
|
|
15
|
|
|
|
|
14
|
|
|
|
15
|
|
|
|
|
16
|
|
|
|
15
|
|
|
|
|
12
|
|
|
533
|
|
|
|
|
|
|
# JS `Number(v)` mirror. Numeric coerces via Perl's implicit |
|
534
|
|
|
|
|
|
|
# numeric context; non-numeric / undef yield real numeric NaN |
|
535
|
|
|
|
|
|
|
# (`'nan' + 0`) so downstream arithmetic propagates correctly |
|
536
|
|
|
|
|
|
|
# (`Math.floor(NaN) === NaN`). Returning the literal string |
|
537
|
|
|
|
|
|
|
# "NaN" would conflate the user-passing-the-string-"NaN" case |
|
538
|
|
|
|
|
|
|
# with the parse-failure case, and break NaN detection in |
|
539
|
|
|
|
|
|
|
# downstream helpers. |
|
540
|
15
|
100
|
|
|
|
25
|
return 0 + 'nan' unless defined $value; |
|
541
|
14
|
100
|
|
|
|
71
|
return $value + 0 if looks_like_number($value); |
|
542
|
4
|
|
|
|
|
8
|
return 0 + 'nan'; |
|
543
|
|
|
|
|
|
|
} |
|
544
|
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
# NaN is the only float for which `$x != $x` holds. Used as the |
|
546
|
|
|
|
|
|
|
# portable sentinel check in floor/ceil/round. |
|
547
|
11
|
|
|
11
|
|
13
|
sub _is_nan { my $n = shift; return $n != $n } |
|
|
11
|
|
|
|
|
28
|
|
|
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
|
0
|
|
0
|
0
|
|
0
|
sub _is_inf { my $n = shift; return $n == 9**9**9 || $n == -9**9**9 } |
|
|
0
|
|
|
|
|
0
|
|
|
552
|
|
|
|
|
|
|
|
|
553
|
3
|
|
|
3
|
0
|
1329
|
sub floor ($self, $value) { |
|
|
3
|
|
|
|
|
4
|
|
|
|
3
|
|
|
|
|
4
|
|
|
|
3
|
|
|
|
|
4
|
|
|
554
|
3
|
|
|
|
|
6
|
my $n = $self->number($value); |
|
555
|
3
|
100
|
|
|
|
6
|
return $n if _is_nan($n); |
|
556
|
2
|
|
|
|
|
24
|
return POSIX::floor($n); |
|
557
|
|
|
|
|
|
|
} |
|
558
|
|
|
|
|
|
|
|
|
559
|
3
|
|
|
3
|
0
|
177
|
sub ceil ($self, $value) { |
|
|
3
|
|
|
|
|
4
|
|
|
|
3
|
|
|
|
|
4
|
|
|
|
3
|
|
|
|
|
3
|
|
|
560
|
3
|
|
|
|
|
6
|
my $n = $self->number($value); |
|
561
|
3
|
100
|
|
|
|
4
|
return $n if _is_nan($n); |
|
562
|
2
|
|
|
|
|
8
|
return POSIX::ceil($n); |
|
563
|
|
|
|
|
|
|
} |
|
564
|
|
|
|
|
|
|
|
|
565
|
5
|
|
|
5
|
0
|
175
|
sub round ($self, $value) { |
|
|
5
|
|
|
|
|
5
|
|
|
|
5
|
|
|
|
|
5
|
|
|
|
5
|
|
|
|
|
6
|
|
|
566
|
5
|
|
|
|
|
10
|
my $n = $self->number($value); |
|
567
|
5
|
100
|
|
|
|
8
|
return $n if _is_nan($n); |
|
568
|
|
|
|
|
|
|
# POSIX has no `round`. JS `Math.round` rounds half toward |
|
569
|
|
|
|
|
|
|
# +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n |
|
570
|
|
|
|
|
|
|
# + 0.5)` reproduces that for both signs. |
|
571
|
4
|
|
|
|
|
19
|
return POSIX::floor($n + 0.5); |
|
572
|
|
|
|
|
|
|
} |
|
573
|
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
575
|
|
|
|
|
|
|
# Array / String method helpers (#1448 Tier A) |
|
576
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
577
|
|
|
|
|
|
|
# |
|
578
|
|
|
|
|
|
|
# `Array.prototype.includes(x)` and `String.prototype.includes(sub)` |
|
579
|
|
|
|
|
|
|
# share a method name in JS; the JSX parser can't tell the two |
|
580
|
|
|
|
|
|
|
# receiver shapes apart without TS type inference, so both lower to |
|
581
|
|
|
|
|
|
|
# the same IR node (`array-method` / method `includes`). This helper |
|
582
|
|
|
|
|
|
|
# dispatches at the Perl level via `ref()`: |
|
583
|
|
|
|
|
|
|
# - ARRAY ref: scan elements with `eq`; one defined-vs-undef |
|
584
|
|
|
|
|
|
|
# hop matches JS's `===` for null/undefined. |
|
585
|
|
|
|
|
|
|
# - scalar: `index($recv, $sub) != -1`, with both args |
|
586
|
|
|
|
|
|
|
# coerced through `// ''` so an undef receiver / |
|
587
|
|
|
|
|
|
|
# needle doesn't trip Perl's substr warning. |
|
588
|
|
|
|
|
|
|
# Anything else (HASH ref, code ref) returns false — matches the |
|
589
|
|
|
|
|
|
|
# JS semantic where `.includes` is only defined on Array / |
|
590
|
|
|
|
|
|
|
# TypedArray / String. |
|
591
|
|
|
|
|
|
|
|
|
592
|
13
|
|
|
13
|
0
|
1899
|
sub includes ($self, $recv, $elem) { |
|
|
13
|
|
|
|
|
16
|
|
|
|
13
|
|
|
|
|
15
|
|
|
|
13
|
|
|
|
|
13
|
|
|
|
13
|
|
|
|
|
12
|
|
|
593
|
13
|
100
|
|
|
|
26
|
if (ref($recv) eq 'ARRAY') { |
|
594
|
6
|
|
|
|
|
8
|
for my $item (@$recv) { |
|
595
|
10
|
100
|
|
|
|
27
|
if (!defined $item) { |
|
596
|
1
|
50
|
|
|
|
6
|
return 1 if !defined $elem; |
|
597
|
0
|
|
|
|
|
0
|
next; |
|
598
|
|
|
|
|
|
|
} |
|
599
|
9
|
100
|
100
|
|
|
30
|
return 1 if defined $elem && $item eq $elem; |
|
600
|
|
|
|
|
|
|
} |
|
601
|
3
|
|
|
|
|
9
|
return 0; |
|
602
|
|
|
|
|
|
|
} |
|
603
|
7
|
100
|
|
|
|
14
|
return 0 if ref($recv); |
|
604
|
5
|
100
|
100
|
|
|
25
|
return index($recv // '', $elem // '') != -1 ? 1 : 0; |
|
|
|
|
50
|
|
|
|
|
|
605
|
|
|
|
|
|
|
} |
|
606
|
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
# `Array.prototype.filter(fn)` / `.every(fn)` / `.some(fn)`. The Xslate adapter |
|
608
|
|
|
|
|
|
|
# lowers a JS arrow predicate to a Kolon lambda (`-> $x { ... }`), which is |
|
609
|
|
|
|
|
|
|
# callable from Perl as a code ref, and emits `$bf.filter($arr, )`. |
|
610
|
|
|
|
|
|
|
# `filter` returns a new arrayref; `every` / `some` return 1/0. Non-array / |
|
611
|
|
|
|
|
|
|
# empty receivers follow JS (`filter` → [], `every` → true, `some` → false). |
|
612
|
|
|
|
|
|
|
# (The Mojo adapter lowers these shapes inline and never reaches these methods.) |
|
613
|
0
|
|
|
0
|
0
|
0
|
sub filter ($self, $recv, $pred) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
614
|
0
|
0
|
|
|
|
0
|
return [] unless ref($recv) eq 'ARRAY'; |
|
615
|
0
|
|
|
|
|
0
|
return [ grep { $pred->($_) } @$recv ]; |
|
|
0
|
|
|
|
|
0
|
|
|
616
|
|
|
|
|
|
|
} |
|
617
|
|
|
|
|
|
|
|
|
618
|
0
|
|
|
0
|
0
|
0
|
sub every ($self, $recv, $pred) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
619
|
0
|
0
|
|
|
|
0
|
return 1 unless ref($recv) eq 'ARRAY'; |
|
620
|
0
|
0
|
|
|
|
0
|
for my $item (@$recv) { return 0 unless $pred->($item) } |
|
|
0
|
|
|
|
|
0
|
|
|
621
|
0
|
|
|
|
|
0
|
return 1; |
|
622
|
|
|
|
|
|
|
} |
|
623
|
|
|
|
|
|
|
|
|
624
|
0
|
|
|
0
|
0
|
0
|
sub some ($self, $recv, $pred) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
625
|
0
|
0
|
|
|
|
0
|
return 0 unless ref($recv) eq 'ARRAY'; |
|
626
|
0
|
0
|
|
|
|
0
|
for my $item (@$recv) { return 1 if $pred->($item) } |
|
|
0
|
|
|
|
|
0
|
|
|
627
|
0
|
|
|
|
|
0
|
return 0; |
|
628
|
|
|
|
|
|
|
} |
|
629
|
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
# `Array.prototype.find(fn)` / `.findIndex(fn)` / `.findLast(fn)` / |
|
631
|
|
|
|
|
|
|
# `.findLastIndex(fn)` — same Kolon-lambda predicate mechanism as filter. The |
|
632
|
|
|
|
|
|
|
# camelCase JS names lower to these snake_case methods (like index_of / |
|
633
|
|
|
|
|
|
|
# last_index_of). `find` / `find_last` return the matching element (or undef → |
|
634
|
|
|
|
|
|
|
# JS `undefined`); the index forms return the 0-based position (or -1). |
|
635
|
0
|
|
|
0
|
0
|
0
|
sub find ($self, $recv, $pred) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
636
|
0
|
0
|
|
|
|
0
|
return undef unless ref($recv) eq 'ARRAY'; |
|
637
|
0
|
0
|
|
|
|
0
|
for my $item (@$recv) { return $item if $pred->($item) } |
|
|
0
|
|
|
|
|
0
|
|
|
638
|
0
|
|
|
|
|
0
|
return undef; |
|
639
|
|
|
|
|
|
|
} |
|
640
|
|
|
|
|
|
|
|
|
641
|
0
|
|
|
0
|
0
|
0
|
sub find_index ($self, $recv, $pred) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
642
|
0
|
0
|
|
|
|
0
|
return -1 unless ref($recv) eq 'ARRAY'; |
|
643
|
0
|
0
|
|
|
|
0
|
for my $i (0 .. $#$recv) { return $i if $pred->($recv->[$i]) } |
|
|
0
|
|
|
|
|
0
|
|
|
644
|
0
|
|
|
|
|
0
|
return -1; |
|
645
|
|
|
|
|
|
|
} |
|
646
|
|
|
|
|
|
|
|
|
647
|
0
|
|
|
0
|
0
|
0
|
sub find_last ($self, $recv, $pred) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
648
|
0
|
0
|
|
|
|
0
|
return undef unless ref($recv) eq 'ARRAY'; |
|
649
|
0
|
0
|
|
|
|
0
|
for my $i (reverse 0 .. $#$recv) { return $recv->[$i] if $pred->($recv->[$i]) } |
|
|
0
|
|
|
|
|
0
|
|
|
650
|
0
|
|
|
|
|
0
|
return undef; |
|
651
|
|
|
|
|
|
|
} |
|
652
|
|
|
|
|
|
|
|
|
653
|
0
|
|
|
0
|
0
|
0
|
sub find_last_index ($self, $recv, $pred) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
654
|
0
|
0
|
|
|
|
0
|
return -1 unless ref($recv) eq 'ARRAY'; |
|
655
|
0
|
0
|
|
|
|
0
|
for my $i (reverse 0 .. $#$recv) { return $i if $pred->($recv->[$i]) } |
|
|
0
|
|
|
|
|
0
|
|
|
656
|
0
|
|
|
|
|
0
|
return -1; |
|
657
|
|
|
|
|
|
|
} |
|
658
|
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
# `String.prototype.toLowerCase()` / `.toUpperCase()`. Kolon has a builtin |
|
660
|
|
|
|
|
|
|
# `.join` array method (so the adapter uses that directly) but no builtin |
|
661
|
|
|
|
|
|
|
# `lc` / `uc`, so these live on the runtime object. `CORE::` avoids recursing |
|
662
|
|
|
|
|
|
|
# into these methods. |
|
663
|
0
|
0
|
|
0
|
0
|
0
|
sub lc ($self, $s) { return defined $s ? CORE::lc($s) : '' } |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
664
|
0
|
0
|
|
0
|
0
|
0
|
sub uc ($self, $s) { return defined $s ? CORE::uc($s) : '' } |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
665
|
|
|
|
|
|
|
|
|
666
|
|
|
|
|
|
|
# `Array.prototype.join(sep)` with JS semantics: separator defaults to ",", |
|
667
|
|
|
|
|
|
|
# and undefined / null elements render as empty (`[1,,2].join(",")` → "1,,2"). |
|
668
|
|
|
|
|
|
|
# Kolon has a builtin `.join`, but routing through the runtime keeps the |
|
669
|
|
|
|
|
|
|
# JS-compat element handling in one place. `CORE::join` avoids recursing. |
|
670
|
0
|
|
|
0
|
0
|
0
|
sub join ($self, $recv, $sep = undef) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
671
|
0
|
0
|
|
|
|
0
|
return '' unless ref($recv) eq 'ARRAY'; |
|
672
|
0
|
|
0
|
|
|
0
|
$sep //= ','; |
|
673
|
0
|
0
|
|
|
|
0
|
return CORE::join($sep, map { defined $_ ? $_ : '' } @$recv); |
|
|
0
|
|
|
|
|
0
|
|
|
674
|
|
|
|
|
|
|
} |
|
675
|
|
|
|
|
|
|
|
|
676
|
|
|
|
|
|
|
# `.length` — JS works on BOTH arrays (element count) and strings (character |
|
677
|
|
|
|
|
|
|
# count); Kolon's builtin `.size()` is array-only and faults on a string. So |
|
678
|
|
|
|
|
|
|
# dispatch on ref type here. `CORE::length` avoids recursing into this method. |
|
679
|
0
|
|
|
0
|
0
|
0
|
sub length ($self, $recv) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
680
|
0
|
0
|
|
|
|
0
|
return scalar @$recv if ref($recv) eq 'ARRAY'; |
|
681
|
0
|
0
|
|
|
|
0
|
return 0 if ref($recv); |
|
682
|
0
|
|
0
|
|
|
0
|
return CORE::length($recv // ''); |
|
683
|
|
|
|
|
|
|
} |
|
684
|
|
|
|
|
|
|
|
|
685
|
|
|
|
|
|
|
# `Array.prototype.indexOf(x)` / `Array.prototype.lastIndexOf(x)` |
|
686
|
|
|
|
|
|
|
# value-equality search (#1448 Tier A). Returns the 0-based position |
|
687
|
|
|
|
|
|
|
# of the first / last matching element, or -1 if not found. |
|
688
|
|
|
|
|
|
|
# Non-array receivers return -1 — matches the JS semantic that |
|
689
|
|
|
|
|
|
|
# `.indexOf` / `.lastIndexOf` are only defined on Array / TypedArray. |
|
690
|
|
|
|
|
|
|
# (The string-position `indexOf` form isn't in Tier A; if it lands |
|
691
|
|
|
|
|
|
|
# later the helper can grow a ref()-dispatch branch like `includes`.) |
|
692
|
|
|
|
|
|
|
|
|
693
|
12
|
|
|
12
|
|
11
|
sub _array_index_of ($recv, $elem, $reverse) { |
|
|
12
|
|
|
|
|
13
|
|
|
|
12
|
|
|
|
|
12
|
|
|
|
12
|
|
|
|
|
11
|
|
|
|
12
|
|
|
|
|
11
|
|
|
694
|
12
|
100
|
|
|
|
46
|
return -1 unless ref($recv) eq 'ARRAY'; |
|
695
|
11
|
100
|
|
|
|
21
|
my @indices = $reverse ? (reverse 0 .. $#{$recv}) : (0 .. $#{$recv}); |
|
|
5
|
|
|
|
|
9
|
|
|
|
6
|
|
|
|
|
12
|
|
|
696
|
11
|
|
|
|
|
15
|
for my $i (@indices) { |
|
697
|
27
|
|
|
|
|
28
|
my $item = $recv->[$i]; |
|
698
|
27
|
100
|
|
|
|
31
|
if (!defined $item) { |
|
699
|
2
|
50
|
|
|
|
9
|
return $i if !defined $elem; |
|
700
|
0
|
|
|
|
|
0
|
next; |
|
701
|
|
|
|
|
|
|
} |
|
702
|
25
|
100
|
66
|
|
|
68
|
return $i if defined $elem && $item eq $elem; |
|
703
|
|
|
|
|
|
|
} |
|
704
|
4
|
|
|
|
|
44
|
return -1; |
|
705
|
|
|
|
|
|
|
} |
|
706
|
|
|
|
|
|
|
|
|
707
|
7
|
|
|
7
|
0
|
2077
|
sub index_of ($self, $recv, $elem) { |
|
|
7
|
|
|
|
|
9
|
|
|
|
7
|
|
|
|
|
8
|
|
|
|
7
|
|
|
|
|
7
|
|
|
|
7
|
|
|
|
|
6
|
|
|
708
|
7
|
|
|
|
|
13
|
return _array_index_of($recv, $elem, 0); |
|
709
|
|
|
|
|
|
|
} |
|
710
|
|
|
|
|
|
|
|
|
711
|
5
|
|
|
5
|
0
|
6
|
sub last_index_of ($self, $recv, $elem) { |
|
|
5
|
|
|
|
|
7
|
|
|
|
5
|
|
|
|
|
4
|
|
|
|
5
|
|
|
|
|
8
|
|
|
|
5
|
|
|
|
|
4
|
|
|
712
|
5
|
|
|
|
|
11
|
return _array_index_of($recv, $elem, 1); |
|
713
|
|
|
|
|
|
|
} |
|
714
|
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
# `Array.prototype.at(i)` — supports negative indices (`.at(-1)` is |
|
716
|
|
|
|
|
|
|
# the last element); out-of-bounds returns undef (which Mojo's |
|
717
|
|
|
|
|
|
|
# auto-escape renders as the empty string, matching JS's `undefined`). |
|
718
|
|
|
|
|
|
|
# Non-array receivers return undef. Matches the Go `bf_at` arithmetic |
|
719
|
|
|
|
|
|
|
# (`length + i` for i < 0) so adapter output stays symmetric. |
|
720
|
|
|
|
|
|
|
|
|
721
|
10
|
|
|
10
|
0
|
2029
|
sub at ($self, $recv, $i) { |
|
|
10
|
|
|
|
|
13
|
|
|
|
10
|
|
|
|
|
9
|
|
|
|
10
|
|
|
|
|
10
|
|
|
|
10
|
|
|
|
|
10
|
|
|
722
|
10
|
100
|
|
|
|
25
|
return undef unless ref($recv) eq 'ARRAY'; |
|
723
|
7
|
50
|
|
|
|
12
|
return undef if !defined $i; |
|
724
|
7
|
|
|
|
|
9
|
my $len = scalar @$recv; |
|
725
|
7
|
100
|
|
|
|
10
|
return undef if $len == 0; |
|
726
|
6
|
100
|
|
|
|
12
|
my $idx = $i < 0 ? $len + $i : $i; |
|
727
|
6
|
100
|
100
|
|
|
20
|
return undef if $idx < 0 || $idx >= $len; |
|
728
|
4
|
|
|
|
|
13
|
return $recv->[$idx]; |
|
729
|
|
|
|
|
|
|
} |
|
730
|
|
|
|
|
|
|
|
|
731
|
|
|
|
|
|
|
# `Array.prototype.concat(other)` — merges two arrays in order |
|
732
|
|
|
|
|
|
|
# into a new ARRAY ref. Non-array operands collapse to empty |
|
733
|
|
|
|
|
|
|
# (matches the Go `bf_concat` semantic so cross-adapter output |
|
734
|
|
|
|
|
|
|
# stays symmetric; differs from JS where a non-Array argument |
|
735
|
|
|
|
|
|
|
# with `Symbol.isConcatSpreadable` would be spread, a behaviour |
|
736
|
|
|
|
|
|
|
# the template-language path never observes). |
|
737
|
|
|
|
|
|
|
|
|
738
|
9
|
|
|
9
|
0
|
1925
|
sub concat ($self, $a, $b) { |
|
|
9
|
|
|
|
|
11
|
|
|
|
9
|
|
|
|
|
9
|
|
|
|
9
|
|
|
|
|
10
|
|
|
|
9
|
|
|
|
|
9
|
|
|
739
|
9
|
|
|
|
|
7
|
my @out; |
|
740
|
9
|
100
|
|
|
|
25
|
push @out, @$a if ref($a) eq 'ARRAY'; |
|
741
|
9
|
100
|
|
|
|
17
|
push @out, @$b if ref($b) eq 'ARRAY'; |
|
742
|
9
|
|
|
|
|
28
|
return \@out; |
|
743
|
|
|
|
|
|
|
} |
|
744
|
|
|
|
|
|
|
|
|
745
|
|
|
|
|
|
|
# `Array.prototype.slice(start, end?)` — carves out a sub-range |
|
746
|
|
|
|
|
|
|
# into a new ARRAY ref. Mirrors the Go `bf_slice` arithmetic so |
|
747
|
|
|
|
|
|
|
# adapter output stays symmetric: |
|
748
|
|
|
|
|
|
|
# - start < 0 → length + start (e.g. -1 = last index) |
|
749
|
|
|
|
|
|
|
# - end < 0 → length + end |
|
750
|
|
|
|
|
|
|
# - start < 0 after clamp → 0 |
|
751
|
|
|
|
|
|
|
# - end > length → length |
|
752
|
|
|
|
|
|
|
# - start >= end → empty |
|
753
|
|
|
|
|
|
|
# - end undef → "to length" |
|
754
|
|
|
|
|
|
|
# Non-array receivers return an empty ARRAY ref. |
|
755
|
|
|
|
|
|
|
|
|
756
|
13
|
|
|
13
|
0
|
2622
|
sub slice ($self, $recv, $start, $end) { |
|
|
13
|
|
|
|
|
15
|
|
|
|
13
|
|
|
|
|
13
|
|
|
|
13
|
|
|
|
|
12
|
|
|
|
13
|
|
|
|
|
14
|
|
|
|
13
|
|
|
|
|
12
|
|
|
757
|
13
|
100
|
|
|
|
34
|
return [] unless ref($recv) eq 'ARRAY'; |
|
758
|
11
|
|
|
|
|
14
|
my $len = scalar @$recv; |
|
759
|
11
|
100
|
|
|
|
22
|
return [] if $len == 0; |
|
760
|
|
|
|
|
|
|
|
|
761
|
10
|
|
50
|
|
|
17
|
my $s = $start // 0; |
|
762
|
10
|
100
|
|
|
|
16
|
$s = $len + $s if $s < 0; |
|
763
|
10
|
50
|
|
|
|
15
|
$s = 0 if $s < 0; |
|
764
|
10
|
100
|
|
|
|
14
|
$s = $len if $s > $len; |
|
765
|
|
|
|
|
|
|
|
|
766
|
10
|
100
|
|
|
|
15
|
my $e = defined $end ? $end : $len; |
|
767
|
10
|
100
|
|
|
|
12
|
$e = $len + $e if $e < 0; |
|
768
|
10
|
50
|
|
|
|
16
|
$e = 0 if $e < 0; |
|
769
|
10
|
50
|
|
|
|
15
|
$e = $len if $e > $len; |
|
770
|
|
|
|
|
|
|
|
|
771
|
10
|
100
|
|
|
|
22
|
return [] if $s >= $e; |
|
772
|
7
|
|
|
|
|
12
|
return [ @{$recv}[$s .. $e - 1] ]; |
|
|
7
|
|
|
|
|
49
|
|
|
773
|
|
|
|
|
|
|
} |
|
774
|
|
|
|
|
|
|
|
|
775
|
|
|
|
|
|
|
# `Array.prototype.reverse()` / `Array.prototype.toReversed()` — |
|
776
|
|
|
|
|
|
|
# both shapes share this lowering. SSR templates render a snapshot |
|
777
|
|
|
|
|
|
|
# of state, so JS's mutate-receiver (`reverse`) vs |
|
778
|
|
|
|
|
|
|
# return-new-array (`toReversed`) distinction has no template- |
|
779
|
|
|
|
|
|
|
# level meaning. Always returns a new ARRAY ref to keep callers |
|
780
|
|
|
|
|
|
|
# safe from accidental aliasing. Non-array receivers return an |
|
781
|
|
|
|
|
|
|
# empty ARRAY ref. |
|
782
|
|
|
|
|
|
|
|
|
783
|
8
|
|
|
8
|
0
|
3030
|
sub reverse ($self, $recv) { |
|
|
8
|
|
|
|
|
9
|
|
|
|
8
|
|
|
|
|
9
|
|
|
|
8
|
|
|
|
|
8
|
|
|
784
|
8
|
100
|
|
|
|
48
|
return [] unless ref($recv) eq 'ARRAY'; |
|
785
|
5
|
|
|
|
|
19
|
return [ reverse @$recv ]; |
|
786
|
|
|
|
|
|
|
} |
|
787
|
|
|
|
|
|
|
|
|
788
|
|
|
|
|
|
|
# `Array.prototype.flat(depth?)` (#1448 Tier C) — flatten nested ARRAY |
|
789
|
|
|
|
|
|
|
# refs `$depth` levels deep. A `$depth` of -1 is the `Infinity` sentinel |
|
790
|
|
|
|
|
|
|
# (flatten fully); 0 returns a shallow copy. Non-ARRAY elements are kept |
|
791
|
|
|
|
|
|
|
# as-is (JS only flattens nested arrays). Non-ARRAY receiver → []. |
|
792
|
0
|
|
|
0
|
0
|
0
|
sub flat ($self, $recv, $depth = 1) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
793
|
0
|
0
|
|
|
|
0
|
return [] unless ref($recv) eq 'ARRAY'; |
|
794
|
0
|
|
|
|
|
0
|
my @out; |
|
795
|
0
|
|
|
|
|
0
|
for my $el (@$recv) { |
|
796
|
0
|
0
|
0
|
|
|
0
|
if ($depth != 0 && ref($el) eq 'ARRAY') { |
|
797
|
0
|
0
|
|
|
|
0
|
my $next = $depth > 0 ? $depth - 1 : $depth; |
|
798
|
0
|
|
|
|
|
0
|
push @out, @{ $self->flat($el, $next) }; |
|
|
0
|
|
|
|
|
0
|
|
|
799
|
|
|
|
|
|
|
} |
|
800
|
|
|
|
|
|
|
else { |
|
801
|
0
|
|
|
|
|
0
|
push @out, $el; |
|
802
|
|
|
|
|
|
|
} |
|
803
|
|
|
|
|
|
|
} |
|
804
|
0
|
|
|
|
|
0
|
return \@out; |
|
805
|
|
|
|
|
|
|
} |
|
806
|
|
|
|
|
|
|
|
|
807
|
|
|
|
|
|
|
# `Array.prototype.flatMap(fn)` value-returning field projection |
|
808
|
|
|
|
|
|
|
# (#1448 Tier C) — map each element through a self / field projection, |
|
809
|
|
|
|
|
|
|
# then flatten one level. `field` reads a HASH-ref key (the raw JS prop |
|
810
|
|
|
|
|
|
|
# name, as `bf->reduce` does); a projected non-ARRAY value is kept as-is |
|
811
|
|
|
|
|
|
|
# (flatMap = map + flat(1)). Non-ARRAY receiver → []. |
|
812
|
0
|
|
|
0
|
0
|
0
|
sub flat_map ($self, $recv, $key_kind, $key) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
813
|
0
|
0
|
|
|
|
0
|
return [] unless ref($recv) eq 'ARRAY'; |
|
814
|
0
|
|
|
|
|
0
|
my @projected; |
|
815
|
0
|
|
|
|
|
0
|
for my $el (@$recv) { |
|
816
|
0
|
0
|
|
|
|
0
|
if ($key_kind eq 'field') { |
|
817
|
|
|
|
|
|
|
# JS `i => i.field` on a non-object yields `undefined`, not the |
|
818
|
|
|
|
|
|
|
# element itself — push `undef` so a scalar element doesn't leak |
|
819
|
|
|
|
|
|
|
# into the output (matches Go's `getFieldValue` returning nil). |
|
820
|
0
|
0
|
|
|
|
0
|
push @projected, ref($el) eq 'HASH' ? $el->{$key} : undef; |
|
821
|
|
|
|
|
|
|
} |
|
822
|
|
|
|
|
|
|
else { |
|
823
|
0
|
|
|
|
|
0
|
push @projected, $el; |
|
824
|
|
|
|
|
|
|
} |
|
825
|
|
|
|
|
|
|
} |
|
826
|
0
|
|
|
|
|
0
|
return $self->flat(\@projected, 1); |
|
827
|
|
|
|
|
|
|
} |
|
828
|
|
|
|
|
|
|
|
|
829
|
|
|
|
|
|
|
# `Array.prototype.flatMap(i => [i.a, i.b])` — array-literal tuple |
|
830
|
|
|
|
|
|
|
# projection (#1448 Tier C). Each `@specs` entry is a [kind, key] arrayref |
|
831
|
|
|
|
|
|
|
# (['self', ''] or ['field', 'a']). For each element, every leaf's value |
|
832
|
|
|
|
|
|
|
# is appended in order. flat(1) removes only the literal wrapper, so an |
|
833
|
|
|
|
|
|
|
# array-valued leaf is appended verbatim (no spread) — i.e. just append |
|
834
|
|
|
|
|
|
|
# each leaf. A non-HASH element under a `field` leaf yields undef (JS |
|
835
|
|
|
|
|
|
|
# `i.field` on a non-object). Non-ARRAY receiver → []. |
|
836
|
0
|
|
|
0
|
0
|
0
|
sub flat_map_tuple ($self, $recv, @specs) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
837
|
0
|
0
|
|
|
|
0
|
return [] unless ref($recv) eq 'ARRAY'; |
|
838
|
0
|
|
|
|
|
0
|
my @out; |
|
839
|
0
|
|
|
|
|
0
|
for my $el (@$recv) { |
|
840
|
0
|
|
|
|
|
0
|
for my $spec (@specs) { |
|
841
|
0
|
|
|
|
|
0
|
my ($kind, $key) = @$spec; |
|
842
|
0
|
0
|
|
|
|
0
|
if ($kind eq 'field') { |
|
843
|
0
|
0
|
|
|
|
0
|
push @out, ref($el) eq 'HASH' ? $el->{$key} : undef; |
|
844
|
|
|
|
|
|
|
} |
|
845
|
|
|
|
|
|
|
else { |
|
846
|
0
|
|
|
|
|
0
|
push @out, $el; |
|
847
|
|
|
|
|
|
|
} |
|
848
|
|
|
|
|
|
|
} |
|
849
|
|
|
|
|
|
|
} |
|
850
|
0
|
|
|
|
|
0
|
return \@out; |
|
851
|
|
|
|
|
|
|
} |
|
852
|
|
|
|
|
|
|
|
|
853
|
|
|
|
|
|
|
# `String.prototype.trim()` — strip leading + trailing whitespace. |
|
854
|
|
|
|
|
|
|
# JS's `String.prototype.trim` matches `\s` in the Unicode sense |
|
855
|
|
|
|
|
|
|
# (any whitespace including non-breaking space U+00A0); Perl's `\s` |
|
856
|
|
|
|
|
|
|
# inside a regex with `/u` flag is the same. Undef receivers return |
|
857
|
|
|
|
|
|
|
# the empty string (matches JS's `String(undefined).trim()` which |
|
858
|
|
|
|
|
|
|
# would be "undefined" → "undefined", but in our template context |
|
859
|
|
|
|
|
|
|
# undef commonly means "missing prop"; rendering the empty string |
|
860
|
|
|
|
|
|
|
# is the safer choice and mirrors the JS-compat divergence we |
|
861
|
|
|
|
|
|
|
# already document for `bf->string(undef) === ""`). |
|
862
|
|
|
|
|
|
|
|
|
863
|
11
|
|
|
11
|
0
|
1849
|
sub trim ($self, $recv) { |
|
|
11
|
|
|
|
|
12
|
|
|
|
11
|
|
|
|
|
12
|
|
|
|
11
|
|
|
|
|
10
|
|
|
864
|
11
|
100
|
|
|
|
21
|
return '' unless defined $recv; |
|
865
|
10
|
100
|
|
|
|
18
|
return '' if ref($recv); |
|
866
|
8
|
|
|
|
|
12
|
my $s = "$recv"; |
|
867
|
8
|
|
|
|
|
35
|
$s =~ s/^\s+|\s+$//gu; |
|
868
|
8
|
|
|
|
|
25
|
return $s; |
|
869
|
|
|
|
|
|
|
} |
|
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
|
0
|
|
|
0
|
0
|
0
|
sub to_fixed ($self, $value, $digits = 0) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
880
|
0
|
|
|
|
|
0
|
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
|
0
|
0
|
|
|
|
0
|
return 'NaN' if _is_nan($n); |
|
885
|
0
|
0
|
|
|
|
0
|
return $n < 0 ? '-Infinity' : 'Infinity' if _is_inf($n); |
|
|
|
0
|
|
|
|
|
|
|
886
|
0
|
0
|
0
|
|
|
0
|
$digits = 0 if !defined $digits || $digits < 0; |
|
887
|
0
|
|
|
|
|
0
|
my $factor = 10 ** $digits; |
|
888
|
0
|
|
|
|
|
0
|
my $rounded = POSIX::floor($n * $factor + 0.5); |
|
889
|
0
|
|
|
|
|
0
|
return sprintf('%.*f', $digits, $rounded / $factor); |
|
890
|
|
|
|
|
|
|
} |
|
891
|
|
|
|
|
|
|
|
|
892
|
|
|
|
|
|
|
# `String.prototype.split(sep)` (#1448 Tier B) — string → ARRAY ref. |
|
893
|
|
|
|
|
|
|
# |
|
894
|
|
|
|
|
|
|
# Two JS-parity wrinkles drive the helper (a bare `split` emit would |
|
895
|
|
|
|
|
|
|
# diverge from both JS and Go): |
|
896
|
|
|
|
|
|
|
# |
|
897
|
|
|
|
|
|
|
# * Perl's `split` treats its first argument as a *regex*, so a |
|
898
|
|
|
|
|
|
|
# separator like '.' or '|' would match far too much. We |
|
899
|
|
|
|
|
|
|
# `quotemeta` it to force literal-string matching, mirroring JS's |
|
900
|
|
|
|
|
|
|
# string-separator semantics (the regex-separator form stays |
|
901
|
|
|
|
|
|
|
# refused upstream — see the parser arm). |
|
902
|
|
|
|
|
|
|
# * Perl's `split` drops trailing empty fields by default; JS keeps |
|
903
|
|
|
|
|
|
|
# them (`"a,".split(",")` is `["a", ""]`). Passing the `-1` limit |
|
904
|
|
|
|
|
|
|
# preserves them, matching JS and Go's `strings.Split`. |
|
905
|
|
|
|
|
|
|
# |
|
906
|
|
|
|
|
|
|
# An empty separator splits into individual characters (JS + Go agree). |
|
907
|
|
|
|
|
|
|
# Undef receiver renders as the single-element `['']` — the same |
|
908
|
|
|
|
|
|
|
# "missing prop → empty string" convention `bf->trim` uses. |
|
909
|
|
|
|
|
|
|
|
|
910
|
17
|
|
|
17
|
0
|
1965
|
sub split ($self, $recv, $sep = undef, $limit = undef) { |
|
|
17
|
|
|
|
|
18
|
|
|
|
17
|
|
|
|
|
19
|
|
|
|
17
|
|
|
|
|
23
|
|
|
|
17
|
|
|
|
|
19
|
|
|
|
17
|
|
|
|
|
35
|
|
|
911
|
17
|
100
|
66
|
|
|
61
|
my $s = defined $recv && !ref($recv) ? "$recv" : ''; |
|
912
|
|
|
|
|
|
|
|
|
913
|
17
|
|
|
|
|
19
|
my @parts; |
|
914
|
17
|
100
|
|
|
|
38
|
if (!defined $sep) { |
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
915
|
|
|
|
|
|
|
# No separator → the whole string in a single-element array |
|
916
|
|
|
|
|
|
|
# (matches JS `"x".split()` / `.split(undefined)`). |
|
917
|
1
|
|
|
|
|
2
|
@parts = ($s); |
|
918
|
|
|
|
|
|
|
} |
|
919
|
|
|
|
|
|
|
elsif ("$sep" eq '') { |
|
920
|
|
|
|
|
|
|
# Empty separator → individual characters. No `-1` limit here: |
|
921
|
|
|
|
|
|
|
# on an empty pattern Perl's `split` with `-1` appends a spurious |
|
922
|
|
|
|
|
|
|
# trailing empty field ("abc" → 'a','b','c',''), which JS/Go don't. |
|
923
|
2
|
|
|
|
|
5
|
@parts = split //, $s; |
|
924
|
|
|
|
|
|
|
} |
|
925
|
|
|
|
|
|
|
elsif ($s eq '') { |
|
926
|
|
|
|
|
|
|
# Empty input with a non-empty separator: JS `"".split(",")` is |
|
927
|
|
|
|
|
|
|
# `[""]` and Go's `strings.Split("", ",")` is `[""]`, but Perl's |
|
928
|
|
|
|
|
|
|
# `split /,/, ''` returns the empty list — special-case for parity. |
|
929
|
1
|
|
|
|
|
2
|
@parts = (''); |
|
930
|
|
|
|
|
|
|
} |
|
931
|
|
|
|
|
|
|
else { |
|
932
|
|
|
|
|
|
|
# `quotemeta` forces literal-string matching (JS string-separator |
|
933
|
|
|
|
|
|
|
# semantics); the `-1` keeps trailing empty fields (JS keeps them, |
|
934
|
|
|
|
|
|
|
# Perl's bare `split` drops them). |
|
935
|
13
|
|
|
|
|
18
|
my $q = quotemeta("$sep"); |
|
936
|
13
|
|
|
|
|
99
|
@parts = split /$q/, $s, -1; |
|
937
|
|
|
|
|
|
|
} |
|
938
|
|
|
|
|
|
|
|
|
939
|
|
|
|
|
|
|
# Optional `limit` caps the number of pieces (JS `split(sep, limit)`). |
|
940
|
|
|
|
|
|
|
# 0 → empty; a negative limit keeps all (JS ToUint32 wrap makes it |
|
941
|
|
|
|
|
|
|
# effectively unbounded) — both match Go's `bf_split`. |
|
942
|
17
|
100
|
|
|
|
30
|
if (defined $limit) { |
|
943
|
4
|
|
|
|
|
5
|
my $n = int($limit); |
|
944
|
4
|
100
|
100
|
|
|
31
|
if ($n == 0) { @parts = () } |
|
|
1
|
100
|
|
|
|
2
|
|
|
945
|
1
|
|
|
|
|
5
|
elsif ($n > 0 && $n < scalar @parts) { @parts = @parts[0 .. $n - 1] } |
|
946
|
|
|
|
|
|
|
} |
|
947
|
|
|
|
|
|
|
|
|
948
|
17
|
|
|
|
|
83
|
return [@parts]; |
|
949
|
|
|
|
|
|
|
} |
|
950
|
|
|
|
|
|
|
|
|
951
|
|
|
|
|
|
|
# `String.prototype.startsWith(prefix, position?)` (#1448 Tier B) — |
|
952
|
|
|
|
|
|
|
# string → boolean (1 / 0). `substr`-anchored literal comparison mirrors |
|
953
|
|
|
|
|
|
|
# Go's `strings.HasPrefix`. An empty prefix is always true (JS parity); |
|
954
|
|
|
|
|
|
|
# undef / non-string receivers coerce to the empty string first. The |
|
955
|
|
|
|
|
|
|
# optional `position` re-anchors the test (clamped to `[0, length]`), |
|
956
|
|
|
|
|
|
|
# matching JS `"abc".startsWith("b", 1)`. |
|
957
|
|
|
|
|
|
|
|
|
958
|
12
|
|
|
12
|
0
|
2598
|
sub starts_with ($self, $recv, $prefix, $position = undef) { |
|
|
12
|
|
|
|
|
13
|
|
|
|
12
|
|
|
|
|
15
|
|
|
|
12
|
|
|
|
|
13
|
|
|
|
12
|
|
|
|
|
14
|
|
|
|
12
|
|
|
|
|
13
|
|
|
959
|
12
|
100
|
66
|
|
|
44
|
my $s = defined $recv && !ref($recv) ? "$recv" : ''; |
|
960
|
12
|
50
|
|
|
|
17
|
my $p = defined $prefix ? "$prefix" : ''; |
|
961
|
12
|
100
|
|
|
|
23
|
if (defined $position) { |
|
962
|
4
|
|
|
|
|
5
|
my $n = int($position); |
|
963
|
4
|
100
|
|
|
|
8
|
$n = 0 if $n < 0; |
|
964
|
4
|
100
|
|
|
|
8
|
$n = CORE::length($s) if $n > CORE::length($s); |
|
965
|
4
|
|
|
|
|
5
|
$s = substr($s, $n); |
|
966
|
|
|
|
|
|
|
} |
|
967
|
12
|
100
|
|
|
|
49
|
return substr($s, 0, CORE::length $p) eq $p ? 1 : 0; |
|
968
|
|
|
|
|
|
|
} |
|
969
|
|
|
|
|
|
|
|
|
970
|
|
|
|
|
|
|
# `String.prototype.endsWith(suffix, endPosition?)` (#1448 Tier B) — |
|
971
|
|
|
|
|
|
|
# string → boolean (1 / 0). Mirrors Go's `strings.HasSuffix`. An empty |
|
972
|
|
|
|
|
|
|
# suffix is always true (JS parity); a suffix longer than the string is |
|
973
|
|
|
|
|
|
|
# false. `substr($s, -length $x)` would mis-read the whole string when |
|
974
|
|
|
|
|
|
|
# `length $x == 0`, so that case short-circuits. The optional |
|
975
|
|
|
|
|
|
|
# `endPosition` treats the string as if it were only that many chars |
|
976
|
|
|
|
|
|
|
# long (clamped to `[0, length]`), matching JS `"abc".endsWith("b", 2)`. |
|
977
|
|
|
|
|
|
|
|
|
978
|
10
|
|
|
10
|
0
|
15
|
sub ends_with ($self, $recv, $suffix, $end_position = undef) { |
|
|
10
|
|
|
|
|
13
|
|
|
|
10
|
|
|
|
|
9
|
|
|
|
10
|
|
|
|
|
11
|
|
|
|
10
|
|
|
|
|
9
|
|
|
|
10
|
|
|
|
|
12
|
|
|
979
|
10
|
50
|
33
|
|
|
46
|
my $s = defined $recv && !ref($recv) ? "$recv" : ''; |
|
980
|
10
|
50
|
|
|
|
17
|
my $x = defined $suffix ? "$suffix" : ''; |
|
981
|
10
|
100
|
|
|
|
17
|
if (defined $end_position) { |
|
982
|
4
|
|
|
|
|
4
|
my $e = int($end_position); |
|
983
|
4
|
100
|
|
|
|
7
|
$e = 0 if $e < 0; |
|
984
|
4
|
100
|
|
|
|
7
|
$e = CORE::length($s) if $e > CORE::length($s); |
|
985
|
4
|
|
|
|
|
7
|
$s = substr($s, 0, $e); |
|
986
|
|
|
|
|
|
|
} |
|
987
|
10
|
100
|
|
|
|
19
|
return 1 if $x eq ''; |
|
988
|
9
|
100
|
|
|
|
18
|
return 0 if CORE::length($s) < CORE::length($x); |
|
989
|
7
|
100
|
|
|
|
27
|
return substr($s, -CORE::length $x) eq $x ? 1 : 0; |
|
990
|
|
|
|
|
|
|
} |
|
991
|
|
|
|
|
|
|
|
|
992
|
|
|
|
|
|
|
# `String.prototype.replace(pattern, replacement)` — string-pattern |
|
993
|
|
|
|
|
|
|
# form only (#1448 Tier B), replacing the FIRST occurrence (JS string- |
|
994
|
|
|
|
|
|
|
# pattern semantics). Spliced via index/substr rather than `s///` so |
|
995
|
|
|
|
|
|
|
# BOTH the pattern and the replacement are literal: no Perl regex |
|
996
|
|
|
|
|
|
|
# metacharacters in the pattern and no `$1` / `$&` interpolation in the |
|
997
|
|
|
|
|
|
|
# replacement. Go's `bf_replace` (strings.Replace, n=1) treats the |
|
998
|
|
|
|
|
|
|
# replacement literally too, so the two adapters stay byte-equal — this |
|
999
|
|
|
|
|
|
|
# diverges from JS only for replacement strings containing `$`-patterns |
|
1000
|
|
|
|
|
|
|
# (rare in template position). An empty pattern inserts the replacement |
|
1001
|
|
|
|
|
|
|
# at the front (`"abc".replace("", "X")` → "Xabc"), matching JS + Go. |
|
1002
|
|
|
|
|
|
|
|
|
1003
|
9
|
|
|
9
|
0
|
3046
|
sub replace ($self, $recv, $pattern, $replacement) { |
|
|
9
|
|
|
|
|
11
|
|
|
|
9
|
|
|
|
|
12
|
|
|
|
9
|
|
|
|
|
9
|
|
|
|
9
|
|
|
|
|
8
|
|
|
|
9
|
|
|
|
|
8
|
|
|
1004
|
9
|
100
|
66
|
|
|
33
|
my $s = defined $recv && !ref($recv) ? "$recv" : ''; |
|
1005
|
9
|
50
|
|
|
|
16
|
my $o = defined $pattern ? "$pattern" : ''; |
|
1006
|
9
|
50
|
|
|
|
12
|
my $n = defined $replacement ? "$replacement" : ''; |
|
1007
|
9
|
100
|
|
|
|
16
|
return $n . $s if $o eq ''; |
|
1008
|
8
|
|
|
|
|
14
|
my $i = index($s, $o); |
|
1009
|
8
|
100
|
|
|
|
14
|
return $s if $i < 0; |
|
1010
|
6
|
|
|
|
|
40
|
return substr($s, 0, $i) . $n . substr($s, $i + CORE::length($o)); |
|
1011
|
|
|
|
|
|
|
} |
|
1012
|
|
|
|
|
|
|
|
|
1013
|
|
|
|
|
|
|
# `String.prototype.repeat(n)` — the receiver concatenated n times |
|
1014
|
|
|
|
|
|
|
# (#1448 Tier B), via Perl's `x` operator. JS throws RangeError for a |
|
1015
|
|
|
|
|
|
|
# negative count, but SSR templates degrade to the empty string rather |
|
1016
|
|
|
|
|
|
|
# than dying mid-render, so a count <= 0 returns "" (Go's `bf_repeat` |
|
1017
|
|
|
|
|
|
|
# applies the same clamp). The count is truncated toward zero |
|
1018
|
|
|
|
|
|
|
# (`int`), matching JS's ToIntegerOrInfinity on `"a".repeat(3.7)`. |
|
1019
|
|
|
|
|
|
|
|
|
1020
|
7
|
|
|
7
|
0
|
1784
|
sub repeat ($self, $recv, $count) { |
|
|
7
|
|
|
|
|
11
|
|
|
|
7
|
|
|
|
|
6
|
|
|
|
7
|
|
|
|
|
8
|
|
|
|
7
|
|
|
|
|
7
|
|
|
1021
|
7
|
100
|
66
|
|
|
26
|
my $s = defined $recv && !ref($recv) ? "$recv" : ''; |
|
1022
|
7
|
50
|
|
|
|
12
|
my $n = defined $count ? int($count) : 0; |
|
1023
|
7
|
100
|
|
|
|
25
|
return $n <= 0 ? '' : $s x $n; |
|
1024
|
|
|
|
|
|
|
} |
|
1025
|
|
|
|
|
|
|
|
|
1026
|
|
|
|
|
|
|
# `String.prototype.padStart` / `padEnd` (#1448 Tier B) — pad the |
|
1027
|
|
|
|
|
|
|
# receiver to `$target` characters with `$pad` (default a single space) |
|
1028
|
|
|
|
|
|
|
# repeated and truncated to fill, prepended or appended. Length is |
|
1029
|
|
|
|
|
|
|
# measured in characters (Perl `length`), matching Go's rune-based |
|
1030
|
|
|
|
|
|
|
# `bf_pad_*` — diverges from JS's UTF-16-unit length only for |
|
1031
|
|
|
|
|
|
|
# astral-plane input. An empty pad, or a receiver already >= `$target`, |
|
1032
|
|
|
|
|
|
|
# returns the receiver unchanged (JS parity). The `$target` is |
|
1033
|
|
|
|
|
|
|
# truncated toward zero (JS ToLength on the first arg). |
|
1034
|
|
|
|
|
|
|
|
|
1035
|
10
|
|
|
10
|
|
10
|
sub _pad ($s, $target, $pad, $at_start) { |
|
|
10
|
|
|
|
|
14
|
|
|
|
10
|
|
|
|
|
9
|
|
|
|
10
|
|
|
|
|
13
|
|
|
|
10
|
|
|
|
|
8
|
|
|
|
10
|
|
|
|
|
10
|
|
|
1036
|
10
|
100
|
|
|
|
15
|
$pad = ' ' unless defined $pad; |
|
1037
|
10
|
|
|
|
|
12
|
$pad = "$pad"; |
|
1038
|
10
|
100
|
|
|
|
20
|
return $s if $pad eq ''; |
|
1039
|
9
|
|
|
|
|
10
|
my $len = CORE::length $s; |
|
1040
|
9
|
|
50
|
|
|
16
|
my $t = int($target // 0); |
|
1041
|
9
|
100
|
|
|
|
16
|
return $s if $len >= $t; |
|
1042
|
8
|
|
|
|
|
10
|
my $need = $t - $len; |
|
1043
|
|
|
|
|
|
|
# Repeat enough copies to cover $need, then trim to exactly $need. |
|
1044
|
8
|
|
|
|
|
25
|
my $fill = substr($pad x (int($need / CORE::length($pad)) + 1), 0, $need); |
|
1045
|
8
|
100
|
|
|
|
37
|
return $at_start ? $fill . $s : $s . $fill; |
|
1046
|
|
|
|
|
|
|
} |
|
1047
|
|
|
|
|
|
|
|
|
1048
|
7
|
|
|
7
|
0
|
1621
|
sub pad_start ($self, $recv, $target, $pad = undef) { |
|
|
7
|
|
|
|
|
10
|
|
|
|
7
|
|
|
|
|
10
|
|
|
|
7
|
|
|
|
|
7
|
|
|
|
7
|
|
|
|
|
9
|
|
|
|
7
|
|
|
|
|
6
|
|
|
1049
|
7
|
100
|
66
|
|
|
29
|
my $s = defined $recv && !ref($recv) ? "$recv" : ''; |
|
1050
|
7
|
|
|
|
|
12
|
return _pad($s, $target, $pad, 1); |
|
1051
|
|
|
|
|
|
|
} |
|
1052
|
|
|
|
|
|
|
|
|
1053
|
3
|
|
|
3
|
0
|
4
|
sub pad_end ($self, $recv, $target, $pad = undef) { |
|
|
3
|
|
|
|
|
4
|
|
|
|
3
|
|
|
|
|
3
|
|
|
|
3
|
|
|
|
|
4
|
|
|
|
3
|
|
|
|
|
3
|
|
|
|
3
|
|
|
|
|
3
|
|
|
1054
|
3
|
50
|
33
|
|
|
12
|
my $s = defined $recv && !ref($recv) ? "$recv" : ''; |
|
1055
|
3
|
|
|
|
|
7
|
return _pad($s, $target, $pad, 0); |
|
1056
|
|
|
|
|
|
|
} |
|
1057
|
|
|
|
|
|
|
|
|
1058
|
|
|
|
|
|
|
# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)` |
|
1059
|
|
|
|
|
|
|
# lowering (#1448 Tier B). Non-mutating — JS's mutate-vs-new |
|
1060
|
|
|
|
|
|
|
# distinction is moot in SSR template context. |
|
1061
|
|
|
|
|
|
|
# |
|
1062
|
|
|
|
|
|
|
# Opts hash-ref. The compiler emits a `keys` list of per-key hashes |
|
1063
|
|
|
|
|
|
|
# in priority order; each hash carries: |
|
1064
|
|
|
|
|
|
|
# |
|
1065
|
|
|
|
|
|
|
# key_kind => 'self' | 'field' |
|
1066
|
|
|
|
|
|
|
# key => '' when key_kind eq 'self'; field name verbatim |
|
1067
|
|
|
|
|
|
|
# from the comparator AST (e.g. 'price', 'createdAt') |
|
1068
|
|
|
|
|
|
|
# when key_kind eq 'field' — no case normalisation |
|
1069
|
|
|
|
|
|
|
# applied. Perl hash lookups are case-sensitive so |
|
1070
|
|
|
|
|
|
|
# the key here must match the actual hash key the |
|
1071
|
|
|
|
|
|
|
# user populated. |
|
1072
|
|
|
|
|
|
|
# compare_type => 'numeric' | 'string' | 'auto' |
|
1073
|
|
|
|
|
|
|
# direction => 'asc' | 'desc' |
|
1074
|
|
|
|
|
|
|
# |
|
1075
|
|
|
|
|
|
|
# Accepted comparator catalogue (gated upstream at parse time — |
|
1076
|
|
|
|
|
|
|
# anything outside refuses with BF101 before reaching this helper): |
|
1077
|
|
|
|
|
|
|
# |
|
1078
|
|
|
|
|
|
|
# (a,b) => a.f - b.f → field, numeric |
|
1079
|
|
|
|
|
|
|
# (a,b) => a - b → self, numeric |
|
1080
|
|
|
|
|
|
|
# (a,b) => a[.f].localeCompare(b[.f]) → field|self, string |
|
1081
|
|
|
|
|
|
|
# (a,b) => a.f > b.f ? 1 : -1 → field|self, auto |
|
1082
|
|
|
|
|
|
|
# any of the above ||-chained → multi-key tie-breaks |
|
1083
|
|
|
|
|
|
|
# (and reversed-operand variants for `desc`). |
|
1084
|
|
|
|
|
|
|
# |
|
1085
|
|
|
|
|
|
|
# `auto` (relational-ternary lowering) compares numerically when both |
|
1086
|
|
|
|
|
|
|
# keys `looks_like_number`, else lexically — Go's `bf_sort` applies the |
|
1087
|
|
|
|
|
|
|
# same rule so the two template adapters stay byte-equal. |
|
1088
|
|
|
|
|
|
|
# |
|
1089
|
|
|
|
|
|
|
# A future `nulls => 'first' | 'last'` knob can land per key without |
|
1090
|
|
|
|
|
|
|
# churn — the opts hash is the right place to grow. |
|
1091
|
|
|
|
|
|
|
|
|
1092
|
16
|
|
|
16
|
0
|
7250
|
sub sort ($self, $recv, $opts = {}) { |
|
|
16
|
|
|
|
|
18
|
|
|
|
16
|
|
|
|
|
18
|
|
|
|
16
|
|
|
|
|
17
|
|
|
|
16
|
|
|
|
|
14
|
|
|
1093
|
16
|
100
|
|
|
|
42
|
return [] unless ref($recv) eq 'ARRAY'; |
|
1094
|
|
|
|
|
|
|
|
|
1095
|
|
|
|
|
|
|
# Normalise the per-key specs (priority order, length >= 1). |
|
1096
|
|
|
|
|
|
|
my @spec = map { |
|
1097
|
|
|
|
|
|
|
{ |
|
1098
|
|
|
|
|
|
|
key_kind => $_->{key_kind} // 'self', |
|
1099
|
|
|
|
|
|
|
key => $_->{key} // '', |
|
1100
|
|
|
|
|
|
|
compare_type => $_->{compare_type} // 'numeric', |
|
1101
|
15
|
|
50
|
|
|
84
|
direction => $_->{direction} // 'asc', |
|
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
1102
|
|
|
|
|
|
|
} |
|
1103
|
13
|
|
50
|
|
|
16
|
} @{ $opts->{keys} // [] }; |
|
|
13
|
|
|
|
|
30
|
|
|
1104
|
13
|
50
|
|
|
|
24
|
return [ @$recv ] unless @spec; |
|
1105
|
|
|
|
|
|
|
|
|
1106
|
|
|
|
|
|
|
# Schwartzian transform: project each item to all its sort keys |
|
1107
|
|
|
|
|
|
|
# once, then compare projected keys. Cheaper than re-resolving the |
|
1108
|
|
|
|
|
|
|
# field accessors inside every comparison for non-trivial arrays. |
|
1109
|
|
|
|
|
|
|
my @keyed = map { |
|
1110
|
13
|
|
|
|
|
19
|
my $item = $_; |
|
|
36
|
|
|
|
|
36
|
|
|
1111
|
|
|
|
|
|
|
my @ks = map { |
|
1112
|
36
|
100
|
66
|
|
|
37
|
$_->{key_kind} eq 'field' && ref($item) eq 'HASH' ? $item->{ $_->{key} } : $item; |
|
|
42
|
|
|
|
|
111
|
|
|
1113
|
|
|
|
|
|
|
} @spec; |
|
1114
|
36
|
|
|
|
|
57
|
[ \@ks, $item ]; |
|
1115
|
|
|
|
|
|
|
} @$recv; |
|
1116
|
|
|
|
|
|
|
|
|
1117
|
|
|
|
|
|
|
my $cmp = sub { |
|
1118
|
35
|
|
|
35
|
|
78
|
for my $i (0 .. $#spec) { |
|
1119
|
39
|
|
|
|
|
42
|
my $sp = $spec[$i]; |
|
1120
|
39
|
|
|
|
|
55
|
my $c = _compare_sort_key($a->[0][$i], $b->[0][$i], $sp->{compare_type}); |
|
1121
|
39
|
100
|
|
|
|
54
|
next if $c == 0; # tie on this key — try the next |
|
1122
|
32
|
100
|
|
|
|
61
|
return $sp->{direction} eq 'desc' ? -$c : $c; |
|
1123
|
|
|
|
|
|
|
} |
|
1124
|
3
|
|
|
|
|
5
|
return 0; |
|
1125
|
13
|
|
|
|
|
45
|
}; |
|
1126
|
|
|
|
|
|
|
|
|
1127
|
13
|
|
|
|
|
40
|
my @sorted = sort $cmp @keyed; |
|
1128
|
13
|
|
|
|
|
24
|
return [ map { $_->[1] } @sorted ]; |
|
|
36
|
|
|
|
|
147
|
|
|
1129
|
|
|
|
|
|
|
} |
|
1130
|
|
|
|
|
|
|
|
|
1131
|
|
|
|
|
|
|
# Compare two projected keys, ascending orientation (-1 / 0 / 1); the |
|
1132
|
|
|
|
|
|
|
# caller negates for 'desc'. 'auto' compares numerically when both |
|
1133
|
|
|
|
|
|
|
# keys look like numbers, else lexically (matches Go's `bf_sort`). |
|
1134
|
|
|
|
|
|
|
# undef coalesces to '' / 0 so the order stays total without warnings. |
|
1135
|
39
|
|
|
39
|
|
37
|
sub _compare_sort_key ($av, $bv, $compare_type) { |
|
|
39
|
|
|
|
|
36
|
|
|
|
39
|
|
|
|
|
33
|
|
|
|
39
|
|
|
|
|
31
|
|
|
|
39
|
|
|
|
|
28
|
|
|
1136
|
39
|
100
|
|
|
|
53
|
if ($compare_type eq 'string') { |
|
1137
|
10
|
|
50
|
|
|
22
|
return ($av // '') cmp ($bv // ''); |
|
|
|
|
50
|
|
|
|
|
|
1138
|
|
|
|
|
|
|
} |
|
1139
|
29
|
100
|
|
|
|
36
|
if ($compare_type eq 'auto') { |
|
1140
|
9
|
100
|
50
|
|
|
91
|
if (looks_like_number($av // '') && looks_like_number($bv // '')) { |
|
|
|
|
50
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
1141
|
6
|
|
50
|
|
|
17
|
return ($av // 0) <=> ($bv // 0); |
|
|
|
|
50
|
|
|
|
|
|
1142
|
|
|
|
|
|
|
} |
|
1143
|
3
|
|
50
|
|
|
13
|
return ($av // '') cmp ($bv // ''); |
|
|
|
|
50
|
|
|
|
|
|
1144
|
|
|
|
|
|
|
} |
|
1145
|
20
|
|
50
|
|
|
48
|
return ($av // 0) <=> ($bv // 0); # numeric |
|
|
|
|
50
|
|
|
|
|
|
1146
|
|
|
|
|
|
|
} |
|
1147
|
|
|
|
|
|
|
|
|
1148
|
|
|
|
|
|
|
# Fold an array into a scalar via the arithmetic-fold catalogue |
|
1149
|
|
|
|
|
|
|
# (#1448 Tier C). Mirrors Go's `bf_reduce` and JS `reduce(fn, init)` / |
|
1150
|
|
|
|
|
|
|
# `reduceRight(fn, init)` for the shapes `(acc, x) => acc x` / |
|
1151
|
|
|
|
|
|
|
# `(acc, x) => acc x.field`: |
|
1152
|
|
|
|
|
|
|
# |
|
1153
|
|
|
|
|
|
|
# bf->reduce($recv, { |
|
1154
|
|
|
|
|
|
|
# op => '+' | '*', |
|
1155
|
|
|
|
|
|
|
# key_kind => 'self' | 'field', |
|
1156
|
|
|
|
|
|
|
# key => '', # when key_kind eq 'field' |
|
1157
|
|
|
|
|
|
|
# type => 'numeric' | 'string', |
|
1158
|
|
|
|
|
|
|
# init => , # number, or string for concat |
|
1159
|
|
|
|
|
|
|
# direction => 'left' | 'right', # 'right' = reduceRight (default 'left') |
|
1160
|
|
|
|
|
|
|
# }) |
|
1161
|
|
|
|
|
|
|
# |
|
1162
|
|
|
|
|
|
|
# Numeric folds accumulate with `+` / `*` (non-numeric keys coalesce to |
|
1163
|
|
|
|
|
|
|
# 0); string folds concatenate via `bf->string` (undef → ''). The init |
|
1164
|
|
|
|
|
|
|
# seeds the accumulator, so an empty array returns it unchanged — exactly |
|
1165
|
|
|
|
|
|
|
# like JS. `direction => 'right'` folds right-to-left (reduceRight); only |
|
1166
|
|
|
|
|
|
|
# observable for string concat, since numeric sum / product commute. |
|
1167
|
|
|
|
|
|
|
# Float stringification can diverge from Go's for inexact binary |
|
1168
|
|
|
|
|
|
|
# fractions (e.g. 0.1 + 0.2); integer sums — the common case — agree. |
|
1169
|
0
|
|
|
0
|
0
|
0
|
sub reduce ($self, $recv, $opts = {}) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
1170
|
0
|
|
0
|
|
|
0
|
my $op = $opts->{op} // '+'; |
|
1171
|
0
|
|
0
|
|
|
0
|
my $key_kind = $opts->{key_kind} // 'self'; |
|
1172
|
0
|
|
0
|
|
|
0
|
my $key = $opts->{key} // ''; |
|
1173
|
0
|
|
0
|
|
|
0
|
my $type = $opts->{type} // 'numeric'; |
|
1174
|
0
|
|
0
|
|
|
0
|
my $direction = $opts->{direction} // 'left'; |
|
1175
|
|
|
|
|
|
|
|
|
1176
|
0
|
0
|
|
|
|
0
|
my @items = ref($recv) eq 'ARRAY' ? @$recv : (); |
|
1177
|
|
|
|
|
|
|
# reduceRight folds right-to-left; reversing the snapshot keeps the |
|
1178
|
|
|
|
|
|
|
# single forward loop below. Only observable for string concat — |
|
1179
|
|
|
|
|
|
|
# numeric sum / product commute. Qualify as CORE::reverse — this |
|
1180
|
|
|
|
|
|
|
# package defines `sub reverse` (the `.reverse()` helper), so a bare |
|
1181
|
|
|
|
|
|
|
# `reverse` is ambiguous under `use warnings`. |
|
1182
|
0
|
0
|
|
|
|
0
|
@items = CORE::reverse(@items) if $direction eq 'right'; |
|
1183
|
0
|
|
|
0
|
|
0
|
my $project = sub ($item) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
1184
|
0
|
0
|
0
|
|
|
0
|
$key_kind eq 'field' && ref($item) eq 'HASH' ? $item->{$key} : $item; |
|
1185
|
0
|
|
|
|
|
0
|
}; |
|
1186
|
|
|
|
|
|
|
|
|
1187
|
0
|
0
|
|
|
|
0
|
if ($type eq 'string') { |
|
1188
|
0
|
|
0
|
|
|
0
|
my $acc = $opts->{init} // ''; |
|
1189
|
0
|
|
|
|
|
0
|
$acc .= $self->string($project->($_)) for @items; |
|
1190
|
0
|
|
|
|
|
0
|
return $acc; |
|
1191
|
|
|
|
|
|
|
} |
|
1192
|
|
|
|
|
|
|
|
|
1193
|
0
|
|
0
|
|
|
0
|
my $acc = $opts->{init} // 0; |
|
1194
|
0
|
|
|
|
|
0
|
for my $item (@items) { |
|
1195
|
0
|
|
|
|
|
0
|
my $n = $project->($item); |
|
1196
|
|
|
|
|
|
|
# Guard `defined` before `looks_like_number` so a missing field |
|
1197
|
|
|
|
|
|
|
# (undef) folds as 0 without an "uninitialized value" warning |
|
1198
|
|
|
|
|
|
|
# under `use warnings` — matching the `$av // ''` style `sort` uses. |
|
1199
|
0
|
0
|
0
|
|
|
0
|
$n = 0 unless defined $n && looks_like_number($n); |
|
1200
|
0
|
0
|
|
|
|
0
|
$op eq '*' ? ($acc *= $n) : ($acc += $n); |
|
1201
|
|
|
|
|
|
|
} |
|
1202
|
0
|
|
|
|
|
0
|
return $acc; |
|
1203
|
|
|
|
|
|
|
} |
|
1204
|
|
|
|
|
|
|
|
|
1205
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
1206
|
|
|
|
|
|
|
# JSX intrinsic-element spread (#1407) |
|
1207
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- |
|
1208
|
|
|
|
|
|
|
# |
|
1209
|
|
|
|
|
|
|
# Mirrors the JS `spreadAttrs` runtime |
|
1210
|
|
|
|
|
|
|
# (`packages/client/src/runtime/spread-attrs.ts`) and the Go adapter's |
|
1211
|
|
|
|
|
|
|
# `bf.SpreadAttrs` so SSR output stays byte-equal across the three |
|
1212
|
|
|
|
|
|
|
# adapters. Generated Mojo templates invoke this as |
|
1213
|
|
|
|
|
|
|
# `<%== bf->spread_attrs($bag) %>`. |
|
1214
|
|
|
|
|
|
|
# |
|
1215
|
|
|
|
|
|
|
# Skip rules: nil/false values, event handlers (`on[A-Z]…` shape |
|
1216
|
|
|
|
|
|
|
# matching JS `key[2] === key[2].toUpperCase()` — true for any |
|
1217
|
|
|
|
|
|
|
# character whose uppercase is itself, including digits and |
|
1218
|
|
|
|
|
|
|
# underscore), `children`. `ref` is intentionally NOT filtered, |
|
1219
|
|
|
|
|
|
|
# matching the JS reference. |
|
1220
|
|
|
|
|
|
|
# |
|
1221
|
|
|
|
|
|
|
# Key remap: className → class, htmlFor → for; SVG camelCase |
|
1222
|
|
|
|
|
|
|
# attrs preserved (case-sensitive XML spec); other camelCase keys |
|
1223
|
|
|
|
|
|
|
# lowered to kebab-case with a leading `-` for an initial |
|
1224
|
|
|
|
|
|
|
# uppercase letter (mirrors JS `key.replace(/([A-Z])/g, '-$1')`). |
|
1225
|
|
|
|
|
|
|
# |
|
1226
|
|
|
|
|
|
|
# `style` is routed through `_style_to_css` so object literals |
|
1227
|
|
|
|
|
|
|
# serialise to a real CSS string instead of Perl's default |
|
1228
|
|
|
|
|
|
|
# `HASH(0x...)` form. |
|
1229
|
|
|
|
|
|
|
# |
|
1230
|
|
|
|
|
|
|
# Output is deterministic: keys are sorted alphabetically before |
|
1231
|
|
|
|
|
|
|
# emission, matching the Go adapter's `sort.Strings(keys)` policy |
|
1232
|
|
|
|
|
|
|
# and Mojo::JSON's marshal order. |
|
1233
|
|
|
|
|
|
|
# |
|
1234
|
|
|
|
|
|
|
# The return value is a Mojo::ByteStream so the calling template's |
|
1235
|
|
|
|
|
|
|
# `<%==` raw-emit skips re-escaping (the helper has already |
|
1236
|
|
|
|
|
|
|
# HTML-escaped each value). |
|
1237
|
|
|
|
|
|
|
|
|
1238
|
|
|
|
|
|
|
my %SVG_CAMEL_CASE_ATTRS = map { $_ => 1 } qw( |
|
1239
|
|
|
|
|
|
|
allowReorder attributeName attributeType autoReverse |
|
1240
|
|
|
|
|
|
|
baseFrequency baseProfile calcMode clipPathUnits |
|
1241
|
|
|
|
|
|
|
contentScriptType contentStyleType diffuseConstant edgeMode |
|
1242
|
|
|
|
|
|
|
externalResourcesRequired filterRes filterUnits glyphRef |
|
1243
|
|
|
|
|
|
|
gradientTransform gradientUnits kernelMatrix kernelUnitLength |
|
1244
|
|
|
|
|
|
|
keyPoints keySplines keyTimes lengthAdjust limitingConeAngle |
|
1245
|
|
|
|
|
|
|
markerHeight markerUnits markerWidth maskContentUnits |
|
1246
|
|
|
|
|
|
|
maskUnits numOctaves pathLength patternContentUnits |
|
1247
|
|
|
|
|
|
|
patternTransform patternUnits pointsAtX pointsAtY pointsAtZ |
|
1248
|
|
|
|
|
|
|
preserveAlpha preserveAspectRatio primitiveUnits refX refY |
|
1249
|
|
|
|
|
|
|
repeatCount repeatDur requiredExtensions requiredFeatures |
|
1250
|
|
|
|
|
|
|
specularConstant specularExponent spreadMethod startOffset |
|
1251
|
|
|
|
|
|
|
stdDeviation stitchTiles surfaceScale systemLanguage |
|
1252
|
|
|
|
|
|
|
tableValues targetX targetY textLength viewBox viewTarget |
|
1253
|
|
|
|
|
|
|
xChannelSelector yChannelSelector zoomAndPan |
|
1254
|
|
|
|
|
|
|
); |
|
1255
|
|
|
|
|
|
|
|
|
1256
|
23
|
|
|
23
|
|
33
|
sub _to_attr_name ($key) { |
|
|
23
|
|
|
|
|
25
|
|
|
|
23
|
|
|
|
|
20
|
|
|
1257
|
23
|
100
|
|
|
|
26
|
return 'class' if $key eq 'className'; |
|
1258
|
22
|
100
|
|
|
|
26
|
return 'for' if $key eq 'htmlFor'; |
|
1259
|
21
|
100
|
|
|
|
61
|
return $key if $SVG_CAMEL_CASE_ATTRS{$key}; |
|
1260
|
|
|
|
|
|
|
# camelCase → kebab-case, with a leading `-` for an initial |
|
1261
|
|
|
|
|
|
|
# uppercase letter (JS-reference parity, even though that case |
|
1262
|
|
|
|
|
|
|
# produces an HTML-invalid attribute name — same documented |
|
1263
|
|
|
|
|
|
|
# behaviour as the Go adapter's `toAttrName`). |
|
1264
|
19
|
|
|
|
|
20
|
my $out = $key; |
|
1265
|
19
|
|
|
|
|
48
|
$out =~ s/([A-Z])/-\L$1/g; |
|
1266
|
19
|
|
|
|
|
28
|
return $out; |
|
1267
|
|
|
|
|
|
|
} |
|
1268
|
|
|
|
|
|
|
|
|
1269
|
24
|
|
|
24
|
|
22
|
sub _html_escape ($value) { |
|
|
24
|
|
|
|
|
23
|
|
|
|
24
|
|
|
|
|
19
|
|
|
1270
|
|
|
|
|
|
|
# HTML attribute-value escape for SSR string emission. The |
|
1271
|
|
|
|
|
|
|
# spread bag's values reach the browser as part of a generated |
|
1272
|
|
|
|
|
|
|
# `key="..."` substring inside the rendered HTML, so the |
|
1273
|
|
|
|
|
|
|
# escape set has to cover everything that could break either |
|
1274
|
|
|
|
|
|
|
# the surrounding double-quoted attribute or the enclosing |
|
1275
|
|
|
|
|
|
|
# tag: `&`, `<`, `>`, `"`, and `'`. Matches Go's |
|
1276
|
|
|
|
|
|
|
# `template.HTMLEscapeString` semantics byte-for-byte (using |
|
1277
|
|
|
|
|
|
|
# `"` / `'` for quotes rather than the named entities) |
|
1278
|
|
|
|
|
|
|
# so the SSR output is identical across the Go and Mojo |
|
1279
|
|
|
|
|
|
|
# adapters (#1407, #1413 review). The CSR-side |
|
1280
|
|
|
|
|
|
|
# `applyRestAttrs` calls `el.setAttribute(name, String(value))` |
|
1281
|
|
|
|
|
|
|
# — which does its own DOM-level escaping in the browser — |
|
1282
|
|
|
|
|
|
|
# so JS doesn't need an explicit escape pass; Perl/Go emit a |
|
1283
|
|
|
|
|
|
|
# string, so we do. |
|
1284
|
24
|
50
|
|
|
|
65
|
my $s = defined $value ? "$value" : ''; |
|
1285
|
24
|
|
|
|
|
33
|
$s =~ s/&/&/g; |
|
1286
|
24
|
|
|
|
|
51
|
$s =~ s/</g; |
|
1287
|
24
|
|
|
|
|
25
|
$s =~ s/>/>/g; |
|
1288
|
24
|
|
|
|
|
25
|
$s =~ s/"/"/g; |
|
1289
|
24
|
|
|
|
|
25
|
$s =~ s/'/'/g; |
|
1290
|
24
|
|
|
|
|
55
|
return $s; |
|
1291
|
|
|
|
|
|
|
} |
|
1292
|
|
|
|
|
|
|
|
|
1293
|
2
|
|
|
2
|
|
3
|
sub _style_to_css ($value) { |
|
|
2
|
|
|
|
|
2
|
|
|
|
2
|
|
|
|
|
11
|
|
|
1294
|
2
|
50
|
|
|
|
4
|
return undef unless defined $value; |
|
1295
|
|
|
|
|
|
|
# Non-hashref values pass through stringified — matches the JS |
|
1296
|
|
|
|
|
|
|
# `typeof value !== 'object'` branch in `styleToCss`. |
|
1297
|
2
|
100
|
|
|
|
5
|
if (ref($value) ne 'HASH') { |
|
1298
|
1
|
|
|
|
|
2
|
my $s = "$value"; |
|
1299
|
1
|
50
|
|
|
|
3
|
return CORE::length $s ? $s : undef; |
|
1300
|
|
|
|
|
|
|
} |
|
1301
|
1
|
|
|
|
|
2
|
my @parts; |
|
1302
|
1
|
|
|
|
|
3
|
for my $key (sort keys %$value) { |
|
1303
|
2
|
|
|
|
|
2
|
my $v = $value->{$key}; |
|
1304
|
2
|
50
|
|
|
|
3
|
next unless defined $v; |
|
1305
|
2
|
|
|
|
|
3
|
my $prop = $key; |
|
1306
|
2
|
|
|
|
|
9
|
$prop =~ s/([A-Z])/-\L$1/g; |
|
1307
|
2
|
|
|
|
|
5
|
push @parts, "$prop:$v"; |
|
1308
|
|
|
|
|
|
|
} |
|
1309
|
1
|
50
|
|
|
|
5
|
return @parts ? CORE::join(';', @parts) : undef; |
|
1310
|
|
|
|
|
|
|
} |
|
1311
|
|
|
|
|
|
|
|
|
1312
|
25
|
|
|
25
|
0
|
167989
|
sub spread_attrs ($self, $bag) { |
|
|
25
|
|
|
|
|
31
|
|
|
|
25
|
|
|
|
|
26
|
|
|
|
25
|
|
|
|
|
19
|
|
|
1313
|
25
|
100
|
100
|
|
|
119
|
return '' unless defined $bag && ref($bag) eq 'HASH'; |
|
1314
|
23
|
|
|
|
|
26
|
my @parts; |
|
1315
|
23
|
|
|
|
|
57
|
for my $key (sort keys %$bag) { |
|
1316
|
|
|
|
|
|
|
# Event handlers: skip when key starts `on` and the third |
|
1317
|
|
|
|
|
|
|
# character is its own uppercase form (uppercase letter, |
|
1318
|
|
|
|
|
|
|
# digit, underscore, …). Mirrors the JS predicate. |
|
1319
|
31
|
100
|
100
|
|
|
80
|
if (CORE::length($key) > 2 && substr($key, 0, 2) eq 'on') { |
|
1320
|
4
|
|
|
|
|
7
|
my $c = substr($key, 2, 1); |
|
1321
|
4
|
100
|
|
|
|
10
|
next if CORE::uc($c) eq $c; |
|
1322
|
|
|
|
|
|
|
} |
|
1323
|
28
|
100
|
|
|
|
44
|
next if $key eq 'children'; |
|
1324
|
27
|
|
|
|
|
30
|
my $val = $bag->{$key}; |
|
1325
|
|
|
|
|
|
|
# null / undef → drop. |
|
1326
|
27
|
100
|
|
|
|
36
|
next unless defined $val; |
|
1327
|
|
|
|
|
|
|
# Boolean values arrive as Mojo::JSON sentinel objects |
|
1328
|
|
|
|
|
|
|
# (`Mojo::JSON::true` / `false`) — both from JSON-deserialised |
|
1329
|
|
|
|
|
|
|
# props and from the test harness's `toPerlLiteral` |
|
1330
|
|
|
|
|
|
|
# (which emits the sentinels rather than plain 0/1 to avoid |
|
1331
|
|
|
|
|
|
|
# conflating booleans with numeric attribute values like |
|
1332
|
|
|
|
|
|
|
# `tabindex="0"`). The contract is: callers MUST use the |
|
1333
|
|
|
|
|
|
|
# sentinels for boolean values; plain Perl scalars 0/1 |
|
1334
|
|
|
|
|
|
|
# render as numeric attribute values, matching how JS |
|
1335
|
|
|
|
|
|
|
# `spreadAttrs` treats a `0`/`1` JS number. |
|
1336
|
26
|
100
|
66
|
|
|
59
|
if (ref($val) eq 'JSON::PP::Boolean' || ref($val) eq 'Mojo::JSON::_Bool') { |
|
1337
|
2
|
100
|
|
|
|
23
|
next unless $val; |
|
1338
|
1
|
|
|
|
|
9
|
push @parts, _to_attr_name($key); |
|
1339
|
1
|
|
|
|
|
2
|
next; |
|
1340
|
|
|
|
|
|
|
} |
|
1341
|
|
|
|
|
|
|
# `style` routes through `_style_to_css` so object literals |
|
1342
|
|
|
|
|
|
|
# serialise to a real CSS string. |
|
1343
|
24
|
100
|
|
|
|
30
|
if ($key eq 'style') { |
|
1344
|
2
|
|
|
|
|
4
|
my $css = _style_to_css($val); |
|
1345
|
2
|
50
|
33
|
|
|
6
|
next unless defined $css && CORE::length $css; |
|
1346
|
2
|
|
|
|
|
5
|
push @parts, qq{style="} . _html_escape($css) . qq{"}; |
|
1347
|
2
|
|
|
|
|
4
|
next; |
|
1348
|
|
|
|
|
|
|
} |
|
1349
|
22
|
|
|
|
|
30
|
my $name = _to_attr_name($key); |
|
1350
|
22
|
|
|
|
|
30
|
push @parts, $name . qq{="} . _html_escape($val) . qq{"}; |
|
1351
|
|
|
|
|
|
|
} |
|
1352
|
23
|
100
|
|
|
|
67
|
return '' unless @parts; |
|
1353
|
|
|
|
|
|
|
# Mark the result raw so the calling template's `<%==` raw-emit |
|
1354
|
|
|
|
|
|
|
# doesn't re-escape the already-escaped values (the Mojo backend |
|
1355
|
|
|
|
|
|
|
# returns a Mojo::ByteStream). |
|
1356
|
22
|
|
|
|
|
37
|
return $self->backend->mark_raw(CORE::join(' ', @parts)); |
|
1357
|
|
|
|
|
|
|
} |
|
1358
|
|
|
|
|
|
|
|
|
1359
|
|
|
|
|
|
|
1; |
|
1360
|
|
|
|
|
|
|
__END__ |