• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / latte / 30275971304

27 Jul 2026 02:32PM UTC coverage: 94.928% (+0.4%) from 94.512%
30275971304

push

github

dg
Released version 3.1.6

6064 of 6388 relevant lines covered (94.93%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.13
/src/Latte/Compiler/TemplateParserHtml.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Latte (https://latte.nette.org)
5
 * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Latte\Compiler;
9

10
use Latte;
11
use Latte\CompileException;
12
use Latte\Compiler\Nodes\AreaNode;
13
use Latte\Compiler\Nodes\FragmentNode;
14
use Latte\Compiler\Nodes\Html;
15
use Latte\ContentType;
16
use Latte\Helpers;
17
use Latte\SecurityViolationException;
18
use function array_keys, array_pop, end, implode, in_array, key, preg_replace, str_starts_with, strlen, substr;
19

20

21
/**
22
 * Template parser extension for HTML.
23
 */
24
final class TemplateParserHtml
25
{
26
        private ?Html\ElementNode $element = null;
27

28
        /** @var array{string, ?Nodes\Php\ExpressionNode, ?Position}|null */
29
        private ?array $endName = null;
30

31
        /** @var \WeakMap<Html\ElementNode, object{tag: mixed, textualName: string, unclosedTags?: array<string>}> */
32
        private \WeakMap $elementData;
33

34

35
        public function __construct(
1✔
36
                private readonly TemplateParser $parser,
37
                /** @var array<string, \Closure(Tag, TemplateParser): (Node|\Generator|void)> */
38
                private readonly array $attrParsers,
39
        ) {
40
                $this->elementData = new \WeakMap;
1✔
41
        }
1✔
42

43

44
        public function getElement(): ?Html\ElementNode
45
        {
46
                return $this->element;
1✔
47
        }
48

49

50
        public function inTextResolve(): ?Node
51
        {
52
                $stream = $this->parser->getStream();
1✔
53
                $token = $stream->peek();
1✔
54
                return match ($token->type) {
1✔
55
                        Token::Html_TagOpen => $this->parseTag(),
1✔
56
                        Token::Html_CommentOpen => $this->parseComment(),
1✔
57
                        Token::Html_BogusOpen => $this->parseBogusTag(),
1✔
58
                        default => $this->parser->inTextResolve(),
1✔
59
                };
60
        }
61

62

63
        public function inTagResolve(FragmentNode $fragment): ?Node
1✔
64
        {
65
                $stream = $this->parser->getStream();
1✔
66
                $token = $stream->peek();
1✔
67
                return match ($token->type) {
1✔
68
                        Token::Html_Name => str_starts_with($token->text, TemplateLexer::NPrefix)
1✔
69
                                ? $this->parseNAttribute()
1✔
70
                                : $this->parseAttribute($fragment),
1✔
71
                        Token::Latte_TagOpen => $this->parseAttribute($fragment),
1✔
72
                        Token::Whitespace => $this->parseAttributeWhitespace(),
1✔
73
                        Token::Html_TagClose => null,
1✔
74
                        default => $this->parser->inTextResolve(),
1✔
75
                };
76
        }
77

78

79
        private function parseTag(): ?Node
80
        {
81
                $stream = $this->parser->getStream();
1✔
82
                $lexer = $this->parser->getLexer();
1✔
83
                $lexer->pushState(TemplateLexer::StateHtmlTag);
1✔
84
                $closing = $stream->tryPeek(1)?->is(Token::Slash);
1✔
85
                $lexer->popState();
1✔
86
                if (!$closing) {
1✔
87
                        return $this->parseElement();
1✔
88
                }
89

90
                if ($this->element
1✔
91
                        && $this->parser->peekTag() === $this->elementData[$this->element]->tag // is directly in the element
1✔
92
                ) {
93
                        $save = $stream->getIndex();
1✔
94
                        $this->endName = [$endText] = $this->parseEndTag();
1✔
95
                        if ($this->element->is($endText) || $this->elementData[$this->element]->textualName === $endText) {
1✔
96
                                return null; // go to parseElement() one level up to close the element
1✔
97
                        }
98
                        $stream->seek($save);
1✔
99
                        if (!in_array($endText, $this->elementData[$this->element]->unclosedTags ?? [], strict: true)) {
1✔
100
                                return null; // go to parseElement() one level up to collapse
1✔
101
                        }
102
                }
103

104
                if ($this->parser->strict) {
1✔
105
                        $stream->throwUnexpectedException(excerpt: '/');
1✔
106
                }
107
                return $this->parseBogusEndTag();
1✔
108
        }
109

110

111
        private function parseElement(): Node
112
        {
113
                $res = new FragmentNode;
1✔
114
                $res->append($this->extractIndentation());
1✔
115
                $res->append($this->parseStartTag($this->element));
1✔
116
                $elem = $this->element;
1✔
117

118
                $stream = $this->parser->getStream();
1✔
119
                $void = $this->resolveVoidness($elem);
1✔
120
                $attrs = $this->prepareNAttrs($elem->nAttributes, $void);
1✔
121
                $outerNodes = $this->openNAttrNodes($attrs[Tag::PrefixNone] ?? []);
1✔
122
                $tagNodes = $this->openNAttrNodes($attrs[Tag::PrefixTag] ?? []);
1✔
123
                if ($tagNodes) {
1✔
124
                        $elem->dynamicTag = $this->finishNAttrNodes($elem->dynamicTag ?? new Nodes\Html\TagNode($elem), $tagNodes);
1✔
125
                }
126

127
                if (!$void) {
1✔
128
                        if ($elem->isRawText()) {
1✔
129
                                $this->parser->getLexer()->pushState(TemplateLexer::StateHtmlRawText, $elem->name);
1✔
130
                        }
131
                        $content = new FragmentNode;
1✔
132
                        if ($token = $stream->tryConsume(Token::Newline)) {
1✔
133
                                $content->append(new Nodes\TextNode($token->text, $token->position, $token->end));
1✔
134
                        }
135

136
                        $innerNodes = $this->openNAttrNodes($attrs[Tag::PrefixInner] ?? []);
1✔
137
                        $this->elementData[$elem]->tag = $this->parser->peekTag();
1✔
138
                        $frag = $this->parser->parseFragment($this->inTextResolve(...));
1✔
139
                        $content->append($this->finishNAttrNodes($frag, $innerNodes));
1✔
140
                        if ($elem->isRawText()) {
1✔
141
                                $this->parser->getLexer()->popState();
1✔
142
                        }
143

144
                        [$endText, $endVariable, $endRange] = $this->endName ?? [null, null, null];
1✔
145
                        $this->endName = null;
1✔
146
                        if ($endText && ($this->element->is($endText) || $this->elementData[$this->element]->textualName === $endText)) {
1✔
147
                                $elem->content = $content;
1✔
148
                                $elem->content->append($this->extractIndentation());
1✔
149
                                $elem->end = $endRange ?? $elem->end;
1✔
150

151
                        } elseif ($outerNodes || $innerNodes
1✔
152
                                || $elem->dynamicTag
1✔
153
                                || $this->parser->strict
1✔
154
                                || $endVariable
1✔
155
                                || $elem->isRawText()
1✔
156
                        ) {
157
                                $stream->throwUnexpectedException(
1✔
158
                                        addendum: ", expecting </{$this->elementData[$elem]->textualName}> for element started $elem->position",
1✔
159
                                        excerpt: $endText ? "/{$endText}>" : ($stream->tryPeek(1)->text ?? '') . ($stream->tryPeek(2)->text ?? ''),
1✔
160
                                );
161
                        } else { // element collapsed to tags
162
                                $res->append($content);
1✔
163
                                $this->element = $elem->parent;
1✔
164
                                if ($this->element && !$stream->is(Token::Html_TagOpen)) {
1✔
165
                                        $this->elementData[$this->element]->unclosedTags[] = $elem->name;
1✔
166
                                }
167
                                return $res;
1✔
168
                        }
169
                }
170

171
                if ($token = $stream->tryConsume(Token::Newline)) {
1✔
172
                        $res->append(new Nodes\TextNode($token->text, $token->position, $token->end));
1✔
173
                }
174

175
                $res = $this->finishNAttrNodes($res, $outerNodes);
1✔
176
                $this->element = $elem->parent;
1✔
177
                return $res;
1✔
178
        }
179

180

181
        private function extractIndentation(): AreaNode
182
        {
183
                if ($this->parser->lastIndentation) {
1✔
184
                        $dolly = clone $this->parser->lastIndentation;
1✔
185
                        $this->parser->lastIndentation->clear();
1✔
186
                        return $dolly;
1✔
187
                } else {
188
                        return new Nodes\NopNode;
1✔
189
                }
190
        }
191

192

193
        /** @param-out Html\ElementNode $elem */
194
        private function parseStartTag(&$elem = null): Html\ElementNode
195
        {
196
                $stream = $this->parser->getStream();
1✔
197
                $lexer = $this->parser->getLexer();
1✔
198
                $openToken = $stream->consume(Token::Html_TagOpen);
1✔
199
                $lexer->pushState(TemplateLexer::StateHtmlTag);
1✔
200

201
                [$textual, $variable] = $this->parseTagName($this->parser->strict);
1✔
202
                if (($this->parser->strict || $variable)
1✔
203
                        && !$stream->is(Token::Whitespace, Token::Slash, Token::Html_TagClose)
1✔
204
                ) {
205
                        $stream->throwUnexpectedException();
×
206
                }
207

208
                $this->parser->lastIndentation = null;
1✔
209
                $this->parser->inHead = false;
1✔
210
                $elem = new Html\ElementNode(
1✔
211
                        name: $variable ? '' : $textual,
1✔
212
                        position: $openToken->position,
1✔
213
                        parent: $this->element,
1✔
214
                        contentType: $this->parser->getContentType(),
1✔
215
                );
216
                $this->elementData[$elem] = (object) [
1✔
217
                        'tag' => $this->parser->peekTag(),
1✔
218
                        'textualName' => $textual,
1✔
219
                ];
220
                $elem->attributes = $this->parser->parseFragment($this->inTagResolve(...));
1✔
221
                $elem->selfClosing = (bool) $stream->tryConsume(Token::Slash);
1✔
222
                if ($variable) {
1✔
223
                        $elem->dynamicTag = new Nodes\Html\TagNode($elem, $variable);
1✔
224
                }
225
                $elem->end = $stream->consume(Token::Html_TagClose)->end;
1✔
226
                $lexer->popState();
1✔
227
                return $elem;
1✔
228
        }
229

230

231
        /** @return array{string, ?Nodes\Php\ExpressionNode, ?Position} */
232
        private function parseEndTag(): array
233
        {
234
                $stream = $this->parser->getStream();
1✔
235
                $lexer = $this->parser->getLexer();
1✔
236
                $stream->consume(Token::Html_TagOpen);
1✔
237
                $lexer->pushState(TemplateLexer::StateHtmlTag);
1✔
238
                $stream->consume(Token::Slash);
1✔
239
                if (isset($this->element->nAttributes['syntax'])) {  // hardcoded
1✔
240
                        $lexer->popSyntax();
1✔
241
                }
242
                [$text, $variable] = $this->parseTagName();
1✔
243
                $stream->tryConsume(Token::Whitespace);
1✔
244
                $closeToken = $stream->consume(Token::Html_TagClose);
1✔
245
                $lexer->popState();
1✔
246
                return [$text, $variable, $closeToken->end];
1✔
247
        }
248

249

250
        /** @return array{string, ?Nodes\Php\ExpressionNode} */
251
        private function parseTagName(bool $strict = true): array
1✔
252
        {
253
                $variable = null;
1✔
254
                $text = '';
1✔
255
                $parts = [];
1✔
256
                $stream = $this->parser->getStream();
1✔
257
                do {
258
                        if ($stream->is(Token::Latte_TagOpen)) {
1✔
259
                                $save = $stream->getIndex();
1✔
260
                                $statement = $this->parser->parseLatteStatement($this->inTagResolve(...));
1✔
261
                                if (!$statement instanceof Nodes\PrintNode) {
1✔
262
                                        if (!$parts || $strict) {
1✔
263
                                                throw new CompileException('Only expression can be used as a HTML tag name.', $statement?->position);
1✔
264
                                        }
265
                                        $stream->seek($save);
1✔
266
                                        break;
1✔
267
                                }
268
                                $parts[] = $statement->expression;
1✔
269
                                $save -= $stream->getIndex();
1✔
270
                                while ($save < 0) {
1✔
271
                                        $text .= $stream->peek($save++)->text;
1✔
272
                                }
273
                                $variable = true;
1✔
274

275
                        } elseif ($token = $stream->tryConsume(Token::Html_Name)) {
1✔
276
                                $parts[] = new Latte\Compiler\Nodes\Php\Scalar\StringNode($token->text, $token->position, $token->end);
1✔
277
                                $text .= $token->text;
1✔
278

279
                        } elseif (!$parts) {
1✔
280
                                $stream->throwUnexpectedException([Token::Html_Name, Token::Latte_TagOpen]);
×
281
                        } else {
282
                                break;
1✔
283
                        }
284
                } while (true);
1✔
285

286
                $variable = $variable
1✔
287
                        ? Latte\Compiler\Nodes\Php\Expression\BinaryOpNode::nest('.', ...$parts)
1✔
288
                        : null;
1✔
289
                return [$text, $variable];
1✔
290
        }
291

292

293
        private function parseBogusEndTag(): Html\BogusTagNode
294
        {
295
                $stream = $this->parser->getStream();
1✔
296
                $lexer = $this->parser->getLexer();
1✔
297
                $openToken = $stream->consume(Token::Html_TagOpen);
1✔
298
                $lexer->pushState(TemplateLexer::StateHtmlTag);
1✔
299
                $this->parser->lastIndentation = null;
1✔
300
                $this->parser->inHead = false;
1✔
301
                $openDelimiter = $openToken->text . $stream->consume(Token::Slash)->text . $stream->consume(Token::Html_Name)->text;
1✔
302
                $wsToken = $stream->tryConsume(Token::Whitespace);
1✔
303
                $closeToken = $stream->consume(Token::Html_TagClose);
1✔
304
                $node = new Html\BogusTagNode(
1✔
305
                        openDelimiter: $openDelimiter,
1✔
306
                        content: new Nodes\TextNode($wsToken->text ?? '', $wsToken?->position, $wsToken?->end),
1✔
307
                        endDelimiter: $closeToken->text,
1✔
308
                        position: $openToken->position,
1✔
309
                        end: $closeToken->end,
1✔
310
                );
311
                $lexer->popState();
1✔
312
                return $node;
1✔
313
        }
314

315

316
        private function parseBogusTag(): Html\BogusTagNode
317
        {
318
                $stream = $this->parser->getStream();
1✔
319
                $lexer = $this->parser->getLexer();
1✔
320
                $openToken = $stream->consume(Token::Html_BogusOpen);
1✔
321
                $lexer->pushState(TemplateLexer::StateHtmlBogus);
1✔
322
                $this->parser->lastIndentation = null;
1✔
323
                $this->parser->inHead = false;
1✔
324
                $content = $this->parser->parseFragment($this->parser->inTextResolve(...));
1✔
325
                $lexer->popState();
1✔
326
                $closeToken = $stream->consume(Token::Html_TagClose);
1✔
327
                return new Html\BogusTagNode(
1✔
328
                        openDelimiter: $openToken->text,
1✔
329
                        content: $content,
330
                        endDelimiter: $closeToken->text,
1✔
331
                        position: $openToken->position,
1✔
332
                        end: $closeToken->end,
1✔
333
                );
334
        }
335

336

337
        private function resolveVoidness(Html\ElementNode $elem): bool
1✔
338
        {
339
                if ($elem->contentType !== ContentType::Html) {
1✔
340
                        return $elem->selfClosing;
1✔
341
                } elseif (Latte\Runtime\HtmlHelpers::isVoidElement($elem->name)) {
1✔
342
                        return true;
1✔
343
                } elseif ($elem->selfClosing) { // auto-correct
1✔
344
                        $elem->content = new Nodes\NopNode;
1✔
345
                        $elem->selfClosing = false;
1✔
346
                        $last = end($elem->attributes->children);
1✔
347
                        if ($last instanceof Nodes\TextNode && $last->isWhitespace()) {
1✔
348
                                array_pop($elem->attributes->children);
1✔
349
                        }
350
                        return true;
1✔
351
                }
352

353
                return $elem->selfClosing;
1✔
354
        }
355

356

357
        private function parseAttributeWhitespace(): Node
358
        {
359
                $stream = $this->parser->getStream();
1✔
360
                $token = $stream->consume(Token::Whitespace);
1✔
361
                return $stream->is(Token::Html_Name) && str_starts_with($stream->peek()->text, TemplateLexer::NPrefix)
1✔
362
                        ? new Nodes\NopNode
1✔
363
                        : new Nodes\TextNode($token->text, $token->position, $token->end);
1✔
364
        }
365

366

367
        private function parseAttribute(FragmentNode $fragment): ?Node
1✔
368
        {
369
                $stream = $this->parser->getStream();
1✔
370
                if ($stream->is(Token::Latte_TagOpen)) {
1✔
371
                        $name = $this->parser->parseLatteStatement();
1✔
372
                        if (!$name instanceof Nodes\PrintNode) {
1✔
373
                                return $name; // value like '<span {if true}attr1=val{/if}>'
1✔
374
                        }
375
                } else {
376
                        $name = $this->parser->parseText();
1✔
377
                }
378

379
                [$value, $quote] = $this->parseAttributeValue() ?? [null, null];
1✔
380
                $attrEnd = $this->parser->getStream()->peek(-1)->end;
1✔
381
                if ($name instanceof Nodes\TextNode && $value instanceof Nodes\PrintNode && $value->modifier->escape) {
1✔
382
                        if (($indent = end($fragment->children)) instanceof Nodes\TextNode && $indent->isWhitespace()) {
1✔
383
                                array_pop($fragment->children);
1✔
384
                        }
385
                        $value->modifier->escape = false;
1✔
386
                        return new Html\ExpressionAttributeNode(
1✔
387
                                name: $name->content,
1✔
388
                                value: $value->expression,
1✔
389
                                modifier: $value->modifier,
1✔
390
                                indentation: $indent->content ?? null,
1✔
391
                                position: $name->position,
1✔
392
                                end: $attrEnd,
393
                        );
394
                }
395

396
                return new Html\AttributeNode(
1✔
397
                        name: $name,
1✔
398
                        value: $value,
399
                        quote: $quote,
400
                        position: $name->position,
1✔
401
                        end: $attrEnd,
402
                );
403
        }
404

405

406
        /** @return ?array{AreaNode|Nodes\PrintNode, ?string} */
407
        private function parseAttributeValue(): ?array
408
        {
409
                $stream = $this->parser->getStream();
1✔
410
                $save = $stream->getIndex();
1✔
411
                $this->consumeIgnored();
1✔
412
                if (!$stream->tryConsume(Token::Equals)) {
1✔
413
                        $stream->seek($save);
1✔
414
                        return null;
1✔
415
                }
416

417
                $this->consumeIgnored();
1✔
418
                if ($quoteToken = $stream->tryConsume(Token::Quote)) {
1✔
419
                        $lexer = $this->parser->getLexer();
1✔
420
                        $lexer->pushState(TemplateLexer::StateHtmlQuotedValue, $quoteToken->text);
1✔
421
                        $value = $this->parser->parseFragment($this->parser->inTextResolve(...))->simplify(allowsNull: false);
1✔
422
                        assert($value !== null);
423
                        $stream->tryConsume(Token::Quote) || $stream->throwUnexpectedException([$quoteToken->text], addendum: ", end of HTML attribute started $quoteToken->position");
1✔
424
                        $lexer->popState();
1✔
425
                        return [$value, $quoteToken->text];
1✔
426
                }
427

428
                $value = $this->parser->parseFragment(
1✔
429
                        fn() => match ($stream->peek()->type) {
1✔
430
                                Token::Html_Name => $this->parser->parseText(),
1✔
431
                                Token::Latte_TagOpen => $this->parser->parseLatteStatement(),
1✔
432
                                Token::Latte_CommentOpen => $this->parser->parseLatteComment(),
433
                                default => null,
1✔
434
                        },
1✔
435
                )->simplify() ?? $stream->throwUnexpectedException();
1✔
436
                return [$value, null];
1✔
437
        }
438

439

440
        private function parseNAttribute(): Nodes\TextNode
441
        {
442
                assert($this->element !== null);
443
                $elem = $this->element;
1✔
444
                $stream = $this->parser->getStream();
1✔
445
                $nameToken = $stream->consume(Token::Html_Name);
1✔
446
                $save = $stream->getIndex();
1✔
447
                $pos = $stream->peek()->position;
1✔
448
                $name = substr($nameToken->text, strlen(TemplateLexer::NPrefix));
1✔
449
                if ($this->parser->peekTag() !== $this->elementData[$elem]->tag) {
1✔
450
                        throw new CompileException("Attribute n:$name must not appear inside {tags}", $nameToken->position);
1✔
451

452
                } elseif (isset($elem->nAttributes[$name])) {
1✔
453
                        throw new CompileException("Found multiple attributes n:$name.", $nameToken->position);
1✔
454
                }
455

456
                $this->consumeIgnored();
1✔
457
                $endToken = $nameToken;
1✔
458
                if ($stream->tryConsume(Token::Equals)) {
1✔
459
                        $lexer = $this->parser->getLexer();
1✔
460
                        $this->consumeIgnored();
1✔
461
                        if ($quoteToken = $stream->tryConsume(Token::Quote)) {
1✔
462
                                $lexer->pushState(TemplateLexer::StateHtmlQuotedNAttrValue, $quoteToken->text);
1✔
463
                                $valueToken = $stream->tryConsume(Token::Text);
1✔
464
                                $pos = $stream->peek()->position;
1✔
465
                                $endToken = $stream->tryConsume(Token::Quote) ?? $stream->throwUnexpectedException([$quoteToken->text], addendum: ", end of n:attribute started $quoteToken->position");
1✔
466
                                $lexer->popState();
1✔
467
                                $tokens = $valueToken ? (new TagLexer)->tokenize($valueToken->text, $valueToken->position) : null;
1✔
468

469
                        } elseif ($openToken = $stream->tryConsume(Token::Latte_TagOpen)) {
1✔
470
                                $lexer->pushState(TemplateLexer::StateLatteContent);
1✔
471
                                $tokens = $this->parser->consumeTag();
1✔
472
                                $endToken = $stream->tryConsume(Token::Latte_TagClose) ?? $stream->throwUnexpectedException([Token::Latte_TagClose], addendum: " started $openToken->position");
1✔
473
                                $lexer->popState();
1✔
474

475
                        } else {
476
                                $endToken = $valueToken = $stream->consume(Token::Html_Name);
1✔
477
                                $tokens = (new TagLexer)->tokenize($valueToken->text, $valueToken->position);
1✔
478
                        }
479
                } else {
480
                        $stream->seek($save);
1✔
481
                }
482
                $tokens ??= [new Token(Token::End, '', $pos)];
1✔
483

484
                $elem->nAttributes[$name] = new Tag(
1✔
485
                        name: preg_replace('~^(inner-|tag-)~', '', $name),
1✔
486
                        tokens: $tokens,
487
                        position: $nameToken->position,
1✔
488
                        end: $endToken->end,
1✔
489
                        prefix: $this->getPrefix($name),
1✔
490
                        inTag: true,
1✔
491
                        htmlElement: $elem,
492
                        nAttribute: $node = new Nodes\TextNode(''),
1✔
493
                );
494
                return $node;
1✔
495
        }
496

497

498
        private function parseComment(): Html\CommentNode
499
        {
500
                $lexer = $this->parser->getLexer();
1✔
501
                $this->parser->lastIndentation = null;
1✔
502
                $this->parser->inHead = false;
1✔
503
                $lexer->pushState(TemplateLexer::StateHtmlComment);
1✔
504
                $stream = $this->parser->getStream();
1✔
505
                $openToken = $stream->consume(Token::Html_CommentOpen);
1✔
506
                $node = new Html\CommentNode(
1✔
507
                        position: $openToken->position,
1✔
508
                        content: $this->parser->parseFragment($this->parser->inTextResolve(...)),
1✔
509
                );
510
                $closeToken = $stream->tryConsume(Token::Html_CommentClose) ?? $stream->throwUnexpectedException([Token::Html_CommentClose], addendum: " started $openToken->position");
1✔
511
                $node->end = $closeToken->end;
1✔
512
                $lexer->popState();
1✔
513
                return $node;
1✔
514
        }
515

516

517
        private function consumeIgnored(): void
518
        {
519
                $stream = $this->parser->getStream();
1✔
520
                $lexer = $this->parser->getLexer();
1✔
521
                do {
522
                        if ($stream->tryConsume(Token::Whitespace)) {
1✔
523
                                continue;
1✔
524
                        }
525
                        if ($stream->tryConsume(Token::Latte_CommentOpen)) {
1✔
526
                                $lexer->pushState(TemplateLexer::StateLatteComment);
1✔
527
                                $stream->tryConsume(Token::Text);
1✔
528
                                $stream->consume(Token::Latte_CommentClose);
1✔
529
                                $lexer->popState();
1✔
530
                                $stream->tryConsume(Token::Newline);
1✔
531
                                continue;
1✔
532
                        }
533
                        return;
1✔
534
                } while (true);
1✔
535
        }
536

537

538
        /**
539
         * @param  array<Tag>  $attrs
540
         * @return array<string, list<Tag>>
541
         */
542
        private function prepareNAttrs(array $attrs, bool $void): array
1✔
543
        {
544
                $res = [];
1✔
545
                foreach ($this->attrParsers as $name => $foo) {
1✔
546
                        if ($tag = $attrs[$name] ?? null) {
1✔
547
                                $prefix = $this->getPrefix($name);
1✔
548
                                if (!$prefix || !$void) {
1✔
549
                                        $res[$prefix][] = $tag;
1✔
550
                                        unset($attrs[$name]);
1✔
551
                                }
552
                        }
553
                }
554

555
                if ($attrs) {
1✔
556
                        $hint = Helpers::getSuggestion(array_keys($this->attrParsers), $k = key($attrs));
1✔
557
                        throw new CompileException('Unexpected attribute n:'
1✔
558
                                . ($hint ? "$k, did you mean n:$hint?" : implode(' and n:', array_keys($attrs))), $attrs[$k]->position);
1✔
559
                }
560

561
                return $res;
1✔
562
        }
563

564

565
        /**
566
         * @param  array<Tag>  $toOpen
567
         * @return array<array{\Generator, Tag}>
568
         */
569
        private function openNAttrNodes(array $toOpen): array
1✔
570
        {
571
                $toClose = [];
1✔
572
                foreach ($toOpen as $tag) {
1✔
573
                        $parser = $this->getAttrParser($tag->name, $tag->position);
1✔
574
                        $this->parser->pushTag($tag);
1✔
575
                        $res = $parser($tag, $this->parser);
1✔
576
                        if ($res instanceof \Generator && $res->valid()) {
1✔
577
                                $toClose[] = [$res, $tag];
1✔
578

579
                        } elseif ($res instanceof AreaNode) {
1✔
580
                                $this->parser->ensureIsConsumed($tag);
1✔
581
                                $res->position = $tag->position;
1✔
582
                                $res->end ??= $tag->end;
1✔
583
                                $tag->replaceNAttribute($res);
1✔
584
                                $this->parser->popTag();
1✔
585

586
                        } elseif (!$res) {
1✔
587
                                $this->parser->ensureIsConsumed($tag);
1✔
588
                                $this->parser->popTag();
1✔
589

590
                        } else {
591
                                throw new CompileException("Unexpected value returned by {$tag->getNotation()} parser.", $tag->position);
×
592
                        }
593
                }
594

595
                return $toClose;
1✔
596
        }
597

598

599
        /** @param  array<array{\Generator, Tag}>  $toClose */
600
        private function finishNAttrNodes(AreaNode $node, array $toClose): AreaNode
1✔
601
        {
602
                while ([$gen, $tag] = array_pop($toClose)) {
1✔
603
                        $gen->send([$node, null]);
1✔
604
                        $gen->valid() && throw new \LogicException("Incorrect behavior of {$tag->getNotation()} parser, more yield calls than expected (on line {$tag->position->line})");
1✔
605
                        $node = $gen->getReturn();
1✔
606
                        if (!$node instanceof AreaNode) {
1✔
607
                                throw new CompileException("Unexpected value returned by {$tag->getNotation()} parser.", $tag->position);
1✔
608
                        }
609

610
                        $node->position = $tag->position;
1✔
611
                        $node->end ??= $tag->end;
1✔
612
                        $this->parser->ensureIsConsumed($tag);
1✔
613
                        $this->parser->popTag();
1✔
614
                }
615

616
                return $node;
1✔
617
        }
618

619

620
        /** @return \Closure(Tag, TemplateParser): (Node|\Generator|void) */
621
        private function getAttrParser(string $name, Position $pos): \Closure
1✔
622
        {
623
                if (!isset($this->attrParsers[$name])) {
1✔
624
                        $hint = ($t = Helpers::getSuggestion(array_keys($this->attrParsers), $name))
×
625
                                ? ", did you mean n:$t?"
×
626
                                : '';
×
627
                        throw new CompileException("Unknown n:{$name}{$hint}", $pos);
×
628
                } elseif (!$this->parser->isTagAllowed($name)) {
1✔
629
                        throw new SecurityViolationException("Attribute n:$name is not allowed", $pos);
1✔
630
                }
631
                return $this->attrParsers[$name];
1✔
632
        }
633

634

635
        private function getPrefix(string $name): string
1✔
636
        {
637
                return match (true) {
638
                        str_starts_with($name, 'inner-') => Tag::PrefixInner,
1✔
639
                        str_starts_with($name, 'tag-') => Tag::PrefixTag,
1✔
640
                        default => Tag::PrefixNone,
1✔
641
                };
642
        }
643
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc