@vituum/vite-plugin-latte 1.0.0 → 1.1.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 (114) hide show
  1. package/index.js +2 -2
  2. package/package.json +10 -11
  3. package/vendor/autoload.php +19 -1
  4. package/vendor/bin/latte-lint +16 -4
  5. package/vendor/composer/ClassLoader.php +72 -65
  6. package/vendor/composer/InstalledVersions.php +21 -12
  7. package/vendor/composer/autoload_classmap.php +12 -8
  8. package/vendor/composer/autoload_namespaces.php +1 -1
  9. package/vendor/composer/autoload_psr4.php +1 -1
  10. package/vendor/composer/autoload_real.php +7 -26
  11. package/vendor/composer/autoload_static.php +13 -9
  12. package/vendor/composer/installed.json +8 -8
  13. package/vendor/composer/installed.php +10 -10
  14. package/vendor/latte/latte/bin/latte-lint +8 -2
  15. package/vendor/latte/latte/composer.json +1 -1
  16. package/vendor/latte/latte/readme.md +6 -6
  17. package/vendor/latte/latte/src/Bridges/Tracy/BlueScreenPanel.php +1 -0
  18. package/vendor/latte/latte/src/Bridges/Tracy/LattePanel.php +3 -2
  19. package/vendor/latte/latte/src/Latte/Compiler/Block.php +0 -3
  20. package/vendor/latte/latte/src/Latte/Compiler/Escaper.php +103 -88
  21. package/vendor/latte/latte/src/Latte/Compiler/ExpressionBuilder.php +2 -1
  22. package/vendor/latte/latte/src/Latte/Compiler/Node.php +0 -4
  23. package/vendor/latte/latte/src/Latte/Compiler/NodeHelpers.php +0 -4
  24. package/vendor/latte/latte/src/Latte/Compiler/NodeTraverser.php +0 -4
  25. package/vendor/latte/latte/src/Latte/Compiler/Nodes/AuxiliaryNode.php +11 -3
  26. package/vendor/latte/latte/src/Latte/Compiler/Nodes/FragmentNode.php +10 -0
  27. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Html/AttributeNode.php +22 -4
  28. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Html/ElementNode.php +35 -12
  29. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ArgumentNode.php +6 -0
  30. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ArrayItemNode.php +12 -0
  31. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/ArrayNode.php +4 -31
  32. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/AssignNode.php +15 -1
  33. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/AssignOpNode.php +11 -0
  34. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/AuxiliaryNode.php +42 -0
  35. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/ClassConstantFetchNode.php +2 -2
  36. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/ClosureNode.php +1 -1
  37. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/ConstantFetchNode.php +8 -0
  38. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/IssetNode.php +15 -1
  39. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/PostOpNode.php +11 -0
  40. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/PreOpNode.php +11 -0
  41. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/{StaticCallNode.php → StaticMethodCallNode.php} +11 -1
  42. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/{StaticCallableNode.php → StaticMethodCallableNode.php} +11 -1
  43. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Expression/TemporaryNode.php +38 -0
  44. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ExpressionNode.php +24 -0
  45. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ListItemNode.php +48 -0
  46. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ListNode.php +56 -0
  47. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/ModifierNode.php +3 -3
  48. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Php/Scalar/InterpolatedStringNode.php +1 -8
  49. package/vendor/latte/latte/src/Latte/Compiler/PhpHelpers.php +30 -0
  50. package/vendor/latte/latte/src/Latte/Compiler/Position.php +4 -8
  51. package/vendor/latte/latte/src/Latte/Compiler/PrintContext.php +14 -7
  52. package/vendor/latte/latte/src/Latte/Compiler/Tag.php +13 -14
  53. package/vendor/latte/latte/src/Latte/Compiler/TagLexer.php +3 -7
  54. package/vendor/latte/latte/src/Latte/Compiler/TagParser.php +52 -3
  55. package/vendor/latte/latte/src/Latte/Compiler/TagParserData.php +354 -322
  56. package/vendor/latte/latte/src/Latte/Compiler/TemplateGenerator.php +6 -5
  57. package/vendor/latte/latte/src/Latte/Compiler/TemplateLexer.php +95 -176
  58. package/vendor/latte/latte/src/Latte/Compiler/TemplateParser.php +40 -33
  59. package/vendor/latte/latte/src/Latte/Compiler/TemplateParserHtml.php +182 -126
  60. package/vendor/latte/latte/src/Latte/Compiler/Token.php +5 -9
  61. package/vendor/latte/latte/src/Latte/Compiler/TokenStream.php +6 -22
  62. package/vendor/latte/latte/src/Latte/Engine.php +37 -8
  63. package/vendor/latte/latte/src/Latte/Essential/Blueprint.php +0 -2
  64. package/vendor/latte/latte/src/Latte/Essential/CachingIterator.php +0 -4
  65. package/vendor/latte/latte/src/Latte/Essential/CoreExtension.php +14 -3
  66. package/vendor/latte/latte/src/Latte/Essential/Filters.php +8 -6
  67. package/vendor/latte/latte/src/Latte/Essential/Nodes/BlockNode.php +1 -2
  68. package/vendor/latte/latte/src/Latte/Essential/Nodes/CaptureNode.php +2 -13
  69. package/vendor/latte/latte/src/Latte/Essential/Nodes/ContentTypeNode.php +1 -1
  70. package/vendor/latte/latte/src/Latte/Essential/Nodes/DefineNode.php +1 -2
  71. package/vendor/latte/latte/src/Latte/Essential/Nodes/EmbedNode.php +1 -1
  72. package/vendor/latte/latte/src/Latte/Essential/Nodes/ExtendsNode.php +0 -3
  73. package/vendor/latte/latte/src/Latte/Essential/Nodes/FirstLastSepNode.php +1 -1
  74. package/vendor/latte/latte/src/Latte/Essential/Nodes/ForNode.php +1 -1
  75. package/vendor/latte/latte/src/Latte/Essential/Nodes/ForeachNode.php +4 -13
  76. package/vendor/latte/latte/src/Latte/Essential/Nodes/IfChangedNode.php +1 -1
  77. package/vendor/latte/latte/src/Latte/Essential/Nodes/IfContentNode.php +4 -1
  78. package/vendor/latte/latte/src/Latte/Essential/Nodes/IfNode.php +5 -3
  79. package/vendor/latte/latte/src/Latte/Essential/Nodes/IncludeBlockNode.php +5 -2
  80. package/vendor/latte/latte/src/Latte/Essential/Nodes/IterateWhileNode.php +6 -4
  81. package/vendor/latte/latte/src/Latte/Essential/Nodes/JumpNode.php +26 -23
  82. package/vendor/latte/latte/src/Latte/Essential/Nodes/NElseNode.php +88 -0
  83. package/vendor/latte/latte/src/Latte/Essential/Nodes/NTagNode.php +20 -28
  84. package/vendor/latte/latte/src/Latte/Essential/Nodes/PrintNode.php +7 -12
  85. package/vendor/latte/latte/src/Latte/Essential/Nodes/RollbackNode.php +1 -1
  86. package/vendor/latte/latte/src/Latte/Essential/Nodes/SpacelessNode.php +1 -1
  87. package/vendor/latte/latte/src/Latte/Essential/Nodes/SwitchNode.php +1 -1
  88. package/vendor/latte/latte/src/Latte/Essential/Nodes/TranslateNode.php +1 -1
  89. package/vendor/latte/latte/src/Latte/Essential/Nodes/TryNode.php +3 -4
  90. package/vendor/latte/latte/src/Latte/Essential/Nodes/WhileNode.php +1 -1
  91. package/vendor/latte/latte/src/Latte/Essential/Passes.php +9 -11
  92. package/vendor/latte/latte/src/Latte/Essential/RawPhpExtension.php +0 -2
  93. package/vendor/latte/latte/src/Latte/Essential/TranslatorExtension.php +6 -1
  94. package/vendor/latte/latte/src/Latte/Loaders/FileLoader.php +0 -2
  95. package/vendor/latte/latte/src/Latte/Loaders/StringLoader.php +0 -2
  96. package/vendor/latte/latte/src/Latte/PositionAwareException.php +1 -1
  97. package/vendor/latte/latte/src/Latte/Runtime/Block.php +0 -4
  98. package/vendor/latte/latte/src/Latte/Runtime/FilterExecutor.php +43 -51
  99. package/vendor/latte/latte/src/Latte/Runtime/FilterInfo.php +0 -2
  100. package/vendor/latte/latte/src/Latte/Runtime/Filters.php +64 -30
  101. package/vendor/latte/latte/src/Latte/Runtime/Html.php +0 -4
  102. package/vendor/latte/latte/src/Latte/Runtime/Template.php +2 -2
  103. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/FunctionCallNode.php +2 -1
  104. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/MethodCallNode.php +1 -1
  105. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/SandboxNode.php +3 -3
  106. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/{StaticCallNode.php → StaticMethodCallNode.php} +3 -3
  107. package/vendor/latte/latte/src/Latte/Sandbox/Nodes/{StaticCallableNode.php → StaticMethodCallableNode.php} +2 -2
  108. package/vendor/latte/latte/src/Latte/Sandbox/RuntimeChecker.php +0 -2
  109. package/vendor/latte/latte/src/Latte/Sandbox/SandboxExtension.php +11 -9
  110. package/vendor/latte/latte/src/Latte/Sandbox/SecurityPolicy.php +0 -2
  111. package/vendor/latte/latte/src/Latte/exceptions.php +2 -11
  112. package/vendor/latte/latte/src/Tools/Linter.php +13 -37
  113. package/vendor/latte/latte/src/Latte/Compiler/Nodes/Html/QuotedValue.php +0 -53
  114. package/vendor/latte/latte/src/Latte/Strict.php +0 -101
@@ -24,13 +24,14 @@ use Latte\SecurityViolationException;
24
24
  */
25
25
  final class TemplateParserHtml
26
26
  {
27
- use Latte\Strict;
28
-
29
27
  /** @var array<string, callable(Tag, TemplateParser): (Node|\Generator|void)> */
30
28
  private array /*readonly*/ $attrParsers;
31
29
  private ?Html\ElementNode $element = null;
32
30
  private TemplateParser /*readonly*/ $parser;
33
31
 
32
+ /** @var array{string, ?Nodes\Php\ExpressionNode} */
33
+ private ?array $endName = null;
34
+
34
35
 
35
36
  public function __construct(TemplateParser $parser, array $attrParsers)
36
37
  {
@@ -50,9 +51,7 @@ final class TemplateParserHtml
50
51
  $stream = $this->parser->getStream();
51
52
  $token = $stream->peek();
52
53
  return match ($token->type) {
53
- Token::Html_TagOpen => $stream->peek(1)?->is(Token::Slash)
54
- ? $this->parseEndTag()
55
- : $this->parseElement(),
54
+ Token::Html_TagOpen => $this->parseTag(),
56
55
  Token::Html_CommentOpen => $this->parseComment(),
57
56
  Token::Html_BogusOpen => $this->parseBogusTag(),
58
57
  default => $this->parser->inTextResolve(),
@@ -76,11 +75,40 @@ final class TemplateParserHtml
76
75
  }
77
76
 
78
77
 
78
+ private function parseTag(): ?Node
79
+ {
80
+ $stream = $this->parser->getStream();
81
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
82
+ if (!$stream->peek(1)?->is(Token::Slash)) {
83
+ return $this->parseElement();
84
+ }
85
+
86
+ if ($this->element
87
+ && $this->parser->peekTag() === $this->element->data->tag // is directly in the element
88
+ ) {
89
+ $save = $stream->getIndex();
90
+ $this->endName = [$endText] = $this->parseEndTag();
91
+ if ($this->element->is($endText) || $this->element->data->textualName === $endText) {
92
+ return null; // go to parseElement() one level up to close the element
93
+ }
94
+ $stream->seek($save);
95
+ if (!in_array($endText, $this->element->data->unclosedTags ?? [], true)) {
96
+ return null; // go to parseElement() one level up to collapse
97
+ }
98
+ }
99
+
100
+ if ($this->parser->strict) {
101
+ $stream->throwUnexpectedException(excerpt: '/');
102
+ }
103
+ return $this->parseBogusEndTag();
104
+ }
105
+
106
+
79
107
  private function parseElement(): Node
80
108
  {
81
109
  $res = new FragmentNode;
82
110
  $res->append($this->extractIndentation());
83
- $res->append($this->parseTag($this->element));
111
+ $res->append($this->parseStartTag($this->element));
84
112
  $elem = $this->element;
85
113
 
86
114
  $stream = $this->parser->getStream();
@@ -102,16 +130,21 @@ final class TemplateParserHtml
102
130
  $frag = $this->parser->parseFragment([$this, 'inTextResolve']);
103
131
  $content->append($this->finishNAttrNodes($frag, $innerNodes));
104
132
 
105
- if ($this->isClosingTag($elem->name)) {
133
+ [$endText, $endVariable] = $this->endName;
134
+ $this->endName = null;
135
+ if ($endText && ($this->element->is($endText) || $this->element->data->textualName === $endText)) {
106
136
  $elem->content = $content;
107
137
  $elem->content->append($this->extractIndentation());
108
- $this->parseTag();
109
138
 
110
139
  } elseif ($outerNodes || $innerNodes || $tagNodes
111
- || ($this->parser->getContentType() === ContentType::Html && in_array(strtolower($elem->name), ['script', 'style'], true))
140
+ || $this->parser->strict
141
+ || $elem->variableName
142
+ || $endVariable
143
+ || $elem->isRawText()
112
144
  ) {
113
145
  $stream->throwUnexpectedException(
114
- addendum: ", expecting </{$elem->name}> for element started " . $elem->position->toWords(),
146
+ addendum: ", expecting </{$elem->data->textualName}> for element started $elem->position",
147
+ excerpt: $endText ? "/{$endText}>" : $stream->peek(1)?->text . $stream->peek(2)?->text,
115
148
  );
116
149
  } else { // element collapsed to tags
117
150
  $res->append($content);
@@ -145,50 +178,112 @@ final class TemplateParserHtml
145
178
  }
146
179
 
147
180
 
148
- private function parseTag(&$elem = null): Html\ElementNode
181
+ private function parseStartTag(&$elem = null): Html\ElementNode
149
182
  {
150
183
  $stream = $this->parser->getStream();
151
184
  $openToken = $stream->consume(Token::Html_TagOpen);
152
- $stream->tryConsume(Token::Slash);
185
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
186
+
187
+ [$textual, $variable] = $this->parseTagName($this->parser->strict);
188
+ if (($this->parser->strict || $variable)
189
+ && !$stream->is(Token::Whitespace, Token::Slash, Token::Html_TagClose)
190
+ ) {
191
+ throw $stream->throwUnexpectedException();
192
+ }
193
+
153
194
  $this->parser->lastIndentation = null;
154
- $this->parser->location = $this->parser::LocationTag;
195
+ $this->parser->inHead = false;
155
196
  $elem = new Html\ElementNode(
156
- name: $stream->consume(Token::Html_Name)->text,
197
+ name: $variable ? '' : $textual,
157
198
  position: $openToken->position,
158
199
  parent: $this->element,
159
200
  data: (object) ['tag' => $this->parser->peekTag()],
201
+ contentType: $this->parser->getContentType(),
160
202
  );
161
203
  $elem->attributes = $this->parser->parseFragment([$this, 'inTagResolve']);
162
204
  $elem->selfClosing = (bool) $stream->tryConsume(Token::Slash);
205
+ $elem->variableName = $variable;
206
+ $elem->data->textualName = $textual;
163
207
  $stream->consume(Token::Html_TagClose);
164
- $this->parser->location = $this->parser::LocationText;
208
+ $state = !$elem->selfClosing && $elem->isRawText()
209
+ ? TemplateLexer::StateHtmlRawText
210
+ : TemplateLexer::StateHtmlText;
211
+ $this->parser->getLexer()->setState($state, $elem->name);
165
212
  return $elem;
166
213
  }
167
214
 
168
215
 
169
- private function parseEndTag(): ?Html\BogusTagNode
216
+ /** @return array{string, ?Nodes\Php\ExpressionNode} */
217
+ private function parseEndTag(): array
170
218
  {
171
219
  $stream = $this->parser->getStream();
172
- $name = $stream->peek(2)->text ?? '';
220
+ $stream->consume(Token::Html_TagOpen);
221
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
222
+ $stream->consume(Token::Slash);
223
+ $name = $this->parseTagName();
224
+ $stream->tryConsume(Token::Whitespace);
225
+ $stream->consume(Token::Html_TagClose);
226
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlText);
227
+ return $name;
228
+ }
173
229
 
174
- if ($this->element
175
- && $this->parser->peekTag() === $this->element->data->tag
176
- && (strcasecmp($name, $this->element->name) === 0
177
- || !in_array($name, $this->element->data->unclosedTags ?? [], true))
178
- ) {
179
- return null; // go back to parseElement()
180
- }
181
230
 
231
+ /** @return array{string, ?Nodes\Php\ExpressionNode} */
232
+ private function parseTagName(bool $strict = true): array
233
+ {
234
+ $variable = $text = null;
235
+ $parts = [];
236
+ $stream = $this->parser->getStream();
237
+ do {
238
+ if ($stream->is(Token::Latte_TagOpen)) {
239
+ $save = $stream->getIndex();
240
+ $statement = $this->parser->parseLatteStatement([$this, 'inTagResolve']);
241
+ if (!$statement instanceof Latte\Essential\Nodes\PrintNode) {
242
+ if (!$parts || $strict) {
243
+ throw new CompileException('Only expression can be used as a HTML tag name.', $statement->position);
244
+ }
245
+ $stream->seek($save);
246
+ break;
247
+ }
248
+ $parts[] = $statement->expression;
249
+ $save -= $stream->getIndex();
250
+ while ($save < 0) {
251
+ $text .= $stream->peek($save++)->text;
252
+ }
253
+ $variable = true;
254
+
255
+ } elseif ($token = $stream->tryConsume(Token::Html_Name)) {
256
+ $parts[] = new Latte\Compiler\Nodes\Php\Scalar\StringNode($token->text, $token->position);
257
+ $text .= $token->text;
258
+
259
+ } elseif (!$parts) {
260
+ throw $stream->throwUnexpectedException([Token::Html_Name, Token::Latte_TagOpen]);
261
+ } else {
262
+ break;
263
+ }
264
+ } while (true);
265
+
266
+ $variable = $variable
267
+ ? Latte\Compiler\Nodes\Php\Expression\BinaryOpNode::nest('.', ...$parts)
268
+ : null;
269
+ return [$text, $variable];
270
+ }
271
+
272
+
273
+ private function parseBogusEndTag(): ?Html\BogusTagNode
274
+ {
275
+ $stream = $this->parser->getStream();
182
276
  $openToken = $stream->consume(Token::Html_TagOpen);
277
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
183
278
  $this->parser->lastIndentation = null;
184
- $this->parser->location = $this->parser::LocationTag;
279
+ $this->parser->inHead = false;
185
280
  $node = new Html\BogusTagNode(
186
- openDelimiter: $openToken->text . $stream->consume(Token::Slash)->text . $stream->tryConsume(Token::Text)?->text,
187
- content: $this->parser->parseFragment([$this, 'inTagResolve']),
281
+ openDelimiter: $openToken->text . $stream->consume(Token::Slash)->text . $stream->consume(Token::Html_Name)->text,
282
+ content: new Nodes\TextNode($stream->tryConsume(Token::Whitespace)->text ?? ''),
188
283
  endDelimiter: $stream->consume(Token::Html_TagClose)->text,
189
284
  position: $openToken->position,
190
285
  );
191
- $this->parser->location = $this->parser::LocationText;
286
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlText);
192
287
  return $node;
193
288
  }
194
289
 
@@ -197,13 +292,11 @@ final class TemplateParserHtml
197
292
  {
198
293
  $stream = $this->parser->getStream();
199
294
  $openToken = $stream->consume(Token::Html_BogusOpen);
295
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlBogus);
200
296
  $this->parser->lastIndentation = null;
201
- $this->parser->location = $this->parser::LocationTag;
202
- $content = $this->parser->parseFragment(fn() => match ($stream->peek()->type) {
203
- Token::Html_TagClose => null,
204
- default => $this->parser->inTextResolve(),
205
- });
206
- $this->parser->location = $this->parser::LocationText;
297
+ $this->parser->inHead = false;
298
+ $content = $this->parser->parseFragment([$this->parser, 'inTextResolve']);
299
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlText);
207
300
  return new Html\BogusTagNode(
208
301
  openDelimiter: $openToken->text,
209
302
  content: $content,
@@ -215,7 +308,7 @@ final class TemplateParserHtml
215
308
 
216
309
  private function resolveVoidness(Html\ElementNode $elem): bool
217
310
  {
218
- if ($this->parser->getContentType() !== ContentType::Html) {
311
+ if ($elem->contentType !== ContentType::Html) {
219
312
  return $elem->selfClosing;
220
313
  } elseif (isset(Helpers::$emptyElements[strtolower($elem->name)])) {
221
314
  return true;
@@ -246,69 +339,56 @@ final class TemplateParserHtml
246
339
  private function parseAttribute(): ?Node
247
340
  {
248
341
  $stream = $this->parser->getStream();
249
- $followsLatte = $stream->is(Token::Latte_TagOpen);
250
- $save = $stream->getIndex();
251
- try {
252
- $name = $this->parser->parseFragment(fn() => match ($stream->peek()->type) {
253
- Token::Html_Name => $this->parser->parseText(),
254
- Token::Latte_TagOpen => $this->parser->parseLatteStatement(),
255
- Token::Latte_CommentOpen => $this->parser->parseLatteComment(),
256
- default => null,
257
- });
258
- } catch (CompileException $e) {
259
- if ($followsLatte // attribute name together with the value inside the tag
260
- && $stream->peek() // it is not lexer exception
261
- ) {
262
- $stream->seek($save);
263
- return $this->parser->parseLatteStatement();
264
- }
265
- throw $e;
266
- }
267
-
268
- if (!$name->children) {
269
- return null;
270
- } elseif (count($name->children) === 1 && $name->children[0] instanceof Nodes\TextNode) {
271
- $name = $name->children[0];
272
- }
273
-
274
- $save = $stream->getIndex();
275
- $this->consumeIgnored();
276
- if ($stream->tryConsume(Token::Equals)) {
277
- $this->consumeIgnored();
278
- $value = match ($stream->peek()->type) {
279
- Token::Quote => $this->parseAttributeQuote(),
280
- Token::Html_Name => $this->parser->parseText(),
281
- Token::Latte_TagOpen => $this->parser->parseFragment(
282
- function (FragmentNode $fragment) use ($stream) {
283
- if ($fragment->children) {
284
- return null;
285
- }
286
- return match ($stream->peek()->type) {
287
- Token::Quote => $this->parseAttributeQuote(),
288
- Token::Html_Name => $this->parser->parseText(),
289
- Token::Latte_TagOpen => $this->parser->parseLatteStatement(),
290
- Token::Latte_CommentOpen => $this->parser->parseLatteComment(),
291
- default => null,
292
- };
293
- },
294
- ),
295
- default => null,
342
+ if ($stream->is(Token::Latte_TagOpen)) {
343
+ $name = $this->parser->parseLatteStatement();
344
+ if (!$name instanceof Latte\Essential\Nodes\PrintNode) {
345
+ return $name; // value like '<span {if true}attr1=val{/if}>'
296
346
  }
297
- ?? $stream->throwUnexpectedException();
298
-
299
347
  } else {
300
- $stream->seek($save);
301
- $value = null;
348
+ $name = $this->parser->parseText();
302
349
  }
303
350
 
351
+ [$value, $quote] = $this->parseAttributeValue();
304
352
  return new Html\AttributeNode(
305
353
  name: $name,
306
354
  value: $value,
355
+ quote: $quote,
307
356
  position: $name->position,
308
357
  );
309
358
  }
310
359
 
311
360
 
361
+ private function parseAttributeValue(): ?array
362
+ {
363
+ $stream = $this->parser->getStream();
364
+ $save = $stream->getIndex();
365
+ $this->consumeIgnored();
366
+ if (!$stream->tryConsume(Token::Equals)) {
367
+ $stream->seek($save);
368
+ return null;
369
+ }
370
+
371
+ $this->consumeIgnored();
372
+ if ($quoteToken = $stream->tryConsume(Token::Quote)) {
373
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlQuotedValue, $quoteToken->text);
374
+ $value = $this->parser->parseFragment([$this->parser, 'inTextResolve']);
375
+ $stream->tryConsume(Token::Quote) || $stream->throwUnexpectedException([$quoteToken->text], addendum: ", end of HTML attribute started $quoteToken->position");
376
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
377
+ return [$value, $quoteToken->text];
378
+ }
379
+
380
+ $value = $this->parser->parseFragment(
381
+ fn() => match ($stream->peek()->type) {
382
+ Token::Html_Name => $this->parser->parseText(),
383
+ Token::Latte_TagOpen => $this->parser->parseLatteStatement(),
384
+ Token::Latte_CommentOpen => $this->parser->parseLatteComment(),
385
+ default => null,
386
+ },
387
+ )->simplify() ?? $stream->throwUnexpectedException();
388
+ return [$value, null];
389
+ }
390
+
391
+
312
392
  private function parseNAttribute(): Nodes\TextNode
313
393
  {
314
394
  $stream = $this->parser->getStream();
@@ -326,10 +406,12 @@ final class TemplateParserHtml
326
406
  $this->consumeIgnored();
327
407
  if ($stream->tryConsume(Token::Equals)) {
328
408
  $this->consumeIgnored();
329
- if ($stream->tryConsume(Token::Quote)) {
409
+ if ($quoteToken = $stream->tryConsume(Token::Quote)) {
410
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlQuotedNAttrValue, $quoteToken->text);
330
411
  $valueToken = $stream->tryConsume(Token::Text);
331
412
  $pos = $stream->peek()->position;
332
- $stream->consume(Token::Quote);
413
+ $stream->tryConsume(Token::Quote) || $stream->throwUnexpectedException([$quoteToken->text], addendum: ", end of n:attribute started $quoteToken->position");
414
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
333
415
  } else {
334
416
  $valueToken = $stream->consume(Token::Html_Name);
335
417
  }
@@ -346,28 +428,10 @@ final class TemplateParserHtml
346
428
  tokens: $tokens,
347
429
  position: $nameToken->position,
348
430
  prefix: $this->getPrefix($name),
349
- location: $this->parser->location,
431
+ inTag: true,
350
432
  htmlElement: $this->element,
351
- data: (object) ['node' => $node = new Nodes\TextNode('')], // TODO: better
352
- );
353
- return $node;
354
- }
355
-
356
-
357
- private function parseAttributeQuote(): Html\QuotedValue
358
- {
359
- $stream = $this->parser->getStream();
360
- $quoteToken = $stream->consume(Token::Quote);
361
- $value = $this->parser->parseFragment(fn() => match ($stream->peek()->type) {
362
- Token::Quote => null,
363
- default => $this->parser->inTextResolve(),
364
- });
365
- $node = new Html\QuotedValue(
366
- value: $value,
367
- quote: $quoteToken->text,
368
- position: $quoteToken->position,
433
+ nAttributeNode: $node = new Nodes\TextNode(''),
369
434
  );
370
- $stream->consume(Token::Quote);
371
435
  return $node;
372
436
  }
373
437
 
@@ -375,17 +439,16 @@ final class TemplateParserHtml
375
439
  private function parseComment(): Html\CommentNode
376
440
  {
377
441
  $this->parser->lastIndentation = null;
378
- $this->parser->location = $this->parser::LocationTag;
442
+ $this->parser->inHead = false;
443
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlComment);
379
444
  $stream = $this->parser->getStream();
445
+ $openToken = $stream->consume(Token::Html_CommentOpen);
380
446
  $node = new Html\CommentNode(
381
- position: $stream->consume(Token::Html_CommentOpen)->position,
382
- content: $this->parser->parseFragment(fn() => match ($stream->peek()->type) {
383
- Token::Html_CommentClose => null,
384
- default => $this->parser->inTextResolve(),
385
- }),
447
+ position: $openToken->position,
448
+ content: $this->parser->parseFragment([$this->parser, 'inTextResolve']),
386
449
  );
387
- $stream->consume(Token::Html_CommentClose);
388
- $this->parser->location = $this->parser::LocationText;
450
+ $stream->tryConsume(Token::Html_CommentClose) || $stream->throwUnexpectedException([Token::Html_CommentClose], addendum: " started $openToken->position");
451
+ $this->parser->getLexer()->setState(TemplateLexer::StateHtmlText);
389
452
  return $node;
390
453
  }
391
454
 
@@ -398,8 +461,10 @@ final class TemplateParserHtml
398
461
  continue;
399
462
  }
400
463
  if ($stream->tryConsume(Token::Latte_CommentOpen)) {
464
+ $this->parser->getLexer()->pushState(TemplateLexer::StateLatteComment);
401
465
  $stream->consume(Token::Text);
402
466
  $stream->consume(Token::Latte_CommentClose);
467
+ $this->parser->getLexer()->popState();
403
468
  $stream->tryConsume(Token::Newline);
404
469
  continue;
405
470
  }
@@ -408,15 +473,6 @@ final class TemplateParserHtml
408
473
  }
409
474
 
410
475
 
411
- private function isClosingTag(string $name): bool
412
- {
413
- $stream = $this->parser->getStream();
414
- return $stream->is(Token::Html_TagOpen)
415
- && $stream->peek(1)->is(Token::Slash)
416
- && strcasecmp($name, $stream->peek(2)->text ?? '') === 0;
417
- }
418
-
419
-
420
476
  private function prepareNAttrs(array $attrs, bool $void): array
421
477
  {
422
478
  $res = [];
@@ -454,7 +510,7 @@ final class TemplateParserHtml
454
510
  if ($res instanceof \Generator && $res->valid()) {
455
511
  $toClose[] = [$res, $tag];
456
512
 
457
- } elseif ($res instanceof Node) {
513
+ } elseif ($res instanceof AreaNode) {
458
514
  $this->parser->ensureIsConsumed($tag);
459
515
  $res->position = $tag->position;
460
516
  $tag->replaceNAttribute($res);
@@ -9,13 +9,9 @@ declare(strict_types=1);
9
9
 
10
10
  namespace Latte\Compiler;
11
11
 
12
- use Latte;
13
-
14
12
 
15
13
  final class Token
16
14
  {
17
- use Latte\Strict;
18
-
19
15
  public const
20
16
  End = 0,
21
17
  Text = 10000,
@@ -124,7 +120,7 @@ final class Token
124
120
  Php_True = 336,
125
121
  Php_False = 337;
126
122
 
127
- public const NAMES = [
123
+ public const Names = [
128
124
  self::End => '[EOF]',
129
125
  self::Text => 'text',
130
126
  self::Whitespace => 'whitespace',
@@ -134,11 +130,11 @@ final class Token
134
130
  self::Equals => "'='",
135
131
  self::Quote => 'quote',
136
132
 
137
- self::Latte_TagOpen => "'{'",
138
- self::Latte_TagClose => "'}'",
133
+ self::Latte_TagOpen => 'Latte tag',
134
+ self::Latte_TagClose => 'end of Latte tag',
139
135
  self::Latte_Name => 'tag name',
140
- self::Latte_CommentOpen => "'{*'",
141
- self::Latte_CommentClose => "'*}'",
136
+ self::Latte_CommentOpen => 'Latte comment',
137
+ self::Latte_CommentClose => 'end of Latte comment',
142
138
 
143
139
  self::Html_TagOpen => 'HTML tag',
144
140
  self::Html_TagClose => 'end of HTML tag',
@@ -9,7 +9,6 @@ declare(strict_types=1);
9
9
 
10
10
  namespace Latte\Compiler;
11
11
 
12
- use Latte;
13
12
  use Latte\CompileException;
14
13
 
15
14
 
@@ -19,8 +18,6 @@ use Latte\CompileException;
19
18
  */
20
19
  final class TokenStream
21
20
  {
22
- use Latte\Strict;
23
-
24
21
  /** @var Token[] */
25
22
  private array $tokens = [];
26
23
  private \Iterator /*readonly*/ $source;
@@ -70,7 +67,7 @@ final class TokenStream
70
67
  {
71
68
  $token = $this->peek();
72
69
  if ($kind && !$token->is(...$kind)) {
73
- $kind = array_map(fn($item) => is_string($item) ? "'$item'" : Token::NAMES[$item], $kind);
70
+ $kind = array_map(fn($item) => is_string($item) ? "'$item'" : Token::Names[$item], $kind);
74
71
  $this->throwUnexpectedException($kind);
75
72
  } elseif (!$token->isEnd()) {
76
73
  $this->index++;
@@ -119,28 +116,15 @@ final class TokenStream
119
116
  * @throws CompileException
120
117
  * @return never
121
118
  */
122
- public function throwUnexpectedException(array $expected = [], string $addendum = ''): void
119
+ public function throwUnexpectedException(array $expected = [], string $addendum = '', string $excerpt = ''): void
123
120
  {
124
- $s = null;
125
- $i = 0;
126
- do {
127
- $token = $this->peek($i++);
128
- if ($token->isEnd()) {
129
- break;
130
- }
131
- $s .= $token->text;
132
- if (strlen($s) > 5) {
133
- break;
134
- }
135
- } while (true);
136
-
137
- $expected = array_map(fn($item) => is_int($item) ? Token::NAMES[$item] : $item, $expected);
138
-
121
+ $token = $this->peek()->text . $excerpt;
122
+ $expected = array_map(fn($item) => is_int($item) ? Token::Names[$item] : $item, $expected);
139
123
  throw new CompileException(
140
124
  'Unexpected '
141
- . ($s === null
125
+ . ($token === ''
142
126
  ? 'end'
143
- : "'" . trim($s, "\n") . "'")
127
+ : "'" . trim($token, "\n") . "'")
144
128
  . ($expected && count($expected) < 5
145
129
  ? ', expecting ' . implode(', ', $expected)
146
130
  : '')
@@ -17,10 +17,13 @@ use Latte\Compiler\Nodes\TemplateNode;
17
17
  */
18
18
  class Engine
19
19
  {
20
- use Strict;
20
+ public const Version = '3.0.12';
21
+ public const VersionId = 30012;
21
22
 
22
- public const VERSION = '3.0.6';
23
- public const VERSION_ID = 30006;
23
+ /** @deprecated use Engine::Version */
24
+ public const
25
+ VERSION = self::Version,
26
+ VERSION_ID = self::VersionId;
24
27
 
25
28
  /** @deprecated use ContentType::* */
26
29
  public const
@@ -42,8 +45,10 @@ class Engine
42
45
  private ?string $tempDirectory = null;
43
46
  private bool $autoRefresh = true;
44
47
  private bool $strictTypes = false;
48
+ private bool $strictParsing = false;
45
49
  private ?Policy $policy = null;
46
50
  private bool $sandboxed = false;
51
+ private ?string $phpBinary = null;
47
52
 
48
53
 
49
54
  public function __construct()
@@ -126,6 +131,10 @@ class Engine
126
131
  throw $e->setSource($source, $name);
127
132
  }
128
133
 
134
+ if ($this->phpBinary) {
135
+ Compiler\PhpHelpers::checkCode($this->phpBinary, $code, "(compiled $name)");
136
+ }
137
+
129
138
  return $code;
130
139
  }
131
140
 
@@ -135,8 +144,8 @@ class Engine
135
144
  */
136
145
  public function parse(string $source): TemplateNode
137
146
  {
138
- $lexer = new Compiler\TemplateLexer;
139
147
  $parser = new Compiler\TemplateParser;
148
+ $parser->strict = $this->strictParsing;
140
149
 
141
150
  foreach ($this->extensions as $extension) {
142
151
  $extension->beforeCompile($this);
@@ -146,7 +155,7 @@ class Engine
146
155
  return $parser
147
156
  ->setContentType($this->contentType)
148
157
  ->setPolicy($this->getPolicy(effective: true))
149
- ->parse($source, $lexer);
158
+ ->parse($source);
150
159
  }
151
160
 
152
161
 
@@ -173,12 +182,12 @@ class Engine
173
182
  */
174
183
  public function generate(TemplateNode $node, string $name): string
175
184
  {
176
- $comment = preg_match('#\n|\?#', $name) ? null : "source: $name";
185
+ $sourceName = preg_match('#\n|\?#', $name) ? null : $name;
177
186
  $generator = new Compiler\TemplateGenerator;
178
187
  return $generator->generate(
179
188
  $node,
180
189
  $this->getTemplateClass($name),
181
- $comment,
190
+ $sourceName,
182
191
  $this->strictTypes,
183
192
  );
184
193
  }
@@ -310,7 +319,7 @@ class Engine
310
319
  {
311
320
  $key = [
312
321
  $this->getLoader()->getUniqueId($name),
313
- self::VERSION,
322
+ self::Version,
314
323
  array_keys((array) $this->functions),
315
324
  $this->contentType,
316
325
  ];
@@ -527,6 +536,19 @@ class Engine
527
536
  }
528
537
 
529
538
 
539
+ public function setStrictParsing(bool $on = true): static
540
+ {
541
+ $this->strictParsing = $on;
542
+ return $this;
543
+ }
544
+
545
+
546
+ public function isStrictParsing(): bool
547
+ {
548
+ return $this->strictParsing;
549
+ }
550
+
551
+
530
552
  public function setLoader(Loader $loader): static
531
553
  {
532
554
  $this->loader = $loader;
@@ -544,6 +566,13 @@ class Engine
544
566
  }
545
567
 
546
568
 
569
+ public function enablePhpLinter(?string $phpBinary): static
570
+ {
571
+ $this->phpBinary = $phpBinary;
572
+ return $this;
573
+ }
574
+
575
+
547
576
  /**
548
577
  * @param object|mixed[] $params
549
578
  * @return mixed[]
@@ -20,8 +20,6 @@ use Nette\PhpGenerator as Php;
20
20
  */
21
21
  final class Blueprint
22
22
  {
23
- use Latte\Strict;
24
-
25
23
  public function printClass(Template $template, ?string $name = null): void
26
24
  {
27
25
  if (!class_exists(Php\ClassType::class)) {