@barefootjs/mojolicious 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/index.js +91 -4
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +91 -4
- package/dist/index.js +91 -4
- package/lib/BarefootJS.pm +288 -0
- package/package.json +3 -3
- package/src/__tests__/mojo-adapter.test.ts +180 -20
- package/src/adapter/mojo-adapter.ts +183 -13
package/lib/BarefootJS.pm
CHANGED
|
@@ -508,6 +508,71 @@ sub reverse ($self, $recv) {
|
|
|
508
508
|
return [ reverse @$recv ];
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
# `Array.prototype.flat(depth?)` (#1448 Tier C) — flatten nested ARRAY
|
|
512
|
+
# refs `$depth` levels deep. A `$depth` of -1 is the `Infinity` sentinel
|
|
513
|
+
# (flatten fully); 0 returns a shallow copy. Non-ARRAY elements are kept
|
|
514
|
+
# as-is (JS only flattens nested arrays). Non-ARRAY receiver → [].
|
|
515
|
+
sub flat ($self, $recv, $depth = 1) {
|
|
516
|
+
return [] unless ref($recv) eq 'ARRAY';
|
|
517
|
+
my @out;
|
|
518
|
+
for my $el (@$recv) {
|
|
519
|
+
if ($depth != 0 && ref($el) eq 'ARRAY') {
|
|
520
|
+
my $next = $depth > 0 ? $depth - 1 : $depth;
|
|
521
|
+
push @out, @{ $self->flat($el, $next) };
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
push @out, $el;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return \@out;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
# `Array.prototype.flatMap(fn)` value-returning field projection
|
|
531
|
+
# (#1448 Tier C) — map each element through a self / field projection,
|
|
532
|
+
# then flatten one level. `field` reads a HASH-ref key (the raw JS prop
|
|
533
|
+
# name, as `bf->reduce` does); a projected non-ARRAY value is kept as-is
|
|
534
|
+
# (flatMap = map + flat(1)). Non-ARRAY receiver → [].
|
|
535
|
+
sub flat_map ($self, $recv, $key_kind, $key) {
|
|
536
|
+
return [] unless ref($recv) eq 'ARRAY';
|
|
537
|
+
my @projected;
|
|
538
|
+
for my $el (@$recv) {
|
|
539
|
+
if ($key_kind eq 'field') {
|
|
540
|
+
# JS `i => i.field` on a non-object yields `undefined`, not the
|
|
541
|
+
# element itself — push `undef` so a scalar element doesn't leak
|
|
542
|
+
# into the output (matches Go's `getFieldValue` returning nil).
|
|
543
|
+
push @projected, ref($el) eq 'HASH' ? $el->{$key} : undef;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
push @projected, $el;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return $self->flat(\@projected, 1);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
# `Array.prototype.flatMap(i => [i.a, i.b])` — array-literal tuple
|
|
553
|
+
# projection (#1448 Tier C). Each `@specs` entry is a [kind, key] arrayref
|
|
554
|
+
# (['self', ''] or ['field', 'a']). For each element, every leaf's value
|
|
555
|
+
# is appended in order. flat(1) removes only the literal wrapper, so an
|
|
556
|
+
# array-valued leaf is appended verbatim (no spread) — i.e. just append
|
|
557
|
+
# each leaf. A non-HASH element under a `field` leaf yields undef (JS
|
|
558
|
+
# `i.field` on a non-object). Non-ARRAY receiver → [].
|
|
559
|
+
sub flat_map_tuple ($self, $recv, @specs) {
|
|
560
|
+
return [] unless ref($recv) eq 'ARRAY';
|
|
561
|
+
my @out;
|
|
562
|
+
for my $el (@$recv) {
|
|
563
|
+
for my $spec (@specs) {
|
|
564
|
+
my ($kind, $key) = @$spec;
|
|
565
|
+
if ($kind eq 'field') {
|
|
566
|
+
push @out, ref($el) eq 'HASH' ? $el->{$key} : undef;
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
push @out, $el;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return \@out;
|
|
574
|
+
}
|
|
575
|
+
|
|
511
576
|
# `String.prototype.trim()` — strip leading + trailing whitespace.
|
|
512
577
|
# JS's `String.prototype.trim` matches `\s` in the Unicode sense
|
|
513
578
|
# (any whitespace including non-breaking space U+00A0); Perl's `\s`
|
|
@@ -526,6 +591,172 @@ sub trim ($self, $recv) {
|
|
|
526
591
|
return $s;
|
|
527
592
|
}
|
|
528
593
|
|
|
594
|
+
# `String.prototype.split(sep)` (#1448 Tier B) — string → ARRAY ref.
|
|
595
|
+
#
|
|
596
|
+
# Two JS-parity wrinkles drive the helper (a bare `split` emit would
|
|
597
|
+
# diverge from both JS and Go):
|
|
598
|
+
#
|
|
599
|
+
# * Perl's `split` treats its first argument as a *regex*, so a
|
|
600
|
+
# separator like '.' or '|' would match far too much. We
|
|
601
|
+
# `quotemeta` it to force literal-string matching, mirroring JS's
|
|
602
|
+
# string-separator semantics (the regex-separator form stays
|
|
603
|
+
# refused upstream — see the parser arm).
|
|
604
|
+
# * Perl's `split` drops trailing empty fields by default; JS keeps
|
|
605
|
+
# them (`"a,".split(",")` is `["a", ""]`). Passing the `-1` limit
|
|
606
|
+
# preserves them, matching JS and Go's `strings.Split`.
|
|
607
|
+
#
|
|
608
|
+
# An empty separator splits into individual characters (JS + Go agree).
|
|
609
|
+
# Undef receiver renders as the single-element `['']` — the same
|
|
610
|
+
# "missing prop → empty string" convention `bf->trim` uses.
|
|
611
|
+
|
|
612
|
+
sub split ($self, $recv, $sep = undef, $limit = undef) {
|
|
613
|
+
my $s = defined $recv && !ref($recv) ? "$recv" : '';
|
|
614
|
+
|
|
615
|
+
my @parts;
|
|
616
|
+
if (!defined $sep) {
|
|
617
|
+
# No separator → the whole string in a single-element array
|
|
618
|
+
# (matches JS `"x".split()` / `.split(undefined)`).
|
|
619
|
+
@parts = ($s);
|
|
620
|
+
}
|
|
621
|
+
elsif ("$sep" eq '') {
|
|
622
|
+
# Empty separator → individual characters. No `-1` limit here:
|
|
623
|
+
# on an empty pattern Perl's `split` with `-1` appends a spurious
|
|
624
|
+
# trailing empty field ("abc" → 'a','b','c',''), which JS/Go don't.
|
|
625
|
+
@parts = split //, $s;
|
|
626
|
+
}
|
|
627
|
+
elsif ($s eq '') {
|
|
628
|
+
# Empty input with a non-empty separator: JS `"".split(",")` is
|
|
629
|
+
# `[""]` and Go's `strings.Split("", ",")` is `[""]`, but Perl's
|
|
630
|
+
# `split /,/, ''` returns the empty list — special-case for parity.
|
|
631
|
+
@parts = ('');
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
# `quotemeta` forces literal-string matching (JS string-separator
|
|
635
|
+
# semantics); the `-1` keeps trailing empty fields (JS keeps them,
|
|
636
|
+
# Perl's bare `split` drops them).
|
|
637
|
+
my $q = quotemeta("$sep");
|
|
638
|
+
@parts = split /$q/, $s, -1;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
# Optional `limit` caps the number of pieces (JS `split(sep, limit)`).
|
|
642
|
+
# 0 → empty; a negative limit keeps all (JS ToUint32 wrap makes it
|
|
643
|
+
# effectively unbounded) — both match Go's `bf_split`.
|
|
644
|
+
if (defined $limit) {
|
|
645
|
+
my $n = int($limit);
|
|
646
|
+
if ($n == 0) { @parts = () }
|
|
647
|
+
elsif ($n > 0 && $n < scalar @parts) { @parts = @parts[0 .. $n - 1] }
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return [@parts];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
# `String.prototype.startsWith(prefix, position?)` (#1448 Tier B) —
|
|
654
|
+
# string → boolean (1 / 0). `substr`-anchored literal comparison mirrors
|
|
655
|
+
# Go's `strings.HasPrefix`. An empty prefix is always true (JS parity);
|
|
656
|
+
# undef / non-string receivers coerce to the empty string first. The
|
|
657
|
+
# optional `position` re-anchors the test (clamped to `[0, length]`),
|
|
658
|
+
# matching JS `"abc".startsWith("b", 1)`.
|
|
659
|
+
|
|
660
|
+
sub starts_with ($self, $recv, $prefix, $position = undef) {
|
|
661
|
+
my $s = defined $recv && !ref($recv) ? "$recv" : '';
|
|
662
|
+
my $p = defined $prefix ? "$prefix" : '';
|
|
663
|
+
if (defined $position) {
|
|
664
|
+
my $n = int($position);
|
|
665
|
+
$n = 0 if $n < 0;
|
|
666
|
+
$n = length($s) if $n > length($s);
|
|
667
|
+
$s = substr($s, $n);
|
|
668
|
+
}
|
|
669
|
+
return substr($s, 0, length $p) eq $p ? 1 : 0;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
# `String.prototype.endsWith(suffix, endPosition?)` (#1448 Tier B) —
|
|
673
|
+
# string → boolean (1 / 0). Mirrors Go's `strings.HasSuffix`. An empty
|
|
674
|
+
# suffix is always true (JS parity); a suffix longer than the string is
|
|
675
|
+
# false. `substr($s, -length $x)` would mis-read the whole string when
|
|
676
|
+
# `length $x == 0`, so that case short-circuits. The optional
|
|
677
|
+
# `endPosition` treats the string as if it were only that many chars
|
|
678
|
+
# long (clamped to `[0, length]`), matching JS `"abc".endsWith("b", 2)`.
|
|
679
|
+
|
|
680
|
+
sub ends_with ($self, $recv, $suffix, $end_position = undef) {
|
|
681
|
+
my $s = defined $recv && !ref($recv) ? "$recv" : '';
|
|
682
|
+
my $x = defined $suffix ? "$suffix" : '';
|
|
683
|
+
if (defined $end_position) {
|
|
684
|
+
my $e = int($end_position);
|
|
685
|
+
$e = 0 if $e < 0;
|
|
686
|
+
$e = length($s) if $e > length($s);
|
|
687
|
+
$s = substr($s, 0, $e);
|
|
688
|
+
}
|
|
689
|
+
return 1 if $x eq '';
|
|
690
|
+
return 0 if length($s) < length($x);
|
|
691
|
+
return substr($s, -length $x) eq $x ? 1 : 0;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
# `String.prototype.replace(pattern, replacement)` — string-pattern
|
|
695
|
+
# form only (#1448 Tier B), replacing the FIRST occurrence (JS string-
|
|
696
|
+
# pattern semantics). Spliced via index/substr rather than `s///` so
|
|
697
|
+
# BOTH the pattern and the replacement are literal: no Perl regex
|
|
698
|
+
# metacharacters in the pattern and no `$1` / `$&` interpolation in the
|
|
699
|
+
# replacement. Go's `bf_replace` (strings.Replace, n=1) treats the
|
|
700
|
+
# replacement literally too, so the two adapters stay byte-equal — this
|
|
701
|
+
# diverges from JS only for replacement strings containing `$`-patterns
|
|
702
|
+
# (rare in template position). An empty pattern inserts the replacement
|
|
703
|
+
# at the front (`"abc".replace("", "X")` → "Xabc"), matching JS + Go.
|
|
704
|
+
|
|
705
|
+
sub replace ($self, $recv, $pattern, $replacement) {
|
|
706
|
+
my $s = defined $recv && !ref($recv) ? "$recv" : '';
|
|
707
|
+
my $o = defined $pattern ? "$pattern" : '';
|
|
708
|
+
my $n = defined $replacement ? "$replacement" : '';
|
|
709
|
+
return $n . $s if $o eq '';
|
|
710
|
+
my $i = index($s, $o);
|
|
711
|
+
return $s if $i < 0;
|
|
712
|
+
return substr($s, 0, $i) . $n . substr($s, $i + length($o));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
# `String.prototype.repeat(n)` — the receiver concatenated n times
|
|
716
|
+
# (#1448 Tier B), via Perl's `x` operator. JS throws RangeError for a
|
|
717
|
+
# negative count, but SSR templates degrade to the empty string rather
|
|
718
|
+
# than dying mid-render, so a count <= 0 returns "" (Go's `bf_repeat`
|
|
719
|
+
# applies the same clamp). The count is truncated toward zero
|
|
720
|
+
# (`int`), matching JS's ToIntegerOrInfinity on `"a".repeat(3.7)`.
|
|
721
|
+
|
|
722
|
+
sub repeat ($self, $recv, $count) {
|
|
723
|
+
my $s = defined $recv && !ref($recv) ? "$recv" : '';
|
|
724
|
+
my $n = defined $count ? int($count) : 0;
|
|
725
|
+
return $n <= 0 ? '' : $s x $n;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
# `String.prototype.padStart` / `padEnd` (#1448 Tier B) — pad the
|
|
729
|
+
# receiver to `$target` characters with `$pad` (default a single space)
|
|
730
|
+
# repeated and truncated to fill, prepended or appended. Length is
|
|
731
|
+
# measured in characters (Perl `length`), matching Go's rune-based
|
|
732
|
+
# `bf_pad_*` — diverges from JS's UTF-16-unit length only for
|
|
733
|
+
# astral-plane input. An empty pad, or a receiver already >= `$target`,
|
|
734
|
+
# returns the receiver unchanged (JS parity). The `$target` is
|
|
735
|
+
# truncated toward zero (JS ToLength on the first arg).
|
|
736
|
+
|
|
737
|
+
sub _pad ($s, $target, $pad, $at_start) {
|
|
738
|
+
$pad = ' ' unless defined $pad;
|
|
739
|
+
$pad = "$pad";
|
|
740
|
+
return $s if $pad eq '';
|
|
741
|
+
my $len = length $s;
|
|
742
|
+
my $t = int($target // 0);
|
|
743
|
+
return $s if $len >= $t;
|
|
744
|
+
my $need = $t - $len;
|
|
745
|
+
# Repeat enough copies to cover $need, then trim to exactly $need.
|
|
746
|
+
my $fill = substr($pad x (int($need / length($pad)) + 1), 0, $need);
|
|
747
|
+
return $at_start ? $fill . $s : $s . $fill;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
sub pad_start ($self, $recv, $target, $pad = undef) {
|
|
751
|
+
my $s = defined $recv && !ref($recv) ? "$recv" : '';
|
|
752
|
+
return _pad($s, $target, $pad, 1);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
sub pad_end ($self, $recv, $target, $pad = undef) {
|
|
756
|
+
my $s = defined $recv && !ref($recv) ? "$recv" : '';
|
|
757
|
+
return _pad($s, $target, $pad, 0);
|
|
758
|
+
}
|
|
759
|
+
|
|
529
760
|
# `Array.prototype.sort(cmp)` / `Array.prototype.toSorted(cmp)`
|
|
530
761
|
# lowering (#1448 Tier B). Non-mutating — JS's mutate-vs-new
|
|
531
762
|
# distinction is moot in SSR template context.
|
|
@@ -616,6 +847,63 @@ sub _compare_sort_key ($av, $bv, $compare_type) {
|
|
|
616
847
|
return ($av // 0) <=> ($bv // 0); # numeric
|
|
617
848
|
}
|
|
618
849
|
|
|
850
|
+
# Fold an array into a scalar via the arithmetic-fold catalogue
|
|
851
|
+
# (#1448 Tier C). Mirrors Go's `bf_reduce` and JS `reduce(fn, init)` /
|
|
852
|
+
# `reduceRight(fn, init)` for the shapes `(acc, x) => acc <op> x` /
|
|
853
|
+
# `(acc, x) => acc <op> x.field`:
|
|
854
|
+
#
|
|
855
|
+
# bf->reduce($recv, {
|
|
856
|
+
# op => '+' | '*',
|
|
857
|
+
# key_kind => 'self' | 'field',
|
|
858
|
+
# key => '<field>', # when key_kind eq 'field'
|
|
859
|
+
# type => 'numeric' | 'string',
|
|
860
|
+
# init => <seed>, # number, or string for concat
|
|
861
|
+
# direction => 'left' | 'right', # 'right' = reduceRight (default 'left')
|
|
862
|
+
# })
|
|
863
|
+
#
|
|
864
|
+
# Numeric folds accumulate with `+` / `*` (non-numeric keys coalesce to
|
|
865
|
+
# 0); string folds concatenate via `bf->string` (undef → ''). The init
|
|
866
|
+
# seeds the accumulator, so an empty array returns it unchanged — exactly
|
|
867
|
+
# like JS. `direction => 'right'` folds right-to-left (reduceRight); only
|
|
868
|
+
# observable for string concat, since numeric sum / product commute.
|
|
869
|
+
# Float stringification can diverge from Go's for inexact binary
|
|
870
|
+
# fractions (e.g. 0.1 + 0.2); integer sums — the common case — agree.
|
|
871
|
+
sub reduce ($self, $recv, $opts = {}) {
|
|
872
|
+
my $op = $opts->{op} // '+';
|
|
873
|
+
my $key_kind = $opts->{key_kind} // 'self';
|
|
874
|
+
my $key = $opts->{key} // '';
|
|
875
|
+
my $type = $opts->{type} // 'numeric';
|
|
876
|
+
my $direction = $opts->{direction} // 'left';
|
|
877
|
+
|
|
878
|
+
my @items = ref($recv) eq 'ARRAY' ? @$recv : ();
|
|
879
|
+
# reduceRight folds right-to-left; reversing the snapshot keeps the
|
|
880
|
+
# single forward loop below. Only observable for string concat —
|
|
881
|
+
# numeric sum / product commute. Qualify as CORE::reverse — this
|
|
882
|
+
# package defines `sub reverse` (the `.reverse()` helper), so a bare
|
|
883
|
+
# `reverse` is ambiguous under `use warnings`.
|
|
884
|
+
@items = CORE::reverse(@items) if $direction eq 'right';
|
|
885
|
+
my $project = sub ($item) {
|
|
886
|
+
$key_kind eq 'field' && ref($item) eq 'HASH' ? $item->{$key} : $item;
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
if ($type eq 'string') {
|
|
890
|
+
my $acc = $opts->{init} // '';
|
|
891
|
+
$acc .= $self->string($project->($_)) for @items;
|
|
892
|
+
return $acc;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
my $acc = $opts->{init} // 0;
|
|
896
|
+
for my $item (@items) {
|
|
897
|
+
my $n = $project->($item);
|
|
898
|
+
# Guard `defined` before `looks_like_number` so a missing field
|
|
899
|
+
# (undef) folds as 0 without an "uninitialized value" warning
|
|
900
|
+
# under `use warnings` — matching the `$av // ''` style `sort` uses.
|
|
901
|
+
$n = 0 unless defined $n && looks_like_number($n);
|
|
902
|
+
$op eq '*' ? ($acc *= $n) : ($acc += $n);
|
|
903
|
+
}
|
|
904
|
+
return $acc;
|
|
905
|
+
}
|
|
906
|
+
|
|
619
907
|
# ---------------------------------------------------------------------------
|
|
620
908
|
# JSX intrinsic-element spread (#1407)
|
|
621
909
|
# ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/mojolicious",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Mojolicious EP template adapter for BarefootJS - generates .html.ep files from IR",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,14 +52,14 @@
|
|
|
52
52
|
"directory": "packages/adapter-mojolicious"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@barefootjs/shared": "0.
|
|
55
|
+
"@barefootjs/shared": "0.6.1"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@barefootjs/jsx": ">=0.2.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
62
|
-
"@barefootjs/jsx": "0.
|
|
62
|
+
"@barefootjs/jsx": "0.6.1",
|
|
63
63
|
"typescript": "^5.0.0"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -395,9 +395,16 @@ export function C() {
|
|
|
395
395
|
// The nested-higher-order-in-filter-predicate shape also lowers
|
|
396
396
|
// now (#1443 PR4) — moved to a positive-output test below.
|
|
397
397
|
const cases: { name: string; body: string; needle: string }[] = [
|
|
398
|
-
|
|
398
|
+
// The arithmetic-fold `.reduce(fn, init)` catalogue now lowers
|
|
399
|
+
// (positive-output tests below + the reduce-* conformance
|
|
400
|
+
// fixtures); the no-init form stays refused — JS throws on an
|
|
401
|
+
// empty array there, which a template can't mirror.
|
|
402
|
+
{ name: 'reduce (no init)', body: `<div>{items().reduce((s, x) => s + x)}</div>`, needle: '.reduce(' },
|
|
399
403
|
{ name: 'forEach', body: `<ul>{items().forEach(x => x)}</ul>`, needle: '.forEach(' },
|
|
400
|
-
|
|
404
|
+
// Self / field / field-tuple `.flatMap` now lowers (#1448 Tier C) —
|
|
405
|
+
// even as a loop base — so those moved to positive tests below. A
|
|
406
|
+
// tuple with a non-leaf element (a string literal) stays refused.
|
|
407
|
+
{ name: 'flatMap (literal element)', body: `<div>{items().flatMap(x => [x.tag, "x"])}</div>`, needle: '.flatMap(' },
|
|
401
408
|
]
|
|
402
409
|
|
|
403
410
|
for (const { name, body, needle } of cases) {
|
|
@@ -928,11 +935,26 @@ import { fixture as arrayLastIndexOfFixture } from '../../../adapter-tests/fixtu
|
|
|
928
935
|
import { fixture as arrayAtFixture } from '../../../adapter-tests/fixtures/methods/array-at'
|
|
929
936
|
import { fixture as arrayConcatFixture } from '../../../adapter-tests/fixtures/methods/array-concat'
|
|
930
937
|
import { fixture as arraySliceFixture } from '../../../adapter-tests/fixtures/methods/array-slice'
|
|
938
|
+
import { fixture as arraySliceCopyFixture } from '../../../adapter-tests/fixtures/methods/array-slice-copy'
|
|
939
|
+
import { fixture as arrayJoinDefaultFixture } from '../../../adapter-tests/fixtures/methods/array-join-default'
|
|
940
|
+
import { fixture as arrayAtDefaultFixture } from '../../../adapter-tests/fixtures/methods/array-at-default'
|
|
941
|
+
import { fixture as arrayConcatCopyFixture } from '../../../adapter-tests/fixtures/methods/array-concat-copy'
|
|
931
942
|
import { fixture as arrayReverseFixture } from '../../../adapter-tests/fixtures/methods/array-reverse'
|
|
932
943
|
import { fixture as arrayToReversedFixture } from '../../../adapter-tests/fixtures/methods/array-toReversed'
|
|
933
944
|
import { fixture as stringToLowerCaseFixture } from '../../../adapter-tests/fixtures/methods/string-toLowerCase'
|
|
934
945
|
import { fixture as stringToUpperCaseFixture } from '../../../adapter-tests/fixtures/methods/string-toUpperCase'
|
|
935
946
|
import { fixture as stringTrimFixture } from '../../../adapter-tests/fixtures/methods/string-trim'
|
|
947
|
+
// #1448 Tier B — string methods.
|
|
948
|
+
import { fixture as stringSplitFixture } from '../../../adapter-tests/fixtures/methods/string-split'
|
|
949
|
+
import { fixture as stringSplitLimitFixture } from '../../../adapter-tests/fixtures/methods/string-split-limit'
|
|
950
|
+
import { fixture as stringStartsWithFixture } from '../../../adapter-tests/fixtures/methods/string-startsWith'
|
|
951
|
+
import { fixture as stringStartsWithPositionFixture } from '../../../adapter-tests/fixtures/methods/string-startsWith-position'
|
|
952
|
+
import { fixture as stringEndsWithFixture } from '../../../adapter-tests/fixtures/methods/string-endsWith'
|
|
953
|
+
import { fixture as stringEndsWithPositionFixture } from '../../../adapter-tests/fixtures/methods/string-endsWith-position'
|
|
954
|
+
import { fixture as stringReplaceFixture } from '../../../adapter-tests/fixtures/methods/string-replace'
|
|
955
|
+
import { fixture as stringRepeatFixture } from '../../../adapter-tests/fixtures/methods/string-repeat'
|
|
956
|
+
import { fixture as stringPadStartFixture } from '../../../adapter-tests/fixtures/methods/string-padStart'
|
|
957
|
+
import { fixture as stringPadEndFixture } from '../../../adapter-tests/fixtures/methods/string-padEnd'
|
|
936
958
|
// #1448 Tier B — .sort / .toSorted fixtures (loop-chained + standalone).
|
|
937
959
|
import { fixture as arraySortFieldAscFixture } from '../../../adapter-tests/fixtures/methods/array-sort-field-asc'
|
|
938
960
|
import { fixture as arraySortFieldDescFixture } from '../../../adapter-tests/fixtures/methods/array-sort-field-desc'
|
|
@@ -945,6 +967,12 @@ import { fixture as arrayToSortedFixture } from '../../../adapter-tests/fixtures
|
|
|
945
967
|
import { fixture as arrayEntriesFixture } from '../../../adapter-tests/fixtures/methods/array-entries'
|
|
946
968
|
import { fixture as arrayKeysFixture } from '../../../adapter-tests/fixtures/methods/array-keys'
|
|
947
969
|
import { fixture as arrayValuesFixture } from '../../../adapter-tests/fixtures/methods/array-values'
|
|
970
|
+
// #1448 Tier C — .reduce(fn, init) arithmetic-fold catalogue.
|
|
971
|
+
import { fixture as reduceSumFieldFixture } from '../../../adapter-tests/fixtures/methods/reduce-sum-field'
|
|
972
|
+
import { fixture as reduceSumSelfFixture } from '../../../adapter-tests/fixtures/methods/reduce-sum-self'
|
|
973
|
+
import { fixture as reduceConcatFixture } from '../../../adapter-tests/fixtures/methods/reduce-concat'
|
|
974
|
+
import { fixture as reduceProductFixture } from '../../../adapter-tests/fixtures/methods/reduce-product'
|
|
975
|
+
import { fixture as reduceRightConcatFixture } from '../../../adapter-tests/fixtures/methods/reduce-right-concat'
|
|
948
976
|
|
|
949
977
|
describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
950
978
|
const cases = [
|
|
@@ -955,6 +983,12 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
|
955
983
|
{ fixture: arrayAtFixture, expect: 'bf->at($items, -1)' },
|
|
956
984
|
{ fixture: arrayConcatFixture, expect: 'bf->concat($left, $right)' },
|
|
957
985
|
{ fixture: arraySliceFixture, expect: 'bf->slice($items, 1, 3)' },
|
|
986
|
+
// #1448 full-arity — zero-arg defaults.
|
|
987
|
+
{ fixture: arraySliceCopyFixture, expect: 'bf->slice($items, 0, undef)' },
|
|
988
|
+
{ fixture: arrayJoinDefaultFixture, expect: `join(',', @{$items})` },
|
|
989
|
+
// `.at()` → index 0; `.concat()` → the receiver (shallow copy).
|
|
990
|
+
{ fixture: arrayAtDefaultFixture, expect: 'bf->at($items, 0)' },
|
|
991
|
+
{ fixture: arrayConcatCopyFixture, expect: `join('|', @{$items})` },
|
|
958
992
|
{ fixture: arrayReverseFixture, expect: 'bf->reverse($items)' },
|
|
959
993
|
// .toReversed shares the helper with .reverse — pinning both
|
|
960
994
|
// routings catches a future divergence between them.
|
|
@@ -962,6 +996,23 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
|
962
996
|
{ fixture: stringToLowerCaseFixture,expect: 'lc($value)' },
|
|
963
997
|
{ fixture: stringToUpperCaseFixture,expect: 'uc($value)' },
|
|
964
998
|
{ fixture: stringTrimFixture, expect: 'bf->trim($value)' },
|
|
999
|
+
// #1448 Tier B — string → array. `.split(',')` lowers to
|
|
1000
|
+
// `bf->split`, here chained into `.join('|')` so the array ref is
|
|
1001
|
+
// observable (`join('|', @{bf->split($value, ',')})`).
|
|
1002
|
+
{ fixture: stringSplitFixture, expect: `bf->split($value, ',')` },
|
|
1003
|
+
{ fixture: stringSplitLimitFixture, expect: `bf->split($value, ',', 2)` },
|
|
1004
|
+
// #1448 Tier B — string → boolean at condition position (`% if`).
|
|
1005
|
+
{ fixture: stringStartsWithFixture, expect: 'bf->starts_with($value, $prefix)' },
|
|
1006
|
+
{ fixture: stringStartsWithPositionFixture, expect: `bf->starts_with($value, 'world', 6)` },
|
|
1007
|
+
{ fixture: stringEndsWithFixture, expect: 'bf->ends_with($value, $suffix)' },
|
|
1008
|
+
{ fixture: stringEndsWithPositionFixture, expect: `bf->ends_with($value, 'hello', 5)` },
|
|
1009
|
+
// #1448 Tier B — string → string, first-occurrence replace.
|
|
1010
|
+
{ fixture: stringReplaceFixture, expect: `bf->replace($value, 'o', '0')` },
|
|
1011
|
+
// #1448 Tier B — string → string, repeat n times.
|
|
1012
|
+
{ fixture: stringRepeatFixture, expect: 'bf->repeat($value, 3)' },
|
|
1013
|
+
// #1448 Tier B — string → string, padded to a target width.
|
|
1014
|
+
{ fixture: stringPadStartFixture, expect: `bf->pad_start($value, 5, '0')` },
|
|
1015
|
+
{ fixture: stringPadEndFixture, expect: `bf->pad_end($value, 5, '.')` },
|
|
965
1016
|
// #1448 Tier B — sort / toSorted. The loop-chained field cases
|
|
966
1017
|
// hoist into a `my $bf_iter_lN = bf->sort(...)` local; the
|
|
967
1018
|
// standalone primitive cases inline the call. Each comparison key
|
|
@@ -982,6 +1033,17 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
|
982
1033
|
{ fixture: arrayKeysFixture, expect: '% for my $k (0..$#{$items})' },
|
|
983
1034
|
// .values() → standard for loop (same as plain .map())
|
|
984
1035
|
{ fixture: arrayValuesFixture, expect: '% my $v = $items->[$_i];' },
|
|
1036
|
+
// #1448 Tier C — .reduce(fn, init) arithmetic fold. The structured
|
|
1037
|
+
// ReduceOp lowers to a single `bf->reduce(...)` call with op /
|
|
1038
|
+
// key / type / init / direction in the options hash. Each shape
|
|
1039
|
+
// exercises one arm: field-numeric sum, self-numeric sum,
|
|
1040
|
+
// string-concat fold, the product (`*`) operator, and the
|
|
1041
|
+
// right-to-left `direction` of reduceRight.
|
|
1042
|
+
{ fixture: reduceSumFieldFixture, expect: `bf->reduce($items, { op => '+', key_kind => 'field', key => 'duration', type => 'numeric', init => 0, direction => 'left' })` },
|
|
1043
|
+
{ fixture: reduceSumSelfFixture, expect: `bf->reduce($nums, { op => '+', key_kind => 'self', type => 'numeric', init => 0, direction => 'left' })` },
|
|
1044
|
+
{ fixture: reduceConcatFixture, expect: `bf->reduce($items, { op => '+', key_kind => 'field', key => 'label', type => 'string', init => '', direction => 'left' })` },
|
|
1045
|
+
{ fixture: reduceProductFixture, expect: `bf->reduce($items, { op => '*', key_kind => 'field', key => 'qty', type => 'numeric', init => 1, direction => 'left' })` },
|
|
1046
|
+
{ fixture: reduceRightConcatFixture, expect: `bf->reduce($items, { op => '+', key_kind => 'field', key => 'label', type => 'string', init => '', direction => 'right' })` },
|
|
985
1047
|
]
|
|
986
1048
|
|
|
987
1049
|
for (const { fixture, expect: expectedHelper } of cases) {
|
|
@@ -1003,6 +1065,73 @@ describe('MojoAdapter - #1448 Tier A/B fixture-driven lowering pins', () => {
|
|
|
1003
1065
|
}
|
|
1004
1066
|
})
|
|
1005
1067
|
|
|
1068
|
+
describe('MojoAdapter - #1448 Tier C .flat(depth?)', () => {
|
|
1069
|
+
function emitFlat(expr: string): string {
|
|
1070
|
+
const a = new MojoAdapter()
|
|
1071
|
+
const ir = compileToIR(`
|
|
1072
|
+
function C({ rows }: { rows: number[][] }) {
|
|
1073
|
+
return <div>{${expr}}</div>
|
|
1074
|
+
}
|
|
1075
|
+
export { C }
|
|
1076
|
+
`, a)
|
|
1077
|
+
return a.generate(ir).template ?? ''
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
test('.flat() emits bf->flat with default depth 1', () => {
|
|
1081
|
+
expect(emitFlat('rows.flat()')).toContain('bf->flat($rows, 1)')
|
|
1082
|
+
})
|
|
1083
|
+
|
|
1084
|
+
test('.flat(2) emits the explicit depth', () => {
|
|
1085
|
+
expect(emitFlat('rows.flat(2)')).toContain('bf->flat($rows, 2)')
|
|
1086
|
+
})
|
|
1087
|
+
|
|
1088
|
+
test('.flat(Infinity) emits the -1 full-depth sentinel', () => {
|
|
1089
|
+
expect(emitFlat('rows.flat(Infinity)')).toContain('bf->flat($rows, -1)')
|
|
1090
|
+
})
|
|
1091
|
+
})
|
|
1092
|
+
|
|
1093
|
+
describe('MojoAdapter - #1448 Tier C .flatMap(field projection)', () => {
|
|
1094
|
+
function emitFlatMap(expr: string): string {
|
|
1095
|
+
const a = new MojoAdapter()
|
|
1096
|
+
const ir = compileToIR(`
|
|
1097
|
+
function C({ rows }: { rows: { a: string; b: string; tags: string[] }[] }) {
|
|
1098
|
+
return <div>{${expr}}</div>
|
|
1099
|
+
}
|
|
1100
|
+
export { C }
|
|
1101
|
+
`, a)
|
|
1102
|
+
return a.generate(ir).template ?? ''
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
test('.flatMap(i => i.field) emits bf->flat_map with the raw field key', () => {
|
|
1106
|
+
expect(emitFlatMap('rows.flatMap(i => i.tags).join(" ")')).toContain(`bf->flat_map($rows, 'field', 'tags')`)
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
test('.flatMap(i => i) emits the self projection', () => {
|
|
1110
|
+
expect(emitFlatMap('rows.flatMap(i => i).join(" ")')).toContain(`bf->flat_map($rows, 'self', '')`)
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
test('.flatMap(i => [i.a, i.b]) emits bf->flat_map_tuple with leaf specs', () => {
|
|
1114
|
+
expect(emitFlatMap('rows.flatMap(i => [i.a, i.b]).join(" ")')).toContain(`bf->flat_map_tuple($rows, ['field', 'a'], ['field', 'b'])`)
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
test('tuple self + field leaves', () => {
|
|
1118
|
+
expect(emitFlatMap('rows.flatMap(i => [i, i.a]).join(" ")')).toContain(`bf->flat_map_tuple($rows, ['self', ''], ['field', 'a'])`)
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
test('field-projection flatMap as a loop base lowers (no BF101)', () => {
|
|
1122
|
+
const a = new MojoAdapter()
|
|
1123
|
+
const ir = compileToIR(`'use client'
|
|
1124
|
+
import { createSignal } from '@barefootjs/client'
|
|
1125
|
+
export function C() {
|
|
1126
|
+
const [items] = createSignal<{ tags: string[] }[]>([])
|
|
1127
|
+
return <ul>{items().flatMap(x => x.tags).map(t => <li key={t}>{t}</li>)}</ul>
|
|
1128
|
+
}`, a)
|
|
1129
|
+
const template = a.generate(ir).template ?? ''
|
|
1130
|
+
expect((a.errors ?? []).filter(e => e.code === 'BF101')).toEqual([])
|
|
1131
|
+
expect(template).toContain(`bf->flat_map($items, 'field', 'tags')`)
|
|
1132
|
+
})
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1006
1135
|
// =============================================================================
|
|
1007
1136
|
// #1448 — `/* @client */` escape hatch for STILL-UNSUPPORTED methods
|
|
1008
1137
|
// =============================================================================
|
|
@@ -1061,19 +1190,29 @@ export function C() {
|
|
|
1061
1190
|
// Perl fragment that must NOT survive into the template (the pre-fix
|
|
1062
1191
|
// silent-footgun output for the string rows).
|
|
1063
1192
|
const unsupported: Array<{ name: string; expr: string; badEmit: string }> = [
|
|
1064
|
-
// Tier C array methods.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1193
|
+
// Tier C array methods. The arithmetic-fold `.reduce(fn, init)`
|
|
1194
|
+
// catalogue now lowers (pinned in the positive reduce-* fixtures);
|
|
1195
|
+
// the no-initial-value form stays refused — JS throws on an empty
|
|
1196
|
+
// array there, which a template can't mirror.
|
|
1197
|
+
{ name: 'reduce (no init)', expr: `items().reduce((a, b) => a + b.n)`, badEmit: '->{reduce}' },
|
|
1198
|
+
// Self / field / field-tuple `.flatMap` now lowers (#1448 Tier C); a
|
|
1199
|
+
// tuple with a non-leaf element (here a string literal) stays refused.
|
|
1200
|
+
{ name: 'flatMap (literal element)', expr: `items().flatMap(i => [i.name, "x"])`, badEmit: '->{flatMap}' },
|
|
1201
|
+
// Lowered methods whose MEANINGFUL extra argument isn't lowered yet
|
|
1202
|
+
// (#1448): the `fromIndex` of `.includes`/`.indexOf`/`.lastIndexOf`
|
|
1203
|
+
// and the variadic `.concat`. The parser refuses these (silently
|
|
1204
|
+
// dropping the arg would change the result). (The zero-arg defaults
|
|
1205
|
+
// `.join()`/`.slice()` and JS-ignored trailing args like `.trim(1)`
|
|
1206
|
+
// are accepted — pinned in the positive blocks.)
|
|
1207
|
+
{ name: 'includes (2-arg fromIndex)', expr: `items().includes("a", 1)`, badEmit: '->{includes}' },
|
|
1208
|
+
{ name: 'concat (variadic)', expr: `items().concat(items(), items())`, badEmit: '->{concat}' },
|
|
1068
1209
|
// Tier B/C string methods — previously slipped through with no
|
|
1069
|
-
// diagnostic; now routed through the AST / `isSupported` gate.
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
{ name: 'padStart', expr: `name().padStart(5, "0")`, badEmit: '->{padStart}' },
|
|
1076
|
-
{ name: 'padEnd', expr: `name().padEnd(5, "0")`, badEmit: '->{padEnd}' },
|
|
1210
|
+
// diagnostic; now routed through the AST / `isSupported` gate. The
|
|
1211
|
+
// full Tier B string set (`split`, `startsWith`, `endsWith`,
|
|
1212
|
+
// `replace`, `repeat`, `padStart`, `padEnd`) has since landed its
|
|
1213
|
+
// full-arity lowering and moved to the positive fixture-pin block
|
|
1214
|
+
// above (the regex-pattern `replace` form stays refused — pinned
|
|
1215
|
+
// separately below). `charAt` is Tier C and stays refused entirely.
|
|
1077
1216
|
{ name: 'charAt', expr: `name().charAt(0)`, badEmit: '->{charAt}' },
|
|
1078
1217
|
]
|
|
1079
1218
|
for (const { name, expr, badEmit } of unsupported) {
|
|
@@ -1118,22 +1257,41 @@ export function C(props: { config: string }) {
|
|
|
1118
1257
|
})
|
|
1119
1258
|
|
|
1120
1259
|
// Predicate-level use of an unsupported string method also fails the
|
|
1121
|
-
// build loudly (intended): a `.filter(t => t.name.
|
|
1260
|
+
// build loudly (intended): a `.filter(t => t.name.charAt(0) === "a")`
|
|
1122
1261
|
// whose predicate calls one of the gated methods now refuses the whole
|
|
1123
1262
|
// loop with BF101 (via the shared `isSupported` predicate gate in
|
|
1124
|
-
// jsx-to-ir) rather than lowering to a broken `->{
|
|
1263
|
+
// jsx-to-ir) rather than lowering to a broken `->{charAt}` inside
|
|
1125
1264
|
// the grep. Pinning this so the loud-failure contract can't silently
|
|
1126
|
-
// regress back to the old emit-broken-template behaviour.
|
|
1265
|
+
// regress back to the old emit-broken-template behaviour. (`charAt`
|
|
1266
|
+
// is a Tier C method that stays refused — earlier this test used
|
|
1267
|
+
// `startsWith`, which has since landed its Tier B lowering.)
|
|
1127
1268
|
test('unsupported string method inside a .filter() predicate raises BF101', () => {
|
|
1128
1269
|
const result = compileJSX(`
|
|
1129
1270
|
"use client"
|
|
1130
1271
|
import { createSignal } from "@barefootjs/client"
|
|
1131
1272
|
export function C() {
|
|
1132
1273
|
const [items, setItems] = createSignal<{ name: string }[]>([])
|
|
1133
|
-
return <ul>{items().filter(t => t.name.
|
|
1274
|
+
return <ul>{items().filter(t => t.name.charAt(0) === "a").map(t => <li key={t.name}>{t.name}</li>)}</ul>
|
|
1275
|
+
}
|
|
1276
|
+
`.trimStart(), 'test.tsx', { adapter: new MojoAdapter() })
|
|
1277
|
+
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1280
|
+
// The string-pattern form of `.replace` lowers (#1448 Tier B), but
|
|
1281
|
+
// the regex-pattern form stays refused with BF101 — the Perl `s///`
|
|
1282
|
+
// vs Go `regexp.ReplaceAllString` flavour gap is the open design
|
|
1283
|
+
// question. Pinning the refusal so it can't regress into a broken
|
|
1284
|
+
// `->{replace}` emit for the regex form.
|
|
1285
|
+
test('regex-pattern .replace raises BF101 (string-pattern form is lowered)', () => {
|
|
1286
|
+
const result = compileJSX(`
|
|
1287
|
+
function C({ value }: { value: string }) {
|
|
1288
|
+
return <div>{value.replace(/o/g, "0")}</div>
|
|
1134
1289
|
}
|
|
1290
|
+
export { C }
|
|
1135
1291
|
`.trimStart(), 'test.tsx', { adapter: new MojoAdapter() })
|
|
1136
1292
|
expect(result.errors?.some(e => e.code === 'BF101')).toBe(true)
|
|
1293
|
+
const template = result.files?.find(f => f.path.endsWith('.html.ep'))?.content ?? ''
|
|
1294
|
+
expect(template).not.toContain('->{replace}')
|
|
1137
1295
|
})
|
|
1138
1296
|
|
|
1139
1297
|
// Tier B `.sort` / `.toSorted` follow-ups still refused with BF021.
|
|
@@ -1172,7 +1330,9 @@ export function C() {
|
|
|
1172
1330
|
// more `HASH ref` crash), so we assert the build error rather than a
|
|
1173
1331
|
// render crash. Skipped on hosts without Mojolicious installed.
|
|
1174
1332
|
test('e2e: @client renders placeholder; bare is caught at build with BF101', async () => {
|
|
1175
|
-
|
|
1333
|
+
// Uses the Tier C `charAt` (still refused) — earlier this test used
|
|
1334
|
+
// `repeat`, which has since landed its #1448 Tier B lowering.
|
|
1335
|
+
const bare = emit(`name().charAt(0)`, false)
|
|
1176
1336
|
expect(bare.errors.some(e => e.code === 'BF101')).toBe(true)
|
|
1177
1337
|
|
|
1178
1338
|
try {
|
|
@@ -1182,7 +1342,7 @@ export function C() {
|
|
|
1182
1342
|
import { createSignal } from "@barefootjs/client"
|
|
1183
1343
|
export function C() {
|
|
1184
1344
|
const [name, setName] = createSignal("hello")
|
|
1185
|
-
return <div>{/* @client */ name().
|
|
1345
|
+
return <div>{/* @client */ name().charAt(0)}</div>
|
|
1186
1346
|
}
|
|
1187
1347
|
`.trimStart(),
|
|
1188
1348
|
adapter: new MojoAdapter(),
|