TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Event\EvaluateModifierFunctionEvent;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\ReferenceChildNode;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierCopyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierReferenceLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierUnsetLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\ConstantAwareTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\Token;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Common methods of both AST builders.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
abstract class AbstractAstBuilder
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $flatConstants = [];
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
protected function handleIdentifierUnsetLine(IdentifierUnsetLine $line, CurrentObjectPath $currentObjectPath): void
|
||||
{
|
||||
$node = $currentObjectPath->getFirst();
|
||||
$identifierStream = $line->getIdentifierTokenStream()->reset();
|
||||
while ($identifierToken = $identifierStream->getNext()) {
|
||||
if (!$foundNode = $node->getChildByName($identifierToken->getValue())) {
|
||||
break;
|
||||
}
|
||||
$nextIdentifierToken = $identifierStream->peekNext();
|
||||
if ($nextIdentifierToken) {
|
||||
$node = $foundNode;
|
||||
continue;
|
||||
}
|
||||
$node->removeChildByName($identifierToken->getValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleIdentifierCopyLine(IdentifierCopyLine $line, RootNode $rootNode, CurrentObjectPath $currentObjectPath): ?NodeInterface
|
||||
{
|
||||
$sourceIdentifierStream = $line->getValueTokenStream()->reset();
|
||||
$sourceNode = $rootNode;
|
||||
if ($sourceIdentifierStream->isRelative()) {
|
||||
// Entry node is current node from current object path if relative, otherwise RootNode.
|
||||
$sourceNode = $currentObjectPath->getLast();
|
||||
}
|
||||
while ($identifierToken = $sourceIdentifierStream->getNext()) {
|
||||
// Go through source token stream and locate the sourceNode to copy from.
|
||||
if (!$sourceNode = $sourceNode->getChildByName($identifierToken->getValue())) {
|
||||
// Source node not found - nothing to do for this line
|
||||
return null;
|
||||
}
|
||||
}
|
||||
$isSourceNodeValueNull = true;
|
||||
if ($sourceNode->getValue() !== null) {
|
||||
// When the source node value is not null, it will override the target node value if that exists.
|
||||
$isSourceNodeValueNull = false;
|
||||
}
|
||||
|
||||
// Locate/create the targets parent node the copied source should be added as child to,
|
||||
// and get the name of the node we're dealing with.
|
||||
$targetIdentifierTokenStream = $line->getIdentifierTokenStream()->reset();
|
||||
$targetParentNode = $currentObjectPath->getFirst();
|
||||
$targetTokenName = null;
|
||||
while ($targetToken = $targetIdentifierTokenStream->getNext()) {
|
||||
$targetTokenName = $targetToken->getValue();
|
||||
if (!($targetIdentifierTokenStream->peekNext() ?? false)) {
|
||||
break;
|
||||
}
|
||||
if (!$foundNode = $targetParentNode->getChildByName($targetTokenName)) {
|
||||
// Add new node as new child of current last element in path
|
||||
$foundNode = new ChildNode($targetTokenName);
|
||||
$targetParentNode->addChild($foundNode);
|
||||
}
|
||||
$targetParentNode = $foundNode;
|
||||
}
|
||||
|
||||
$existingTarget = null;
|
||||
if ($isSourceNodeValueNull) {
|
||||
// When the node to copy has no value, but the existing target has,
|
||||
// the value from the existing target is kept. Also, if the existing
|
||||
// node is a ReferenceChildNode and the source does not override this,
|
||||
// source children are added to the existing reference instead of
|
||||
// dropping the existing target.
|
||||
$existingTarget = $targetParentNode->getChildByName($targetTokenName);
|
||||
$existingTargetNodeValue = $existingTarget?->getValue();
|
||||
} else {
|
||||
// Blindly remove existing target node if exists and the value is not overridden by source.
|
||||
$targetParentNode->removeChildByName($targetTokenName);
|
||||
}
|
||||
if ($existingTarget instanceof ReferenceChildNode) {
|
||||
// When existing target is a ReferenceChildNode, keep it and
|
||||
// copy children from source into existing target.
|
||||
$targetNode = $existingTarget;
|
||||
foreach ($sourceNode->getNextChild() as $sourceChild) {
|
||||
$targetNode->addChild(clone $sourceChild);
|
||||
}
|
||||
} else {
|
||||
// Clone full source node tree, update name and add as child to parent node.
|
||||
/** @var ChildNodeInterface $targetNode */
|
||||
$targetNode = clone $sourceNode;
|
||||
$targetNode->updateName($targetTokenName);
|
||||
$targetParentNode->addChild($targetNode);
|
||||
}
|
||||
if ($isSourceNodeValueNull && $existingTargetNodeValue) {
|
||||
// If value of old existing target should be kept, set in now.
|
||||
$targetNode->setValue($existingTargetNodeValue);
|
||||
}
|
||||
|
||||
return $targetNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* "foo =< bar": Prepare a reference resolving.
|
||||
* Note this does *not* resolve "=<" itself at this point since this operator can only be
|
||||
* evaluated after the full AST has been established. Also, having a full AST-traverser run
|
||||
* that does this is *very* expensive and "=<" is only done for "tt_content.myElement" and
|
||||
* "lib.parseFunc" anyways. As such, "=<" is NOT a language construct itself and the AST-parser
|
||||
* only marks nodes that use it by using the special node "ObjectReference".
|
||||
* Resolving then happens "lazy" and "on demand" in ContentObjectRenderer cObjGetSingle()
|
||||
* and mergeTSRef() for frontend "setup" TypoScript.
|
||||
*/
|
||||
protected function handleIdentifierReferenceLine(IdentifierReferenceLine $line, CurrentObjectPath $currentObjectPath): NodeInterface
|
||||
{
|
||||
$tokenStream = $line->getIdentifierTokenStream();
|
||||
$node = $currentObjectPath->getFirst();
|
||||
$identifierStream = $tokenStream->reset();
|
||||
while ($identifierToken = $identifierStream->getNext()) {
|
||||
$nextIdentifier = $identifierStream->peekNext();
|
||||
$identifierTokenValue = $identifierToken->getValue();
|
||||
if (!($node->getChildByName($identifierTokenValue)) && $nextIdentifier) {
|
||||
// Add new node as new child of current last element in path
|
||||
$foundNode = new ChildNode($identifierTokenValue);
|
||||
$node->addChild($foundNode);
|
||||
} elseif (!$node->getChildByName($identifierTokenValue) && $nextIdentifier === null) {
|
||||
// Parent of target node exists, but target node does not. Add new reference child.
|
||||
$foundNode = new ReferenceChildNode($identifierTokenValue);
|
||||
$foundNode->setReferenceSourceStream($line->getValueTokenStream());
|
||||
$node->addChild($foundNode);
|
||||
} elseif (($foundNode = $node->getChildByName($identifierTokenValue)) && $nextIdentifier === null) {
|
||||
// Target node exists already. We create a new one, remove old, but transfer existing children from old to new.
|
||||
$newNode = new ReferenceChildNode($identifierTokenValue);
|
||||
$newNode->setReferenceSourceStream($line->getValueTokenStream());
|
||||
foreach ($foundNode->getNextChild() as $existingNodeChild) {
|
||||
$newNode->addChild($existingNodeChild);
|
||||
}
|
||||
$node->removeChildByName($identifierTokenValue);
|
||||
$node->addChild($newNode);
|
||||
$foundNode = $newNode;
|
||||
}
|
||||
$node = $foundNode;
|
||||
}
|
||||
return $node;
|
||||
}
|
||||
|
||||
protected function getOrAddNodeFromIdentifierStream(CurrentObjectPath $currentObjectPath, IdentifierTokenStream $tokenStream): NodeInterface
|
||||
{
|
||||
$node = $currentObjectPath->getFirst();
|
||||
$identifierStream = $tokenStream->reset();
|
||||
while ($identifierToken = $identifierStream->getNext()) {
|
||||
$identifierTokenValue = $identifierToken->getValue();
|
||||
if (!$foundNode = $node->getChildByName($identifierTokenValue)) {
|
||||
// Add new node as new child of current last element in path
|
||||
$foundNode = new ChildNode($identifierTokenValue);
|
||||
$node->addChild($foundNode);
|
||||
}
|
||||
$node = $foundNode;
|
||||
}
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate operator functions, example TypoScript:
|
||||
* "page.10.value := appendString(foo)"
|
||||
*/
|
||||
protected function evaluateValueModifier(Token $functionNameToken, TokenStreamInterface $functionArgumentTokenStream, ?string $originalValue): ?string
|
||||
{
|
||||
$functionName = $functionNameToken->getValue();
|
||||
// Constants are evaluated via __toString() of ConstantAwareTokenStream and thus need current constants.
|
||||
// This implements constants in function arguments: "foo := addToList({$my.constant})"
|
||||
if ($functionArgumentTokenStream instanceof ConstantAwareTokenStream) {
|
||||
$functionArgumentTokenStream->setFlatConstants($this->flatConstants);
|
||||
}
|
||||
$functionArgument = (string)$functionArgumentTokenStream;
|
||||
switch ($functionName) {
|
||||
case 'prependString':
|
||||
return $functionArgument . $originalValue;
|
||||
case 'appendString':
|
||||
return $originalValue . $functionArgument;
|
||||
case 'removeString':
|
||||
return str_replace($functionArgument, '', $originalValue);
|
||||
case 'replaceString':
|
||||
$functionValueArray = explode('|', $functionArgument, 2);
|
||||
$fromStr = $functionValueArray[0];
|
||||
$toStr = $functionValueArray[1] ?? '';
|
||||
return str_replace($fromStr, $toStr, $originalValue);
|
||||
case 'addToList':
|
||||
return ($originalValue !== null ? $originalValue . ',' : '') . $functionArgument;
|
||||
case 'removeFromList':
|
||||
$existingElements = GeneralUtility::trimExplode(',', $originalValue ?? '');
|
||||
$removeElements = GeneralUtility::trimExplode(',', $functionArgument);
|
||||
if (!empty($removeElements)) {
|
||||
return implode(',', array_diff($existingElements, $removeElements));
|
||||
}
|
||||
return $originalValue;
|
||||
case 'uniqueList':
|
||||
$elements = GeneralUtility::trimExplode(',', $originalValue ?? '');
|
||||
return implode(',', array_unique($elements));
|
||||
case 'reverseList':
|
||||
$elements = GeneralUtility::trimExplode(',', $originalValue ?? '');
|
||||
return implode(',', array_reverse($elements));
|
||||
case 'sortList':
|
||||
$elements = GeneralUtility::trimExplode(',', $originalValue ?? '');
|
||||
$arguments = GeneralUtility::trimExplode(',', $functionArgument);
|
||||
$arguments = array_map('strtolower', $arguments);
|
||||
$sortFlags = SORT_REGULAR;
|
||||
if (in_array('numeric', $arguments)) {
|
||||
$sortFlags = SORT_NUMERIC;
|
||||
// If the sorting modifier "numeric" is given, all values
|
||||
// are checked and an exception is thrown if a non-numeric value is given
|
||||
// otherwise there is a different behaviour between PHP 7 and PHP 5.x
|
||||
// See also the warning on http://us.php.net/manual/en/function.sort.php
|
||||
foreach ($elements as $element) {
|
||||
if (!is_numeric($element)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'The list "' . $originalValue . '" should be sorted numerically but contains a non-numeric value',
|
||||
1650893781
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
sort($elements, $sortFlags);
|
||||
if (in_array('descending', $arguments)) {
|
||||
$elements = array_reverse($elements);
|
||||
}
|
||||
return implode(',', $elements);
|
||||
case 'getEnv':
|
||||
$environmentValue = getenv(trim($functionArgument));
|
||||
if ($environmentValue !== false) {
|
||||
return $environmentValue;
|
||||
}
|
||||
return $originalValue;
|
||||
default:
|
||||
return $this->eventDispatcher->dispatch(new EvaluateModifierFunctionEvent($functionName, $functionArgument, $originalValue))->getValue() ?? $originalValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPathStack;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\BlockCloseLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierAssignmentLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierBlockOpenLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierCopyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierFunctionLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierReferenceLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierUnsetLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\ConstantAwareTokenStream;
|
||||
|
||||
/**
|
||||
* The main TypoScript AST builder.
|
||||
*
|
||||
* This creates a tree of Nodes, starting with the root node. Each node can have
|
||||
* children. The implementation basically iterates a LineStream created by the
|
||||
* tokenizers, and creates AST depending on the line type. It handles all the
|
||||
* different operator lines like "=", "<" and so on.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
#[Autoconfigure(public: true), AsAlias(AstBuilderInterface::class)]
|
||||
final class AstBuilder extends AbstractAstBuilder implements AstBuilderInterface
|
||||
{
|
||||
public function __construct(
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
) {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $flatConstants
|
||||
*/
|
||||
public function build(LineStream $lineStream, RootNode $ast, array $flatConstants = []): RootNode
|
||||
{
|
||||
$this->flatConstants = $flatConstants;
|
||||
|
||||
$currentObjectPath = new CurrentObjectPath($ast);
|
||||
$currentObjectPathStack = new CurrentObjectPathStack();
|
||||
$currentObjectPathStack->push($currentObjectPath);
|
||||
|
||||
foreach ($lineStream->getNextLine() as $line) {
|
||||
if ($line instanceof IdentifierAssignmentLine) {
|
||||
// "foo = bar" and "foo ( bar )": Single and multi line assignments
|
||||
$this->handleIdentifierAssignmentLine($line, $currentObjectPath);
|
||||
} elseif ($line instanceof IdentifierBlockOpenLine) {
|
||||
// "foo {": Opening a block - push to object path stack
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$currentObjectPath = (new CurrentObjectPath($node));
|
||||
$currentObjectPathStack->push($currentObjectPath);
|
||||
} elseif ($line instanceof BlockCloseLine) {
|
||||
// "}": Closing a block - pop from object path stack
|
||||
$currentObjectPath = $currentObjectPathStack->pop();
|
||||
} elseif ($line instanceof IdentifierUnsetLine) {
|
||||
// "foo >": Remove a path
|
||||
$this->handleIdentifierUnsetLine($line, $currentObjectPath);
|
||||
} elseif ($line instanceof IdentifierCopyLine) {
|
||||
// "foo < bar": Copy a node source path to a target path
|
||||
$this->handleIdentifierCopyLine($line, $ast, $currentObjectPath);
|
||||
} elseif ($line instanceof IdentifierFunctionLine) {
|
||||
// "foo := addToList(42)": Evaluate functions
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$node->setValue($this->evaluateValueModifier($line->getFunctionNameToken(), $line->getFunctionValueTokenStream(), $node->getValue()));
|
||||
} elseif ($line instanceof IdentifierReferenceLine) {
|
||||
// "foo =< bar": Prepare a reference resolving
|
||||
$this->handleIdentifierReferenceLine($line, $currentObjectPath);
|
||||
}
|
||||
}
|
||||
|
||||
return $ast;
|
||||
}
|
||||
|
||||
private function handleIdentifierAssignmentLine(IdentifierAssignmentLine $line, CurrentObjectPath $currentObjectPath): void
|
||||
{
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$valueTokenStream = $line->getValueTokenStream();
|
||||
if ($valueTokenStream instanceof ConstantAwareTokenStream) {
|
||||
$valueTokenStream = clone $valueTokenStream;
|
||||
$valueTokenStream->setFlatConstants($this->flatConstants);
|
||||
$node->setValue((string)$valueTokenStream);
|
||||
return;
|
||||
}
|
||||
$node->setValue((string)$valueTokenStream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
|
||||
|
||||
/**
|
||||
* The main TypoScript AST builder.
|
||||
*
|
||||
* This creates a tree of Nodes, starting with the root node. Each node can have
|
||||
* children. The implementation basically iterates a LineStream created by the
|
||||
* tokenizers, and creates AST depending on the line type. It handles all the
|
||||
* different operator lines like "=", "<" and so on.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
interface AstBuilderInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $flatConstants
|
||||
*/
|
||||
public function build(LineStream $lineStream, RootNode $ast, array $flatConstants = []): RootNode;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPathStack;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\BlockCloseLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\CommentLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\EmptyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierAssignmentLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierBlockOpenLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierCopyLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierFunctionLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierReferenceLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\IdentifierUnsetLine;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Line\LineStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\ConstantAwareTokenStream;
|
||||
|
||||
/**
|
||||
* Secondary TypoScript AST builder.
|
||||
*
|
||||
* This creates a tree of Nodes, starting with the root node. Each node can have
|
||||
* children. The implementation basically iterates a LineStream created by the
|
||||
* tokenizers, and creates AST depending on the line type. It handles all the
|
||||
* different operator lines like "=", "<" and so on.
|
||||
*
|
||||
* This AST builder is comment aware: Comments are assigned to nodes. This is used
|
||||
* in ext:tstemplate and page TSconfig backend modules to add the comment related
|
||||
* TypoScript functionality.
|
||||
*
|
||||
* This AST builder variant adds runtime overhead and is slower than the main
|
||||
* AstBuilder class.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class CommentAwareAstBuilder extends AbstractAstBuilder implements AstBuilderInterface
|
||||
{
|
||||
public function __construct(
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
) {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $flatConstants
|
||||
*/
|
||||
public function build(LineStream $lineStream, RootNode $ast, array $flatConstants = []): RootNode
|
||||
{
|
||||
$this->flatConstants = $flatConstants;
|
||||
|
||||
$currentObjectPath = new CurrentObjectPath($ast);
|
||||
$currentObjectPathStack = new CurrentObjectPathStack();
|
||||
$currentObjectPathStack->push($currentObjectPath);
|
||||
|
||||
$previousLineComments = [];
|
||||
while ($line = $lineStream->getNext()) {
|
||||
$node = null;
|
||||
if ($line instanceof IdentifierAssignmentLine) {
|
||||
// "foo = bar" and "foo ( bar )": Single and multi line assignments
|
||||
$node = $this->handleIdentifierAssignmentLine($line, $currentObjectPath);
|
||||
if ($previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
} elseif ($line instanceof IdentifierBlockOpenLine) {
|
||||
// "foo {": Opening a block - push to object path stack
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
if ($previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
$currentObjectPath = (new CurrentObjectPath($node));
|
||||
$currentObjectPathStack->push($currentObjectPath);
|
||||
} elseif ($line instanceof BlockCloseLine) {
|
||||
// "}": Closing a block - pop from object path stack
|
||||
$currentObjectPath = $currentObjectPathStack->pop();
|
||||
} elseif ($line instanceof IdentifierUnsetLine) {
|
||||
// "foo >": Remove a path
|
||||
$this->handleIdentifierUnsetLine($line, $currentObjectPath);
|
||||
} elseif ($line instanceof IdentifierCopyLine) {
|
||||
// "foo < bar": Copy a node source path to a target path
|
||||
$node = $this->handleIdentifierCopyLine($line, $ast, $currentObjectPath);
|
||||
if ($node && $previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
} elseif ($line instanceof IdentifierFunctionLine) {
|
||||
// "foo := addToList(42)": Evaluate functions
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$functionValueTokenStream = $line->getFunctionValueTokenStream();
|
||||
$node->setValue($this->evaluateValueModifier($line->getFunctionNameToken(), clone $functionValueTokenStream, $node->getValue()));
|
||||
if ($functionValueTokenStream instanceof ConstantAwareTokenStream) {
|
||||
// @todo: This is a bit unfortunate. When multiple functions manipulate a value after each other,
|
||||
// only the stream of the last one is preserved, previous ones are lost. This way, the BE modules
|
||||
// can not reflect when previous functions used constants. One idea to solve this is to turn existing
|
||||
// nodes into special "function" nodes that park single operations, which are then executed lazy when
|
||||
// a node value is string'ified. The BE modules could then render full lists of value manipulations and
|
||||
// show how values evolve. Another idea is to change setOriginalValueTokenStream() to gather multiple
|
||||
// streams - probably together with the current value at this point in time, which would also allow
|
||||
// rendering how a value evolves over time as well.
|
||||
$node->setOriginalValueTokenStream($functionValueTokenStream);
|
||||
}
|
||||
if ($previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
} elseif ($line instanceof IdentifierReferenceLine) {
|
||||
// "foo =< bar": Prepare a reference resolving
|
||||
$node = $this->handleIdentifierReferenceLine($line, $currentObjectPath);
|
||||
if ($previousLineComments) {
|
||||
foreach ($previousLineComments as $previousLineComment) {
|
||||
$node->addComment($previousLineComment);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
} elseif ($line instanceof CommentLine) {
|
||||
$nextLine = $lineStream->peekNext();
|
||||
if ($currentObjectPath->getLast() instanceof RootNode && ($nextLine === null || $nextLine instanceof EmptyLine)) {
|
||||
$previousLineComments[] = $line->getTokenStream();
|
||||
foreach ($previousLineComments as $commentLineTokenStream) {
|
||||
$ast->addComment($commentLineTokenStream);
|
||||
}
|
||||
$previousLineComments = [];
|
||||
}
|
||||
if ($nextLine instanceof CommentLine) {
|
||||
$previousLineComments[] = $line->getTokenStream();
|
||||
}
|
||||
if ($nextLine instanceof IdentifierAssignmentLine
|
||||
|| $nextLine instanceof IdentifierBlockOpenLine
|
||||
|| $nextLine instanceof IdentifierCopyLine
|
||||
|| $nextLine instanceof IdentifierFunctionLine
|
||||
|| $nextLine instanceof IdentifierReferenceLine
|
||||
) {
|
||||
$previousLineComments[] = $line->getTokenStream();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slightly different from AstBuilder since it sets 'previousValue'
|
||||
*/
|
||||
private function handleIdentifierAssignmentLine(IdentifierAssignmentLine $line, CurrentObjectPath $currentObjectPath): NodeInterface
|
||||
{
|
||||
$node = $this->getOrAddNodeFromIdentifierStream($currentObjectPath, $line->getIdentifierTokenStream());
|
||||
$valueTokenStream = $line->getValueTokenStream();
|
||||
if ($valueTokenStream instanceof ConstantAwareTokenStream) {
|
||||
$node->setOriginalValueTokenStream($valueTokenStream);
|
||||
$valueTokenStream = clone $valueTokenStream;
|
||||
$valueTokenStream->setFlatConstants($this->flatConstants);
|
||||
$node->setPreviousValue($node->getValue());
|
||||
$node->setValue((string)$valueTokenStream);
|
||||
return $node;
|
||||
}
|
||||
$node->setPreviousValue($node->getValue());
|
||||
$node->setValue((string)$valueTokenStream);
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* Internal state class to track the current hierarchy in tree.
|
||||
* This is important in combination with block open "{" and block
|
||||
* close "}" brackets.
|
||||
* Also used in BE Template Object Browser tree rendering.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class CurrentObjectPath
|
||||
{
|
||||
/**
|
||||
* @var NodeInterface[]
|
||||
*/
|
||||
private array $path;
|
||||
|
||||
public function __construct(NodeInterface ...$path)
|
||||
{
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
public function append(NodeInterface $node): void
|
||||
{
|
||||
$this->path[] = $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NodeInterface[]
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn current object path into a string. Quote dots in keys.
|
||||
* Used in BE Template Object Browser tree, expand and search handling.
|
||||
* Not implementing __toString() here since Fluid can't call this.
|
||||
*
|
||||
* Example:
|
||||
* page.10.foo\.bar.baz
|
||||
*/
|
||||
public function getPathAsString(): string
|
||||
{
|
||||
$flatArray = [];
|
||||
foreach ($this->getAll() as $pathNode) {
|
||||
if ($pathNode instanceof RootNode) {
|
||||
continue;
|
||||
}
|
||||
$name = $pathNode->getName();
|
||||
if ($name === '') {
|
||||
throw new \RuntimeException('Node names must not be empty string', 1658578645);
|
||||
}
|
||||
$flatArray[] = addcslashes($name, '.');
|
||||
}
|
||||
return implode('.', $flatArray);
|
||||
}
|
||||
|
||||
public function getFirst(): NodeInterface
|
||||
{
|
||||
return reset($this->path);
|
||||
}
|
||||
|
||||
public function getLast(): NodeInterface
|
||||
{
|
||||
return array_last($this->path);
|
||||
}
|
||||
|
||||
public function getSecondLast(): NodeInterface
|
||||
{
|
||||
return array_slice($this->path, -2, 1)[0];
|
||||
}
|
||||
|
||||
public function removeLast(): void
|
||||
{
|
||||
array_pop($this->path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath;
|
||||
|
||||
/**
|
||||
* A stack for CurrentObjectPath: When opening a block "{",
|
||||
* CurrentObjectPath is pushed, when closing a block "}", it
|
||||
* is popped from this stack.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class CurrentObjectPathStack
|
||||
{
|
||||
/**
|
||||
* @var CurrentObjectPath[]
|
||||
*/
|
||||
private array $stack = [];
|
||||
private int $stackSize = 0;
|
||||
|
||||
public function push(CurrentObjectPath $path): void
|
||||
{
|
||||
$this->stack[] = $path;
|
||||
$this->stackSize++;
|
||||
}
|
||||
|
||||
public function pop(): CurrentObjectPath
|
||||
{
|
||||
if ($this->stackSize === 1) {
|
||||
// Never pop the very last element off from the stack. This is the
|
||||
// RootNode. This prevents errors when TypoScript has a closing
|
||||
// curly bracket '}' too much.
|
||||
return $this->getCurrent();
|
||||
}
|
||||
array_pop($this->stack);
|
||||
$this->stackSize--;
|
||||
return $this->getCurrent();
|
||||
}
|
||||
|
||||
public function getCurrent(): CurrentObjectPath
|
||||
{
|
||||
return array_last($this->stack);
|
||||
}
|
||||
|
||||
public function getFirst(): CurrentObjectPath
|
||||
{
|
||||
return reset($this->stack);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this event are able to implement own ":=" TypoScript modifier functions, example:
|
||||
*
|
||||
* foo = myOriginalValue
|
||||
* foo := myNewFunction(myFunctionArgument)
|
||||
*
|
||||
* Listeners should take care function names can not overlap with function names
|
||||
* from other extensions and should thus namespace, example naming: "extNewsSortFunction()"
|
||||
*/
|
||||
final class EvaluateModifierFunctionEvent
|
||||
{
|
||||
private ?string $value = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $functionName,
|
||||
private readonly string $functionArgument,
|
||||
private readonly ?string $originalValue,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The function name, for example "extNewsSortFunction" when using "foo := extNewsSortFunction()"
|
||||
*/
|
||||
public function getFunctionName(): string
|
||||
{
|
||||
return $this->functionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional function argument, for example "myArgument" when using "foo := extNewsSortFunction(myArgument)"
|
||||
* If the argument contained constants, those have been resolved at this point.
|
||||
*/
|
||||
public function getFunctionArgument(): string
|
||||
{
|
||||
return $this->functionArgument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Original / current value, for example "fooValue" when using:
|
||||
* foo = fooValue
|
||||
* foo := extNewsSortFunction(myArgument)
|
||||
*/
|
||||
public function getOriginalValue(): ?string
|
||||
{
|
||||
return $this->originalValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the updated value calculated by a listener.
|
||||
* Note you can not set to null to "unset", since getValue() falls back to
|
||||
* originalValue in this case. Set to empty string instead for this edge case.
|
||||
*/
|
||||
public function setValue(string $value): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by AstBuilder to fetch the updated value, falls back to given original value.
|
||||
* Can be used by Listeners to see if a previous listener changed the value already
|
||||
* by comparing with getOriginalValue().
|
||||
*/
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Merger;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* Frontend TypoScript 'setup' has the main 'config' section for global config,
|
||||
* plus a per type / typeNum specific PAGE 'config' (often page.config) that can
|
||||
* override global 'config' per type / typeNum.
|
||||
*
|
||||
* This class merges both into the final 'config', later available in Request
|
||||
* attribute 'frontend.typoscript' getConfigTree() and getConfigArray().
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final readonly class SetupConfigMerger
|
||||
{
|
||||
public function merge(?ChildNodeInterface $config, ?ChildNodeInterface $pageConfig): RootNode
|
||||
{
|
||||
$configResult = new RootNode();
|
||||
if ($config) {
|
||||
foreach ($config->getNextChild() as $child) {
|
||||
$configResult->addChild($child);
|
||||
}
|
||||
}
|
||||
if (!$pageConfig) {
|
||||
return $configResult;
|
||||
}
|
||||
$this->mergeRecursive($pageConfig, $configResult);
|
||||
return $configResult;
|
||||
}
|
||||
|
||||
private function mergeRecursive(ChildNodeInterface $mergeFrom, NodeInterface $mergeTo): void
|
||||
{
|
||||
foreach ($mergeFrom->getNextChild() as $mergeFromChild) {
|
||||
$mergeToChild = $mergeTo->getChildByName($mergeFromChild->getName());
|
||||
if (!$mergeToChild) {
|
||||
$mergeTo->addChild($mergeFromChild);
|
||||
continue;
|
||||
}
|
||||
$mergeFromChildValue = $mergeFromChild->getValue();
|
||||
if ($mergeFromChildValue !== null && $mergeFromChildValue !== $mergeToChild->getValue()) {
|
||||
$mergeToChild->setValue($mergeFromChildValue);
|
||||
}
|
||||
$this->mergeRecursive($mergeFromChild, $mergeToChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Node;
|
||||
|
||||
/**
|
||||
* Generic child node. Implements common methods of NodeInterface used
|
||||
* in all Node classes.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
abstract class AbstractChildNode extends AbstractNode implements ChildNodeInterface
|
||||
{
|
||||
public function __construct(protected string $name) {}
|
||||
|
||||
/**
|
||||
* Dereference children on clone().
|
||||
* Used with '<' operator to create a deep-copy of the tree to copy.
|
||||
*/
|
||||
public function __clone(): void
|
||||
{
|
||||
foreach ($this->children as $childName => $child) {
|
||||
$this->children[$childName] = clone $child;
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function toArray(): ?array
|
||||
{
|
||||
if (!$this->hasChildren()) {
|
||||
return null;
|
||||
}
|
||||
$result = [];
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$childName = $child->getName();
|
||||
$childValue = $child->getValue();
|
||||
if ($child instanceof ReferenceChildNode) {
|
||||
// Hack for b/w compat parsing of `=<` operator. See ContentObjectRenderer cObjGetSingle() and mergeTSRef()
|
||||
// @todo: adding the whitespace after '<' is another bit of a hack here ... maybe solve in tokenizer?
|
||||
// compare this for what happens when doing 'foo = bar' in old parser: Is the whitespace kept for
|
||||
// value to not trigger the ref lookup to often if doing 'foo = <div...' ?
|
||||
// @todo: same situation in RootNode!
|
||||
$childValue = '< ' . $child->getReferenceSourceStream();
|
||||
}
|
||||
if ($childValue !== null) {
|
||||
$result[$child->getName()] = $childValue;
|
||||
}
|
||||
$grandChildren = $child->toArray();
|
||||
if ($grandChildren !== null) {
|
||||
$result[$childName . '.'] = $grandChildren;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function flatten(string $prefix = ''): array
|
||||
{
|
||||
$flatArray = [];
|
||||
$prefixedQuotedNodeName = $prefix . addcslashes($this->getName(), '.');
|
||||
if (!$this->isValueNull()) {
|
||||
$flatArray[$prefixedQuotedNodeName] = $this->getValue();
|
||||
}
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$flatArray = array_merge($flatArray, $child->flatten($prefixedQuotedNodeName . '.'));
|
||||
}
|
||||
return $flatArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Node;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
|
||||
/**
|
||||
* Generic node. Implements common methods of NodeInterface used
|
||||
* in all Node classes.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
abstract class AbstractNode implements NodeInterface
|
||||
{
|
||||
private ?string $identifier = null;
|
||||
protected string $name;
|
||||
private ?string $value = null;
|
||||
private ?string $previousValue = null;
|
||||
|
||||
/**
|
||||
* @var array<string, ChildNodeInterface>
|
||||
*/
|
||||
protected array $children = [];
|
||||
private ?TokenStreamInterface $originalValueTokenStream = null;
|
||||
private array $comments = [];
|
||||
|
||||
/**
|
||||
* When storing to cache, we only store FE relevant properties and skip
|
||||
* various BE related properties which then default to class defaults when
|
||||
* unserialized. This is done to create smaller php cache files.
|
||||
*/
|
||||
final public function __serialize(): array
|
||||
{
|
||||
return $this->serialize();
|
||||
}
|
||||
|
||||
protected function serialize(): array
|
||||
{
|
||||
$result = [
|
||||
'name' => $this->name,
|
||||
'children' => $this->children,
|
||||
];
|
||||
if ($this->value !== null) {
|
||||
$result['value'] = $this->value;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function setIdentifier(string $identifier): void
|
||||
{
|
||||
$this->identifier = hash('xxh3', $identifier);
|
||||
$childCounter = 0;
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$child->setIdentifier($this->identifier . $childCounter);
|
||||
$childCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This forces $this->name NOT to be readonly.
|
||||
* Used with '<' operator on tree root to copy:
|
||||
* foo = value
|
||||
* bar < foo
|
||||
* The 'foo' object node is copied, but added to AST as name 'bar'
|
||||
*/
|
||||
public function updateName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
if ($this->identifier === null) {
|
||||
throw new \RuntimeException(
|
||||
'Identifier has not been initialized. This happens when getIdentifier() is called on'
|
||||
. ' trees retrieved from cache. The identifier is not supposed to be used in this context.',
|
||||
1674620169
|
||||
);
|
||||
}
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
public function addChild(ChildNodeInterface $node): void
|
||||
{
|
||||
$this->children[$node->getName()] = $node;
|
||||
}
|
||||
|
||||
public function getChildByName(string $name): ?ChildNodeInterface
|
||||
{
|
||||
return $this->children[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note this does *not* choke if that child does not exist, so we can "blindly" remove without error.
|
||||
*/
|
||||
public function removeChildByName(string $name): void
|
||||
{
|
||||
unset($this->children[$name]);
|
||||
}
|
||||
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
return !empty($this->children);
|
||||
}
|
||||
|
||||
public function getNextChild(): iterable
|
||||
{
|
||||
foreach ($this->children as $child) {
|
||||
yield $child;
|
||||
}
|
||||
}
|
||||
|
||||
public function sortChildren(): void
|
||||
{
|
||||
ksort($this->children, SORT_FLAG_CASE | SORT_STRING);
|
||||
}
|
||||
|
||||
public function setValue(?string $value): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function appendValue(string $value): void
|
||||
{
|
||||
$this->value .= $value;
|
||||
}
|
||||
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function isValueNull(): bool
|
||||
{
|
||||
return $this->value === null;
|
||||
}
|
||||
|
||||
public function setPreviousValue(?string $value): void
|
||||
{
|
||||
$this->previousValue = $value;
|
||||
}
|
||||
|
||||
public function getPreviousValue(): ?string
|
||||
{
|
||||
return $this->previousValue;
|
||||
}
|
||||
|
||||
public function setOriginalValueTokenStream(?TokenStreamInterface $tokenStream): void
|
||||
{
|
||||
$this->originalValueTokenStream = $tokenStream;
|
||||
}
|
||||
|
||||
public function getOriginalValueTokenStream(): ?TokenStreamInterface
|
||||
{
|
||||
return $this->originalValueTokenStream;
|
||||
}
|
||||
|
||||
public function addComment(TokenStreamInterface $tokenStream): void
|
||||
{
|
||||
$this->comments[] = $tokenStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TokenStreamInterface[]
|
||||
*/
|
||||
public function getComments(): array
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Node;
|
||||
|
||||
/**
|
||||
* A generic child node that is not the node root.
|
||||
*/
|
||||
final class ChildNode extends AbstractChildNode {}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Node;
|
||||
|
||||
/**
|
||||
* The created AST consists of a NodeRoot object with nested NodeObject children.
|
||||
* This is the interface implemented by all children nodes (not RootNode).
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
interface ChildNodeInterface extends NodeInterface {}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Node;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
|
||||
/**
|
||||
* The created AST consists of a NodeRoot object with nested NodeObject children.
|
||||
* This is the main interface to any node type.
|
||||
*
|
||||
* Example TypoScript:
|
||||
* "foo = fooValue"
|
||||
* "foo.bar = barValue"
|
||||
* This creates a RootNode with one ChildNode name "foo" and value "fooValue",
|
||||
* that has a child ChildNode name "bar" and value "barValue".
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
interface NodeInterface
|
||||
{
|
||||
/**
|
||||
* An identifier for this node. Typically, a hash of some kind. This identifier
|
||||
* is unique within the tree, by being created from the parent identifier plus
|
||||
* the name. This identifier is used in the backend, when referencing single nodes.
|
||||
* Calculating identifiers is initiated by calling setIdentifier() on RootNode, which
|
||||
* will recurse the tree. Call this on the final tree, after AST calculation finished,
|
||||
* so AST building itself does not need to fiddle with identifier updates when for
|
||||
* instance tree parts are cloned using '<' operator.
|
||||
* Note this value is skipped when persisting to caches since it's a Backend related
|
||||
* thing that does not use cached context: When retrieving nodes from cache (e.g. in Frontend),
|
||||
* the identifier is null and calling the getter will throw an exception.
|
||||
*/
|
||||
public function setIdentifier(string $identifier): void;
|
||||
public function getIdentifier(): string;
|
||||
|
||||
/**
|
||||
* Helper methods for node name.
|
||||
*/
|
||||
public function getName(): ?string;
|
||||
public function updateName(string $name): void;
|
||||
|
||||
/**
|
||||
* Helper methods to operate on children.
|
||||
*/
|
||||
public function addChild(ChildNodeInterface $node): void;
|
||||
public function getChildByName(string $name): ?ChildNodeInterface;
|
||||
public function removeChildByName(string $name): void;
|
||||
public function hasChildren(): bool;
|
||||
/**
|
||||
* @return iterable<ChildNodeInterface>
|
||||
*/
|
||||
public function getNextChild(): iterable;
|
||||
public function sortChildren(): void;
|
||||
|
||||
/**
|
||||
* Helper methods for value.
|
||||
*/
|
||||
public function setValue(?string $value): void;
|
||||
public function appendValue(string $value): void;
|
||||
public function getValue(): ?string;
|
||||
public function isValueNull(): bool;
|
||||
|
||||
/**
|
||||
* Previous value is only set by comment aware ast builder. It is used in
|
||||
* constant editor to see if a value has been changed.
|
||||
*/
|
||||
public function setPreviousValue(?string $value): void;
|
||||
public function getPreviousValue(): ?string;
|
||||
|
||||
/**
|
||||
* Helper method for backend object browser to retrieve the original
|
||||
* stream when a constant substitution happened, only set by CommentAwareAstBuilder.
|
||||
*/
|
||||
public function setOriginalValueTokenStream(?TokenStreamInterface $tokenStream): void;
|
||||
public function getOriginalValueTokenStream(): ?TokenStreamInterface;
|
||||
|
||||
/**
|
||||
* Helper methods to attach TypoScript tokens to a node.
|
||||
* This is used in ext:tstemplate "Constant Editor" and "Object Browser" and handled
|
||||
* by CommentAwareAstBuilder.
|
||||
*/
|
||||
public function addComment(TokenStreamInterface $tokenStream): void;
|
||||
/**
|
||||
* @return TokenStreamInterface[]
|
||||
*/
|
||||
public function getComments(): array;
|
||||
|
||||
/**
|
||||
* b/w compat method to turn AST into an array.
|
||||
* Note we're NOT using magic __toArray() here to avoid calling array-cast of AST by
|
||||
* accident: toArray() should be called explicitly if needed, which makes it much easier
|
||||
* to drop this b/w compat method when we later want to drop that layer.
|
||||
*
|
||||
* Note RootNode *always* returns an array, while ObjectNode's may return null.
|
||||
*/
|
||||
public function toArray(): ?array;
|
||||
|
||||
/**
|
||||
* Flatten the tree. A RootNode with a ChildNode "foo" and value "fooValue", with this
|
||||
* ChildNode again having a ChildNode "bar" and value "barValue" becomes:
|
||||
* [
|
||||
* 'foo' => 'fooValue',
|
||||
* 'foo.bar' => 'barValue',
|
||||
* ]
|
||||
*
|
||||
* Flattening a TypoScript tree is especially used for constants to quickly look
|
||||
* up constants when parsing setup node value streams that use T_CONSTANT tokens.
|
||||
*/
|
||||
public function flatten(string $prefix = ''): array;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Node;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\IdentifierTokenStream;
|
||||
|
||||
/**
|
||||
* A node object created for LineIdentifierReference lines which use the T_OPERATOR_REFERENCE
|
||||
* operator and have a TokenStreamIdentifier stream for "the right side" of the expression.
|
||||
*
|
||||
* The reference operator is nasty, since it's no "true" reference / pointer:
|
||||
* foo.bar = barValue1
|
||||
* baz =< foo
|
||||
* baz.bar = barValue2
|
||||
* This ends up with "barValue1" for "foo.bar", and "barValue2" for "baz.bar". "barValue1"
|
||||
* for "foo.bar" is kept!
|
||||
*
|
||||
* Note the reference operator *only* works for TS "setup" code, not for "constants", and it
|
||||
* is only resolved in these cases. See ContentObjectRenderer->cObjGetSingle() for details.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class ReferenceChildNode extends AbstractChildNode
|
||||
{
|
||||
private ?IdentifierTokenStream $referenceSourceStream;
|
||||
|
||||
protected function serialize(): array
|
||||
{
|
||||
$result = parent::serialize();
|
||||
if ($this->referenceSourceStream !== null) {
|
||||
$result['referenceSourceStream'] = $this->referenceSourceStream;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function setReferenceSourceStream(?IdentifierTokenStream $referenceSourceStream): void
|
||||
{
|
||||
$this->referenceSourceStream = $referenceSourceStream;
|
||||
}
|
||||
|
||||
public function getReferenceSourceStream(): IdentifierTokenStream
|
||||
{
|
||||
return $this->referenceSourceStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Node;
|
||||
|
||||
/**
|
||||
* AST entry node.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class RootNode extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* Attempting to clone the RootNode indicates a bug in AstBuilder.
|
||||
* It should never happen.
|
||||
*/
|
||||
public function __clone(): void
|
||||
{
|
||||
throw new \LogicException('Can not clone RootNode', 1655988945);
|
||||
}
|
||||
|
||||
/**
|
||||
* RootNode has no properties to cache, just children.
|
||||
*/
|
||||
protected function serialize(): array
|
||||
{
|
||||
return [
|
||||
'children' => $this->children,
|
||||
];
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function updateName(string $name): void
|
||||
{
|
||||
throw new \RuntimeException('RootNode has no name. Don\'t call updateName().', 1653743453);
|
||||
}
|
||||
|
||||
public function setValue(?string $value): void
|
||||
{
|
||||
throw new \RuntimeException('RootNode has no value. Don\'t call setValue().', 1653743454);
|
||||
}
|
||||
|
||||
public function appendValue(string $value): void
|
||||
{
|
||||
throw new \RuntimeException('RootNode has no value. Don\'t call appendValue().', 1653743455);
|
||||
}
|
||||
|
||||
public function getValue(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isValueNull(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$childName = $child->getName();
|
||||
if ($child instanceof ReferenceChildNode) {
|
||||
// Hack for b/w compat parsing of `=<` operator. See ContentObjectRenderer cObjGetSingle() and mergeTSRef()
|
||||
$childValue = '< ' . $child->getReferenceSourceStream();
|
||||
} else {
|
||||
$childValue = $child->getValue();
|
||||
}
|
||||
if ($childValue !== null) {
|
||||
$result[$childName] = $childValue;
|
||||
}
|
||||
$grandChildren = $child->toArray();
|
||||
if ($grandChildren !== null) {
|
||||
$result[$childName . '.'] = $grandChildren;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function flatten(string $prefix = ''): array
|
||||
{
|
||||
$flatArray = [];
|
||||
foreach ($this->getNextChild() as $child) {
|
||||
$flatArray = array_merge($flatArray, $child->flatten(''));
|
||||
}
|
||||
return $flatArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Traverser;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Visitor\AstVisitorInterface;
|
||||
|
||||
/**
|
||||
* Traverse the entire AST.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class AstTraverser
|
||||
{
|
||||
/**
|
||||
* @param AstVisitorInterface[] $visitors
|
||||
*/
|
||||
public function traverse(RootNode $rootNode, array $visitors): void
|
||||
{
|
||||
foreach ($visitors as $visitor) {
|
||||
if (!$visitor instanceof AstVisitorInterface) {
|
||||
throw new \RuntimeException(
|
||||
'Visitors must implement AstTreeVisitorInterface',
|
||||
1689244842
|
||||
);
|
||||
}
|
||||
}
|
||||
$currentObjectPath = new CurrentObjectPath();
|
||||
$this->traverseRecursive($visitors, $rootNode, $rootNode, $currentObjectPath, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AstVisitorInterface[] $visitors
|
||||
*/
|
||||
private function traverseRecursive(array $visitors, RootNode $nodeRoot, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
$currentObjectPath->append($node);
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->visitBeforeChildren($nodeRoot, $node, $currentObjectPath, $currentDepth);
|
||||
}
|
||||
foreach ($node->getNextChild() as $child) {
|
||||
$this->traverseRecursive($visitors, $nodeRoot, $child, $currentObjectPath, $currentDepth + 1);
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->visit($nodeRoot, $child, $currentObjectPath, $currentDepth);
|
||||
}
|
||||
}
|
||||
foreach ($visitors as $visitor) {
|
||||
$visitor->visitAfterChildren($nodeRoot, $node, $currentObjectPath, $currentDepth);
|
||||
}
|
||||
$currentObjectPath->removeLast();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Visitor;
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStream;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenStreamInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\Tokenizer\Token\TokenType;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Gather comments suitable for constant editor.
|
||||
*
|
||||
* @internal This is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
final class AstConstantCommentVisitor implements AstVisitorInterface
|
||||
{
|
||||
private array $categories = [
|
||||
'basic' => [
|
||||
'label' => 'Basic',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'menu' => [
|
||||
'label' => 'Menu',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'content' => [
|
||||
'label' => 'Content',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'page' => [
|
||||
'label' => 'Page',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'advanced' => [
|
||||
'label' => 'Advanced',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
'all' => [
|
||||
'label' => 'All',
|
||||
'usageCount' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
private array $subCategories = [
|
||||
'enable' => [
|
||||
'label' => 'Enable features',
|
||||
'sorting' => 'a',
|
||||
],
|
||||
'dims' => [
|
||||
'label' => 'Dimensions, widths, heights, pixels',
|
||||
'sorting' => 'b',
|
||||
],
|
||||
'file' => [
|
||||
'label' => 'Files',
|
||||
'sorting' => 'c',
|
||||
],
|
||||
'typo' => [
|
||||
'label' => 'Typography',
|
||||
'sorting' => 'd',
|
||||
],
|
||||
'color' => [
|
||||
'label' => 'Colors',
|
||||
'sorting' => 'e',
|
||||
],
|
||||
'links' => [
|
||||
'label' => 'Links and targets',
|
||||
'sorting' => 'f',
|
||||
],
|
||||
'language' => [
|
||||
'label' => 'Language specific constants',
|
||||
'sorting' => 'g',
|
||||
],
|
||||
'cheader' => [
|
||||
'label' => 'Content: \'Header\'',
|
||||
'sorting' => 'ma',
|
||||
],
|
||||
'cheader_g' => [
|
||||
'label' => 'Content: \'Header\', Graphical',
|
||||
'sorting' => 'ma',
|
||||
],
|
||||
'ctext' => [
|
||||
'label' => 'Content: \'Text\'',
|
||||
'sorting' => 'mb',
|
||||
],
|
||||
'cimage' => [
|
||||
'label' => 'Content: \'Image\'',
|
||||
'sorting' => 'md',
|
||||
],
|
||||
'ctextmedia' => [
|
||||
'label' => 'Content: \'Textmedia\'',
|
||||
'sorting' => 'ml',
|
||||
],
|
||||
'cbullets' => [
|
||||
'label' => 'Content: \'Bullet list\'',
|
||||
'sorting' => 'me',
|
||||
],
|
||||
'ctable' => [
|
||||
'label' => 'Content: \'Table\'',
|
||||
'sorting' => 'mf',
|
||||
],
|
||||
'cuploads' => [
|
||||
'label' => 'Content: \'Filelinks\'',
|
||||
'sorting' => 'mg',
|
||||
],
|
||||
'cmultimedia' => [
|
||||
'label' => 'Content: \'Multimedia\'',
|
||||
'sorting' => 'mh',
|
||||
],
|
||||
'cmedia' => [
|
||||
'label' => 'Content: \'Media\'',
|
||||
'sorting' => 'mr',
|
||||
],
|
||||
'cmailform' => [
|
||||
'label' => 'Content: \'Form\'',
|
||||
'sorting' => 'mi',
|
||||
],
|
||||
'csearch' => [
|
||||
'label' => 'Content: \'Search\'',
|
||||
'sorting' => 'mj',
|
||||
],
|
||||
'clogin' => [
|
||||
'label' => 'Content: \'Login\'',
|
||||
'sorting' => 'mk',
|
||||
],
|
||||
'cmenu' => [
|
||||
'label' => 'Content: \'Menu/Sitemap\'',
|
||||
'sorting' => 'mm',
|
||||
],
|
||||
'cshortcut' => [
|
||||
'label' => 'Content: \'Insert records\'',
|
||||
'sorting' => 'mn',
|
||||
],
|
||||
'clist' => [
|
||||
'label' => 'Content: \'List of records\'',
|
||||
'sorting' => 'mo',
|
||||
],
|
||||
'chtml' => [
|
||||
'label' => 'Content: \'HTML\'',
|
||||
'sorting' => 'mq',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Helper hack variable to have a unique sub category order if no sub category is given.
|
||||
*/
|
||||
private int $subCategoryCounter = 0;
|
||||
|
||||
private array $currentTemplateFlatConstants = [];
|
||||
|
||||
private array $constants = [];
|
||||
|
||||
public function visitBeforeChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
if ($node instanceof RootNode) {
|
||||
$rootNodeComments = $rootNode->getComments();
|
||||
foreach ($rootNodeComments as $comment) {
|
||||
$this->subCategoryCounter++;
|
||||
// Additional custom categories are attached as comments to root node
|
||||
$this->parseCustomCategoryAndSubCategories($comment);
|
||||
}
|
||||
} else {
|
||||
$nodeComments = $node->getComments();
|
||||
foreach ($nodeComments as $comment) {
|
||||
$this->subCategoryCounter++;
|
||||
$parsedCommentArray = $this->parseNodeComment($comment, $node->getName(), $node->getValue());
|
||||
if (empty($parsedCommentArray)) {
|
||||
continue;
|
||||
}
|
||||
$currentDottedPath = $currentObjectPath->getPathAsString();
|
||||
if (array_key_exists($currentDottedPath, $this->constants)) {
|
||||
// A constant definition can be defined only once. Stop when trying to override.
|
||||
continue;
|
||||
}
|
||||
$parsedCommentArray['name'] = $currentDottedPath;
|
||||
$parsedCommentArray['idName'] = str_replace('.', '-', $currentDottedPath);
|
||||
$parsedCommentArray['value'] = $node->getValue();
|
||||
$parsedCommentArray['default_value'] = $node->getPreviousValue() ?? $node->getValue() ?? '[Empty]';
|
||||
$parsedCommentArray['isInCurrentTemplate'] = false;
|
||||
if (array_key_exists($currentDottedPath, $this->currentTemplateFlatConstants)) {
|
||||
$parsedCommentArray['isInCurrentTemplate'] = true;
|
||||
}
|
||||
$this->constants[$currentDottedPath] = $parsedCommentArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setCurrentTemplateFlatConstants(array $currentTemplateFlatConstants)
|
||||
{
|
||||
$this->currentTemplateFlatConstants = $currentTemplateFlatConstants;
|
||||
}
|
||||
|
||||
public function getConstants(): array
|
||||
{
|
||||
return $this->constants;
|
||||
}
|
||||
|
||||
public function getCategories(): array
|
||||
{
|
||||
return $this->categories;
|
||||
}
|
||||
|
||||
private function parseNodeComment(TokenStreamInterface $commentTokenStream, string $nodeName, ?string $currentValue = null): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$parsedCommentArray = [];
|
||||
$commentTokenStream->reset();
|
||||
$trimmedTokenStream = new TokenStream();
|
||||
while ($token = $commentTokenStream->getNext()) {
|
||||
if ($token->getType() !== TokenType::T_BLANK) {
|
||||
$trimmedTokenStream->append($token);
|
||||
}
|
||||
}
|
||||
$firstTokenType = $trimmedTokenStream->peekNext()->getType();
|
||||
if ($firstTokenType !== TokenType::T_COMMENT_ONELINE_HASH && $firstTokenType !== TokenType::T_COMMENT_ONELINE_DOUBLESLASH) {
|
||||
// Ignore multiline comments, only '#' and '//' allowed here
|
||||
return $parsedCommentArray;
|
||||
}
|
||||
$commentString = trim((string)$trimmedTokenStream);
|
||||
// Get rid of '#' and '//'
|
||||
$commentString = trim(preg_replace('/^[#\\/]*/', '', $commentString));
|
||||
if (empty($commentString)) {
|
||||
return $parsedCommentArray;
|
||||
}
|
||||
// "# cat=my custom: custom1/customsub1; type=string; label=custom1 customsub1 test1"
|
||||
$commentParts = explode(';', $commentString);
|
||||
foreach ($commentParts as $commentPart) {
|
||||
if (!str_contains($commentPart, '=')) {
|
||||
// Whatever it is, we ignore it.
|
||||
continue;
|
||||
}
|
||||
$partArray = explode('=', $commentPart, 2);
|
||||
$partKey = strtolower(trim($partArray[0]));
|
||||
$partValue = trim($partArray[1] ?? '');
|
||||
if (empty($partKey) || empty($partValue)) {
|
||||
continue;
|
||||
}
|
||||
if ($partKey === 'type') {
|
||||
if (str_starts_with($partValue, 'int+')) {
|
||||
$parsedCommentArray['type'] = 'int+';
|
||||
$parsedCommentArray['typeIntPlusMin'] = 0;
|
||||
preg_match('/int\+\[(.*)\]/is', $partValue, $typeMatches);
|
||||
if (!empty($typeMatches[1]) && str_contains($typeMatches[1], '-')) {
|
||||
$intPlusExplodedRange = GeneralUtility::intExplode('-', $typeMatches[1]);
|
||||
$parsedCommentArray['typeIntPlusMin'] = $intPlusExplodedRange[0];
|
||||
$parsedCommentArray['typeHint'] = 'Greater than ' . $intPlusExplodedRange[0];
|
||||
if ($intPlusExplodedRange[1] > 0) {
|
||||
$parsedCommentArray['typeIntPlusMax'] = $intPlusExplodedRange[1];
|
||||
$parsedCommentArray['typeHint'] = 'Range ' . $intPlusExplodedRange[0] . ' - ' . $intPlusExplodedRange[1];
|
||||
}
|
||||
}
|
||||
} elseif (str_starts_with($partValue, 'int')) {
|
||||
preg_match('/int\[(.*)\]/is', $partValue, $typeMatches);
|
||||
$parsedCommentArray['type'] = 'int';
|
||||
if (!empty($typeMatches[1]) && str_contains($typeMatches[1], '-')) {
|
||||
$rangeArray = mb_str_split($typeMatches[1]);
|
||||
$negativeStart = false;
|
||||
$negativeStop = false;
|
||||
$gotSeparatorDash = false;
|
||||
$start = null;
|
||||
$stop = null;
|
||||
foreach ($rangeArray as $index => $char) {
|
||||
if ($index === 0 && $char === '-') {
|
||||
$negativeStart = true;
|
||||
} elseif ($char === '-' && !$gotSeparatorDash) {
|
||||
$gotSeparatorDash = true;
|
||||
} elseif (!$gotSeparatorDash) {
|
||||
$start .= $char;
|
||||
} elseif ($stop === null && $char === '-') {
|
||||
$negativeStop = true;
|
||||
} else {
|
||||
$stop .= $char;
|
||||
}
|
||||
}
|
||||
if ($start !== null) {
|
||||
if ($negativeStart) {
|
||||
$start = (int)$start * -1;
|
||||
}
|
||||
$parsedCommentArray['typeIntMin'] = (string)$start;
|
||||
$parsedCommentArray['typeHint'] = 'Greater than ' . $start;
|
||||
}
|
||||
if ($stop !== null) {
|
||||
if ($negativeStop) {
|
||||
$stop = (int)$stop * -1;
|
||||
}
|
||||
$parsedCommentArray['typeIntMax'] = (string)$stop;
|
||||
$parsedCommentArray['typeHint'] = 'Range ' . $start . ' - ' . $stop;
|
||||
}
|
||||
}
|
||||
} elseif ($partValue === 'wrap') {
|
||||
$parsedCommentArray['type'] = 'wrap';
|
||||
$splitValue = explode('|', $currentValue ?? '');
|
||||
$parsedCommentArray['wrapStart'] = $splitValue[0];
|
||||
$parsedCommentArray['wrapEnd'] = $splitValue[1] ?? '';
|
||||
} elseif (str_starts_with($partValue, 'offset')) {
|
||||
$parsedCommentArray['type'] = 'offset';
|
||||
preg_match('/offset\[(.*)\]/is', $partValue, $typeMatches);
|
||||
$labelArray = explode(',', $typeMatches[1] ?? '');
|
||||
$valueArray = explode(',', $currentValue ?? '');
|
||||
$parsedCommentArray['labelValueArray'] = [
|
||||
[
|
||||
'label' => (!empty($labelArray[0])) ? $labelArray[0] : 'x',
|
||||
'value' => (!empty($valueArray[0])) ? $valueArray[0] : '',
|
||||
],
|
||||
[
|
||||
'label' => $labelArray[1] ?? 'y',
|
||||
'value' => $valueArray[1] ?? '',
|
||||
],
|
||||
];
|
||||
for ($i = 2; $i <= 5; $i++) {
|
||||
if (!($labelArray[$i] ?? false)) {
|
||||
break;
|
||||
}
|
||||
$parsedCommentArray['labelValueArray'][] = [
|
||||
'label' => $labelArray[$i],
|
||||
'value' => $valueArray[$i] ?? '',
|
||||
];
|
||||
}
|
||||
} elseif (str_starts_with($partValue, 'options')) {
|
||||
preg_match('/options\\s*\[(.*)\]/is', $partValue, $typeMatches);
|
||||
if (!empty($typeMatches[1] ?? '')) {
|
||||
$parsedCommentArray['type'] = 'options';
|
||||
$labelValueStringArray = GeneralUtility::trimExplode(',', $typeMatches[1], true);
|
||||
foreach ($labelValueStringArray as $labelValueString) {
|
||||
$labelValueArray = explode('=', $labelValueString, 2);
|
||||
$label = $labelValueArray[0];
|
||||
$value = $labelValueArray[1] ?? $labelValueArray[0];
|
||||
$selected = false;
|
||||
if ($value === $currentValue) {
|
||||
$selected = true;
|
||||
}
|
||||
$parsedCommentArray['labelValueArray'][] = [
|
||||
'label' => $languageService->sL($label),
|
||||
'value' => $value,
|
||||
'selected' => $selected,
|
||||
];
|
||||
}
|
||||
}
|
||||
} elseif (str_starts_with($partValue, 'boolean')) {
|
||||
$parsedCommentArray['type'] = 'boolean';
|
||||
preg_match('/boolean\\s*\[(.*)\]/is', $partValue, $typeMatches);
|
||||
$parsedCommentArray['trueValue'] = '1';
|
||||
if (!empty($typeMatches[1] ?? '')) {
|
||||
$parsedCommentArray['trueValue'] = $typeMatches[1];
|
||||
}
|
||||
} elseif (str_starts_with($partValue, 'user')) {
|
||||
preg_match('/user\\s*\[(.*)\]/is', $partValue, $typeMatches);
|
||||
if (!empty($typeMatches[1] ?? '')) {
|
||||
$parsedCommentArray['type'] = 'user';
|
||||
$userFunction = $typeMatches[1];
|
||||
$userFunctionParams = [
|
||||
'fieldName' => $nodeName,
|
||||
'fieldValue' => $currentValue,
|
||||
];
|
||||
$parsedCommentArray['html'] = (string)GeneralUtility::callUserFunction(
|
||||
$userFunction,
|
||||
$userFunctionParams
|
||||
);
|
||||
}
|
||||
} elseif ($partValue === 'comment') {
|
||||
$parsedCommentArray['type'] = 'comment';
|
||||
} elseif ($partValue === 'color') {
|
||||
$parsedCommentArray['type'] = 'color';
|
||||
} else {
|
||||
$parsedCommentArray['type'] = 'string';
|
||||
}
|
||||
} elseif ($partKey === 'cat') {
|
||||
$categorySplitArray = explode('/', strtolower($partValue));
|
||||
$mainCategory = strtolower(trim($categorySplitArray[0]));
|
||||
if (empty($mainCategory)) {
|
||||
return [];
|
||||
}
|
||||
if (isset($this->categories[$mainCategory])) {
|
||||
$this->categories[$mainCategory]['usageCount']++;
|
||||
} else {
|
||||
$this->categories[$mainCategory] = [
|
||||
'usageCount' => 1,
|
||||
'label' => $mainCategory,
|
||||
];
|
||||
}
|
||||
$parsedCommentArray['cat'] = $mainCategory;
|
||||
$subCategory = trim($categorySplitArray[1] ?? '');
|
||||
$subCategoryOrder = trim($categorySplitArray[2] ?? '');
|
||||
if ($subCategory && array_key_exists($subCategory, $this->subCategories)) {
|
||||
$parsedCommentArray['subcat_name'] = $subCategory;
|
||||
$parsedCommentArray['subcat_label'] = $languageService->sL($this->subCategories[$subCategory]['label']);
|
||||
$sortIdentifier = empty($subCategoryOrder) ? $this->subCategoryCounter : $subCategoryOrder;
|
||||
$parsedCommentArray['subcat_sorting_first'] = $this->subCategories[$subCategory]['sorting'];
|
||||
$parsedCommentArray['subcat_sorting_second'] = $sortIdentifier . 'z';
|
||||
} elseif ($subCategoryOrder !== '') {
|
||||
// "0" is a valid key for an assignment like "# cat=foo//0; type=boolean; label=some config"
|
||||
$parsedCommentArray['subcat_name'] = 'other';
|
||||
$parsedCommentArray['subcat_label'] = 'Other';
|
||||
$parsedCommentArray['subcat_sorting_first'] = 'o';
|
||||
$parsedCommentArray['subcat_sorting_second'] = $subCategoryOrder . 'z';
|
||||
} else {
|
||||
$parsedCommentArray['subcat_name'] = 'other';
|
||||
$parsedCommentArray['subcat_label'] = 'Other';
|
||||
$parsedCommentArray['subcat_sorting_first'] = 'o';
|
||||
$parsedCommentArray['subcat_sorting_second'] = $this->subCategoryCounter . 'z';
|
||||
}
|
||||
} elseif ($partKey === 'label') {
|
||||
$fullLabel = $languageService->sL($partValue);
|
||||
$splitLabelArray = explode(':', $fullLabel, 2);
|
||||
$parsedCommentArray['label'] = $splitLabelArray[0];
|
||||
$parsedCommentArray['description'] = $splitLabelArray[1] ?? '';
|
||||
}
|
||||
}
|
||||
if (!array_key_exists('cat', $parsedCommentArray)) {
|
||||
// At least 'category' must be there, everything else is optional.
|
||||
return [];
|
||||
}
|
||||
$parsedCommentArray['type'] ??= 'string';
|
||||
return $parsedCommentArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RootNode comments for additional custom categories and sub categories
|
||||
* and register them in $this properties.
|
||||
*/
|
||||
private function parseCustomCategoryAndSubCategories(TokenStreamInterface $commentTokenStream): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$firstTokenType = $commentTokenStream->peekNext()->getType();
|
||||
if ($firstTokenType !== TokenType::T_COMMENT_ONELINE_HASH && $firstTokenType !== TokenType::T_COMMENT_ONELINE_DOUBLESLASH) {
|
||||
// Ignore multiline comments, only '#' and '//' allowed here
|
||||
return;
|
||||
}
|
||||
$commentString = trim((string)$commentTokenStream);
|
||||
// Get rid of '#' and '//'
|
||||
$commentString = trim(preg_replace('/^[#\\/]*/', '', $commentString));
|
||||
if (empty($commentString)) {
|
||||
return;
|
||||
}
|
||||
// "# customcategory=myCustomCategoryKey=My custom category label"
|
||||
if (str_contains($commentString, '=') && str_starts_with(strtolower($commentString), 'customcategory')) {
|
||||
$customCategoryArray = explode('=', $commentString, 3);
|
||||
if (strtolower(trim($customCategoryArray[0])) !== 'customcategory'
|
||||
|| empty(trim($customCategoryArray[1]))
|
||||
|| empty(trim($customCategoryArray[2]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$categoryKey = strtolower($customCategoryArray[1]);
|
||||
$categoryLabel = $customCategoryArray[2];
|
||||
if (!isset($this->categories[$categoryKey])) {
|
||||
$this->categories[$categoryKey] = [
|
||||
'usageCount' => 0,
|
||||
'label' => $languageService->sL($categoryLabel),
|
||||
];
|
||||
}
|
||||
return;
|
||||
}
|
||||
// "customsubcategory=120=My custom sub category label"
|
||||
if (str_contains($commentString, '=') && str_starts_with(strtolower($commentString), 'customsubcategory')) {
|
||||
$customSubCategoryArray = explode('=', $commentString, 3);
|
||||
if (strtolower(trim($customSubCategoryArray[0])) !== 'customsubcategory'
|
||||
|| empty(trim($customSubCategoryArray[1]))
|
||||
|| empty(trim($customSubCategoryArray[2]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$subCategoryKey = strtolower($customSubCategoryArray[1]);
|
||||
$subCategoryLabel = $customSubCategoryArray[2];
|
||||
if (!isset($this->subCategories[$subCategoryKey])) {
|
||||
$this->subCategories[$subCategoryKey] = [
|
||||
'label' => $languageService->sL($subCategoryLabel),
|
||||
'sorting' => $this->subCategoryCounter,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function visit(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
|
||||
public function visitAfterChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Visitor;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* Find a single node in tree identified by node identifier.
|
||||
*
|
||||
* @internal This is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
final class AstNodeFinderVisitor implements AstVisitorInterface
|
||||
{
|
||||
private string $nodeIdentifier;
|
||||
private ?NodeInterface $foundNode = null;
|
||||
private ?CurrentObjectPath $foundNodeCurrentObjectPath = null;
|
||||
|
||||
public function setNodeIdentifier(string $nodeIdentifier)
|
||||
{
|
||||
$this->nodeIdentifier = $nodeIdentifier;
|
||||
}
|
||||
|
||||
public function getFoundNode(): ?NodeInterface
|
||||
{
|
||||
return $this->foundNode;
|
||||
}
|
||||
|
||||
public function getFoundNodeCurrentObjectPath(): ?CurrentObjectPath
|
||||
{
|
||||
return $this->foundNodeCurrentObjectPath;
|
||||
}
|
||||
|
||||
public function visitBeforeChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
if ($node->getIdentifier() === $this->nodeIdentifier) {
|
||||
$this->foundNode = $node;
|
||||
$this->foundNodeCurrentObjectPath = clone $currentObjectPath;
|
||||
}
|
||||
}
|
||||
|
||||
public function visit(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
|
||||
public function visitAfterChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Visitor;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* Sort all children alphabetically. Used in backend Object Browser.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
final class AstSortChildrenVisitor implements AstVisitorInterface
|
||||
{
|
||||
public function visitBeforeChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
$node->sortChildren();
|
||||
}
|
||||
|
||||
public function visit(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
|
||||
public function visitAfterChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void
|
||||
{
|
||||
// Implement interface
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\TypoScript\AST\Visitor;
|
||||
|
||||
use TYPO3\CMS\Core\TypoScript\AST\CurrentObjectPath\CurrentObjectPath;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\NodeInterface;
|
||||
use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
|
||||
|
||||
/**
|
||||
* An interface implemented by all visitors of AstTraverser.
|
||||
*
|
||||
* @internal: Internal AST structure.
|
||||
*/
|
||||
interface AstVisitorInterface
|
||||
{
|
||||
public function visitBeforeChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void;
|
||||
|
||||
public function visit(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void;
|
||||
|
||||
public function visitAfterChildren(RootNode $rootNode, NodeInterface $node, CurrentObjectPath $currentObjectPath, int $currentDepth): void;
|
||||
}
|
||||
Reference in New Issue
Block a user