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 )