@vituum/vite-plugin-latte 1.2.1 → 1.4.0

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.
Files changed (43) hide show
  1. package/index.js +5 -3
  2. package/index.php +2 -0
  3. package/package.json +6 -6
  4. package/vendor/autoload.php +1 -1
  5. package/vendor/composer/InstalledVersions.php +7 -4
  6. package/vendor/composer/autoload_classmap.php +1 -0
  7. package/vendor/composer/autoload_real.php +4 -4
  8. package/vendor/composer/autoload_static.php +3 -2
  9. package/vendor/composer/installed.json +21 -20
  10. package/vendor/composer/installed.php +8 -8
  11. package/vendor/latte/latte/composer.json +6 -5
  12. package/vendor/latte/latte/readme.md +27 -9
  13. package/vendor/latte/latte/src/Bridges/Tracy/templates/LattePanel.panel.phtml +4 -1
  14. package/vendor/latte/latte/src/Latte/Compiler/TagLexer.php +1 -1
  15. package/vendor/latte/latte/src/Latte/Compiler/TagParserData.php +129 -129
  16. package/vendor/latte/latte/src/Latte/Compiler/TemplateGenerator.php +1 -1
  17. package/vendor/latte/latte/src/Latte/Compiler/TemplateParser.php +1 -1
  18. package/vendor/latte/latte/src/Latte/Engine.php +22 -2
  19. package/vendor/latte/latte/src/Latte/Essential/CachingIterator.php +2 -3
  20. package/vendor/latte/latte/src/Latte/Essential/CoreExtension.php +9 -1
  21. package/vendor/latte/latte/src/Latte/Essential/Filters.php +110 -10
  22. package/vendor/latte/latte/src/Latte/Essential/Nodes/ImportNode.php +8 -2
  23. package/vendor/latte/latte/src/Latte/Essential/Nodes/VarNode.php +14 -18
  24. package/vendor/latte/latte/src/Latte/Essential/TranslatorExtension.php +1 -1
  25. package/vendor/latte/latte/src/Latte/Loaders/FileLoader.php +5 -4
  26. package/vendor/latte/latte/src/Latte/Runtime/Template.php +1 -1
  27. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/FunctionCallNode.php +0 -1
  28. package/vendor/nette/utils/composer.json +1 -1
  29. package/vendor/nette/utils/readme.md +25 -26
  30. package/vendor/nette/utils/src/Iterators/CachingIterator.php +5 -19
  31. package/vendor/nette/utils/src/Iterators/Mapper.php +1 -2
  32. package/vendor/nette/utils/src/Utils/Arrays.php +61 -29
  33. package/vendor/nette/utils/src/Utils/Callback.php +1 -1
  34. package/vendor/nette/utils/src/Utils/FileSystem.php +15 -2
  35. package/vendor/nette/utils/src/Utils/Finder.php +1 -1
  36. package/vendor/nette/utils/src/Utils/Helpers.php +3 -0
  37. package/vendor/nette/utils/src/Utils/Image.php +33 -46
  38. package/vendor/nette/utils/src/Utils/Iterables.php +100 -20
  39. package/vendor/nette/utils/src/Utils/Reflection.php +7 -5
  40. package/vendor/nette/utils/src/Utils/Strings.php +27 -8
  41. package/vendor/nette/utils/src/Utils/Type.php +2 -2
  42. package/vendor/nette/utils/src/Utils/exceptions.php +5 -5
  43. package/vendor/nette/utils/src/exceptions.php +20 -15
@@ -23,6 +23,9 @@ use function is_array, is_string, count, strlen;
23
23
  */
24
24
  final class Filters
25
25
  {
26
+ public ?string $locale = null;
27
+
28
+
26
29
  /**
27
30
  * Converts HTML to plain text.
28
31
  */
@@ -35,7 +38,7 @@ final class Filters
35
38
 
36
39
 
37
40
  /**
38
- * Removes tags from HTML (but remains HTML entites).
41
+ * Removes tags from HTML (but remains HTML entities).
39
42
  */
40
43
  public static function stripTags(FilterInfo $info, $s): string
41
44
  {
@@ -168,14 +171,11 @@ final class Filters
168
171
  */
169
172
  public static function date(string|int|\DateTimeInterface|\DateInterval|null $time, ?string $format = null): ?string
170
173
  {
174
+ $format ??= Latte\Runtime\Filters::$dateFormat;
171
175
  if ($time == null) { // intentionally ==
172
176
  return null;
173
- }
174
-
175
- $format ??= Latte\Runtime\Filters::$dateFormat;
176
- if ($time instanceof \DateInterval) {
177
+ } elseif ($time instanceof \DateInterval) {
177
178
  return $time->format($format);
178
-
179
179
  } elseif (is_numeric($time)) {
180
180
  $time = (new \DateTime)->setTimestamp((int) $time);
181
181
  } elseif (!$time instanceof \DateTimeInterface) {
@@ -194,10 +194,75 @@ final class Filters
194
194
  }
195
195
 
196
196
 
197
+ /**
198
+ * Date/time formatting according to locale.
199
+ */
200
+ public function localDate(
201
+ string|int|\DateTimeInterface|null $value,
202
+ ?string $format = null,
203
+ ?string $date = null,
204
+ ?string $time = null,
205
+ ): ?string
206
+ {
207
+ if ($this->locale === null) {
208
+ throw new Latte\RuntimeException('Filter |localDate requires the locale to be set using Engine::setLocale()');
209
+ } elseif ($value == null) { // intentionally ==
210
+ return null;
211
+ } elseif (is_numeric($value)) {
212
+ $value = (new \DateTime)->setTimestamp((int) $value);
213
+ } elseif (!$value instanceof \DateTimeInterface) {
214
+ $value = new \DateTime($value);
215
+ $errors = \DateTime::getLastErrors();
216
+ if (!empty($errors['warnings'])) {
217
+ throw new \InvalidArgumentException(reset($errors['warnings']));
218
+ }
219
+ }
220
+
221
+ if ($format === null) {
222
+ $xlt = ['' => \IntlDateFormatter::NONE, 'full' => \IntlDateFormatter::FULL, 'long' => \IntlDateFormatter::LONG, 'medium' => \IntlDateFormatter::MEDIUM, 'short' => \IntlDateFormatter::SHORT,
223
+ 'relative-full' => \IntlDateFormatter::RELATIVE_FULL, 'relative-long' => \IntlDateFormatter::RELATIVE_LONG, 'relative-medium' => \IntlDateFormatter::RELATIVE_MEDIUM, 'relative-short' => \IntlDateFormatter::RELATIVE_SHORT];
224
+ $date ??= $time === null ? 'long' : null;
225
+ $formatter = new \IntlDateFormatter($this->locale, $xlt[$date], $xlt[$time]);
226
+ } else {
227
+ $formatter = new \IntlDateFormatter($this->locale, pattern: (new \IntlDatePatternGenerator($this->locale))->getBestPattern($format));
228
+ }
229
+
230
+ $res = $formatter->format($value);
231
+ $res = preg_replace('~(\d\.) ~', "\$1\u{a0}", $res);
232
+ return $res;
233
+ }
234
+
235
+
236
+ /**
237
+ * Formats a number with grouped thousands and optionally decimal digits according to locale.
238
+ */
239
+ public function number(
240
+ float $number,
241
+ string|int $patternOrDecimals = 0,
242
+ string $decimalSeparator = '.',
243
+ string $thousandsSeparator = ',',
244
+ ): string
245
+ {
246
+ if (is_int($patternOrDecimals) && $patternOrDecimals < 0) {
247
+ throw new Latte\RuntimeException('Filter |number: the number of decimal must not be negative');
248
+ } elseif ($this->locale === null || func_num_args() > 2) {
249
+ return number_format($number, $patternOrDecimals, $decimalSeparator, $thousandsSeparator);
250
+ }
251
+
252
+ $formatter = new \NumberFormatter($this->locale, \NumberFormatter::DECIMAL);
253
+ if (is_string($patternOrDecimals)) {
254
+ $formatter->setPattern($patternOrDecimals);
255
+ } else {
256
+ $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $patternOrDecimals);
257
+ }
258
+ return $formatter->format($number);
259
+ }
260
+
261
+
197
262
  /**
198
263
  * Converts to human-readable file size.
199
264
  */
200
- public static function bytes(float $bytes, int $precision = 2): string
265
+ public function bytes(float $bytes, int $precision = 2): string
201
266
  {
202
267
  $bytes = round($bytes);
203
268
  $units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
@@ -209,7 +274,15 @@ final class Filters
209
274
  $bytes /= 1024;
210
275
  }
211
276
 
212
- return round($bytes, $precision) . ' ' . $unit;
277
+ if ($this->locale === null) {
278
+ $bytes = (string) round($bytes, $precision);
279
+ } else {
280
+ $formatter = new \NumberFormatter($this->locale, \NumberFormatter::DECIMAL);
281
+ $formatter->setAttribute(\NumberFormatter::MAX_FRACTION_DIGITS, $precision);
282
+ $bytes = $formatter->format($bytes);
283
+ }
284
+
285
+ return $bytes . ' ' . $unit;
213
286
  }
214
287
 
215
288
 
@@ -455,7 +528,7 @@ final class Filters
455
528
  * @param iterable<K, V> $data
456
529
  * @return iterable<K, V>
457
530
  */
458
- public static function sort(
531
+ public function sort(
459
532
  iterable $data,
460
533
  ?\Closure $comparison = null,
461
534
  string|int|\Closure|null $by = null,
@@ -469,7 +542,16 @@ final class Filters
469
542
  $by = $byKey === true ? null : $byKey;
470
543
  }
471
544
 
472
- $comparison ??= fn($a, $b) => $a <=> $b;
545
+ if ($comparison) {
546
+ } elseif ($this->locale === null) {
547
+ $comparison = fn($a, $b) => $a <=> $b;
548
+ } else {
549
+ $collator = new \Collator($this->locale);
550
+ $comparison = fn($a, $b) => is_string($a) && is_string($b)
551
+ ? $collator->compare($a, $b)
552
+ : $a <=> $b;
553
+ }
554
+
473
555
  $comparison = match (true) {
474
556
  $by === null => $comparison,
475
557
  $by instanceof \Closure => fn($a, $b) => $comparison($by($a), $by($b)),
@@ -524,6 +606,24 @@ final class Filters
524
606
  }
525
607
 
526
608
 
609
+ /**
610
+ * Filters elements according to a given $predicate. Maintains original keys.
611
+ * @template K
612
+ * @template V
613
+ * @param iterable<K, V> $iterable
614
+ * @param callable(V, K, iterable<K, V>): bool $predicate
615
+ * @return iterable<K, V>
616
+ */
617
+ public static function filter(iterable $iterable, callable $predicate): iterable
618
+ {
619
+ foreach ($iterable as $k => $v) {
620
+ if ($predicate($v, $k, $iterable)) {
621
+ yield $k => $v;
622
+ }
623
+ }
624
+ }
625
+
626
+
527
627
  /**
528
628
  * Returns value clamped to the inclusive range of min and max.
529
629
  */
@@ -9,6 +9,7 @@ declare(strict_types=1);
9
9
 
10
10
  namespace Latte\Essential\Nodes;
11
11
 
12
+ use Latte\Compiler\Nodes\Php\Expression\ArrayNode;
12
13
  use Latte\Compiler\Nodes\Php\ExpressionNode;
13
14
  use Latte\Compiler\Nodes\StatementNode;
14
15
  use Latte\Compiler\PrintContext;
@@ -16,11 +17,12 @@ use Latte\Compiler\Tag;
16
17
 
17
18
 
18
19
  /**
19
- * {import "file"}
20
+ * {import "file"[, args]}
20
21
  */
21
22
  class ImportNode extends StatementNode
22
23
  {
23
24
  public ExpressionNode $file;
25
+ public ArrayNode $args;
24
26
 
25
27
 
26
28
  public static function create(Tag $tag): static
@@ -28,6 +30,8 @@ class ImportNode extends StatementNode
28
30
  $tag->expectArguments();
29
31
  $node = new static;
30
32
  $node->file = $tag->parser->parseUnquotedStringOrExpression();
33
+ $tag->parser->stream->tryConsume(',');
34
+ $node->args = $tag->parser->parseArguments();
31
35
  return $node;
32
36
  }
33
37
 
@@ -35,8 +39,9 @@ class ImportNode extends StatementNode
35
39
  public function print(PrintContext $context): string
36
40
  {
37
41
  return $context->format(
38
- '$this->createTemplate(%node, $this->params, "import")->render() %line;',
42
+ '$this->createTemplate(%node, %node? + $this->params, "import")->render() %line;',
39
43
  $this->file,
44
+ $this->args,
40
45
  $this->position,
41
46
  );
42
47
  }
@@ -45,5 +50,6 @@ class ImportNode extends StatementNode
45
50
  public function &getIterator(): \Generator
46
51
  {
47
52
  yield $this->file;
53
+ yield $this->args;
48
54
  }
49
55
  }
@@ -10,8 +10,10 @@ declare(strict_types=1);
10
10
  namespace Latte\Essential\Nodes;
11
11
 
12
12
  use Latte\Compiler\Nodes\Php\Expression\AssignNode;
13
+ use Latte\Compiler\Nodes\Php\Expression\AssignOpNode;
14
+ use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
15
+ use Latte\Compiler\Nodes\Php\Expression\TernaryNode;
13
16
  use Latte\Compiler\Nodes\Php\Expression\VariableNode;
14
- use Latte\Compiler\Nodes\Php\ExpressionNode;
15
17
  use Latte\Compiler\Nodes\Php\Scalar\NullNode;
16
18
  use Latte\Compiler\Nodes\StatementNode;
17
19
  use Latte\Compiler\PrintContext;
@@ -67,25 +69,19 @@ class VarNode extends StatementNode
67
69
  public function print(PrintContext $context): string
68
70
  {
69
71
  $res = [];
70
- if ($this->default) {
71
- foreach ($this->assignments as $assign) {
72
+ foreach ($this->assignments as $assign) {
73
+ if ($this->default) {
72
74
  assert($assign->var instanceof VariableNode);
73
- if ($assign->var->name instanceof ExpressionNode) {
74
- $var = $assign->var->name->print($context);
75
- } else {
76
- $var = $context->encodeString($assign->var->name);
77
- }
78
- $res[] = $var . ' => ' . $assign->expr->print($context);
75
+ $assign = new AssignOpNode(
76
+ $assign->var,
77
+ '??',
78
+ new TernaryNode(
79
+ new AuxiliaryNode(fn() => 'array_key_exists(' . $context->encodeString($assign->var->name) . ', get_defined_vars())'),
80
+ new NullNode,
81
+ $assign->expr,
82
+ ),
83
+ );
79
84
  }
80
-
81
- return $context->format(
82
- 'extract([%raw], EXTR_SKIP) %line;',
83
- implode(', ', $res),
84
- $this->position,
85
- );
86
- }
87
-
88
- foreach ($this->assignments as $assign) {
89
85
  $res[] = $assign->print($context);
90
86
  }
91
87
 
@@ -50,7 +50,7 @@ final class TranslatorExtension extends Latte\Extension
50
50
  public function getFilters(): array
51
51
  {
52
52
  return [
53
- 'translate' => fn(Latte\Runtime\FilterInfo $fi, ...$args): string => $this->translator
53
+ 'translate' => fn(Latte\Runtime\FilterInfo $fi, ...$args) => $this->translator
54
54
  ? ($this->translator)(...$args)
55
55
  : $args[0],
56
56
  ];
@@ -59,7 +59,7 @@ class FileLoader implements Latte\Loader
59
59
  */
60
60
  public function getReferredName(string $file, string $referringFile): string
61
61
  {
62
- if ($this->baseDir || !preg_match('#/|\\\\|[a-z][a-z0-9+.-]*:#iA', $file)) {
62
+ if ($this->baseDir || !preg_match('#/|\\\\|[a-z]:|phar:#iA', $file)) {
63
63
  $file = $this->normalizePath($referringFile . '/../' . $file);
64
64
  }
65
65
 
@@ -78,15 +78,16 @@ class FileLoader implements Latte\Loader
78
78
 
79
79
  protected static function normalizePath(string $path): string
80
80
  {
81
+ preg_match('#^([a-z]:|phar://.+?/)?(.*)#i', $path, $m);
81
82
  $res = [];
82
- foreach (explode('/', strtr($path, '\\', '/')) as $part) {
83
- if ($part === '..' && $res && end($res) !== '..') {
83
+ foreach (explode('/', strtr($m[2], '\\', '/')) as $part) {
84
+ if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
84
85
  array_pop($res);
85
86
  } elseif ($part !== '.') {
86
87
  $res[] = $part;
87
88
  }
88
89
  }
89
90
 
90
- return implode(DIRECTORY_SEPARATOR, $res);
91
+ return $m[1] . implode(DIRECTORY_SEPARATOR, $res);
91
92
  }
92
93
  }
@@ -138,7 +138,7 @@ class Template
138
138
 
139
139
  $params = $this->prepare();
140
140
 
141
- if ($this->parentName === null && isset($this->global->coreParentFinder)) {
141
+ if ($this->parentName === null && !$this->referringTemplate && isset($this->global->coreParentFinder)) {
142
142
  $this->parentName = ($this->global->coreParentFinder)($this);
143
143
  }
144
144
 
@@ -26,6 +26,5 @@ class FunctionCallNode extends Expression\FunctionCallNode
26
26
  return '$this->global->sandbox->call('
27
27
  . $context->memberAsString($this->name) . ', '
28
28
  . $context->argumentsAsArray($this->args) . ')';
29
-
30
29
  }
31
30
  }
@@ -15,7 +15,7 @@
15
15
  }
16
16
  ],
17
17
  "require": {
18
- "php": ">=8.0 <8.4"
18
+ "php": "8.0 - 8.4"
19
19
  },
20
20
  "require-dev": {
21
21
  "nette/tester": "^2.5",
@@ -1,5 +1,4 @@
1
- Nette Utility Classes
2
- =====================
1
+ [![Nette Utils](https://github.com/nette/utils/assets/194960/c33fdb74-0652-4cad-ac6e-c1ce0d29e32a)](https://doc.nette.org/en/utils)
3
2
 
4
3
  [![Downloads this Month](https://img.shields.io/packagist/dm/nette/utils.svg)](https://packagist.org/packages/nette/utils)
5
4
  [![Tests](https://github.com/nette/utils/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/utils/actions)
@@ -11,25 +10,27 @@ Nette Utility Classes
11
10
  Introduction
12
11
  ------------
13
12
 
14
- In package nette/utils you will find a set of [useful classes](https://doc.nette.org/utils) for everyday use:
15
-
16
- - [Arrays](https://doc.nette.org/utils/arrays) - manipulate arrays
17
- - [Callback](https://doc.nette.org/utils/callback) - PHP callbacks
18
- - [Date and Time](https://doc.nette.org/utils/datetime) - modify times and dates
19
- - [Filesystem](https://doc.nette.org/utils/filesystem) - copying, renaming,
20
- - [Finder](https://doc.nette.org/utils/finder) - finds files and directories
21
- - [Helper Functions](https://doc.nette.org/utils/helpers)
22
- - [HTML elements](https://doc.nette.org/utils/html-elements) - generate HTML
23
- - [Images](https://doc.nette.org/utils/images) - crop, resize, rotate images
24
- - [JSON](https://doc.nette.org/utils/json) - encoding and decoding
25
- - [Generating Random Strings](https://doc.nette.org/utils/random)
26
- - [Paginator](https://doc.nette.org/utils/paginator) - pagination math
27
- - [PHP Reflection](https://doc.nette.org/utils/reflection)
28
- - [Strings](https://doc.nette.org/utils/strings) - useful text functions
29
- - [SmartObject](https://doc.nette.org/utils/smartobject) - PHP object enhancements
30
- - [Validation](https://doc.nette.org/utils/validators) - validate inputs
31
- - [Type](https://doc.nette.org/utils/type) - PHP data type
32
-
13
+ In package nette/utils you will find a set of useful classes for everyday use:
14
+
15
+ [Arrays](https://doc.nette.org/utils/arrays)<br>
16
+ [Callback](https://doc.nette.org/utils/callback) - PHP callbacks<br>
17
+ [Filesystem](https://doc.nette.org/utils/filesystem) - copying, renaming, …<br>
18
+ [Finder](https://doc.nette.org/utils/finder) - finds files and directories<br>
19
+ [Floats](https://doc.nette.org/utils/floats) - floating point numbers<br>
20
+ [Helper Functions](https://doc.nette.org/utils/helpers)<br>
21
+ [HTML elements](https://doc.nette.org/utils/html-elements) - generate HTML<br>
22
+ [Images](https://doc.nette.org/utils/images) - crop, resize, rotate images<br>
23
+ [Iterables](https://doc.nette.org/utils/iterables) <br>
24
+ [JSON](https://doc.nette.org/utils/json) - encoding and decoding<br>
25
+ [Generating Random Strings](https://doc.nette.org/utils/random)<br>
26
+ [Paginator](https://doc.nette.org/utils/paginator) - pagination math<br>
27
+ [PHP Reflection](https://doc.nette.org/utils/reflection)<br>
28
+ [Strings](https://doc.nette.org/utils/strings) - useful text functions<br>
29
+ [SmartObject](https://doc.nette.org/utils/smartobject) - PHP object enhancements<br>
30
+ [Type](https://doc.nette.org/utils/type) - PHP data type<br>
31
+ ✅ [Validation](https://doc.nette.org/utils/validators) - validate inputs<br>
32
+
33
+  <!---->
33
34
 
34
35
  Installation
35
36
  ------------
@@ -40,11 +41,9 @@ The recommended way to install is via Composer:
40
41
  composer require nette/utils
41
42
  ```
42
43
 
43
- - Nette Utils 4.0 is compatible with PHP 8.0 to 8.3
44
- - Nette Utils 3.2 is compatible with PHP 7.2 to 8.3
45
- - Nette Utils 3.1 is compatible with PHP 7.1 to 8.0
46
- - Nette Utils 3.0 is compatible with PHP 7.1 to 8.0
47
- - Nette Utils 2.5 is compatible with PHP 5.6 to 8.0
44
+ Nette Utils 4.0 is compatible with PHP 8.0 to 8.4.
45
+
46
+  <!---->
48
47
 
49
48
  [Support Me](https://github.com/sponsors/dg)
50
49
  --------------------------------------------
@@ -31,26 +31,12 @@ class CachingIterator extends \CachingIterator implements \Countable
31
31
  private int $counter = 0;
32
32
 
33
33
 
34
- public function __construct($iterator)
34
+ public function __construct(iterable|\stdClass $iterable)
35
35
  {
36
- if (is_array($iterator) || $iterator instanceof \stdClass) {
37
- $iterator = new \ArrayIterator($iterator);
38
-
39
- } elseif ($iterator instanceof \IteratorAggregate) {
40
- do {
41
- $iterator = $iterator->getIterator();
42
- } while ($iterator instanceof \IteratorAggregate);
43
-
44
- assert($iterator instanceof \Iterator);
45
-
46
- } elseif ($iterator instanceof \Iterator) {
47
- } elseif ($iterator instanceof \Traversable) {
48
- $iterator = new \IteratorIterator($iterator);
49
- } else {
50
- throw new Nette\InvalidArgumentException(sprintf('Invalid argument passed to %s; array or Traversable expected, %s given.', self::class, get_debug_type($iterator)));
51
- }
52
-
53
- parent::__construct($iterator, 0);
36
+ $iterable = $iterable instanceof \stdClass
37
+ ? new \ArrayIterator($iterable)
38
+ : Nette\Utils\Iterables::toIterator($iterable);
39
+ parent::__construct($iterable, 0);
54
40
  }
55
41
 
56
42
 
@@ -10,9 +10,8 @@ declare(strict_types=1);
10
10
  namespace Nette\Iterators;
11
11
 
12
12
 
13
-
14
13
  /**
15
- * Applies the callback to the elements of the inner iterator.
14
+ * @deprecated use Nette\Utils\Iterables::map()
16
15
  */
17
16
  class Mapper extends \IteratorIterator
18
17
  {
@@ -122,10 +122,11 @@ class Arrays
122
122
 
123
123
  /**
124
124
  * Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
125
- * The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
126
- * @template T
127
- * @param array<T> $array
128
- * @return ?T
125
+ * @template K of int|string
126
+ * @template V
127
+ * @param array<K, V> $array
128
+ * @param ?callable(V, K, array<K, V>): bool $predicate
129
+ * @return ?V
129
130
  */
130
131
  public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed
131
132
  {
@@ -138,10 +139,11 @@ class Arrays
138
139
 
139
140
  /**
140
141
  * Returns the last item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
141
- * The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
142
- * @template T
143
- * @param array<T> $array
144
- * @return ?T
142
+ * @template K of int|string
143
+ * @template V
144
+ * @param array<K, V> $array
145
+ * @param ?callable(V, K, array<K, V>): bool $predicate
146
+ * @return ?V
145
147
  */
146
148
  public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed
147
149
  {
@@ -154,7 +156,11 @@ class Arrays
154
156
 
155
157
  /**
156
158
  * Returns the key of first item (matching the specified predicate if given) or null if there is no such item.
157
- * The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
159
+ * @template K of int|string
160
+ * @template V
161
+ * @param array<K, V> $array
162
+ * @param ?callable(V, K, array<K, V>): bool $predicate
163
+ * @return ?K
158
164
  */
159
165
  public static function firstKey(array $array, ?callable $predicate = null): int|string|null
160
166
  {
@@ -172,7 +178,11 @@ class Arrays
172
178
 
173
179
  /**
174
180
  * Returns the key of last item (matching the specified predicate if given) or null if there is no such item.
175
- * The $predicate has the signature `function (mixed $value, int|string $key, array $array): bool`.
181
+ * @template K of int|string
182
+ * @template V
183
+ * @param array<K, V> $array
184
+ * @param ?callable(V, K, array<K, V>): bool $predicate
185
+ * @return ?K
176
186
  */
177
187
  public static function lastKey(array $array, ?callable $predicate = null): int|string|null
178
188
  {
@@ -267,7 +277,8 @@ class Arrays
267
277
  */
268
278
  public static function isList(mixed $value): bool
269
279
  {
270
- return is_array($value) && (PHP_VERSION_ID < 80100
280
+ return is_array($value) && (
281
+ PHP_VERSION_ID < 80100
271
282
  ? !$value || array_keys($value) === range(0, count($value) - 1)
272
283
  : array_is_list($value)
273
284
  );
@@ -368,12 +379,11 @@ class Arrays
368
379
 
369
380
 
370
381
  /**
371
- * Tests whether at least one element in the array passes the test implemented by the provided function,
372
- * which has the signature `function ($value, $key, array $array): bool`.
373
- * @template K
382
+ * Tests whether at least one element in the array passes the test implemented by the provided function.
383
+ * @template K of int|string
374
384
  * @template V
375
- * @param iterable<K, V> $array
376
- * @param callable(V, K, ($array is array ? array<K, V> : iterable<K, V>)): bool $predicate
385
+ * @param array<K, V> $array
386
+ * @param callable(V, K, array<K, V>): bool $predicate
377
387
  */
378
388
  public static function some(iterable $array, callable $predicate): bool
379
389
  {
@@ -388,12 +398,11 @@ class Arrays
388
398
 
389
399
 
390
400
  /**
391
- * Tests whether all elements in the array pass the test implemented by the provided function,
392
- * which has the signature `function ($value, $key, array $array): bool`.
393
- * @template K
401
+ * Tests whether all elements in the array pass the test implemented by the provided function.
402
+ * @template K of int|string
394
403
  * @template V
395
- * @param iterable<K, V> $array
396
- * @param callable(V, K, ($array is array ? array<K, V> : iterable<K, V>)): bool $predicate
404
+ * @param array<K, V> $array
405
+ * @param callable(V, K, array<K, V>): bool $predicate
397
406
  */
398
407
  public static function every(iterable $array, callable $predicate): bool
399
408
  {
@@ -409,11 +418,10 @@ class Arrays
409
418
 
410
419
  /**
411
420
  * Returns a new array containing all key-value pairs matching the given $predicate.
412
- * The callback has the signature `function (mixed $value, int|string $key, array $array): bool`.
413
- * @template K of array-key
421
+ * @template K of int|string
414
422
  * @template V
415
- * @param array<K, V> $array
416
- * @param callable(V, K, array<K, V>): bool $predicate
423
+ * @param array<K, V> $array
424
+ * @param callable(V, K, array<K, V>): bool $predicate
417
425
  * @return array<K, V>
418
426
  */
419
427
  public static function filter(array $array, callable $predicate): array
@@ -430,12 +438,11 @@ class Arrays
430
438
 
431
439
  /**
432
440
  * Returns an array containing the original keys and results of applying the given transform function to each element.
433
- * The function has signature `function ($value, $key, array $array): mixed`.
434
- * @template K of array-key
441
+ * @template K of int|string
435
442
  * @template V
436
443
  * @template R
437
- * @param iterable<K, V> $array
438
- * @param callable(V, K, ($array is array ? array<K, V> : iterable<K, V>)): R $transformer
444
+ * @param array<K, V> $array
445
+ * @param callable(V, K, array<K, V>): R $transformer
439
446
  * @return array<K, R>
440
447
  */
441
448
  public static function map(iterable $array, callable $transformer): array
@@ -449,6 +456,31 @@ class Arrays
449
456
  }
450
457
 
451
458
 
459
+ /**
460
+ * Returns an array containing new keys and values generated by applying the given transform function to each element.
461
+ * If the function returns null, the element is skipped.
462
+ * @template K of int|string
463
+ * @template V
464
+ * @template ResK of int|string
465
+ * @template ResV
466
+ * @param array<K, V> $array
467
+ * @param callable(V, K, array<K, V>): ?array{ResK, ResV} $transformer
468
+ * @return array<ResK, ResV>
469
+ */
470
+ public static function mapWithKeys(array $array, callable $transformer): array
471
+ {
472
+ $res = [];
473
+ foreach ($array as $k => $v) {
474
+ $pair = $transformer($v, $k, $array);
475
+ if ($pair) {
476
+ $res[$pair[0]] = $pair[1];
477
+ }
478
+ }
479
+
480
+ return $res;
481
+ }
482
+
483
+
452
484
  /**
453
485
  * Invokes all callbacks and returns array of results.
454
486
  * @param callable[] $callbacks
@@ -94,7 +94,7 @@ final class Callback
94
94
  }
95
95
 
96
96
  if (is_string($callable) && str_contains($callable, '::')) {
97
- return new ReflectionMethod($callable);
97
+ return new ReflectionMethod(...explode('::', $callable, 2));
98
98
  } elseif (is_array($callable)) {
99
99
  return new ReflectionMethod($callable[0], $callable[1]);
100
100
  } elseif (is_object($callable) && !$callable instanceof \Closure) {
@@ -271,7 +271,7 @@ final class FileSystem
271
271
  */
272
272
  public static function isAbsolute(string $path): bool
273
273
  {
274
- return (bool) preg_match('#([a-z]:)?[/\\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
274
+ return (bool) preg_match('#([a-z]:)?[/\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
275
275
  }
276
276
 
277
277
 
@@ -280,7 +280,7 @@ final class FileSystem
280
280
  */
281
281
  public static function normalizePath(string $path): string
282
282
  {
283
- $parts = $path === '' ? [] : preg_split('~[/\\\\]+~', $path);
283
+ $parts = $path === '' ? [] : preg_split('~[/\\\]+~', $path);
284
284
  $res = [];
285
285
  foreach ($parts as $part) {
286
286
  if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
@@ -305,6 +305,19 @@ final class FileSystem
305
305
  }
306
306
 
307
307
 
308
+ /**
309
+ * Resolves a path against a base path. If the path is absolute, returns it directly, if it's relative, joins it with the base path.
310
+ */
311
+ public static function resolvePath(string $basePath, string $path): string
312
+ {
313
+ return match (true) {
314
+ self::isAbsolute($path) => self::platformSlashes($path),
315
+ $path === '' => self::platformSlashes($basePath),
316
+ default => self::joinPaths($basePath, $path),
317
+ };
318
+ }
319
+
320
+
308
321
  /**
309
322
  * Converts backslashes to slashes.
310
323
  */