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

nette / latte / 8336142063

19 Mar 2024 01:30AM UTC coverage: 94.097% (+0.03%) from 94.071%
8336142063

push

github

dg
hasBlock() fixed template retrieval [Closes #357]

1 of 1 new or added line in 1 file covered. (100.0%)

37 existing lines in 8 files now uncovered.

5037 of 5353 relevant lines covered (94.1%)

0.94 hits per line

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

97.87
/src/Latte/Compiler/TemplateParserHtml.php
1
<?php
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
declare(strict_types=1);
9

10
namespace Latte\Compiler;
11

12
use Latte;
13
use Latte\CompileException;
14
use Latte\Compiler\Nodes\AreaNode;
15
use Latte\Compiler\Nodes\FragmentNode;
16
use Latte\Compiler\Nodes\Html;
17
use Latte\ContentType;
18
use Latte\Helpers;
19
use Latte\SecurityViolationException;
20

21

22
/**
23
 * Template parser extension for HTML.
24
 */
25
final class TemplateParserHtml
26
{
27
        /** @var array<string, callable(Tag, TemplateParser): (Node|\Generator|void)> */
28
        private array /*readonly*/ $attrParsers;
29
        private ?Html\ElementNode $element = null;
30
        private TemplateParser /*readonly*/ $parser;
31

32
        /** @var array{string, ?Nodes\Php\ExpressionNode} */
33
        private ?array $endName = null;
34

35

36
        public function __construct(TemplateParser $parser, array $attrParsers)
1✔
37
        {
38
                $this->parser = $parser;
1✔
39
                $this->attrParsers = $attrParsers;
1✔
40
        }
1✔
41

42

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

48

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

61

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

77

78
        private function parseTag(): ?Node
79
        {
80
                $stream = $this->parser->getStream();
1✔
81
                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
1✔
82
                if (!$stream->peek(1)?->is(Token::Slash)) {
1✔
83
                        return $this->parseElement();
1✔
84
                }
85

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

100
                if ($this->parser->strict) {
1✔
101
                        $stream->throwUnexpectedException(excerpt: '/');
1✔
102
                }
103
                return $this->parseBogusEndTag();
1✔
104
        }
105

106

107
        private function parseElement(): Node
108
        {
109
                $res = new FragmentNode;
1✔
110
                $res->append($this->extractIndentation());
1✔
111
                $res->append($this->parseStartTag($this->element));
1✔
112
                $elem = $this->element;
1✔
113

114
                $stream = $this->parser->getStream();
1✔
115
                $void = $this->resolveVoidness($elem);
1✔
116
                $attrs = $this->prepareNAttrs($elem->nAttributes, $void);
1✔
117
                $outerNodes = $this->openNAttrNodes($attrs[Tag::PrefixNone] ?? []);
1✔
118
                $tagNodes = $this->openNAttrNodes($attrs[Tag::PrefixTag] ?? []);
1✔
119
                $elem->tagNode = $this->finishNAttrNodes($elem->tagNode, $tagNodes);
1✔
120
                $elem->captureTagName = (bool) $tagNodes;
1✔
121

122
                if (!$void) {
1✔
123
                        $content = new FragmentNode;
1✔
124
                        if ($token = $stream->tryConsume(Token::Newline)) {
1✔
125
                                $content->append(new Nodes\TextNode($token->text, $token->position));
1✔
126
                        }
127

128
                        $innerNodes = $this->openNAttrNodes($attrs[Tag::PrefixInner] ?? []);
1✔
129
                        $elem->data->tag = $this->parser->peekTag();
1✔
130
                        $frag = $this->parser->parseFragment([$this, 'inTextResolve']);
1✔
131
                        $content->append($this->finishNAttrNodes($frag, $innerNodes));
1✔
132

133
                        [$endText, $endVariable] = $this->endName;
1✔
134
                        $this->endName = null;
1✔
135
                        if ($endText && ($this->element->is($endText) || $this->element->data->textualName === $endText)) {
1✔
136
                                $elem->content = $content;
1✔
137
                                $elem->content->append($this->extractIndentation());
1✔
138

139
                        } elseif ($outerNodes || $innerNodes || $tagNodes
1✔
140
                                || $this->parser->strict
1✔
141
                                || $elem->variableName
1✔
142
                                || $endVariable
1✔
143
                                || $elem->isRawText()
1✔
144
                        ) {
145
                                $stream->throwUnexpectedException(
1✔
146
                                        addendum: ", expecting </{$elem->data->textualName}> for element started $elem->position",
1✔
147
                                        excerpt: $endText ? "/{$endText}>" : $stream->peek(1)?->text . $stream->peek(2)?->text,
1✔
148
                                );
149
                        } else { // element collapsed to tags
150
                                $res->append($content);
1✔
151
                                $this->element = $elem->parent;
1✔
152
                                if ($this->element && !$stream->is(Token::Html_TagOpen)) {
1✔
153
                                        $this->element->data->unclosedTags[] = $elem->name;
1✔
154
                                }
155
                                return $res;
1✔
156
                        }
157
                }
158

159
                if ($token = $stream->tryConsume(Token::Newline)) {
1✔
160
                        $res->append(new Nodes\TextNode($token->text, $token->position));
1✔
161
                }
162

163
                $res = $this->finishNAttrNodes($res, $outerNodes);
1✔
164
                $this->element = $elem->parent;
1✔
165
                return $res;
1✔
166
        }
167

168

169
        private function extractIndentation(): AreaNode
170
        {
171
                if ($this->parser->lastIndentation) {
1✔
172
                        $dolly = clone $this->parser->lastIndentation;
1✔
173
                        $this->parser->lastIndentation->content = '';
1✔
174
                        return $dolly;
1✔
175
                } else {
176
                        return new Nodes\NopNode;
1✔
177
                }
178
        }
179

180

181
        private function parseStartTag(&$elem = null): Html\ElementNode
182
        {
183
                $stream = $this->parser->getStream();
1✔
184
                $openToken = $stream->consume(Token::Html_TagOpen);
1✔
185
                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
1✔
186

187
                [$textual, $variable] = $this->parseTagName($this->parser->strict);
1✔
188
                if (($this->parser->strict || $variable)
1✔
189
                        && !$stream->is(Token::Whitespace, Token::Slash, Token::Html_TagClose)
1✔
190
                ) {
191
                        throw $stream->throwUnexpectedException();
×
192
                }
193

194
                $this->parser->lastIndentation = null;
1✔
195
                $this->parser->inHead = false;
1✔
196
                $elem = new Html\ElementNode(
1✔
197
                        name: $variable ? '' : $textual,
1✔
198
                        position: $openToken->position,
1✔
199
                        parent: $this->element,
1✔
200
                        data: (object) ['tag' => $this->parser->peekTag()],
1✔
201
                        contentType: $this->parser->getContentType(),
1✔
202
                );
203
                $elem->attributes = $this->parser->parseFragment([$this, 'inTagResolve']);
1✔
204
                $elem->selfClosing = (bool) $stream->tryConsume(Token::Slash);
1✔
205
                $elem->variableName = $variable;
1✔
206
                $elem->data->textualName = $textual;
1✔
207
                $stream->consume(Token::Html_TagClose);
1✔
208
                $state = !$elem->selfClosing && $elem->isRawText()
1✔
209
                        ? TemplateLexer::StateHtmlRawText
1✔
210
                        : TemplateLexer::StateHtmlText;
1✔
211
                $this->parser->getLexer()->setState($state, $elem->name);
1✔
212
                return $elem;
1✔
213
        }
214

215

216
        /** @return array{string, ?Nodes\Php\ExpressionNode} */
217
        private function parseEndTag(): array
218
        {
219
                $stream = $this->parser->getStream();
1✔
220
                $lexer = $this->parser->getLexer();
1✔
221
                $stream->consume(Token::Html_TagOpen);
1✔
222
                $lexer->setState(TemplateLexer::StateHtmlTag);
1✔
223
                $stream->consume(Token::Slash);
1✔
224
                if (isset($this->element->nAttributes['syntax'])) {  // hardcoded
1✔
225
                        $lexer->popSyntax();
1✔
226
                }
227
                $name = $this->parseTagName();
1✔
228
                $stream->tryConsume(Token::Whitespace);
1✔
229
                $stream->consume(Token::Html_TagClose);
1✔
230
                $lexer->setState(TemplateLexer::StateHtmlText);
1✔
231
                return $name;
1✔
232
        }
233

234

235
        /** @return array{string, ?Nodes\Php\ExpressionNode} */
236
        private function parseTagName(bool $strict = true): array
1✔
237
        {
238
                $variable = $text = null;
1✔
239
                $parts = [];
1✔
240
                $stream = $this->parser->getStream();
1✔
241
                do {
242
                        if ($stream->is(Token::Latte_TagOpen)) {
1✔
243
                                $save = $stream->getIndex();
1✔
244
                                $statement = $this->parser->parseLatteStatement([$this, 'inTagResolve']);
1✔
245
                                if (!$statement instanceof Latte\Essential\Nodes\PrintNode) {
1✔
246
                                        if (!$parts || $strict) {
1✔
247
                                                throw new CompileException('Only expression can be used as a HTML tag name.', $statement->position);
1✔
248
                                        }
249
                                        $stream->seek($save);
1✔
250
                                        break;
1✔
251
                                }
252
                                $parts[] = $statement->expression;
1✔
253
                                $save -= $stream->getIndex();
1✔
254
                                while ($save < 0) {
1✔
255
                                        $text .= $stream->peek($save++)->text;
1✔
256
                                }
257
                                $variable = true;
1✔
258

259
                        } elseif ($token = $stream->tryConsume(Token::Html_Name)) {
1✔
260
                                $parts[] = new Latte\Compiler\Nodes\Php\Scalar\StringNode($token->text, $token->position);
1✔
261
                                $text .= $token->text;
1✔
262

263
                        } elseif (!$parts) {
1✔
UNCOV
264
                                throw $stream->throwUnexpectedException([Token::Html_Name, Token::Latte_TagOpen]);
×
265
                        } else {
266
                                break;
1✔
267
                        }
268
                } while (true);
1✔
269

270
                $variable = $variable
1✔
271
                        ? Latte\Compiler\Nodes\Php\Expression\BinaryOpNode::nest('.', ...$parts)
1✔
272
                        : null;
1✔
273
                return [$text, $variable];
1✔
274
        }
275

276

277
        private function parseBogusEndTag(): ?Html\BogusTagNode
278
        {
279
                $stream = $this->parser->getStream();
1✔
280
                $openToken = $stream->consume(Token::Html_TagOpen);
1✔
281
                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
1✔
282
                $this->parser->lastIndentation = null;
1✔
283
                $this->parser->inHead = false;
1✔
284
                $node = new Html\BogusTagNode(
1✔
285
                        openDelimiter: $openToken->text . $stream->consume(Token::Slash)->text . $stream->consume(Token::Html_Name)->text,
1✔
286
                        content: new Nodes\TextNode($stream->tryConsume(Token::Whitespace)->text ?? ''),
1✔
287
                        endDelimiter: $stream->consume(Token::Html_TagClose)->text,
1✔
288
                        position: $openToken->position,
1✔
289
                );
290
                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlText);
1✔
291
                return $node;
1✔
292
        }
293

294

295
        private function parseBogusTag(): Html\BogusTagNode
296
        {
297
                $stream = $this->parser->getStream();
1✔
298
                $openToken = $stream->consume(Token::Html_BogusOpen);
1✔
299
                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlBogus);
1✔
300
                $this->parser->lastIndentation = null;
1✔
301
                $this->parser->inHead = false;
1✔
302
                $content = $this->parser->parseFragment([$this->parser, 'inTextResolve']);
1✔
303
                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlText);
1✔
304
                return new Html\BogusTagNode(
1✔
305
                        openDelimiter: $openToken->text,
1✔
306
                        content: $content,
307
                        endDelimiter: $stream->consume(Token::Html_TagClose)->text,
1✔
308
                        position: $openToken->position,
1✔
309
                );
310
        }
311

312

313
        private function resolveVoidness(Html\ElementNode $elem): bool
1✔
314
        {
315
                if ($elem->contentType !== ContentType::Html) {
1✔
316
                        return $elem->selfClosing;
1✔
317
                } elseif (isset(Helpers::$emptyElements[strtolower($elem->name)])) {
1✔
318
                        return true;
1✔
319
                } elseif ($elem->selfClosing) { // auto-correct
1✔
320
                        $elem->content = new Nodes\NopNode;
1✔
321
                        $elem->selfClosing = false;
1✔
322
                        $last = end($elem->attributes->children);
1✔
323
                        if ($last instanceof Nodes\TextNode && $last->isWhitespace()) {
1✔
324
                                array_pop($elem->attributes->children);
1✔
325
                        }
326
                        return true;
1✔
327
                }
328

329
                return $elem->selfClosing;
1✔
330
        }
331

332

333
        private function parseAttributeWhitespace(): Node
334
        {
335
                $stream = $this->parser->getStream();
1✔
336
                $token = $stream->consume(Token::Whitespace);
1✔
337
                return $stream->is(Token::Html_Name) && str_starts_with($stream->peek()->text, TemplateLexer::NPrefix)
1✔
338
                        ? new Nodes\NopNode
1✔
339
                        : new Nodes\TextNode($token->text, $token->position);
1✔
340
        }
341

342

343
        private function parseAttribute(): ?Node
344
        {
345
                $stream = $this->parser->getStream();
1✔
346
                if ($stream->is(Token::Latte_TagOpen)) {
1✔
347
                        $name = $this->parser->parseLatteStatement();
1✔
348
                        if (!$name instanceof Latte\Essential\Nodes\PrintNode) {
1✔
349
                                return $name; // value like '<span {if true}attr1=val{/if}>'
1✔
350
                        }
351
                } else {
352
                        $name = $this->parser->parseText();
1✔
353
                }
354

355
                [$value, $quote] = $this->parseAttributeValue();
1✔
356
                return new Html\AttributeNode(
1✔
357
                        name: $name,
1✔
358
                        value: $value,
359
                        quote: $quote,
360
                        position: $name->position,
1✔
361
                );
362
        }
363

364

365
        private function parseAttributeValue(): ?array
366
        {
367
                $stream = $this->parser->getStream();
1✔
368
                $save = $stream->getIndex();
1✔
369
                $this->consumeIgnored();
1✔
370
                if (!$stream->tryConsume(Token::Equals)) {
1✔
371
                        $stream->seek($save);
1✔
372
                        return null;
1✔
373
                }
374

375
                $this->consumeIgnored();
1✔
376
                if ($quoteToken = $stream->tryConsume(Token::Quote)) {
1✔
377
                        $this->parser->getLexer()->setState(TemplateLexer::StateHtmlQuotedValue, $quoteToken->text);
1✔
378
                        $value = $this->parser->parseFragment([$this->parser, 'inTextResolve']);
1✔
379
                        $stream->tryConsume(Token::Quote) || $stream->throwUnexpectedException([$quoteToken->text], addendum: ", end of HTML attribute started $quoteToken->position");
1✔
380
                        $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
1✔
381
                        return [$value, $quoteToken->text];
1✔
382
                }
383

384
                $value = $this->parser->parseFragment(
1✔
385
                        fn() => match ($stream->peek()->type) {
1✔
386
                                Token::Html_Name => $this->parser->parseText(),
1✔
387
                                Token::Latte_TagOpen => $this->parser->parseLatteStatement(),
1✔
388
                                Token::Latte_CommentOpen => $this->parser->parseLatteComment(),
389
                                default => null,
1✔
390
                        },
1✔
391
                )->simplify() ?? $stream->throwUnexpectedException();
1✔
392
                return [$value, null];
1✔
393
        }
394

395

396
        private function parseNAttribute(): Nodes\TextNode
397
        {
398
                $stream = $this->parser->getStream();
1✔
399
                $nameToken = $stream->consume(Token::Html_Name);
1✔
400
                $save = $stream->getIndex();
1✔
401
                $pos = $stream->peek()->position;
1✔
402
                $name = substr($nameToken->text, strlen(TemplateLexer::NPrefix));
1✔
403
                if ($this->parser->peekTag() !== $this->element->data->tag) {
1✔
404
                        throw new CompileException("Attribute n:$name must not appear inside {tags}", $nameToken->position);
1✔
405

406
                } elseif (isset($this->element->nAttributes[$name])) {
1✔
407
                        throw new CompileException("Found multiple attributes n:$name.", $nameToken->position);
1✔
408
                }
409

410
                $this->consumeIgnored();
1✔
411
                if ($stream->tryConsume(Token::Equals)) {
1✔
412
                        $this->consumeIgnored();
1✔
413
                        if ($quoteToken = $stream->tryConsume(Token::Quote)) {
1✔
414
                                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlQuotedNAttrValue, $quoteToken->text);
1✔
415
                                $valueToken = $stream->tryConsume(Token::Text);
1✔
416
                                $pos = $stream->peek()->position;
1✔
417
                                $stream->tryConsume(Token::Quote) || $stream->throwUnexpectedException([$quoteToken->text], addendum: ", end of n:attribute started $quoteToken->position");
1✔
418
                                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlTag);
1✔
419
                        } else {
420
                                $valueToken = $stream->consume(Token::Html_Name);
1✔
421
                        }
422
                        if ($valueToken) {
1✔
423
                                $tokens = (new TagLexer)->tokenize($valueToken->text, $valueToken->position);
1✔
424
                        }
425
                } else {
426
                        $stream->seek($save);
1✔
427
                }
428
                $tokens ??= [new Token(Token::End, '', $pos)];
1✔
429

430
                $this->element->nAttributes[$name] = new Tag(
1✔
431
                        name: preg_replace('~(inner-|tag-|)~', '', $name),
1✔
432
                        tokens: $tokens,
433
                        position: $nameToken->position,
1✔
434
                        prefix: $this->getPrefix($name),
1✔
435
                        inTag: true,
1✔
436
                        htmlElement: $this->element,
1✔
437
                        nAttributeNode: $node = new Nodes\TextNode(''),
1✔
438
                );
439
                return $node;
1✔
440
        }
441

442

443
        private function parseComment(): Html\CommentNode
444
        {
445
                $this->parser->lastIndentation = null;
1✔
446
                $this->parser->inHead = false;
1✔
447
                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlComment);
1✔
448
                $stream = $this->parser->getStream();
1✔
449
                $openToken = $stream->consume(Token::Html_CommentOpen);
1✔
450
                $node = new Html\CommentNode(
1✔
451
                        position: $openToken->position,
1✔
452
                        content: $this->parser->parseFragment([$this->parser, 'inTextResolve']),
1✔
453
                );
454
                $stream->tryConsume(Token::Html_CommentClose) || $stream->throwUnexpectedException([Token::Html_CommentClose], addendum: " started $openToken->position");
1✔
455
                $this->parser->getLexer()->setState(TemplateLexer::StateHtmlText);
1✔
456
                return $node;
1✔
457
        }
458

459

460
        private function consumeIgnored(): void
461
        {
462
                $stream = $this->parser->getStream();
1✔
463
                do {
464
                        if ($stream->tryConsume(Token::Whitespace)) {
1✔
465
                                continue;
1✔
466
                        }
467
                        if ($stream->tryConsume(Token::Latte_CommentOpen)) {
1✔
468
                                $this->parser->getLexer()->pushState(TemplateLexer::StateLatteComment);
1✔
469
                                $stream->consume(Token::Text);
1✔
470
                                $stream->consume(Token::Latte_CommentClose);
1✔
471
                                $this->parser->getLexer()->popState();
1✔
472
                                $stream->tryConsume(Token::Newline);
1✔
473
                                continue;
1✔
474
                        }
475
                        return;
1✔
476
                } while (true);
1✔
477
        }
478

479

480
        private function prepareNAttrs(array $attrs, bool $void): array
1✔
481
        {
482
                $res = [];
1✔
483
                foreach ($this->attrParsers as $name => $foo) {
1✔
484
                        if ($tag = $attrs[$name] ?? null) {
1✔
485
                                $prefix = $this->getPrefix($name);
1✔
486
                                if (!$prefix || !$void) {
1✔
487
                                        $res[$prefix][] = $tag;
1✔
488
                                        unset($attrs[$name]);
1✔
489
                                }
490
                        }
491
                }
492

493
                if ($attrs) {
1✔
494
                        $hint = Helpers::getSuggestion(array_keys($this->attrParsers), $k = key($attrs));
1✔
495
                        throw new CompileException('Unexpected attribute n:'
1✔
496
                                . ($hint ? "$k, did you mean n:$hint?" : implode(' and n:', array_keys($attrs))), $attrs[$k]->position);
1✔
497
                }
498

499
                return $res;
1✔
500
        }
501

502

503
        /**
504
         * @param  array<Tag>  $toOpen
505
         * @return array<array{\Generator, Tag}>
506
         */
507
        private function openNAttrNodes(array $toOpen): array
1✔
508
        {
509
                $toClose = [];
1✔
510
                foreach ($toOpen as $tag) {
1✔
511
                        $parser = $this->getAttrParser($tag->name, $tag->position);
1✔
512
                        $this->parser->pushTag($tag);
1✔
513
                        $res = $parser($tag, $this->parser);
1✔
514
                        if ($res instanceof \Generator && $res->valid()) {
1✔
515
                                $toClose[] = [$res, $tag];
1✔
516

517
                        } elseif ($res instanceof AreaNode) {
1✔
518
                                $this->parser->ensureIsConsumed($tag);
1✔
519
                                $res->position = $tag->position;
1✔
520
                                $tag->replaceNAttribute($res);
1✔
521
                                $this->parser->popTag();
1✔
522

523
                        } elseif (!$res) {
1✔
524
                                $this->parser->ensureIsConsumed($tag);
1✔
525
                                $this->parser->popTag();
1✔
526

527
                        } else {
UNCOV
528
                                throw new CompileException("Unexpected value returned by {$tag->getNotation()} parser.", $tag->position);
×
529
                        }
530
                }
531

532
                return $toClose;
1✔
533
        }
534

535

536
        /** @param  array<array{\Generator, Tag}>  $toClose */
537
        private function finishNAttrNodes(AreaNode $node, array $toClose): AreaNode
1✔
538
        {
539
                while ([$gen, $tag] = array_pop($toClose)) {
1✔
540
                        $gen->send([$node, null]);
1✔
541
                        $node = $gen->getReturn();
1✔
542
                        $node->position = $tag->position;
1✔
543
                        $this->parser->popTag();
1✔
544
                        $this->parser->ensureIsConsumed($tag);
1✔
545
                }
546

547
                return $node;
1✔
548
        }
549

550

551
        /** @return callable(Tag, TemplateParser): (Node|\Generator|void) */
552
        private function getAttrParser(string $name, Position $pos): callable
1✔
553
        {
554
                if (!isset($this->attrParsers[$name])) {
1✔
UNCOV
555
                        $hint = ($t = Helpers::getSuggestion(array_keys($this->attrParsers), $name))
×
UNCOV
556
                                ? ", did you mean n:$t?"
×
UNCOV
557
                                : '';
×
UNCOV
558
                        throw new CompileException("Unknown n:{$name}{$hint}", $pos);
×
559
                } elseif (!$this->parser->isTagAllowed($name)) {
1✔
560
                        throw new SecurityViolationException("Attribute n:$name is not allowed", $pos);
1✔
561
                }
562
                return $this->attrParsers[$name];
1✔
563
        }
564

565

566
        private function getPrefix(string $name): string
1✔
567
        {
568
                return match (true) {
569
                        str_starts_with($name, 'inner-') => Tag::PrefixInner,
1✔
570
                        str_starts_with($name, 'tag-') => Tag::PrefixTag,
1✔
571
                        default => Tag::PrefixNone,
1✔
572
                };
573
        }
574
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc