BarefootJS
view release on metacpan or search on metacpan
lib/BarefootJS.pm view on Meta::CPAN
next;
}
if ($d->{isRestProps}) {
$extra{$name} = exists $props->{$name} ? $props->{$name} : $d->{value};
next;
}
my $prop_name = $d->{propName};
if (defined $prop_name && exists $props->{$prop_name} && defined $props->{$prop_name}) {
$extra{$name} = $props->{$prop_name};
} else {
$extra{$name} = $d->{value};
}
}
return %extra;
}
# ---------------------------------------------------------------------------
# Script Output
# ---------------------------------------------------------------------------
sub scripts ($self) {
my @tags;
for my $path (@{$self->_scripts}) {
push @tags, qq{<script type="module" src="$path"></script>};
}
return join("\n", @tags);
}
# ---------------------------------------------------------------------------
# Streaming SSR (Out-of-Order)
# ---------------------------------------------------------------------------
sub streaming_bootstrap ($self) {
return q{<script>(function(){function s(id){var a=document.querySelector('[bf-async="'+id+'"]');var t=document.querySelector('template[bf-async-resolve="'+id+'"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('b...
}
sub async_boundary ($self, $id, $fallback_html) {
# The fallback comes in via Mojo `begin %>...<% end` capture (see
# MojoAdapter::renderAsync), which produces a CODE ref returning a
# Mojo::ByteStream. Materialize it through the backend so the rendered
# HTML embeds in the placeholder rather than the CODE ref's
# stringification.
$fallback_html = $self->backend->materialize($fallback_html);
return qq{<div bf-async="$id">$fallback_html</div>};
}
sub async_resolve ($self, $id, $content_html) {
return qq{<template bf-async-resolve="$id">$content_html</template><script>__bf_swap("$id")</script>};
}
# ---------------------------------------------------------------------------
# JS-compat callees (#1189) â invoked from generated Mojo templates as
# <%= bf->json($val) %>, <%= bf->floor($val) %>, etc. The MojoAdapter's
# `templatePrimitives` registry emits these helper calls in place of the
# corresponding JS callees (`JSON.stringify`, `Math.floor`, â¦) so the SSR
# template can render value-equivalent output without a JS engine.
#
# Failure policy mirrors the Go adapter (#1188): user-data marshalling
# (json) bubbles errors so Mojolicious aborts loudly on cycles /
# unsupported values rather than silently producing an empty payload.
# Numeric coercion follows JS semantics (NaN propagates as the special
# string 'NaN'; non-numeric input returns 'NaN' rather than 0). Strings
# always coerce to a string representation.
# ---------------------------------------------------------------------------
sub json ($self, $value) {
# Mojo::JSON::to_json returns a character string (not bytes), suitable
# for embedding in HTML output via Mojo::ByteStream / `<%==`.
#
# Documented divergence from JS: JS distinguishes `null` (renders as
# "null") from `undefined` (`JSON.stringify(undefined)` returns the
# JS value `undefined`, not a string). Perl has no such distinction
# â both map to `undef`. We choose the `null` rendering for SSR
# ergonomics: an unset prop becomes the string "null" rather than
# the literal text "undefined" or an empty attribute. Matches the
# `null` case of JS exactly; diverges from the `undefined` case.
return $self->backend->encode_json($value);
}
sub string ($self, $value) {
# JS `String(v)` mirror. `undef` renders as the empty string here so
# an unset prop doesn't surface as a literal "undefined" / "null"
# in user-facing HTML â same divergence the Go adapter documents
# for `bf_string`.
return defined $value ? "$value" : '';
}
sub number ($self, $value) {
# JS `Number(v)` mirror. Numeric coerces via Perl's implicit
# numeric context; non-numeric / undef yield real numeric NaN
# (`'nan' + 0`) so downstream arithmetic propagates correctly
# (`Math.floor(NaN) === NaN`). Returning the literal string
# "NaN" would conflate the user-passing-the-string-"NaN" case
# with the parse-failure case, and break NaN detection in
# downstream helpers.
return 0 + 'nan' unless defined $value;
return $value + 0 if looks_like_number($value);
return 0 + 'nan';
}
# NaN is the only float for which `$x != $x` holds. Used as the
# portable sentinel check in floor/ceil/round.
sub _is_nan { my $n = shift; return $n != $n }
# True for +/-Infinity. `9**9**9` is Perl's portable infinity literal; a
# finite number is always strictly less than +Inf in magnitude.
sub _is_inf { my $n = shift; return $n == 9**9**9 || $n == -9**9**9 }
sub floor ($self, $value) {
my $n = $self->number($value);
return $n if _is_nan($n);
return POSIX::floor($n);
}
sub ceil ($self, $value) {
my $n = $self->number($value);
return $n if _is_nan($n);
return POSIX::ceil($n);
}
sub round ($self, $value) {
my $n = $self->number($value);
return $n if _is_nan($n);
# POSIX has no `round`. JS `Math.round` rounds half toward
# +Infinity (so `Math.round(-1.5) === -1`, not -2). `floor(n
# + 0.5)` reproduces that for both signs.
return POSIX::floor($n + 0.5);
}
# ---------------------------------------------------------------------------
# Array / String method helpers (#1448 Tier A)
# ---------------------------------------------------------------------------
#
# `Array.prototype.includes(x)` and `String.prototype.includes(sub)`
# share a method name in JS; the JSX parser can't tell the two
# receiver shapes apart without TS type inference, so both lower to
# the same IR node (`array-method` / method `includes`). This helper
# dispatches at the Perl level via `ref()`:
# - ARRAY ref: scan elements with `BarefootJS::Evaluator::_same_value_zero`,
# matching `Array.prototype.includes`'s SameValueZero
# semantics (no cross-type coercion, e.g. `[2].includes("2")`
# is false; NaN matches NaN) â the same algorithm the
# evaluator's serialized-callback path already uses for
# `.includes`, so both positions agree. This used to be a
# stringy `eq` scan, which coerced numbers to strings
# (`[2].includes("2")` was true) and diverged from JS.
# - scalar: `index($recv, $sub) != -1`, with both args
# coerced through `// ''` so an undef receiver /
# needle doesn't trip Perl's substr warning.
# Anything else (HASH ref, code ref) returns false â matches the
# JS semantic where `.includes` is only defined on Array /
# TypedArray / String.
sub includes ($self, $recv, $elem) {
if (ref($recv) eq 'ARRAY') {
for my $item (@$recv) {
return 1 if BarefootJS::Evaluator::_same_value_zero($item, $elem);
}
return 0;
}
return 0 if ref($recv);
return index($recv // '', $elem // '') != -1 ? 1 : 0;
}
# `Array.prototype.filter(fn)` / `.every(fn)` / `.some(fn)`. The Xslate adapter
# lowers a JS arrow predicate to a Kolon lambda (`-> $x { ... }`), which is
# callable from Perl as a code ref, and emits `$bf.filter($arr, <lambda>)`.
# `filter` returns a new arrayref; `every` / `some` return 1/0. Non-array /
# empty receivers follow JS (`filter` â [], `every` â true, `some` â false).
# (The Mojo adapter lowers these shapes inline and never reaches these methods.)
sub filter ($self, $recv, $pred) {
return [] unless ref($recv) eq 'ARRAY';
return [ grep { $pred->($_) } @$recv ];
}
sub every ($self, $recv, $pred) {
return 1 unless ref($recv) eq 'ARRAY';
for my $item (@$recv) { return 0 unless $pred->($item) }
return 1;
}
sub some ($self, $recv, $pred) {
return 0 unless ref($recv) eq 'ARRAY';
for my $item (@$recv) { return 1 if $pred->($item) }
return 0;
}
# `Array.prototype.find(fn)` / `.findIndex(fn)` / `.findLast(fn)` /
# `.findLastIndex(fn)` â same Kolon-lambda predicate mechanism as filter. The
# camelCase JS names lower to these snake_case methods (like index_of /
# last_index_of). `find` / `find_last` return the matching element (or undef â
# JS `undefined`); the index forms return the 0-based position (or -1).
sub find ($self, $recv, $pred) {
return undef unless ref($recv) eq 'ARRAY';
for my $item (@$recv) { return $item if $pred->($item) }
return undef;
}
sub find_index ($self, $recv, $pred) {
return -1 unless ref($recv) eq 'ARRAY';
for my $i (0 .. $#$recv) { return $i if $pred->($recv->[$i]) }
return -1;
lib/BarefootJS.pm view on Meta::CPAN
# - start < 0 after clamp â 0
# - end > length â length
# - start >= end â empty
# - end undef â "to length"
# Non-array receivers return an empty ARRAY ref.
sub slice ($self, $recv, $start, $end) {
return [] unless ref($recv) eq 'ARRAY';
my $len = scalar @$recv;
return [] if $len == 0;
my $s = $start // 0;
$s = $len + $s if $s < 0;
$s = 0 if $s < 0;
$s = $len if $s > $len;
my $e = defined $end ? $end : $len;
$e = $len + $e if $e < 0;
$e = 0 if $e < 0;
$e = $len if $e > $len;
return [] if $s >= $e;
return [ @{$recv}[$s .. $e - 1] ];
}
# `Array.prototype.reverse()` / `Array.prototype.toReversed()` â
# both shapes share this lowering. SSR templates render a snapshot
# of state, so JS's mutate-receiver (`reverse`) vs
# return-new-array (`toReversed`) distinction has no template-
# level meaning. Always returns a new ARRAY ref to keep callers
# safe from accidental aliasing. Non-array receivers return an
# empty ARRAY ref.
sub reverse ($self, $recv) {
return [] unless ref($recv) eq 'ARRAY';
return [ reverse @$recv ];
}
# `Array.prototype.flat(depth?)` (#1448 Tier C) â flatten nested ARRAY
# refs `$depth` levels deep. A `$depth` of -1 is the `Infinity` sentinel
# (flatten fully); 0 returns a shallow copy. Non-ARRAY elements are kept
# as-is (JS only flattens nested arrays). Non-ARRAY receiver â [].
sub flat ($self, $recv, $depth = 1) {
return [] unless ref($recv) eq 'ARRAY';
my @out;
for my $el (@$recv) {
if ($depth != 0 && ref($el) eq 'ARRAY') {
my $next = $depth > 0 ? $depth - 1 : $depth;
push @out, @{ $self->flat($el, $next) };
}
else {
push @out, $el;
}
}
return \@out;
}
# `Array.prototype.flat(depth)` with a DYNAMIC depth (#2094) â the depth is
# itself an arbitrary runtime value (e.g. a prop), not a compile-time literal,
# so it must be coerced with JS's `ToIntegerOrInfinity` before delegating to
# `flat` above: truncate toward zero; negative -> 0; NaN / non-numeric -> 0;
# +Infinity or a huge finite value -> flatten fully.
#
# Deliberately a SEPARATE entry point from `flat`, not a smarter version of
# it: the literal-depth path's `-1` argument is a compile-time SENTINEL baked
# into the template source, meaning "the source literally said `Infinity`". A
# genuinely dynamic depth that happens to evaluate to `-1` at render time
# means the OPPOSITE in real JS (`[1,[2]].flat(-1)` never recurses â same as
# `.flat(0)`). Since both paths would otherwise hand the same literal-looking
# argument to one shared function, that function couldn't tell which case
# it's in â so the two stay separate. Mirrors Go's `FlatDynamicDepth` /
# `coerceFlatDepth` in bf.go.
sub flat_dynamic ($self, $recv, $depth) {
return $self->flat($recv, _coerce_flat_depth($depth));
}
# _coerce_flat_depth: JS `ToIntegerOrInfinity` for a dynamic `.flat(depth)`
# argument, collapsed to `flat`'s int contract (`-1` == flatten fully).
#
# Perl's scalar type system blurs string/number duality, so `looks_like_number`
# (already the codebase's ToNumber-style coercion check, see BarefootJS::
# Evaluator's `_to_number`) is reused here rather than inventing a new
# convention. `looks_like_number` on this Perl (5.38) already recognises the
# strings "Infinity" / "-Infinity" / "NaN" as numeric (verified empirically:
# `perl -MScalar::Util=looks_like_number -e 'print looks_like_number("Infinity")'`
# prints 1), and `$str + 0` on those strings yields the corresponding Perl
# non-finite double, so no extra string special-casing is needed here.
sub _coerce_flat_depth ($depth) {
return 0 unless defined $depth;
my $f;
if (ref($depth) eq 'JSON::PP::Boolean') {
$f = $depth ? 1 : 0;
}
elsif (!ref($depth) && looks_like_number($depth)) {
$f = $depth + 0;
}
else {
# undef handled above; anything else non-numeric (a plain string
# that isn't a number, a HASH/ARRAY ref, ...) coerces via NaN.
return 0;
}
return 0 if $f != $f; # NaN
return -1 if $f == 9**9**9; # +Infinity -> flatten fully sentinel
return 0 if $f == -(9**9**9); # -Infinity -> 0
my $trunc = int($f); # Perl's int() truncates toward zero
return 0 if $trunc < 0;
return -1 if $trunc > 1_000_000; # huge finite ~= flatten fully
return $trunc;
}
# `Array.prototype.flatMap(fn)` value-returning field projection
# (#1448 Tier C) â map each element through a self / field projection,
# then flatten one level. `field` reads a HASH-ref key (the raw JS prop
# name, as `bf->reduce` does); a projected non-ARRAY value is kept as-is
# (flatMap = map + flat(1)). Non-ARRAY receiver â [].
sub flat_map ($self, $recv, $key_kind, $key) {
return [] unless ref($recv) eq 'ARRAY';
my @projected;
for my $el (@$recv) {
if ($key_kind eq 'field') {
# JS `i => i.field` on a non-object yields `undefined`, not the
# element itself â push `undef` so a scalar element doesn't leak
# into the output (matches Go's `getFieldValue` returning nil).
push @projected, ref($el) eq 'HASH' ? $el->{$key} : undef;
}
else {
push @projected, $el;
}
}
return $self->flat(\@projected, 1);
}
# `Array.prototype.flatMap(i => [i.a, i.b])` â array-literal tuple
# projection (#1448 Tier C). Each `@specs` entry is a [kind, key] arrayref
# (['self', ''] or ['field', 'a']). For each element, every leaf's value
# is appended in order. flat(1) removes only the literal wrapper, so an
# array-valued leaf is appended verbatim (no spread) â i.e. just append
# each leaf. A non-HASH element under a `field` leaf yields undef (JS
# `i.field` on a non-object). Non-ARRAY receiver â [].
sub flat_map_tuple ($self, $recv, @specs) {
return [] unless ref($recv) eq 'ARRAY';
my @out;
for my $el (@$recv) {
for my $spec (@specs) {
my ($kind, $key) = @$spec;
if ($kind eq 'field') {
push @out, ref($el) eq 'HASH' ? $el->{$key} : undef;
}
else {
push @out, $el;
}
}
}
return \@out;
}
# `String.prototype.trim()` â strip leading + trailing whitespace.
# JS's `String.prototype.trim` matches `\s` in the Unicode sense
# (any whitespace including non-breaking space U+00A0); Perl's `\s`
# inside a regex with `/u` flag is the same. Undef receivers return
# the empty string (matches JS's `String(undefined).trim()` which
# would be "undefined" â "undefined", but in our template context
# undef commonly means "missing prop"; rendering the empty string
# is the safer choice and mirrors the JS-compat divergence we
# already document for `bf->string(undef) === ""`).
sub trim ($self, $recv) {
return '' unless defined $recv;
return '' if ref($recv);
my $s = "$recv";
$s =~ s/^\s+|\s+$//gu;
return $s;
}
# `Number.prototype.toFixed(digits)` (#1897) â fixed-decimal string with
# zero-padding. JS rounds the scaled integer half toward +Infinity (the
# spec's "pick the larger n" tie-break), so `(2.5).toFixed(0)` is "3";
# bare `sprintf("%.*f")` would round half-to-even ("2"), diverging. Scale
# by 10**digits, round with `floor(x + 0.5)` (the same tie-break the
# `round` helper uses), then format the exact multiple. A negative
# `digits` clamps to 0, mirroring how the adapters default an omitted
# argument.
sub to_fixed ($self, $value, $digits = 0) {
my $n = $self->number($value);
# JS toFixed returns the STRINGS "NaN" / "Infinity" / "-Infinity" for
# non-finite inputs; the numeric values would stringify per-platform
# ("nan"/"inf"/...) and diverge.
return 'NaN' if _is_nan($n);
return $n < 0 ? '-Infinity' : 'Infinity' if _is_inf($n);
$digits = 0 if !defined $digits || $digits < 0;
my $factor = 10 ** $digits;
my $rounded = POSIX::floor($n * $factor + 0.5);
return sprintf('%.*f', $digits, $rounded / $factor);
}
# `String.prototype.split(sep)` (#1448 Tier B) â string â ARRAY ref.
#
# Two JS-parity wrinkles drive the helper (a bare `split` emit would
# diverge from both JS and Go):
#
# * Perl's `split` treats its first argument as a *regex*, so a
# separator like '.' or '|' would match far too much. We
# `quotemeta` it to force literal-string matching, mirroring JS's
# string-separator semantics (the regex-separator form stays
# refused upstream â see the parser arm).
# * Perl's `split` drops trailing empty fields by default; JS keeps
# them (`"a,".split(",")` is `["a", ""]`). Passing the `-1` limit
# preserves them, matching JS and Go's `strings.Split`.
#
# An empty separator splits into individual characters (JS + Go agree).
# Undef receiver renders as the single-element `['']` â the same
# "missing prop â empty string" convention `bf->trim` uses.
sub split ($self, $recv, $sep = undef, $limit = undef) {
my $s = defined $recv && !ref($recv) ? "$recv" : '';
my @parts;
if (!defined $sep) {
# No separator â the whole string in a single-element array
# (matches JS `"x".split()` / `.split(undefined)`).
@parts = ($s);
}
elsif ("$sep" eq '') {
# Empty separator â individual characters. No `-1` limit here:
# on an empty pattern Perl's `split` with `-1` appends a spurious
# trailing empty field ("abc" â 'a','b','c',''), which JS/Go don't.
@parts = split //, $s;
}
elsif ($s eq '') {
# Empty input with a non-empty separator: JS `"".split(",")` is
# `[""]` and Go's `strings.Split("", ",")` is `[""]`, but Perl's
# `split /,/, ''` returns the empty list â special-case for parity.
@parts = ('');
}
else {
# `quotemeta` forces literal-string matching (JS string-separator
# semantics); the `-1` keeps trailing empty fields (JS keeps them,
# Perl's bare `split` drops them).
my $q = quotemeta("$sep");
@parts = split /$q/, $s, -1;
}
# Optional `limit` caps the number of pieces (JS `split(sep, limit)`).
# 0 â empty; a negative limit keeps all (JS ToUint32 wrap makes it
# effectively unbounded) â both match Go's `bf_split`.
if (defined $limit) {
my $n = int($limit);
if ($n == 0) { @parts = () }
( run in 0.867 second using v1.01-cache-2.11-cpan-9581c071862 )