@vituum/vite-plugin-latte 1.0.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 (123) hide show
  1. package/index.js +2 -2
  2. package/latte/PlaceholderFunction.php +2 -2
  3. package/package.json +10 -11
  4. package/vendor/autoload.php +18 -0
  5. package/vendor/bin/latte-lint +16 -4
  6. package/vendor/composer/ClassLoader.php +72 -65
  7. package/vendor/composer/InstalledVersions.php +21 -12
  8. package/vendor/composer/autoload_classmap.php +14 -8
  9. package/vendor/composer/autoload_namespaces.php +1 -1
  10. package/vendor/composer/autoload_psr4.php +1 -1
  11. package/vendor/composer/autoload_real.php +3 -22
  12. package/vendor/composer/autoload_static.php +13 -7
  13. package/vendor/composer/installed.json +8 -8
  14. package/vendor/composer/installed.php +10 -10
  15. package/vendor/latte/latte/bin/latte-lint +8 -2
  16. package/vendor/latte/latte/composer.json +1 -1
  17. package/vendor/latte/latte/readme.md +6 -6
  18. package/vendor/latte/latte/src/Bridges/Tracy/BlueScreenPanel.php +1 -0
  19. package/vendor/latte/latte/src/Bridges/Tracy/LattePanel.php +3 -2
  20. package/vendor/latte/latte/src/Latte/Compiler/Block.php +0 -3
  21. package/vendor/latte/latte/src/Latte/Compiler/Escaper.php +113 -89
  22. package/vendor/latte/latte/src/Latte/Compiler/ExpressionBuilder.php +2 -1
  23. package/vendor/latte/latte/src/Latte/Compiler/Node.php +0 -4
  24. package/vendor/latte/latte/src/Latte/Compiler/NodeHelpers.php +0 -4
  25. package/vendor/latte/latte/src/Latte/Compiler/NodeTraverser.php +0 -4
  26. package/vendor/latte/latte/src/Latte/Compiler/Nodes/AuxiliaryNode.php +11 -3
  27. package/vendor/latte/latte/src/Latte/Compiler/Nodes/FragmentNode.php +10 -0
  28. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Html/AttributeNode.php +22 -4
  29. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Html/ElementNode.php +35 -12
  30. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ArgumentNode.php +6 -0
  31. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ArrayItemNode.php +12 -0
  32. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/ArrayNode.php +4 -31
  33. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/AssignNode.php +15 -1
  34. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/AssignOpNode.php +11 -0
  35. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/AuxiliaryNode.php +42 -0
  36. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/ClassConstantFetchNode.php +2 -2
  37. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/ClosureNode.php +1 -1
  38. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/ConstantFetchNode.php +8 -0
  39. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/IssetNode.php +15 -1
  40. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/PostOpNode.php +11 -0
  41. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/PreOpNode.php +11 -0
  42. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/{StaticCallNode.php → StaticMethodCallNode.php} +11 -1
  43. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/{StaticCallableNode.php → StaticMethodCallableNode.php} +11 -1
  44. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/TemporaryNode.php +38 -0
  45. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ExpressionNode.php +24 -0
  46. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/FilterNode.php +1 -1
  47. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ListItemNode.php +48 -0
  48. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ListNode.php +56 -0
  49. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ModifierNode.php +5 -5
  50. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/NameNode.php +11 -21
  51. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Scalar/InterpolatedStringNode.php +1 -8
  52. package/vendor/latte/latte/src/Latte/Compiler/PhpHelpers.php +30 -0
  53. package/vendor/latte/latte/src/Latte/Compiler/Position.php +4 -8
  54. package/vendor/latte/latte/src/Latte/Compiler/PrintContext.php +15 -8
  55. package/vendor/latte/latte/src/Latte/Compiler/Tag.php +13 -14
  56. package/vendor/latte/latte/src/Latte/Compiler/TagLexer.php +5 -9
  57. package/vendor/latte/latte/src/Latte/Compiler/TagParser.php +52 -3
  58. package/vendor/latte/latte/src/Latte/Compiler/TagParserData.php +353 -326
  59. package/vendor/latte/latte/src/Latte/Compiler/TemplateGenerator.php +6 -5
  60. package/vendor/latte/latte/src/Latte/Compiler/TemplateLexer.php +105 -178
  61. package/vendor/latte/latte/src/Latte/Compiler/TemplateParser.php +40 -33
  62. package/vendor/latte/latte/src/Latte/Compiler/TemplateParserHtml.php +186 -126
  63. package/vendor/latte/latte/src/Latte/Compiler/Token.php +5 -9
  64. package/vendor/latte/latte/src/Latte/Compiler/TokenStream.php +6 -22
  65. package/vendor/latte/latte/src/Latte/Engine.php +136 -95
  66. package/vendor/latte/latte/src/Latte/Essential/AuxiliaryIterator.php +46 -0
  67. package/vendor/latte/latte/src/Latte/Essential/Blueprint.php +42 -27
  68. package/vendor/latte/latte/src/Latte/Essential/CachingIterator.php +0 -4
  69. package/vendor/latte/latte/src/Latte/Essential/CoreExtension.php +81 -66
  70. package/vendor/latte/latte/src/Latte/Essential/Filters.php +103 -43
  71. package/vendor/latte/latte/src/Latte/Essential/Nodes/BlockNode.php +1 -2
  72. package/vendor/latte/latte/src/Latte/Essential/Nodes/CaptureNode.php +2 -13
  73. package/vendor/latte/latte/src/Latte/Essential/Nodes/ContentTypeNode.php +8 -1
  74. package/vendor/latte/latte/src/Latte/Essential/Nodes/DefineNode.php +1 -2
  75. package/vendor/latte/latte/src/Latte/Essential/Nodes/EmbedNode.php +1 -1
  76. package/vendor/latte/latte/src/Latte/Essential/Nodes/ExtendsNode.php +0 -3
  77. package/vendor/latte/latte/src/Latte/Essential/Nodes/FirstLastSepNode.php +1 -1
  78. package/vendor/latte/latte/src/Latte/Essential/Nodes/ForNode.php +1 -1
  79. package/vendor/latte/latte/src/Latte/Essential/Nodes/ForeachNode.php +40 -13
  80. package/vendor/latte/latte/src/Latte/Essential/Nodes/IfChangedNode.php +1 -1
  81. package/vendor/latte/latte/src/Latte/Essential/Nodes/IfContentNode.php +4 -1
  82. package/vendor/latte/latte/src/Latte/Essential/Nodes/IfNode.php +5 -3
  83. package/vendor/latte/latte/src/Latte/Essential/Nodes/IncludeBlockNode.php +5 -2
  84. package/vendor/latte/latte/src/Latte/Essential/Nodes/IterateWhileNode.php +6 -4
  85. package/vendor/latte/latte/src/Latte/Essential/Nodes/JumpNode.php +26 -23
  86. package/vendor/latte/latte/src/Latte/Essential/Nodes/NElseNode.php +88 -0
  87. package/vendor/latte/latte/src/Latte/Essential/Nodes/NTagNode.php +20 -28
  88. package/vendor/latte/latte/src/Latte/Essential/Nodes/PrintNode.php +7 -12
  89. package/vendor/latte/latte/src/Latte/Essential/Nodes/RollbackNode.php +1 -1
  90. package/vendor/latte/latte/src/Latte/Essential/Nodes/SpacelessNode.php +1 -1
  91. package/vendor/latte/latte/src/Latte/Essential/Nodes/SwitchNode.php +1 -1
  92. package/vendor/latte/latte/src/Latte/Essential/Nodes/TemplatePrintNode.php +25 -3
  93. package/vendor/latte/latte/src/Latte/Essential/Nodes/TranslateNode.php +1 -1
  94. package/vendor/latte/latte/src/Latte/Essential/Nodes/TryNode.php +3 -4
  95. package/vendor/latte/latte/src/Latte/Essential/Nodes/VarPrintNode.php +9 -2
  96. package/vendor/latte/latte/src/Latte/Essential/Nodes/WhileNode.php +1 -1
  97. package/vendor/latte/latte/src/Latte/Essential/Passes.php +16 -58
  98. package/vendor/latte/latte/src/Latte/Essential/RawPhpExtension.php +0 -2
  99. package/vendor/latte/latte/src/Latte/Essential/TranslatorExtension.php +6 -1
  100. package/vendor/latte/latte/src/Latte/Helpers.php +3 -1
  101. package/vendor/latte/latte/src/Latte/Loader.php +1 -0
  102. package/vendor/latte/latte/src/Latte/Loaders/FileLoader.php +1 -4
  103. package/vendor/latte/latte/src/Latte/Loaders/StringLoader.php +0 -2
  104. package/vendor/latte/latte/src/Latte/PositionAwareException.php +1 -1
  105. package/vendor/latte/latte/src/Latte/Runtime/Block.php +0 -4
  106. package/vendor/latte/latte/src/Latte/Runtime/FilterExecutor.php +43 -51
  107. package/vendor/latte/latte/src/Latte/Runtime/FilterInfo.php +0 -2
  108. package/vendor/latte/latte/src/Latte/Runtime/Filters.php +64 -33
  109. package/vendor/latte/latte/src/Latte/Runtime/FunctionExecutor.php +68 -0
  110. package/vendor/latte/latte/src/Latte/Runtime/Html.php +0 -4
  111. package/vendor/latte/latte/src/Latte/Runtime/Template.php +3 -5
  112. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/FunctionCallNode.php +2 -1
  113. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/MethodCallNode.php +1 -1
  114. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/SandboxNode.php +3 -3
  115. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/{StaticCallNode.php → StaticMethodCallNode.php} +3 -3
  116. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/{StaticCallableNode.php → StaticMethodCallableNode.php} +2 -2
  117. package/vendor/latte/latte/src/Latte/Sandbox/RuntimeChecker.php +0 -2
  118. package/vendor/latte/latte/src/Latte/Sandbox/SandboxExtension.php +11 -9
  119. package/vendor/latte/latte/src/Latte/Sandbox/SecurityPolicy.php +0 -2
  120. package/vendor/latte/latte/src/Latte/exceptions.php +2 -11
  121. package/vendor/latte/latte/src/Tools/Linter.php +13 -37
  122. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Html/QuotedValue.php +0 -53
  123. package/vendor/latte/latte/src/Latte/Strict.php +0 -101
@@ -11,10 +11,10 @@ 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;
17
+ use Latte\Runtime;
18
18
  use Latte\RuntimeException;
19
19
  use Nette;
20
20
 
@@ -24,14 +24,19 @@ use Nette;
24
24
  */
25
25
  final class CoreExtension extends Latte\Extension
26
26
  {
27
- use Latte\Strict;
27
+ private Latte\Engine $engine;
28
+ private Filters $filters;
28
29
 
29
- private array $functions;
30
+
31
+ public function __construct()
32
+ {
33
+ $this->filters = new Filters;
34
+ }
30
35
 
31
36
 
32
37
  public function beforeCompile(Latte\Engine $engine): void
33
38
  {
34
- $this->functions = $engine->getFunctions();
39
+ $this->engine = $engine;
35
40
  }
36
41
 
37
42
 
@@ -92,6 +97,7 @@ final class CoreExtension extends Latte\Extension
92
97
  'ifset' => [Nodes\IfNode::class, 'create'],
93
98
  'ifchanged' => [Nodes\IfChangedNode::class, 'create'],
94
99
  'n:ifcontent' => [Nodes\IfContentNode::class, 'create'],
100
+ 'n:else' => [Nodes\NElseNode::class, 'create'],
95
101
  'switch' => [Nodes\SwitchNode::class, 'create'],
96
102
  ];
97
103
  }
@@ -100,18 +106,19 @@ final class CoreExtension extends Latte\Extension
100
106
  public function getFilters(): array
101
107
  {
102
108
  return [
103
- 'batch' => [Filters::class, 'batch'],
104
- 'breakLines' => [Filters::class, 'breaklines'],
105
- 'breaklines' => [Filters::class, 'breaklines'],
106
- 'bytes' => [Filters::class, 'bytes'],
109
+ 'batch' => [$this->filters, 'batch'],
110
+ 'breakLines' => [$this->filters, 'breaklines'],
111
+ 'breaklines' => [$this->filters, 'breaklines'],
112
+ 'bytes' => [$this->filters, 'bytes'],
107
113
  'capitalize' => extension_loaded('mbstring')
108
- ? [Filters::class, 'capitalize']
109
- : function () { throw new RuntimeException('Filter |capitalize requires mbstring extension.'); },
110
- 'ceil' => [Filters::class, 'ceil'],
111
- 'clamp' => [Filters::class, 'clamp'],
112
- 'dataStream' => [Filters::class, 'dataStream'],
113
- 'datastream' => [Filters::class, 'dataStream'],
114
- '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'],
115
122
  'escape' => [Latte\Runtime\Filters::class, 'nop'],
116
123
  'escapeCss' => [Latte\Runtime\Filters::class, 'escapeCss'],
117
124
  'escapeHtml' => [Latte\Runtime\Filters::class, 'escapeHtml'],
@@ -120,50 +127,50 @@ final class CoreExtension extends Latte\Extension
120
127
  'escapeJs' => [Latte\Runtime\Filters::class, 'escapeJs'],
121
128
  'escapeUrl' => 'rawurlencode',
122
129
  'escapeXml' => [Latte\Runtime\Filters::class, 'escapeXml'],
123
- 'explode' => [Filters::class, 'explode'],
124
- 'first' => [Filters::class, 'first'],
130
+ 'explode' => [$this->filters, 'explode'],
131
+ 'first' => [$this->filters, 'first'],
125
132
  'firstUpper' => extension_loaded('mbstring')
126
- ? [Filters::class, 'firstUpper']
127
- : function () { throw new RuntimeException('Filter |firstUpper requires mbstring extension.'); },
128
- 'floor' => [Filters::class, 'floor'],
129
- 'checkUrl' => [Latte\Runtime\Filters::class, 'safeUrl'],
130
- 'implode' => [Filters::class, 'implode'],
131
- 'indent' => [Filters::class, 'indent'],
132
- 'join' => [Filters::class, 'implode'],
133
- 'last' => [Filters::class, 'last'],
134
- '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'],
135
142
  'lower' => extension_loaded('mbstring')
136
- ? [Filters::class, 'lower']
137
- : function () { throw new RuntimeException('Filter |lower requires mbstring extension.'); },
143
+ ? [$this->filters, 'lower']
144
+ : fn() => throw new RuntimeException('Filter |lower requires mbstring extension.'),
138
145
  'number' => 'number_format',
139
- 'padLeft' => [Filters::class, 'padLeft'],
140
- 'padRight' => [Filters::class, 'padRight'],
141
- 'query' => [Filters::class, 'query'],
142
- 'random' => [Filters::class, 'random'],
143
- 'repeat' => [Filters::class, 'repeat'],
144
- 'replace' => [Filters::class, 'replace'],
145
- 'replaceRe' => [Filters::class, 'replaceRe'],
146
- 'replaceRE' => [Filters::class, 'replaceRe'],
147
- 'reverse' => [Filters::class, 'reverse'],
148
- 'round' => [Filters::class, 'round'],
149
- 'slice' => [Filters::class, 'slice'],
150
- 'sort' => [Filters::class, 'sort'],
151
- 'spaceless' => [Filters::class, 'strip'],
152
- 'split' => [Filters::class, 'explode'],
153
- 'strip' => [Filters::class, 'strip'], // obsolete
154
- 'stripHtml' => [Filters::class, 'stripHtml'],
155
- 'striphtml' => [Filters::class, 'stripHtml'],
156
- 'stripTags' => [Filters::class, 'stripTags'],
157
- 'striptags' => [Filters::class, 'stripTags'],
158
- 'substr' => [Filters::class, 'substring'],
159
- 'trim' => [Filters::class, 'trim'],
160
- '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'],
161
168
  'upper' => extension_loaded('mbstring')
162
- ? [Filters::class, 'upper']
163
- : function () { throw new RuntimeException('Filter |upper requires mbstring extension.'); },
169
+ ? [$this->filters, 'upper']
170
+ : fn() => throw new RuntimeException('Filter |upper requires mbstring extension.'),
164
171
  'webalize' => class_exists(Nette\Utils\Strings::class)
165
172
  ? [Nette\Utils\Strings::class, 'webalize']
166
- : function () { throw new RuntimeException('Filter |webalize requires nette/utils package.'); },
173
+ : fn() => throw new RuntimeException('Filter |webalize requires nette/utils package.'),
167
174
  ];
168
175
  }
169
176
 
@@ -171,24 +178,28 @@ final class CoreExtension extends Latte\Extension
171
178
  public function getFunctions(): array
172
179
  {
173
180
  return [
174
- 'clamp' => [Filters::class, 'clamp'],
175
- 'divisibleBy' => [Filters::class, 'divisibleBy'],
176
- 'even' => [Filters::class, 'even'],
177
- 'first' => [Filters::class, 'first'],
178
- 'last' => [Filters::class, 'last'],
179
- 'odd' => [Filters::class, 'odd'],
180
- 'slice' => [Filters::class, 'slice'],
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),
181
190
  ];
182
191
  }
183
192
 
184
193
 
185
194
  public function getPasses(): array
186
195
  {
196
+ $passes = new Passes($this->engine);
187
197
  return [
188
- 'internalVariables' => [Passes::class, 'internalVariablesPass'],
189
- 'overwrittenVariables' => [Passes::class, 'overwrittenVariablesPass'],
190
- 'customFunctions' => fn(TemplateNode $node) => Passes::customFunctionsPass($node, $this->functions),
191
- '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
+ 'nElse' => [Nodes\NElseNode::class, 'processPass'],
192
203
  ];
193
204
  }
194
205
 
@@ -222,13 +233,17 @@ final class CoreExtension extends Latte\Extension
222
233
  */
223
234
  private function parseSyntax(Tag $tag, TemplateParser $parser): \Generator
224
235
  {
236
+ if ($tag->isNAttribute() && $tag->prefix !== $tag::PrefixNone) {
237
+ throw new Latte\CompileException("Use n:syntax instead of {$tag->getNotation()}", $tag->position);
238
+ }
225
239
  $tag->expectArguments();
226
240
  $token = $tag->parser->stream->consume();
227
241
  $lexer = $parser->getLexer();
228
- $saved = [$lexer->openDelimiter, $lexer->closeDelimiter];
229
242
  $lexer->setSyntax($token->text, $tag->isNAttribute() ? null : $tag->name);
230
243
  [$inner] = yield;
231
- [$lexer->openDelimiter, $lexer->closeDelimiter] = $saved;
244
+ if (!$tag->isNAttribute()) {
245
+ $lexer->popSyntax();
246
+ }
232
247
  return $inner;
233
248
  }
234
249
  }
@@ -120,7 +120,7 @@ final class Filters
120
120
  } elseif ($info->contentType === ContentType::Html) {
121
121
  $s = preg_replace_callback('#<(textarea|pre).*?</\1#si', fn($m) => strtr($m[0], " \t\r\n", "\x1F\x1E\x1D\x1A"), $s);
122
122
  if (preg_last_error()) {
123
- throw new Latte\RegexpException;
123
+ throw new Latte\RuntimeException(preg_last_error_msg());
124
124
  }
125
125
 
126
126
  $s = preg_replace('#(?:^|[\r\n]+)(?=[^\r\n])#', '$0' . str_repeat($chars, $level), $s);
@@ -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
  {
@@ -250,7 +245,7 @@ final class Filters
250
245
  {
251
246
  $res = preg_replace($pattern, $replacement, $subject);
252
247
  if (preg_last_error()) {
253
- throw new Latte\RegexpException;
248
+ throw new Latte\RuntimeException(preg_last_error_msg());
254
249
  }
255
250
 
256
251
  return $res;
@@ -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
 
@@ -372,9 +360,11 @@ final class Filters
372
360
 
373
361
  private static function strLength(string $s): int
374
362
  {
375
- return function_exists('mb_strlen')
376
- ? mb_strlen($s, 'UTF-8')
377
- : strlen(utf8_decode($s));
363
+ return match (true) {
364
+ extension_loaded('mbstring') => mb_strlen($s, 'UTF-8'),
365
+ extension_loaded('iconv') => iconv_strlen($s, 'UTF-8'),
366
+ default => strlen(@utf8_decode($s)), // deprecated
367
+ };
378
368
  }
379
369
 
380
370
 
@@ -386,7 +376,7 @@ final class Filters
386
376
  $charlist = preg_quote($charlist, '#');
387
377
  $s = preg_replace('#^[' . $charlist . ']+|[' . $charlist . ']+$#Du', '', (string) $s);
388
378
  if (preg_last_error()) {
389
- throw new Latte\RegexpException;
379
+ throw new Latte\RuntimeException(preg_last_error_msg());
390
380
  }
391
381
 
392
382
  return $s;
@@ -420,7 +410,7 @@ final class Filters
420
410
  /**
421
411
  * Reverses string or array.
422
412
  */
423
- 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
424
414
  {
425
415
  if (is_array($val)) {
426
416
  return array_reverse($val, $preserveKeys);
@@ -435,7 +425,7 @@ final class Filters
435
425
  /**
436
426
  * Chunks items by returning an array of arrays with the given number of items.
437
427
  */
438
- public static function batch(array|\Traversable $list, int $length, $rest = null): \Generator
428
+ public static function batch(iterable $list, int $length, $rest = null): \Generator
439
429
  {
440
430
  $batch = [];
441
431
  foreach ($list as $key => $value) {
@@ -459,14 +449,78 @@ final class Filters
459
449
 
460
450
 
461
451
  /**
462
- * Sorts an array.
463
- * @param mixed[] $array
464
- * @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>
465
457
  */
466
- public static function sort(array $array, ?\Closure $callback = null): array
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
467
464
  {
468
- $callback ? uasort($array, $callback) : asort($array);
469
- return $array;
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>>
500
+ */
501
+ public static function group(iterable $data, string|int|\Closure $by): iterable
502
+ {
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
+ ));
470
524
  }
471
525
 
472
526
 
@@ -522,23 +576,29 @@ final class Filters
522
576
 
523
577
 
524
578
  /**
525
- * 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.
526
580
  */
527
- public static function first(string|array $value): mixed
581
+ public static function first(string|iterable $value): mixed
528
582
  {
529
- return is_array($value)
530
- ? (count($value) ? reset($value) : null)
531
- : 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;
532
592
  }
533
593
 
534
594
 
535
595
  /**
536
- * 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.
537
597
  */
538
598
  public static function last(string|array $value): mixed
539
599
  {
540
600
  return is_array($value)
541
- ? (count($value) ? end($value) : null)
601
+ ? ($value[array_key_last($value)] ?? null)
542
602
  : self::substring($value, -1);
543
603
  }
544
604
 
@@ -40,7 +40,7 @@ class BlockNode extends StatementNode
40
40
  {
41
41
  $tag->outputMode = $tag::OutputRemoveIndentation;
42
42
  $stream = $tag->parser->stream;
43
- $node = new static;
43
+ $node = $tag->node = new static;
44
44
 
45
45
  if (!$stream->is('|', Token::End)) {
46
46
  $layer = $tag->parser->tryConsumeTokenBeforeUnquotedString('local')
@@ -52,7 +52,6 @@ class BlockNode extends StatementNode
52
52
 
53
53
  if (!$node->block->isDynamic()) {
54
54
  $parser->checkBlockIsUnique($node->block);
55
- $tag->data->block = $node->block; // for {include}
56
55
  }
57
56
  }
58
57
 
@@ -11,9 +11,7 @@ namespace Latte\Essential\Nodes;
11
11
 
12
12
  use Latte\CompileException;
13
13
  use Latte\Compiler\Escaper;
14
- use Latte\Compiler\Node;
15
14
  use Latte\Compiler\Nodes\AreaNode;
16
- use Latte\Compiler\Nodes\Php\Expression;
17
15
  use Latte\Compiler\Nodes\Php\ExpressionNode;
18
16
  use Latte\Compiler\Nodes\Php\ModifierNode;
19
17
  use Latte\Compiler\Nodes\StatementNode;
@@ -36,7 +34,7 @@ class CaptureNode extends StatementNode
36
34
  {
37
35
  $tag->expectArguments();
38
36
  $variable = $tag->parser->parseExpression();
39
- if (!self::canBeAssignedTo($variable)) {
37
+ if (!$variable->isWritable()) {
40
38
  $text = '';
41
39
  $i = 0;
42
40
  while ($token = $tag->parser->stream->peek(--$i)) {
@@ -45,7 +43,7 @@ class CaptureNode extends StatementNode
45
43
 
46
44
  throw new CompileException("It is not possible to write into '$text' in " . $tag->getNotation(), $tag->position);
47
45
  }
48
- $node = new static;
46
+ $node = $tag->node = new static;
49
47
  $node->variable = $variable;
50
48
  $node->modifier = $tag->parser->parseModifier();
51
49
  [$node->content] = yield;
@@ -80,15 +78,6 @@ class CaptureNode extends StatementNode
80
78
  }
81
79
 
82
80
 
83
- private static function canBeAssignedTo(Node $node): bool
84
- {
85
- return $node instanceof Expression\VariableNode
86
- || $node instanceof Expression\ArrayAccessNode
87
- || $node instanceof Expression\PropertyFetchNode
88
- || $node instanceof Expression\StaticPropertyFetchNode;
89
- }
90
-
91
-
92
81
  public function &getIterator(): \Generator
93
82
  {
94
83
  yield $this->variable;
@@ -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
@@ -32,11 +33,12 @@ class ContentTypeNode extends StatementNode
32
33
  while (!$tag->parser->stream->consume()->isEnd());
33
34
  $type = trim($tag->parser->text);
34
35
 
35
- if (!$tag->isInHead() && !($tag->htmlElement?->name === 'script' && str_contains($type, 'html'))) {
36
+ if (!$tag->isInHead() && !($tag->htmlElement?->is('script') && str_contains($type, 'html'))) {
36
37
  throw new CompileException('{contentType} is allowed only in template header.', $tag->position);
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
@@ -42,11 +42,10 @@ class DefineNode extends StatementNode
42
42
  $tag->parser->stream->tryConsume('#');
43
43
  $name = $tag->parser->parseUnquotedStringOrExpression();
44
44
 
45
- $node = new static;
45
+ $node = $tag->node = new static;
46
46
  $node->block = new Block($name, $layer, $tag);
47
47
  if (!$node->block->isDynamic()) {
48
48
  $parser->checkBlockIsUnique($node->block);
49
- $tag->data->block = $node->block; // for {include}
50
49
  $tag->parser->stream->tryConsume(',');
51
50
  $node->block->parameters = self::parseParameters($tag);
52
51
  }
@@ -43,7 +43,7 @@ class EmbedNode extends StatementNode
43
43
  $tag->outputMode = $tag::OutputRemoveIndentation;
44
44
  $tag->expectArguments();
45
45
 
46
- $node = new static;
46
+ $node = $tag->node = new static;
47
47
  $mode = $tag->parser->tryConsumeTokenBeforeUnquotedString('block', 'file')?->text;
48
48
  $node->name = $tag->parser->parseUnquotedStringOrExpression();
49
49
  $node->mode = $mode ?? ($node->name instanceof StringNode && preg_match('~[\w-]+$~DA', $node->name->value) ? 'block' : 'file');
@@ -33,8 +33,6 @@ class ExtendsNode extends StatementNode
33
33
  $node = new static;
34
34
  if (!$tag->isInHead()) {
35
35
  throw new CompileException("{{$tag->name}} must be placed in template head.", $tag->position);
36
- } elseif (isset($tag->data->extends)) {
37
- throw new CompileException("Multiple {{$tag->name}} declarations are not allowed.", $tag->position);
38
36
  } elseif ($tag->parser->stream->tryConsume('auto')) {
39
37
  $node->extends = new NullNode;
40
38
  } elseif ($tag->parser->stream->tryConsume('none')) {
@@ -42,7 +40,6 @@ class ExtendsNode extends StatementNode
42
40
  } else {
43
41
  $node->extends = $tag->parser->parseUnquotedStringOrExpression();
44
42
  }
45
- $tag->data->extends = true;
46
43
  return $node;
47
44
  }
48
45
 
@@ -34,7 +34,7 @@ class FirstLastSepNode extends StatementNode
34
34
  /** @return \Generator<int, ?array, array{AreaNode, ?Tag}, static> */
35
35
  public static function create(Tag $tag): \Generator
36
36
  {
37
- $node = new static;
37
+ $node = $tag->node = new static;
38
38
  $node->name = $tag->name;
39
39
  $node->width = $tag->parser->isEnd() ? null : $tag->parser->parseExpression();
40
40
 
@@ -35,7 +35,7 @@ class ForNode extends StatementNode
35
35
  {
36
36
  $tag->expectArguments();
37
37
  $stream = $tag->parser->stream;
38
- $node = new static;
38
+ $node = $tag->node = new static;
39
39
  while (!$stream->is(';')) {
40
40
  $node->init[] = $tag->parser->parseExpression();
41
41
  $stream->tryConsume(',');