@vituum/vite-plugin-latte 1.1.0 → 1.2.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 (34) hide show
  1. package/latte/PlaceholderFunction.php +2 -2
  2. package/package.json +1 -1
  3. package/vendor/autoload.php +1 -1
  4. package/vendor/composer/autoload_classmap.php +2 -0
  5. package/vendor/composer/autoload_real.php +4 -4
  6. package/vendor/composer/autoload_static.php +4 -2
  7. package/vendor/composer/installed.json +7 -7
  8. package/vendor/composer/installed.php +5 -5
  9. package/vendor/latte/latte/src/Latte/Compiler/Escaper.php +11 -2
  10. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/FilterNode.php +1 -1
  11. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ModifierNode.php +2 -2
  12. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/NameNode.php +11 -21
  13. package/vendor/latte/latte/src/Latte/Compiler/PrintContext.php +1 -1
  14. package/vendor/latte/latte/src/Latte/Compiler/TagLexer.php +2 -2
  15. package/vendor/latte/latte/src/Latte/Compiler/TagParserData.php +275 -280
  16. package/vendor/latte/latte/src/Latte/Compiler/TemplateGenerator.php +4 -4
  17. package/vendor/latte/latte/src/Latte/Compiler/TemplateLexer.php +10 -2
  18. package/vendor/latte/latte/src/Latte/Compiler/TemplateParserHtml.php +6 -2
  19. package/vendor/latte/latte/src/Latte/Engine.php +109 -97
  20. package/vendor/latte/latte/src/Latte/Essential/AuxiliaryIterator.php +46 -0
  21. package/vendor/latte/latte/src/Latte/Essential/Blueprint.php +42 -25
  22. package/vendor/latte/latte/src/Latte/Essential/CoreExtension.php +76 -72
  23. package/vendor/latte/latte/src/Latte/Essential/Filters.php +95 -37
  24. package/vendor/latte/latte/src/Latte/Essential/Nodes/ContentTypeNode.php +7 -0
  25. package/vendor/latte/latte/src/Latte/Essential/Nodes/ForeachNode.php +36 -0
  26. package/vendor/latte/latte/src/Latte/Essential/Nodes/TemplatePrintNode.php +25 -3
  27. package/vendor/latte/latte/src/Latte/Essential/Nodes/VarPrintNode.php +9 -2
  28. package/vendor/latte/latte/src/Latte/Essential/Passes.php +10 -50
  29. package/vendor/latte/latte/src/Latte/Helpers.php +3 -1
  30. package/vendor/latte/latte/src/Latte/Loader.php +1 -0
  31. package/vendor/latte/latte/src/Latte/Loaders/FileLoader.php +1 -2
  32. package/vendor/latte/latte/src/Latte/Runtime/Filters.php +1 -4
  33. package/vendor/latte/latte/src/Latte/Runtime/FunctionExecutor.php +68 -0
  34. package/vendor/latte/latte/src/Latte/Runtime/Template.php +1 -3
@@ -11,7 +11,6 @@ namespace Latte\Essential;
11
11
 
12
12
  use Latte;
13
13
  use Latte\Compiler\Nodes\Php\Scalar;
14
- use Latte\Compiler\Nodes\TemplateNode;
15
14
  use Latte\Compiler\Nodes\TextNode;
16
15
  use Latte\Compiler\Tag;
17
16
  use Latte\Compiler\TemplateParser;
@@ -25,21 +24,19 @@ use Nette;
25
24
  */
26
25
  final class CoreExtension extends Latte\Extension
27
26
  {
28
- private array $functions;
29
- private bool $strict;
30
- private Runtime\Template $template;
27
+ private Latte\Engine $engine;
28
+ private Filters $filters;
31
29
 
32
30
 
33
- public function beforeCompile(Latte\Engine $engine): void
31
+ public function __construct()
34
32
  {
35
- $this->functions = $engine->getFunctions();
36
- $this->strict = $engine->isStrictParsing();
33
+ $this->filters = new Filters;
37
34
  }
38
35
 
39
36
 
40
- public function beforeRender(Runtime\Template $template): void
37
+ public function beforeCompile(Latte\Engine $engine): void
41
38
  {
42
- $this->template = $template;
39
+ $this->engine = $engine;
43
40
  }
44
41
 
45
42
 
@@ -109,18 +106,19 @@ final class CoreExtension extends Latte\Extension
109
106
  public function getFilters(): array
110
107
  {
111
108
  return [
112
- 'batch' => [Filters::class, 'batch'],
113
- 'breakLines' => [Filters::class, 'breaklines'],
114
- 'breaklines' => [Filters::class, 'breaklines'],
115
- 'bytes' => [Filters::class, 'bytes'],
109
+ 'batch' => [$this->filters, 'batch'],
110
+ 'breakLines' => [$this->filters, 'breaklines'],
111
+ 'breaklines' => [$this->filters, 'breaklines'],
112
+ 'bytes' => [$this->filters, 'bytes'],
116
113
  'capitalize' => extension_loaded('mbstring')
117
- ? [Filters::class, 'capitalize']
118
- : function () { throw new RuntimeException('Filter |capitalize requires mbstring extension.'); },
119
- 'ceil' => [Filters::class, 'ceil'],
120
- 'clamp' => [Filters::class, 'clamp'],
121
- 'dataStream' => [Filters::class, 'dataStream'],
122
- 'datastream' => [Filters::class, 'dataStream'],
123
- 'date' => [Filters::class, 'date'],
114
+ ? [$this->filters, 'capitalize']
115
+ : fn() => throw new RuntimeException('Filter |capitalize requires mbstring extension.'),
116
+ 'ceil' => [$this->filters, 'ceil'],
117
+ 'checkUrl' => [Latte\Runtime\Filters::class, 'safeUrl'],
118
+ 'clamp' => [$this->filters, 'clamp'],
119
+ 'dataStream' => [$this->filters, 'dataStream'],
120
+ 'datastream' => [$this->filters, 'dataStream'],
121
+ 'date' => [$this->filters, 'date'],
124
122
  'escape' => [Latte\Runtime\Filters::class, 'nop'],
125
123
  'escapeCss' => [Latte\Runtime\Filters::class, 'escapeCss'],
126
124
  'escapeHtml' => [Latte\Runtime\Filters::class, 'escapeHtml'],
@@ -129,50 +127,50 @@ final class CoreExtension extends Latte\Extension
129
127
  'escapeJs' => [Latte\Runtime\Filters::class, 'escapeJs'],
130
128
  'escapeUrl' => 'rawurlencode',
131
129
  'escapeXml' => [Latte\Runtime\Filters::class, 'escapeXml'],
132
- 'explode' => [Filters::class, 'explode'],
133
- 'first' => [Filters::class, 'first'],
130
+ 'explode' => [$this->filters, 'explode'],
131
+ 'first' => [$this->filters, 'first'],
134
132
  'firstUpper' => extension_loaded('mbstring')
135
- ? [Filters::class, 'firstUpper']
136
- : function () { throw new RuntimeException('Filter |firstUpper requires mbstring extension.'); },
137
- 'floor' => [Filters::class, 'floor'],
138
- 'checkUrl' => [Latte\Runtime\Filters::class, 'safeUrl'],
139
- 'implode' => [Filters::class, 'implode'],
140
- 'indent' => [Filters::class, 'indent'],
141
- 'join' => [Filters::class, 'implode'],
142
- 'last' => [Filters::class, 'last'],
143
- 'length' => [Filters::class, 'length'],
133
+ ? [$this->filters, 'firstUpper']
134
+ : fn() => throw new RuntimeException('Filter |firstUpper requires mbstring extension.'),
135
+ 'floor' => [$this->filters, 'floor'],
136
+ 'group' => [$this->filters, 'group'],
137
+ 'implode' => [$this->filters, 'implode'],
138
+ 'indent' => [$this->filters, 'indent'],
139
+ 'join' => [$this->filters, 'implode'],
140
+ 'last' => [$this->filters, 'last'],
141
+ 'length' => [$this->filters, 'length'],
144
142
  'lower' => extension_loaded('mbstring')
145
- ? [Filters::class, 'lower']
146
- : function () { throw new RuntimeException('Filter |lower requires mbstring extension.'); },
143
+ ? [$this->filters, 'lower']
144
+ : fn() => throw new RuntimeException('Filter |lower requires mbstring extension.'),
147
145
  'number' => 'number_format',
148
- 'padLeft' => [Filters::class, 'padLeft'],
149
- 'padRight' => [Filters::class, 'padRight'],
150
- 'query' => [Filters::class, 'query'],
151
- 'random' => [Filters::class, 'random'],
152
- 'repeat' => [Filters::class, 'repeat'],
153
- 'replace' => [Filters::class, 'replace'],
154
- 'replaceRe' => [Filters::class, 'replaceRe'],
155
- 'replaceRE' => [Filters::class, 'replaceRe'],
156
- 'reverse' => [Filters::class, 'reverse'],
157
- 'round' => [Filters::class, 'round'],
158
- 'slice' => [Filters::class, 'slice'],
159
- 'sort' => [Filters::class, 'sort'],
160
- 'spaceless' => [Filters::class, 'strip'],
161
- 'split' => [Filters::class, 'explode'],
162
- 'strip' => [Filters::class, 'strip'], // obsolete
163
- 'stripHtml' => [Filters::class, 'stripHtml'],
164
- 'striphtml' => [Filters::class, 'stripHtml'],
165
- 'stripTags' => [Filters::class, 'stripTags'],
166
- 'striptags' => [Filters::class, 'stripTags'],
167
- 'substr' => [Filters::class, 'substring'],
168
- 'trim' => [Filters::class, 'trim'],
169
- 'truncate' => [Filters::class, 'truncate'],
146
+ 'padLeft' => [$this->filters, 'padLeft'],
147
+ 'padRight' => [$this->filters, 'padRight'],
148
+ 'query' => [$this->filters, 'query'],
149
+ 'random' => [$this->filters, 'random'],
150
+ 'repeat' => [$this->filters, 'repeat'],
151
+ 'replace' => [$this->filters, 'replace'],
152
+ 'replaceRe' => [$this->filters, 'replaceRe'],
153
+ 'replaceRE' => [$this->filters, 'replaceRe'],
154
+ 'reverse' => [$this->filters, 'reverse'],
155
+ 'round' => [$this->filters, 'round'],
156
+ 'slice' => [$this->filters, 'slice'],
157
+ 'sort' => [$this->filters, 'sort'],
158
+ 'spaceless' => [$this->filters, 'strip'],
159
+ 'split' => [$this->filters, 'explode'],
160
+ 'strip' => [$this->filters, 'strip'], // obsolete
161
+ 'stripHtml' => [$this->filters, 'stripHtml'],
162
+ 'striphtml' => [$this->filters, 'stripHtml'],
163
+ 'stripTags' => [$this->filters, 'stripTags'],
164
+ 'striptags' => [$this->filters, 'stripTags'],
165
+ 'substr' => [$this->filters, 'substring'],
166
+ 'trim' => [$this->filters, 'trim'],
167
+ 'truncate' => [$this->filters, 'truncate'],
170
168
  'upper' => extension_loaded('mbstring')
171
- ? [Filters::class, 'upper']
172
- : function () { throw new RuntimeException('Filter |upper requires mbstring extension.'); },
169
+ ? [$this->filters, 'upper']
170
+ : fn() => throw new RuntimeException('Filter |upper requires mbstring extension.'),
173
171
  'webalize' => class_exists(Nette\Utils\Strings::class)
174
172
  ? [Nette\Utils\Strings::class, 'webalize']
175
- : function () { throw new RuntimeException('Filter |webalize requires nette/utils package.'); },
173
+ : fn() => throw new RuntimeException('Filter |webalize requires nette/utils package.'),
176
174
  ];
177
175
  }
178
176
 
@@ -180,25 +178,27 @@ final class CoreExtension extends Latte\Extension
180
178
  public function getFunctions(): array
181
179
  {
182
180
  return [
183
- 'clamp' => [Filters::class, 'clamp'],
184
- 'divisibleBy' => [Filters::class, 'divisibleBy'],
185
- 'even' => [Filters::class, 'even'],
186
- 'first' => [Filters::class, 'first'],
187
- 'last' => [Filters::class, 'last'],
188
- 'odd' => [Filters::class, 'odd'],
189
- 'slice' => [Filters::class, 'slice'],
190
- 'hasBlock' => fn(string $name): bool => $this->template->hasBlock($name),
181
+ 'clamp' => [$this->filters, 'clamp'],
182
+ 'divisibleBy' => [$this->filters, 'divisibleBy'],
183
+ 'even' => [$this->filters, 'even'],
184
+ 'first' => [$this->filters, 'first'],
185
+ 'group' => [$this->filters, 'group'],
186
+ 'last' => [$this->filters, 'last'],
187
+ 'odd' => [$this->filters, 'odd'],
188
+ 'slice' => [$this->filters, 'slice'],
189
+ 'hasBlock' => fn(Runtime\Template $template, string $name): bool => $template->hasBlock($name),
191
190
  ];
192
191
  }
193
192
 
194
193
 
195
194
  public function getPasses(): array
196
195
  {
196
+ $passes = new Passes($this->engine);
197
197
  return [
198
- 'internalVariables' => fn(TemplateNode $node) => Passes::internalVariablesPass($node, $this->strict),
199
- 'overwrittenVariables' => [Passes::class, 'overwrittenVariablesPass'],
200
- 'customFunctions' => fn(TemplateNode $node) => Passes::customFunctionsPass($node, $this->functions),
201
- 'moveTemplatePrintToHead' => [Passes::class, 'moveTemplatePrintToHeadPass'],
198
+ 'internalVariables' => [$passes, 'forbiddenVariablesPass'],
199
+ 'overwrittenVariables' => [Nodes\ForeachNode::class, 'overwrittenVariablesPass'],
200
+ 'customFunctions' => [$passes, 'customFunctionsPass'],
201
+ 'moveTemplatePrintToHead' => [Nodes\TemplatePrintNode::class, 'moveToHeadPass'],
202
202
  'nElse' => [Nodes\NElseNode::class, 'processPass'],
203
203
  ];
204
204
  }
@@ -233,13 +233,17 @@ final class CoreExtension extends Latte\Extension
233
233
  */
234
234
  private function parseSyntax(Tag $tag, TemplateParser $parser): \Generator
235
235
  {
236
+ if ($tag->isNAttribute() && $tag->prefix !== $tag::PrefixNone) {
237
+ throw new Latte\CompileException("Use n:syntax instead of {$tag->getNotation()}", $tag->position);
238
+ }
236
239
  $tag->expectArguments();
237
240
  $token = $tag->parser->stream->consume();
238
241
  $lexer = $parser->getLexer();
239
- $saved = [$lexer->openDelimiter, $lexer->closeDelimiter];
240
242
  $lexer->setSyntax($token->text, $tag->isNAttribute() ? null : $tag->name);
241
243
  [$inner] = yield;
242
- [$lexer->openDelimiter, $lexer->closeDelimiter] = $saved;
244
+ if (!$tag->isNAttribute()) {
245
+ $lexer->popSyntax();
246
+ }
243
247
  return $inner;
244
248
  }
245
249
  }
@@ -172,17 +172,12 @@ final class Filters
172
172
  return null;
173
173
  }
174
174
 
175
- if (!isset($format)) {
176
- $format = Latte\Runtime\Filters::$dateFormat;
177
- }
178
-
175
+ $format ??= Latte\Runtime\Filters::$dateFormat;
179
176
  if ($time instanceof \DateInterval) {
180
177
  return $time->format($format);
181
178
 
182
179
  } elseif (is_numeric($time)) {
183
- $time = new \DateTime('@' . $time);
184
- $time->setTimeZone(new \DateTimeZone(date_default_timezone_get()));
185
-
180
+ $time = (new \DateTime)->setTimestamp((int) $time);
186
181
  } elseif (!$time instanceof \DateTimeInterface) {
187
182
  $time = new \DateTime($time);
188
183
  }
@@ -200,7 +195,7 @@ final class Filters
200
195
 
201
196
 
202
197
  /**
203
- * Converts to human readable file size.
198
+ * Converts to human-readable file size.
204
199
  */
205
200
  public static function bytes(float $bytes, int $precision = 2): string
206
201
  {
@@ -262,10 +257,7 @@ final class Filters
262
257
  */
263
258
  public static function dataStream(string $data, ?string $type = null): string
264
259
  {
265
- if ($type === null) {
266
- $type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
267
- }
268
-
260
+ $type ??= finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
269
261
  return 'data:' . ($type ? "$type;" : '') . 'base64,' . base64_encode($data);
270
262
  }
271
263
 
@@ -273,7 +265,7 @@ final class Filters
273
265
  public static function breaklines(string|Stringable|null $s): Html
274
266
  {
275
267
  $s = htmlspecialchars((string) $s, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
276
- return new Html(nl2br($s, Latte\Runtime\Filters::$xml));
268
+ return new Html(nl2br($s, false));
277
269
  }
278
270
 
279
271
 
@@ -283,15 +275,11 @@ final class Filters
283
275
  public static function substring(string|Stringable|null $s, int $start, ?int $length = null): string
284
276
  {
285
277
  $s = (string) $s;
286
- if ($length === null) {
287
- $length = self::strLength($s);
288
- }
289
-
290
- if (function_exists('mb_substr')) {
291
- return mb_substr($s, $start, $length, 'UTF-8'); // MB is much faster
292
- }
293
-
294
- return iconv_substr($s, $start, $length, 'UTF-8');
278
+ return match (true) {
279
+ extension_loaded('mbstring') => mb_substr($s, $start, $length, 'UTF-8'),
280
+ extension_loaded('iconv') => iconv_substr($s, $start, $length, 'UTF-8'),
281
+ default => throw new Latte\RuntimeException("Filter |substr requires 'mbstring' or 'iconv' extension."),
282
+ };
295
283
  }
296
284
 
297
285
 
@@ -422,7 +410,7 @@ final class Filters
422
410
  /**
423
411
  * Reverses string or array.
424
412
  */
425
- public static function reverse(string|array|\Traversable $val, bool $preserveKeys = false): string|array
413
+ public static function reverse(string|iterable $val, bool $preserveKeys = false): string|array
426
414
  {
427
415
  if (is_array($val)) {
428
416
  return array_reverse($val, $preserveKeys);
@@ -437,7 +425,7 @@ final class Filters
437
425
  /**
438
426
  * Chunks items by returning an array of arrays with the given number of items.
439
427
  */
440
- public static function batch(array|\Traversable $list, int $length, $rest = null): \Generator
428
+ public static function batch(iterable $list, int $length, $rest = null): \Generator
441
429
  {
442
430
  $batch = [];
443
431
  foreach ($list as $key => $value) {
@@ -461,14 +449,78 @@ final class Filters
461
449
 
462
450
 
463
451
  /**
464
- * Sorts an array.
465
- * @param mixed[] $array
466
- * @return mixed[]
452
+ * Sorts elements using the comparison function and preserves the key association.
453
+ * @template K
454
+ * @template V
455
+ * @param iterable<K, V> $data
456
+ * @return iterable<K, V>
457
+ */
458
+ public static function sort(
459
+ iterable $data,
460
+ ?\Closure $comparison = null,
461
+ string|int|\Closure|null $by = null,
462
+ string|int|\Closure|bool $byKey = false,
463
+ ): iterable
464
+ {
465
+ if ($byKey !== false) {
466
+ if ($by !== null) {
467
+ throw new \InvalidArgumentException('Filter |sort cannot use both $by and $byKey.');
468
+ }
469
+ $by = $byKey === true ? null : $byKey;
470
+ }
471
+
472
+ $comparison ??= fn($a, $b) => $a <=> $b;
473
+ $comparison = match (true) {
474
+ $by === null => $comparison,
475
+ $by instanceof \Closure => fn($a, $b) => $comparison($by($a), $by($b)),
476
+ default => fn($a, $b) => $comparison(is_array($a) ? $a[$by] : $a->$by, is_array($b) ? $b[$by] : $b->$by),
477
+ };
478
+
479
+ if (is_array($data)) {
480
+ $byKey ? uksort($data, $comparison) : uasort($data, $comparison);
481
+ return $data;
482
+ }
483
+
484
+ $pairs = [];
485
+ foreach ($data as $key => $value) {
486
+ $pairs[] = [$key, $value];
487
+ }
488
+ uasort($pairs, fn($a, $b) => $byKey ? $comparison($a[0], $b[0]) : $comparison($a[1], $b[1]));
489
+
490
+ return new AuxiliaryIterator($pairs);
491
+ }
492
+
493
+
494
+ /**
495
+ * Groups elements by the element indices and preserves the key association and order.
496
+ * @template K
497
+ * @template V
498
+ * @param iterable<K, V> $data
499
+ * @return iterable<iterable<K, V>>
467
500
  */
468
- public static function sort(array $array, ?\Closure $callback = null): array
501
+ public static function group(iterable $data, string|int|\Closure $by): iterable
469
502
  {
470
- $callback ? uasort($array, $callback) : asort($array);
471
- return $array;
503
+ $fn = $by instanceof \Closure ? $by : fn($a) => is_array($a) ? $a[$by] : $a->$by;
504
+ $keys = $groups = [];
505
+
506
+ foreach ($data as $k => $v) {
507
+ $groupKey = $fn($v, $k);
508
+ if (!$groups || $prevKey !== $groupKey) {
509
+ $index = array_search($groupKey, $keys, true);
510
+ if ($index === false) {
511
+ $index = count($keys);
512
+ $keys[$index] = $groupKey;
513
+ }
514
+ $prevKey = $groupKey;
515
+ }
516
+ $groups[$index][] = [$k, $v];
517
+ }
518
+
519
+ return new AuxiliaryIterator(array_map(
520
+ fn($key, $group) => [$key, new AuxiliaryIterator($group)],
521
+ $keys,
522
+ $groups,
523
+ ));
472
524
  }
473
525
 
474
526
 
@@ -524,23 +576,29 @@ final class Filters
524
576
 
525
577
 
526
578
  /**
527
- * Returns the first item from the array or null if array is empty.
579
+ * Returns the first element in an array or character in a string, or null if none.
528
580
  */
529
- public static function first(string|array $value): mixed
581
+ public static function first(string|iterable $value): mixed
530
582
  {
531
- return is_array($value)
532
- ? (count($value) ? reset($value) : null)
533
- : self::substring($value, 0, 1);
583
+ if (is_string($value)) {
584
+ return self::substring($value, 0, 1);
585
+ }
586
+
587
+ foreach ($value as $item) {
588
+ return $item;
589
+ }
590
+
591
+ return null;
534
592
  }
535
593
 
536
594
 
537
595
  /**
538
- * Returns the last item from the array or null if array is empty.
596
+ * Returns the last element in an array or character in a string, or null if none.
539
597
  */
540
598
  public static function last(string|array $value): mixed
541
599
  {
542
600
  return is_array($value)
543
- ? (count($value) ? end($value) : null)
601
+ ? ($value[array_key_last($value)] ?? null)
544
602
  : self::substring($value, -1);
545
603
  }
546
604
 
@@ -24,6 +24,7 @@ class ContentTypeNode extends StatementNode
24
24
  {
25
25
  public string $contentType;
26
26
  public ?string $mimeType = null;
27
+ public bool $inScript;
27
28
 
28
29
 
29
30
  public static function create(Tag $tag, TemplateParser $parser): static
@@ -37,6 +38,7 @@ class ContentTypeNode extends StatementNode
37
38
  }
38
39
 
39
40
  $node = new static;
41
+ $node->inScript = (bool) $tag->htmlElement;
40
42
  $node->contentType = match (true) {
41
43
  str_contains($type, 'html') => ContentType::Html,
42
44
  str_contains($type, 'xml') => ContentType::Xml,
@@ -56,6 +58,11 @@ class ContentTypeNode extends StatementNode
56
58
 
57
59
  public function print(PrintContext $context): string
58
60
  {
61
+ if ($this->inScript) {
62
+ $context->getEscaper()->enterHtmlRaw($this->contentType);
63
+ return '';
64
+ }
65
+
59
66
  $context->beginEscape()->enterContentType($this->contentType);
60
67
 
61
68
  return $this->mimeType
@@ -10,11 +10,16 @@ declare(strict_types=1);
10
10
  namespace Latte\Essential\Nodes;
11
11
 
12
12
  use Latte\CompileException;
13
+ use Latte\Compiler\Node;
13
14
  use Latte\Compiler\Nodes\AreaNode;
15
+ use Latte\Compiler\Nodes\AuxiliaryNode;
14
16
  use Latte\Compiler\Nodes\NopNode;
17
+ use Latte\Compiler\Nodes\Php\Expression\VariableNode;
15
18
  use Latte\Compiler\Nodes\Php\ExpressionNode;
16
19
  use Latte\Compiler\Nodes\Php\ListNode;
17
20
  use Latte\Compiler\Nodes\StatementNode;
21
+ use Latte\Compiler\Nodes\TemplateNode;
22
+ use Latte\Compiler\NodeTraverser;
18
23
  use Latte\Compiler\Position;
19
24
  use Latte\Compiler\PrintContext;
20
25
  use Latte\Compiler\Tag;
@@ -143,4 +148,35 @@ class ForeachNode extends StatementNode
143
148
  yield $this->else;
144
149
  }
145
150
  }
151
+
152
+
153
+ /**
154
+ * Pass: checks if foreach overrides template variables.
155
+ */
156
+ public static function overwrittenVariablesPass(TemplateNode $node): void
157
+ {
158
+ $vars = [];
159
+ (new NodeTraverser)->traverse($node, function (Node $node) use (&$vars) {
160
+ if ($node instanceof self && $node->checkArgs) {
161
+ foreach ([$node->key, $node->value] as $var) {
162
+ if ($var instanceof VariableNode) {
163
+ $vars[$var->name][] = $node->position->line;
164
+ }
165
+ }
166
+ }
167
+ });
168
+ if ($vars) {
169
+ array_unshift($node->head->children, new AuxiliaryNode(fn(PrintContext $context) => $context->format(
170
+ <<<'XX'
171
+ if (!$this->getReferringTemplate() || $this->getReferenceType() === 'extends') {
172
+ foreach (array_intersect_key(%dump, $this->params) as $ʟ_v => $ʟ_l) {
173
+ trigger_error("Variable \$$ʟ_v overwritten in foreach on line $ʟ_l");
174
+ }
175
+ }
176
+
177
+ XX,
178
+ array_map(fn($l) => implode(', ', $l), $vars),
179
+ )));
180
+ }
181
+ }
146
182
  }
@@ -9,15 +9,17 @@ declare(strict_types=1);
9
9
 
10
10
  namespace Latte\Essential\Nodes;
11
11
 
12
+ use Latte\Compiler\Node;
13
+ use Latte\Compiler\Nodes;
12
14
  use Latte\Compiler\Nodes\StatementNode;
13
- use Latte\Compiler\PhpHelpers;
15
+ use Latte\Compiler\NodeTraverser;
14
16
  use Latte\Compiler\PrintContext;
15
17
  use Latte\Compiler\Tag;
16
18
  use Latte\Compiler\Token;
17
19
 
18
20
 
19
21
  /**
20
- * {templatePrint [ClassName]}
22
+ * {templatePrint [ParentClass]}
21
23
  */
22
24
  class TemplatePrintNode extends StatementNode
23
25
  {
@@ -34,7 +36,13 @@ class TemplatePrintNode extends StatementNode
34
36
 
35
37
  public function print(PrintContext $context): string
36
38
  {
37
- return '(new Latte\Essential\Blueprint)->printClass($this, ' . PhpHelpers::dump($this->template) . '); exit;';
39
+ return $context->format(<<<'XX'
40
+ $ʟ_bp = new Latte\Essential\Blueprint;
41
+ $ʟ_bp->printBegin();
42
+ $ʟ_bp->printClass($ʟ_bp->generateTemplateClass($this->getParameters(), extends: %dump));
43
+ $ʟ_bp->printEnd();
44
+ exit;
45
+ XX, $this->template);
38
46
  }
39
47
 
40
48
 
@@ -42,4 +50,18 @@ class TemplatePrintNode extends StatementNode
42
50
  {
43
51
  false && yield;
44
52
  }
53
+
54
+
55
+ /**
56
+ * Pass: moves this node to head.
57
+ */
58
+ public static function moveToHeadPass(Nodes\TemplateNode $templateNode): void
59
+ {
60
+ (new NodeTraverser)->traverse($templateNode->main, function (Node $node) use ($templateNode) {
61
+ if ($node instanceof self) {
62
+ array_unshift($templateNode->head->children, $node);
63
+ return new Nodes\NopNode;
64
+ }
65
+ });
66
+ }
45
67
  }
@@ -33,9 +33,16 @@ class VarPrintNode extends StatementNode
33
33
 
34
34
  public function print(PrintContext $context): string
35
35
  {
36
- $vars = $this->all ? 'get_defined_vars()'
36
+ $vars = $this->all
37
+ ? 'get_defined_vars()'
37
38
  : 'array_diff_key(get_defined_vars(), $this->getParameters())';
38
- return "(new Latte\\Essential\\Blueprint)->printVars($vars); exit;";
39
+ return <<<XX
40
+ \$ʟ_bp = new Latte\\Essential\\Blueprint;
41
+ \$ʟ_bp->printBegin();
42
+ \$ʟ_bp->printVars($vars);
43
+ \$ʟ_bp->printEnd();
44
+ exit;
45
+ XX;
39
46
  }
40
47
 
41
48