|
| 1 | +<?php |
| 2 | + |
| 3 | +/* |
| 4 | + * This file is part of Twig. |
| 5 | + * |
| 6 | + * (c) Fabien Potencier |
| 7 | + * |
| 8 | + * For the full copyright and license information, please view the LICENSE |
| 9 | + * file that was distributed with this source code. |
| 10 | + */ |
| 11 | + |
| 12 | +namespace Twig\NodeVisitor; |
| 13 | + |
| 14 | +use Twig\Environment; |
| 15 | +use Twig\Error\SyntaxError; |
| 16 | +use Twig\Node\BlockReferenceNode; |
| 17 | +use Twig\Node\ConfigNode; |
| 18 | +use Twig\Node\ModuleNode; |
| 19 | +use Twig\Node\Node; |
| 20 | +use Twig\Node\NodeCaptureInterface; |
| 21 | +use Twig\Node\Nodes; |
| 22 | +use Twig\Node\TextNode; |
| 23 | + |
| 24 | +/** |
| 25 | + * @author Fabien Potencier <fabien@symfony.com> |
| 26 | + * |
| 27 | + * @internal |
| 28 | + */ |
| 29 | +final class CorrectnessNodeVisitor implements NodeVisitorInterface |
| 30 | +{ |
| 31 | + private ?\WeakMap $rootNodes = null; |
| 32 | + // in a tag node that does not support "block" nodes (all of them except "block") |
| 33 | + private ?Node $currentTagNode = null; |
| 34 | + private bool $hasParent = false; |
| 35 | + private ?\WeakMap $blockNodes = null; |
| 36 | + private int $currentBlockNodeLevel = 0; |
| 37 | + |
| 38 | + public function enterNode(Node $node, Environment $env): Node |
| 39 | + { |
| 40 | + if ($node instanceof ModuleNode) { |
| 41 | + $this->rootNodes = new \WeakMap(); |
| 42 | + $this->hasParent = $node->hasNode('parent'); |
| 43 | + |
| 44 | + // allows to identify when we enter/leave the block nodes |
| 45 | + $this->blockNodes = new \WeakMap(); |
| 46 | + foreach ($node->getNode('blocks') as $n) { |
| 47 | + $this->blockNodes[$n] = true; |
| 48 | + } |
| 49 | + |
| 50 | + $body = $node->getNode('body')->getNode('0'); |
| 51 | + // see Parser::subparse() which does not wrap the parsed Nodes if there is only one node |
| 52 | + foreach (\count($body) ? $body : new Nodes([$body]) as $k => $n) { |
| 53 | + // check that this root node of a child template only contains empty output nodes |
| 54 | + if ($this->hasParent && !$this->isEmptyOutputNode($n)) { |
| 55 | + throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $n->getTemplateLine(), $n->getSourceContext()); |
| 56 | + } |
| 57 | + $this->rootNodes[$n] = true; |
| 58 | + } |
| 59 | + |
| 60 | + return $node; |
| 61 | + } |
| 62 | + |
| 63 | + if (isset($this->blockNodes[$node])) { |
| 64 | + ++$this->currentBlockNodeLevel; |
| 65 | + } |
| 66 | + |
| 67 | + if ($this->hasParent && $node->getNodeTag() && !$node instanceof BlockReferenceNode) { |
| 68 | + $this->currentTagNode = $node; |
| 69 | + } |
| 70 | + |
| 71 | + if ($node instanceof ConfigNode && !isset($this->rootNodes[$node])) { |
| 72 | + throw new SyntaxError(\sprintf('The "%s" tag must always be at the root of the body of a template.', $node->getNodeTag()), $node->getTemplateLine(), $node->getSourceContext()); |
| 73 | + } |
| 74 | + |
| 75 | + if ($this->currentTagNode && $node instanceof BlockReferenceNode) { |
| 76 | + if ($this->currentTagNode instanceof NodeCaptureInterface || \count($this->blockNodes) > 1) { |
| 77 | + trigger_deprecation('twig/twig', '3.14', \sprintf('Having a "block" tag under a "%s" tag (line %d) is deprecated in %s at line %d.', $this->currentTagNode->getNodeTag(), $this->currentTagNode->getTemplateLine(), $node->getSourceContext()->getName(), $node->getTemplateLine())); |
| 78 | + } else { |
| 79 | + throw new SyntaxError(\sprintf('A "block" tag cannot be under a "%s" tag (line %d).', $this->currentTagNode->getNodeTag(), $this->currentTagNode->getTemplateLine()), $node->getTemplateLine(), $node->getSourceContext()); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + return $node; |
| 84 | + } |
| 85 | + |
| 86 | + public function leaveNode(Node $node, Environment $env): Node |
| 87 | + { |
| 88 | + if ($node instanceof ModuleNode) { |
| 89 | + $this->rootNodes = null; |
| 90 | + $this->hasParent = false; |
| 91 | + $this->blockNodes = null; |
| 92 | + $this->currentBlockNodeLevel = 0; |
| 93 | + } |
| 94 | + if ($this->hasParent && $node->getNodeTag() && !$node instanceof BlockReferenceNode) { |
| 95 | + $this->currentTagNode = null; |
| 96 | + } |
| 97 | + if ($this->hasParent && isset($this->blockNodes[$node])) { |
| 98 | + --$this->currentBlockNodeLevel; |
| 99 | + } |
| 100 | + |
| 101 | + return $node; |
| 102 | + } |
| 103 | + |
| 104 | + public function getPriority(): int |
| 105 | + { |
| 106 | + return -255; |
| 107 | + } |
| 108 | + |
| 109 | + /** |
| 110 | + * Returns true if the node never outputs anything or if the output is empty. |
| 111 | + */ |
| 112 | + private function isEmptyOutputNode(Node $node): bool |
| 113 | + { |
| 114 | + if ($node instanceof NodeCaptureInterface) { |
| 115 | + // a "block" tag in such a node will serve as a block definition AND be displayed in place as well |
| 116 | + return true; |
| 117 | + } |
| 118 | + |
| 119 | + // Can the text be considered "empty" (only whitespace)? |
| 120 | + if ($node instanceof TextNode) { |
| 121 | + return $node->isBlank(); |
| 122 | + } |
| 123 | + |
| 124 | + foreach ($node as $n) { |
| 125 | + if (!$this->isEmptyOutputNode($n)) { |
| 126 | + return false; |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + return true; |
| 131 | + } |
| 132 | +} |
0 commit comments