@barefootjs/php 0.18.3 → 0.18.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/php",
3
- "version": "0.18.3",
3
+ "version": "0.18.5",
4
4
  "description": "BarefootJS engine-agnostic PHP runtime -- pluggable rendering backend; the shared basis for Twig, Blade and other PHP engine integrations",
5
5
  "type": "module",
6
6
  "files": [
@@ -680,6 +680,33 @@ final class BarefootJS
680
680
  return (is_nan($n) || is_infinite($n)) ? $n : floor($n + 0.5);
681
681
  }
682
682
 
683
+ /** `Math.min(a, b)` / `Math.max(a, b)` -- two-arg forms only (#2168
684
+ * math-methods). JS returns NaN if either operand is NaN. */
685
+ public function min($a, $b): float
686
+ {
687
+ $x = $this->number($a);
688
+ $y = $this->number($b);
689
+ if (is_nan($x)) return $x;
690
+ if (is_nan($y)) return $y;
691
+ return $x < $y ? $x : $y;
692
+ }
693
+
694
+ public function max($a, $b): float
695
+ {
696
+ $x = $this->number($a);
697
+ $y = $this->number($b);
698
+ if (is_nan($x)) return $x;
699
+ if (is_nan($y)) return $y;
700
+ return $x > $y ? $x : $y;
701
+ }
702
+
703
+ /** `Math.abs()` (#2168 math-methods). */
704
+ public function abs($value): float
705
+ {
706
+ $n = $this->number($value);
707
+ return is_nan($n) ? $n : abs($n);
708
+ }
709
+
683
710
  // -----------------------------------------------------------------
684
711
  // Array / String method helpers (#1448 Tier A)
685
712
  // -----------------------------------------------------------------
@@ -852,6 +879,46 @@ final class BarefootJS
852
879
  return mb_strlen($this->scalarOrEmpty($recv), 'UTF-8');
853
880
  }
854
881
 
882
+ /** True for a JS *object* value under the canonical value convention
883
+ * (`Evaluator::isJsArray`'s docstring): a `stdClass` (the live
884
+ * representation for a `json_decode()`-sourced object prop) or a
885
+ * non-list PHP array (a compiler-baked object literal, `toPhpLiteral`'s
886
+ * `stdClass`-cast form notwithstanding -- defensive, in case a bare
887
+ * assoc array ever reaches here directly). */
888
+ private function isJsObject($recv): bool
889
+ {
890
+ return $recv instanceof \stdClass || (is_array($recv) && !Evaluator::isJsArray($recv));
891
+ }
892
+
893
+ /**
894
+ * `Object.entries(x)` (#2168 object-entries-map) -- returns a PHP assoc
895
+ * array (`key => value`) so the generated Twig/Blade `for`/`@foreach`
896
+ * unpacks key + value directly via the language's own 2-variable
897
+ * iteration form. `(array)` on a `stdClass` (or a bare assoc array)
898
+ * preserves the object's OWN declaration/insertion order -- a PHP
899
+ * array's iteration order IS its insertion order by language
900
+ * guarantee, unlike e.g. a Go `map[string]T` or a Perl hash, which
901
+ * have no such guarantee (see `IRLoop.objectIteration`'s docstring,
902
+ * `packages/jsx/src/types.ts`, for why those adapters take a
903
+ * different, sorted-order approach instead).
904
+ */
905
+ public function entries($recv): array
906
+ {
907
+ return $this->isJsObject($recv) ? (array) $recv : [];
908
+ }
909
+
910
+ /** `Object.keys(x)` -- see `entries()`. */
911
+ public function keys($recv): array
912
+ {
913
+ return $this->isJsObject($recv) ? array_keys((array) $recv) : [];
914
+ }
915
+
916
+ /** `Object.values(x)` -- see `entries()`. */
917
+ public function values($recv): array
918
+ {
919
+ return $this->isJsObject($recv) ? array_values((array) $recv) : [];
920
+ }
921
+
855
922
  private function arrayIndexOf($recv, $elem, bool $reverse): int
856
923
  {
857
924
  if (!Evaluator::isJsArray($recv)) {
@@ -914,9 +981,23 @@ final class BarefootJS
914
981
  return $out;
915
982
  }
916
983
 
917
- /** `Array.prototype.slice(start, end?)`. */
918
- public function slice($recv, $start, $end): array
919
- {
984
+ /** `Array.prototype.slice(start, end?)` AND `String.prototype.slice`
985
+ * (the `string-slice` divergence) -- the adapter emits the same
986
+ * `slice(recv, start, end)` call for both receiver shapes (it can't
987
+ * disambiguate string vs. array at compile time), so this dispatches
988
+ * on the receiver's PHP type, mirroring `includes` above. String
989
+ * length/positions use `mb_strlen`/`mb_substr` (UTF-8 code points,
990
+ * not bytes), matching this runtime's other string helpers. */
991
+ public function slice($recv, $start, $end)
992
+ {
993
+ if (is_string($recv)) {
994
+ $len = mb_strlen($recv, 'UTF-8');
995
+ [$s, $e] = $this->clampSliceRange($len, $start, $end);
996
+ if ($s >= $e) {
997
+ return '';
998
+ }
999
+ return mb_substr($recv, (int) $s, (int) ($e - $s), 'UTF-8');
1000
+ }
920
1001
  if (!Evaluator::isJsArray($recv)) {
921
1002
  return [];
922
1003
  }
@@ -925,6 +1006,16 @@ final class BarefootJS
925
1006
  if ($len === 0) {
926
1007
  return [];
927
1008
  }
1009
+ [$s, $e] = $this->clampSliceRange($len, $start, $end);
1010
+ if ($s >= $e) {
1011
+ return [];
1012
+ }
1013
+ return array_slice($arr, (int) $s, (int) ($e - $s));
1014
+ }
1015
+
1016
+ /** Shared bounds arithmetic for both `slice` branches above. */
1017
+ private function clampSliceRange(int $len, $start, $end): array
1018
+ {
928
1019
  $s = $start ?? 0;
929
1020
  if ($s < 0) {
930
1021
  $s += $len;
@@ -937,10 +1028,7 @@ final class BarefootJS
937
1028
  }
938
1029
  $e = max($e, 0);
939
1030
  $e = min($e, $len);
940
- if ($s >= $e) {
941
- return [];
942
- }
943
- return array_slice($arr, (int) $s, (int) ($e - $s));
1031
+ return [$s, $e];
944
1032
  }
945
1033
 
946
1034
  public function reverse($recv): array
@@ -1117,6 +1205,25 @@ final class BarefootJS
1117
1205
  return (string) preg_replace('/^\s+|\s+$/u', '', $this->string($recv));
1118
1206
  }
1119
1207
 
1208
+ /** `String.prototype.trimStart()` / `.trimEnd()` -- the one-sided
1209
+ * siblings of `trim` above (#2183 follow-up), same `\s` /u regex
1210
+ * restricted to one side. */
1211
+ public function trim_start($recv): string
1212
+ {
1213
+ if ($recv === null || is_array($recv) || $recv instanceof \stdClass) {
1214
+ return '';
1215
+ }
1216
+ return (string) preg_replace('/^\s+/u', '', $this->string($recv));
1217
+ }
1218
+
1219
+ public function trim_end($recv): string
1220
+ {
1221
+ if ($recv === null || is_array($recv) || $recv instanceof \stdClass) {
1222
+ return '';
1223
+ }
1224
+ return (string) preg_replace('/\s+$/u', '', $this->string($recv));
1225
+ }
1226
+
1120
1227
  /** `Number.prototype.toFixed(digits)` -- JS rounds the scaled integer
1121
1228
  * half toward +Infinity (the spec's "pick the larger n" tie-break); a
1122
1229
  * bare `sprintf('%.*f')` would round half-to-even, diverging. */
@@ -1208,6 +1315,29 @@ final class BarefootJS
1208
1315
  return substr($s, 0, $i) . $n . substr($s, $i + strlen($o));
1209
1316
  }
1210
1317
 
1318
+ /** `String.prototype.replaceAll(pattern, replacement)` -- string-pattern
1319
+ * form only (#2182), replacing EVERY occurrence (the all-occurrences
1320
+ * sibling of `replace` above). A non-empty pattern uses PHP's own
1321
+ * `str_replace`, which is global-by-default and treats both the pattern
1322
+ * and the replacement literally (no backreference interpolation) --
1323
+ * matching JS. PHP's `str_replace("", ...)` is a no-op, unlike JS,
1324
+ * which inserts the replacement at every boundary (including before
1325
+ * the first and after the last character,
1326
+ * `"abc".replaceAll("", "X")` -> "XaXbXcX") -- so the empty pattern is
1327
+ * special-cased with `mb_str_split` (UTF-8 code points, matching this
1328
+ * runtime's other string helpers). */
1329
+ public function replace_all($recv, $pattern, $replacement): string
1330
+ {
1331
+ $s = $this->scalarOrEmpty($recv);
1332
+ $o = $this->string($pattern);
1333
+ $n = $this->string($replacement);
1334
+ if ($o === '') {
1335
+ $chars = $s === '' ? [] : mb_str_split($s, 1, 'UTF-8');
1336
+ return implode($n, array_merge([''], $chars, ['']));
1337
+ }
1338
+ return str_replace($o, $n, $s);
1339
+ }
1340
+
1211
1341
  /** `queryHref(base, {...})` (#2042) -- build `"$base?k=v&..."` from a
1212
1342
  * flat (guard, key, value) triple sequence. A pair is included iff its
1213
1343
  * guard is JS-truthy AND its value is a non-empty string; an array value
@@ -146,13 +146,19 @@ function bfv_call(BarefootJS $bf, string $fn, array $args)
146
146
  case 'floor': return $bf->floor($args[0]);
147
147
  case 'ceil': return $bf->ceil($args[0]);
148
148
  case 'round': return $bf->round($args[0]);
149
+ case 'min': return $bf->min($args[0], $args[1]);
150
+ case 'max': return $bf->max($args[0], $args[1]);
151
+ case 'abs': return $bf->abs($args[0]);
149
152
  case 'to_fixed': return $bf->to_fixed(...$args);
150
153
  case 'lower': return $bf->lc($args[0]);
151
154
  case 'upper': return $bf->uc($args[0]);
152
155
  case 'trim': return $bf->trim($args[0]);
156
+ case 'trim_start': return $bf->trim_start($args[0]);
157
+ case 'trim_end': return $bf->trim_end($args[0]);
153
158
  case 'starts_with': return $bf->starts_with(...$args);
154
159
  case 'ends_with': return $bf->ends_with(...$args);
155
160
  case 'replace': return $bf->replace(...$args);
161
+ case 'replace_all': return $bf->replace_all(...$args);
156
162
  case 'repeat': return $bf->repeat(...$args);
157
163
  case 'pad_start': return $bf->pad_start(...$args);
158
164
  case 'pad_end': return $bf->pad_end(...$args);
@@ -188,6 +188,19 @@ bf_test('slice mutation isolation and clamping', function () use ($bf) {
188
188
  bf_assert_eq($src, ['a', 'b', 'c']);
189
189
  });
190
190
 
191
+ bf_test('slice string receiver', function () use ($bf) {
192
+ // The `string-slice` divergence (#2182): a string receiver used to
193
+ // fall through the array-only branch and return an empty array
194
+ // instead of a substring.
195
+ $word = 'barefootjs';
196
+ bf_assert_eq($bf->slice($word, 0, 4), 'bare');
197
+ bf_assert_eq($bf->slice($word, -4, null), 'otjs');
198
+ bf_assert_eq($bf->slice($word, 4, null), 'footjs');
199
+ bf_assert_eq($bf->slice($word, 5, 2), '');
200
+ // Multi-byte: index by character, not byte.
201
+ bf_assert_eq($bf->slice('héllo', 0, 2), 'hé');
202
+ });
203
+
191
204
  bf_test('reverse mutation isolation', function () use ($bf) {
192
205
  bf_assert_eq($bf->reverse(['a', 'b', 'c']), ['c', 'b', 'a']);
193
206
  bf_assert_eq($bf->reverse([]), []);
@@ -211,6 +224,33 @@ bf_test('trim', function () use ($bf) {
211
224
  bf_assert_eq($bf->trim(42), '42');
212
225
  });
213
226
 
227
+ // `trim_start` / `trim_end` -- the one-sided siblings of `trim` above
228
+ // (#2183 follow-up). Padding BOTH sides of the flagship input so a
229
+ // swapped side (or a routed-through-both-sides regression) fails visibly.
230
+ bf_test('trim_start / trim_end', function () use ($bf) {
231
+ bf_assert_eq($bf->trim_start(' padded '), 'padded ');
232
+ bf_assert_eq($bf->trim_end(' padded '), ' padded');
233
+
234
+ bf_assert_eq($bf->trim_start("\t\nleading"), 'leading');
235
+ bf_assert_eq($bf->trim_end('trailing '), 'trailing');
236
+
237
+ bf_assert_eq($bf->trim_start('no-pad'), 'no-pad');
238
+ bf_assert_eq($bf->trim_end('no-pad'), 'no-pad');
239
+
240
+ bf_assert_eq($bf->trim_start(' '), '');
241
+ bf_assert_eq($bf->trim_end(' '), '');
242
+
243
+ bf_assert_eq($bf->trim_start(''), '');
244
+ bf_assert_eq($bf->trim_end(''), '');
245
+
246
+ bf_assert_eq($bf->trim_start(null), '');
247
+ bf_assert_eq($bf->trim_end(null), '');
248
+ bf_assert_eq($bf->trim_start(['a' => 1]), '');
249
+ bf_assert_eq($bf->trim_end(['a' => 1]), '');
250
+ bf_assert_eq($bf->trim_start(42), '42');
251
+ bf_assert_eq($bf->trim_end(42), '42');
252
+ });
253
+
214
254
  bf_test('split', function () use ($bf) {
215
255
  bf_assert_eq($bf->split('a,b,c', ','), ['a', 'b', 'c']);
216
256
  bf_assert_eq($bf->split('a.b.c', '.'), ['a', 'b', 'c']);