Skip to content

Instantly share code, notes, and snippets.

@RafaelKa
Created March 14, 2013 07:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RafaelKa/5159642 to your computer and use it in GitHub Desktop.
Save RafaelKa/5159642 to your computer and use it in GitHub Desktop.
<?php
namespace TYPO3\Fluid\Core\Parser;
/* *
* This script belongs to the TYPO3 Flow package "Fluid". *
* *
* It is free software; you can redistribute it and/or modify it under *
* the terms of the GNU Lesser General Public License, either version 3 *
* of the License, or (at your option) any later version. *
* *
* The TYPO3 project - inspiring people to share! *
* */
/**
* Template parser building up an object syntax tree
*
*/
class TemplateParser {
static public $SCAN_PATTERN_NAMESPACEDECLARATION = '/(?<!\\\\){namespace\s*(?P<identifier>[a-zA-Z]+[a-zA-Z0-9]*)\s*=\s*(?P<phpNamespace>(?:[A-Za-z0-9\.]+|Tx)(?:FLUID_NAMESPACE_SEPARATOR\w+)+)\s*}/m';
static public $SCAN_PATTERN_XMLNSDECLARATION = '/\sxmlns:(?P<identifier>.*?)="(?P<xmlNamespace>.*?)"/m';
/**
* The following two constants are used for tracking whether we are currently
* parsing ViewHelper arguments or not. This is used to parse arrays only as
* ViewHelper argument.
*/
const CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS = 1;
const CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS = 2;
/**
* This regular expression splits the input string at all dynamic tags, AND
* on all <![CDATA[...]]> sections.
*
*/
static public $SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS = '/
(
(?: <\/? # Start dynamic tags
(?:(?:NAMESPACE):[a-zA-Z0-9\\.]+) # A tag consists of the namespace prefix and word characters
(?: # Begin tag arguments
\s*[a-zA-Z0-9:-]+ # Argument Keys
= # =
(?> # either... If we have found an argument, we will not back-track (That does the Atomic Bracket)
"(?:\\\"|[^"])*" # a double-quoted string
|\'(?:\\\\\'|[^\'])*\' # or a single quoted string
)\s* #
)* # Tag arguments can be replaced many times.
\s*
\/?> # Closing tag
)
|(?: # Start match CDATA section
<!\[CDATA\[.*?\]\]>
)
|(?: # Start match escpe character section
\\\.
)
)/xs';
/**
* This regular expression scans if the input string is a ViewHelper tag
*
*/
static public $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG = '/
^< # A Tag begins with <
(?P<NamespaceIdentifier>NAMESPACE): # Then comes the Namespace prefix followed by a :
(?P<MethodIdentifier> # Now comes the Name of the ViewHelper
[a-zA-Z0-9\\.]+
)
(?P<Attributes> # Begin Tag Attributes
(?: # A tag might have multiple attributes
\s*
[a-zA-Z0-9:-]+ # The attribute name
= # =
(?> # either... # If we have found an argument, we will not back-track (That does the Atomic Bracket)
"(?:\\\"|[^"])*" # a double-quoted string
|\'(?:\\\\\'|[^\'])*\' # or a single quoted string
) #
\s*
)* # A tag might have multiple attributes
) # End Tag Attributes
\s*
(?P<Selfclosing>\/?) # A tag might be selfclosing
>$/x';
/**
* This regular expression scans if the input string is a closing ViewHelper
* tag.
*
*/
static public $SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG = '/^<\/(?P<NamespaceIdentifier>NAMESPACE):(?P<MethodIdentifier>[a-zA-Z0-9\\.]+)\s*>$/';
/**
* This regular expression splits the tag arguments into its parts
*
*/
static public $SPLIT_PATTERN_TAGARGUMENTS = '/
(?: #
\s* #
(?P<Argument> # The attribute name
[a-zA-Z0-9:-]+ #
) #
= # =
(?> # If we have found an argument, we will not back-track (That does the Atomic Bracket)
(?P<ValueQuoted> # either...
(?:"(?:\\\"|[^"])*") # a double-quoted string
|(?:\'(?:\\\\\'|[^\'])*\') # or a single quoted string
)
)\s*
)
/xs';
/**
* This pattern detects CDATA sections and outputs the text between opening
* and closing CDATA.
*
*/
static public $SCAN_PATTERN_CDATA = '/^<!\[CDATA\[(.*?)\]\]>$/s';
/**
* This pattern detects an escape sequence character and outputs the escaped character
*
*/
static public $SCAN_PATTERN_ESCAPE_SEQUENZE = '/^\\\(.)/s';
/**
* Pattern which splits the shorthand syntax into different tokens. The
* "shorthand syntax" is everything like {...}
*
*/
static public $SPLIT_PATTERN_SHORTHANDSYNTAX = '/
(
{ # Start of shorthand syntax
(?: # Shorthand syntax is either composed of...
[a-zA-Z0-9\->_:,.()] # Various characters
|"(?:\\\"|[^"])*" # Double-quoted strings
|\'(?:\\\\\'|[^\'])*\' # Single-quoted strings
|(?R) # Other shorthand syntaxes inside, albeit not in a quoted string
|\s+ # Spaces
)+
} # End of shorthand syntax
)/x';
/**
* Pattern which detects the object accessor syntax:
* {object.some.value}, additionally it detects ViewHelpers like
* {f:for(param1:bla)} and chaining like
* {object.some.value->f:bla.blubb()->f:bla.blubb2()}
*
* THIS IS ALMOST THE SAME AS IN $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
*
*/
static public $SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS = '/
^{ # Start of shorthand syntax
# A shorthand syntax is either...
(?P<Object>[a-zA-Z0-9\-_.]*) # ... an object accessor
\s*(?P<Delimiter>(?:->)?)\s*
(?P<ViewHelper> # ... a ViewHelper
[a-zA-Z0-9]+ # Namespace prefix of ViewHelper (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
:
[a-zA-Z0-9\\.]+ # Method Identifier (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
\( # Opening parameter brackets of ViewHelper
(?P<ViewHelperArguments> # Start submatch for ViewHelper arguments. This is taken from $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
(?:
\s*[a-zA-Z0-9\-_]+ # The keys of the array
\s*:\s* # Key|Value delimiter :
(?: # Possible value options:
"(?:\\\"|[^"])*" # Double qouoted string
|\'(?:\\\\\'|[^\'])*\' # Single quoted string
|[a-zA-Z0-9\-_.]+ # variable identifiers
|{(?P>ViewHelperArguments)} # Another sub-array
) # END possible value options
\s*,? # There might be a , to seperate different parts of the array
)* # The above cycle is repeated for all array elements
) # End ViewHelper Arguments submatch
\) # Closing parameter brackets of ViewHelper
)?
(?P<AdditionalViewHelpers> # There can be more than one ViewHelper chained, by adding more -> and the ViewHelper (recursively)
(?:
\s*->\s*
(?P>ViewHelper)
)*
)
}$/x';
/**
* THIS IS ALMOST THE SAME AS $SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS
*
*/
static public $SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER = '/
(?P<NamespaceIdentifier>[a-zA-Z0-9]+) # Namespace prefix of ViewHelper (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
:
(?P<MethodIdentifier>[a-zA-Z0-9\\.]+)
\( # Opening parameter brackets of ViewHelper
(?P<ViewHelperArguments> # Start submatch for ViewHelper arguments. This is taken from $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
(?:
\s*[a-zA-Z0-9\-_]+ # The keys of the array
\s*:\s* # Key|Value delimiter :
(?: # Possible value options:
"(?:\\\"|[^"])*" # Double qouoted string
|\'(?:\\\\\'|[^\'])*\' # Single quoted string
|[a-zA-Z0-9\-_.]+ # variable identifiers
|{(?P>ViewHelperArguments)} # Another sub-array
) # END possible value options
\s*,? # There might be a , to seperate different parts of the array
)* # The above cycle is repeated for all array elements
) # End ViewHelper Arguments submatch
\) # Closing parameter brackets of ViewHelper
/x';
/**
* Pattern which detects the array/object syntax like in JavaScript, so it
* detects strings like:
* {object: value, object2: {nested: array}, object3: "Some string"}
*
* THIS IS ALMOST THE SAME AS IN SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS
*
*/
static public $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS = '/^
(?P<Recursion> # Start the recursive part of the regular expression - describing the array syntax
{ # Each array needs to start with {
(?P<Array> # Start submatch
(?:
\s*[a-zA-Z0-9\-_]+ # The keys of the array
\s*:\s* # Key|Value delimiter :
(?: # Possible value options:
"(?:\\\"|[^"])*" # Double qouoted string
|\'(?:\\\\\'|[^\'])*\' # Single quoted string
|[a-zA-Z0-9\-_.]+ # variable identifiers
|(?P>Recursion) # Another sub-array
) # END possible value options
\s*,? # There might be a , to seperate different parts of the array
)* # The above cycle is repeated for all array elements
) # End array submatch
} # Each array ends with }
)$/x';
/**
* This pattern splits an array into its parts. It is quite similar to the
* pattern above.
*
*/
static public $SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS = '/
(?P<ArrayPart> # Start submatch
(?P<Key>[a-zA-Z0-9\-_]+) # The keys of the array
\s*:\s* # Key|Value delimiter :
(?: # Possible value options:
(?P<QuotedString> # Quoted string
(?:"(?:\\\"|[^"])*")
|(?:\'(?:\\\\\'|[^\'])*\')
)
|(?P<VariableIdentifier>[a-zA-Z][a-zA-Z0-9\-_.]*) # variable identifiers have to start with a letter
|(?P<Number>[0-9.]+) # Number
|{\s*(?P<Subarray>(?:(?P>ArrayPart)\s*,?\s*)+)\s*} # Another sub-array
) # END possible value options
) # End array part submatch
/x';
/**
* This pattern detects the default xml namespace
*
*/
static public $SCAN_PATTERN_DEFAULT_XML_NAMESPACE = '/^http\:\/\/typo3\.org\/ns\/(?P<PhpNamespace>.+)$/s';
/**
* Namespace identifiers and their component name prefix (Associative array).
* @var array
*/
protected $namespaces = array(
'f' => 'TYPO3\Fluid\ViewHelpers'
);
/**
* @var \TYPO3\Flow\Object\ObjectManagerInterface
*/
protected $objectManager;
/**
* @var \TYPO3\Fluid\Core\Parser\Configuration
*/
protected $configuration;
/**
* @var array
*/
protected $settings;
/**
* Constructor. Preprocesses the $SCAN_PATTERN_NAMESPACEDECLARATION by
* inserting the correct namespace separator.
*
*/
public function __construct() {
self::$SCAN_PATTERN_NAMESPACEDECLARATION = str_replace('FLUID_NAMESPACE_SEPARATOR', preg_quote(\TYPO3\Fluid\Fluid::NAMESPACE_SEPARATOR), self::$SCAN_PATTERN_NAMESPACEDECLARATION);
}
/**
* Injects Fluid settings
*
* @param array $settings
*/
public function injectSettings(array $settings) {
$this->settings = $settings;
}
/**
* Inject object factory
*
* @param \TYPO3\Flow\Object\ObjectManagerInterface $objectManager
* @return void
*/
public function injectObjectManager(\TYPO3\Flow\Object\ObjectManagerInterface $objectManager) {
$this->objectManager = $objectManager;
}
/**
* Set the configuration for the parser.
*
* @param \TYPO3\Fluid\Core\Parser\Configuration $configuration
* @return void
*/
public function setConfiguration(\TYPO3\Fluid\Core\Parser\Configuration $configuration = NULL) {
$this->configuration = $configuration;
}
/**
* Parses a given template string and returns a parsed template object.
*
* The resulting ParsedTemplate can then be rendered by calling evaluate() on it.
*
* Normally, you should use a subclass of AbstractTemplateView instead of calling the
* TemplateParser directly.
*
* @param string $templateString The template to parse as a string
* @return \TYPO3\Fluid\Core\Parser\ParsedTemplateInterface Parsed template
* @throws Exception
*/
public function parse($templateString) {
if (!is_string($templateString)) {
throw new \TYPO3\Fluid\Core\Parser\Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
}
$this->reset();
$templateString = $this->extractNamespaceDefinitions($templateString);
$splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
$parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
$variableContainer = $parsingState->getVariableContainer();
if ($variableContainer !== NULL && $variableContainer->exists('layoutName')) {
$parsingState->setLayoutNameNode($variableContainer->get('layoutName'));
}
return $parsingState;
}
/**
* Gets the namespace definitions found.
*
* @return array Namespace identifiers and their component name prefix
*/
public function getNamespaces() {
return $this->namespaces;
}
/**
* Resets the parser to its default values.
*
* @return void
*/
protected function reset() {
$this->namespaces = array(
'f' => 'TYPO3\Fluid\ViewHelpers'
);
}
/**
* Extracts namespace definitions out of the given template string and sets
* $this->namespaces.
*
* @param string $templateString Template string to extract the namespaces from
* @return string The updated template string without namespace declarations inside
* @throws \TYPO3\Fluid\Core\Parser\Exception if a namespace can't be resolved or has been declared already
*/
protected function extractNamespaceDefinitions($templateString) {
$matches = array();
preg_match_all(self::$SCAN_PATTERN_XMLNSDECLARATION, $templateString, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
// skip reserved "f" namespace identifier
if ($match['identifier'] === 'f') {
continue;
}
if (array_key_exists($match['identifier'], $this->namespaces)) {
throw new \TYPO3\Fluid\Core\Parser\Exception(sprintf('Namespace identifier "%s" is already registered. Do not re-declare namespaces!', $match['identifier']), 1331135889);
}
if (isset($this->settings['namespaces'][$match['xmlNamespace']])) {
$phpNamespace = $this->settings['namespaces'][$match['xmlNamespace']];
} else {
$matchedPhpNamespace = array();
if (preg_match(self::$SCAN_PATTERN_DEFAULT_XML_NAMESPACE, $match['xmlNamespace'], $matchedPhpNamespace) === 0) {
continue;
}
$phpNamespace = str_replace('/', '\\', $matchedPhpNamespace['PhpNamespace']);
}
$this->namespaces[$match['identifier']] = $phpNamespace;
}
$matches = array();
preg_match_all(self::$SCAN_PATTERN_NAMESPACEDECLARATION, $templateString, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
if (array_key_exists($match['identifier'], $this->namespaces)) {
throw new \TYPO3\Fluid\Core\Parser\Exception(sprintf('Namespace identifier "%s" is already registered. Do not re-declare namespaces!', $match['identifier']), 1224241246);
}
$this->namespaces[$match['identifier']] = $match['phpNamespace'];
}
if ($matches !== array()) {
$templateString = preg_replace(self::$SCAN_PATTERN_NAMESPACEDECLARATION, '', $templateString);
}
return $templateString;
}
/**
* Splits the template string on all dynamic tags found.
*
* @param string $templateString Template string to split.
* @return array Splitted template
*/
protected function splitTemplateAtDynamicTags($templateString) {
$regularExpression = $this->prepareTemplateRegularExpression(self::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS);
return preg_split($regularExpression, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
}
/**
* Build object tree from the split template
*
* @param array $splitTemplate The split template, so that every tag with a namespace declaration is already a seperate array element.
* @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
* @return \TYPO3\Fluid\Core\Parser\ParsingState
* @throws Exception
*/
protected function buildObjectTree($splitTemplate, $context) {
$regularExpression_openingViewHelperTag = $this->prepareTemplateRegularExpression(self::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG);
$regularExpression_closingViewHelperTag = $this->prepareTemplateRegularExpression(self::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG);
$state = $this->objectManager->get('TYPO3\Fluid\Core\Parser\ParsingState');
$rootNode = $this->objectManager->get('TYPO3\Fluid\Core\Parser\SyntaxTree\RootNode');
$state->setRootNode($rootNode);
$state->pushNodeToStack($rootNode);
foreach ($splitTemplate as $templateElement) {
$matchedVariables = array();
if (preg_match(self::$SCAN_PATTERN_CDATA, $templateElement, $matchedVariables) > 0) {
$this->textHandler($state, $matchedVariables[1]);
} elseif (preg_match(self::$SCAN_PATTERN_ESCAPE_SEQUENZE, $templateElement, $matchedVariables) > 0) {
$this->textHandler($state, $matchedVariables[1]);
} elseif (preg_match($regularExpression_openingViewHelperTag, $templateElement, $matchedVariables) > 0) {
$this->openingViewHelperTagHandler($state, $matchedVariables['NamespaceIdentifier'], $matchedVariables['MethodIdentifier'], $matchedVariables['Attributes'], ($matchedVariables['Selfclosing'] === '' ? FALSE : TRUE));
} elseif (preg_match($regularExpression_closingViewHelperTag, $templateElement, $matchedVariables) > 0) {
$this->closingViewHelperTagHandler($state, $matchedVariables['NamespaceIdentifier'], $matchedVariables['MethodIdentifier']);
} else {
$this->textAndShorthandSyntaxHandler($state, $templateElement, $context);
}
}
if ($state->countNodeStack() !== 1) {
throw new \TYPO3\Fluid\Core\Parser\Exception('Not all tags were closed!', 1238169398);
}
return $state;
}
/**
* Handles an opening or self-closing view helper tag.
*
* @param \TYPO3\Fluid\Core\Parser\ParsingState $state Current parsing state
* @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
* @param string $methodIdentifier Method identifier
* @param string $arguments Arguments string, not yet parsed
* @param boolean $selfclosing true, if the tag is a self-closing tag.
* @return void
*/
protected function openingViewHelperTagHandler(\TYPO3\Fluid\Core\Parser\ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing) {
$argumentsObjectTree = $this->parseArguments($arguments);
$this->initializeViewHelperAndAddItToStack($state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree);
if ($selfclosing) {
$node = $state->popNodeFromStack();
$this->callInterceptor($node, \TYPO3\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
}
}
/**
* Initialize the given ViewHelper and adds it to the current node and to
* the stack.
*
* @param \TYPO3\Fluid\Core\Parser\ParsingState $state Current parsing state
* @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
* @param string $methodIdentifier Method identifier
* @param array $argumentsObjectTree Arguments object tree
* @return void
* @throws Exception
*/
protected function initializeViewHelperAndAddItToStack(\TYPO3\Fluid\Core\Parser\ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree) {
if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
throw new \TYPO3\Fluid\Core\Parser\Exception('Namespace could not be resolved. This exception should never be thrown!', 1224254792);
}
$viewHelper = $this->objectManager->get($this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier));
// The following three checks are only done *in an uncached template*, and not needed anymore in the cached version
$expectedViewHelperArguments = $viewHelper->prepareArguments();
$this->abortIfUnregisteredArgumentsExist($expectedViewHelperArguments, $argumentsObjectTree);
$this->abortIfRequiredArgumentsAreMissing($expectedViewHelperArguments, $argumentsObjectTree);
$this->rewriteBooleanNodesInArgumentsObjectTree($expectedViewHelperArguments, $argumentsObjectTree);
$currentViewHelperNode = $this->objectManager->get('TYPO3\Fluid\Core\Parser\SyntaxTree\ViewHelperNode', $viewHelper, $argumentsObjectTree);
$state->getNodeFromStack()->addChildNode($currentViewHelperNode);
if ($viewHelper instanceof \TYPO3\Fluid\Core\ViewHelper\Facets\ChildNodeAccessInterface && !($viewHelper instanceof \TYPO3\Fluid\Core\ViewHelper\Facets\CompilableInterface)) {
$state->setCompilable(FALSE);
}
// PostParse Facet
if ($viewHelper instanceof \TYPO3\Fluid\Core\ViewHelper\Facets\PostParseInterface) {
// Don't just use $viewHelper::postParseEvent(...),
// as this will break with PHP < 5.3.
call_user_func(array($viewHelper, 'postParseEvent'), $currentViewHelperNode, $argumentsObjectTree, $state->getVariableContainer());
}
$this->callInterceptor($currentViewHelperNode, \TYPO3\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER, $state);
$state->pushNodeToStack($currentViewHelperNode);
}
/**
* Throw an exception if there are arguments which were not registered
* before.
*
* @param array $expectedArguments Array of \TYPO3\Fluid\Core\ViewHelper\ArgumentDefinition of all expected arguments
* @param array $actualArguments Actual arguments
* @throws \TYPO3\Fluid\Core\Parser\Exception
*/
protected function abortIfUnregisteredArgumentsExist($expectedArguments, $actualArguments) {
$expectedArgumentNames = array();
foreach ($expectedArguments as $expectedArgument) {
$expectedArgumentNames[] = $expectedArgument->getName();
}
foreach (array_keys($actualArguments) as $argumentName) {
if (!in_array($argumentName, $expectedArgumentNames)) {
throw new \TYPO3\Fluid\Core\Parser\Exception('Argument "' . $argumentName . '" was not registered.', 1237823695);
}
}
}
/**
* Throw an exception if required arguments are missing
*
* @param array $expectedArguments Array of \TYPO3\Fluid\Core\ViewHelper\ArgumentDefinition of all expected arguments
* @param array $actualArguments Actual arguments
* @throws \TYPO3\Fluid\Core\Parser\Exception
*/
protected function abortIfRequiredArgumentsAreMissing($expectedArguments, $actualArguments) {
$actualArgumentNames = array_keys($actualArguments);
foreach ($expectedArguments as $expectedArgument) {
if ($expectedArgument->isRequired() && !in_array($expectedArgument->getName(), $actualArgumentNames)) {
throw new \TYPO3\Fluid\Core\Parser\Exception('Required argument "' . $expectedArgument->getName() . '" was not supplied.', 1237823699);
}
}
}
/**
* Wraps the argument tree, if a node is boolean, into a Boolean syntax tree node
*
* @param array $argumentDefinitions the argument definitions, key is the argument name, value is the ArgumentDefinition object
* @param array $argumentsObjectTree the arguments syntax tree, key is the argument name, value is an AbstractNode
* @return void
*/
protected function rewriteBooleanNodesInArgumentsObjectTree($argumentDefinitions, &$argumentsObjectTree) {
foreach ($argumentDefinitions as $argumentName => $argumentDefinition) {
if ($argumentDefinition->getType() === 'boolean' && isset($argumentsObjectTree[$argumentName])) {
$argumentsObjectTree[$argumentName] = new \TYPO3\Fluid\Core\Parser\SyntaxTree\BooleanNode($argumentsObjectTree[$argumentName]);
}
}
}
/**
* Resolve a viewhelper name.
*
* @param string $namespaceIdentifier Namespace identifier for the view helper.
* @param string $methodIdentifier Method identifier, might be hierarchical like "link.url"
* @return string The fully qualified class name of the viewhelper
*/
protected function resolveViewHelperName($namespaceIdentifier, $methodIdentifier) {
$explodedViewHelperName = explode('.', $methodIdentifier);
if (count($explodedViewHelperName) > 1) {
$className = implode(\TYPO3\Fluid\Fluid::NAMESPACE_SEPARATOR, array_map('ucfirst', $explodedViewHelperName));
} else {
$className = ucfirst($explodedViewHelperName[0]);
}
$className .= 'ViewHelper';
$name = $this->namespaces[$namespaceIdentifier] . \TYPO3\Fluid\Fluid::NAMESPACE_SEPARATOR . $className;
return $name;
}
/**
* Handles a closing view helper tag
*
* @param \TYPO3\Fluid\Core\Parser\ParsingState $state The current parsing state
* @param string $namespaceIdentifier Namespace identifier for the closing tag.
* @param string $methodIdentifier Method identifier.
* @return void
* @throws \TYPO3\Fluid\Core\Parser\Exception
*/
protected function closingViewHelperTagHandler(\TYPO3\Fluid\Core\Parser\ParsingState $state, $namespaceIdentifier, $methodIdentifier) {
if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
throw new \TYPO3\Fluid\Core\Parser\Exception('Namespace could not be resolved. This exception should never be thrown!', 1224256186);
}
$lastStackElement = $state->popNodeFromStack();
if (!($lastStackElement instanceof \TYPO3\Fluid\Core\Parser\SyntaxTree\ViewHelperNode)) {
throw new \TYPO3\Fluid\Core\Parser\Exception('You closed a templating tag which you never opened!', 1224485838);
}
if ($lastStackElement->getViewHelperClassName() != $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier)) {
throw new \TYPO3\Fluid\Core\Parser\Exception('Templating tags not properly nested. Expected: ' . $lastStackElement->getViewHelperClassName() . '; Actual: ' . $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier), 1224485398);
}
$this->callInterceptor($lastStackElement, \TYPO3\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
}
/**
* Handles the appearance of an object accessor (like {posts.author.email}).
* Creates a new instance of \TYPO3\Fluid\ObjectAccessorNode.
*
* Handles ViewHelpers as well which are in the shorthand syntax.
*
* @param \TYPO3\Fluid\Core\Parser\ParsingState $state The current parsing state
* @param string $objectAccessorString String which identifies which objects to fetch
* @param string $delimiter
* @param string $viewHelperString
* @param string $additionalViewHelpersString
* @return void
*/
protected function objectAccessorHandler(\TYPO3\Fluid\Core\Parser\ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString) {
$viewHelperString .= $additionalViewHelpersString;
$numberOfViewHelpers = 0;
// The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
// Resolves bug #5107.
if (strlen($delimiter) === 0 && strlen($viewHelperString) > 0) {
$viewHelperString = $objectAccessorString . $viewHelperString;
$objectAccessorString = '';
}
// ViewHelpers
$matches = array();
if (strlen($viewHelperString) > 0 && preg_match_all(self::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
// The last ViewHelper has to be added first for correct chaining.
foreach (array_reverse($matches) as $singleMatch) {
if (strlen($singleMatch['ViewHelperArguments']) > 0) {
$arguments = $this->postProcessArgumentsForObjectAccessor(
$this->recursiveArrayHandler($singleMatch['ViewHelperArguments'])
);
} else {
$arguments = array();
}
$this->initializeViewHelperAndAddItToStack($state, $singleMatch['NamespaceIdentifier'], $singleMatch['MethodIdentifier'], $arguments);
$numberOfViewHelpers++;
}
}
// Object Accessor
if (strlen($objectAccessorString) > 0) {
$node = $this->objectManager->get('TYPO3\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode', $objectAccessorString);
$this->callInterceptor($node, \TYPO3\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
$state->getNodeFromStack()->addChildNode($node);
}
// Close ViewHelper Tags if needed.
for ($i=0; $i<$numberOfViewHelpers; $i++) {
$node = $state->popNodeFromStack();
$this->callInterceptor($node, \TYPO3\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
}
}
/**
* Call all interceptors registered for a given interception point.
*
* @param \TYPO3\Fluid\Core\Parser\SyntaxTree\NodeInterface $node The syntax tree node which can be modified by the interceptors.
* @param integer $interceptionPoint the interception point. One of the \TYPO3\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
* @param \TYPO3\Fluid\Core\Parser\ParsingState $state the parsing state
* @return void
*/
protected function callInterceptor(\TYPO3\Fluid\Core\Parser\SyntaxTree\NodeInterface &$node, $interceptionPoint, \TYPO3\Fluid\Core\Parser\ParsingState $state) {
if ($this->configuration !== NULL) {
// $this->configuration is UNSET inside the arguments of a ViewHelper.
// That's why the interceptors are only called if the object accesor is not inside a ViewHelper Argument
// This could be a problem if We have a ViewHelper as an argument to another ViewHelper, and an ObjectAccessor nested inside there.
// TODO: Clean up this.
$interceptors = $this->configuration->getInterceptors($interceptionPoint);
if (count($interceptors) > 0) {
foreach ($interceptors as $interceptor) {
$node = $interceptor->process($node, $interceptionPoint, $state);
}
}
}
}
/**
* Post process the arguments for the ViewHelpers in the object accessor
* syntax. We need to convert an array into an array of (only) nodes
*
* @param array $arguments The arguments to be processed
* @return array the processed array
* @todo This method should become superflous once the rest has been refactored, so that this code is not needed.
*/
protected function postProcessArgumentsForObjectAccessor(array $arguments) {
foreach ($arguments as $argumentName => $argumentValue) {
if (!($argumentValue instanceof \TYPO3\Fluid\Core\Parser\SyntaxTree\AbstractNode)) {
$arguments[$argumentName] = $this->objectManager->get('TYPO3\Fluid\Core\Parser\SyntaxTree\TextNode', (string)$argumentValue);
}
}
return $arguments;
}
/**
* Parse arguments of a given tag, and build up the Arguments Object Tree
* for each argument.
* Returns an associative array, where the key is the name of the argument,
* and the value is a single Argument Object Tree.
*
* @param string $argumentsString All arguments as string
* @return array An associative array of objects, where the key is the argument name.
*/
protected function parseArguments($argumentsString) {
$argumentsObjectTree = array();
$matches = array();
if (preg_match_all(self::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
$configurationBackup = $this->configuration;
$this->configuration = NULL;
foreach ($matches as $singleMatch) {
$argument = $singleMatch['Argument'];
$value = $this->unquoteString($singleMatch['ValueQuoted']);
$argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
}
$this->configuration = $configurationBackup;
}
return $argumentsObjectTree;
}
/**
* Build up an argument object tree for the string in $argumentString.
* This builds up the tree for a single argument value.
*
* This method also does some performance optimizations, so in case
* no { or < is found, then we just return a TextNode.
*
* @param string $argumentString
* @return SyntaxTree\AbstractNode the corresponding argument object tree.
*/
protected function buildArgumentObjectTree($argumentString) {
if (strpos($argumentString, '{') === FALSE && strpos($argumentString, '<') === FALSE) {
return $this->objectManager->get('TYPO3\Fluid\Core\Parser\SyntaxTree\TextNode', $argumentString);
}
$splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
$rootNode = $this->buildObjectTree($splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS)->getRootNode();
return $rootNode;
}
/**
* Removes escapings from a given argument string and trims the outermost
* quotes.
*
* This method is meant as a helper for regular expression results.
*
* @param string $quotedValue Value to unquote
* @return string Unquoted value
*/
protected function unquoteString($quotedValue) {
switch ($quotedValue[0]) {
case '"':
$value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
break;
case "'":
$value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
break;
default:
$value = $quotedValue;
}
return str_replace('\\\\', '\\', $value);
}
/**
* Takes a regular expression template and replaces "NAMESPACE" with the
* currently registered namespace identifiers. Returns a regular expression
* which is ready to use.
*
* @param string $regularExpression Regular expression template
* @return string Regular expression ready to be used
*/
protected function prepareTemplateRegularExpression($regularExpression) {
return str_replace('NAMESPACE', implode('|', array_keys($this->namespaces)), $regularExpression);
}
/**
* Handler for everything which is not a ViewHelperNode.
*
* This includes Text, array syntax, and object accessor syntax.
*
* @param \TYPO3\Fluid\Core\Parser\ParsingState $state Current parsing state
* @param string $text Text to process
* @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
* @return void
*/
protected function textAndShorthandSyntaxHandler(\TYPO3\Fluid\Core\Parser\ParsingState $state, $text, $context) {
$sections = preg_split($this->prepareTemplateRegularExpression(self::$SPLIT_PATTERN_SHORTHANDSYNTAX), $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
foreach ($sections as $section) {
$matchedVariables = array();
if (preg_match(self::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
$this->objectAccessorHandler($state, $matchedVariables['Object'], $matchedVariables['Delimiter'], (isset($matchedVariables['ViewHelper']) ? $matchedVariables['ViewHelper'] : ''), (isset($matchedVariables['AdditionalViewHelpers']) ? $matchedVariables['AdditionalViewHelpers'] : ''));
} elseif ($context === self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS && preg_match(self::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0) {
// We only match arrays if we are INSIDE viewhelper arguments
$this->arrayHandler($state, $matchedVariables['Array']);
} else {
$this->textHandler($state, $section);
}
}
}
/**
* Handler for array syntax. This creates the array object recursively and
* adds it to the current node.
*
* @param \TYPO3\Fluid\Core\Parser\ParsingState $state The current parsing state
* @param string $arrayText The array as string.
* @return void
*/
protected function arrayHandler(\TYPO3\Fluid\Core\Parser\ParsingState $state, $arrayText) {
$state->getNodeFromStack()->addChildNode(
$this->objectManager->get('TYPO3\Fluid\Core\Parser\SyntaxTree\ArrayNode', $this->recursiveArrayHandler($arrayText))
);
}
/**
* Recursive function which takes the string representation of an array and
* builds an object tree from it.
*
* Deals with the following value types:
* - Numbers (Integers and Floats)
* - Strings
* - Variables
* - sub-arrays
*
* @param string $arrayText Array text
* @return SyntaxTree\ArrayNode the array node built up
* @throws \TYPO3\Fluid\Core\Parser\Exception
*/
protected function recursiveArrayHandler($arrayText) {
$matches = array();
if (preg_match_all(self::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER) > 0) {
$arrayToBuild = array();
foreach ($matches as $singleMatch) {
$arrayKey = $singleMatch['Key'];
if (!empty($singleMatch['VariableIdentifier'])) {
$arrayToBuild[$arrayKey] = $this->objectManager->get('TYPO3\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode', $singleMatch['VariableIdentifier']);
} elseif (array_key_exists('Number', $singleMatch) && ( !empty($singleMatch['Number']) || $singleMatch['Number'] === '0' ) ) {
$arrayToBuild[$arrayKey] = floatval($singleMatch['Number']);
} elseif ( ( array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']) ) ) {
$argumentString = $this->unquoteString($singleMatch['QuotedString']);
$arrayToBuild[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
} elseif ( array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray'])) {
$arrayToBuild[$arrayKey] = $this->objectManager->get('TYPO3\Fluid\Core\Parser\SyntaxTree\ArrayNode', $this->recursiveArrayHandler($singleMatch['Subarray']));
} else {
throw new \TYPO3\Fluid\Core\Parser\Exception('This exception should never be thrown, as the array value has to be of some type (Value given: "' . var_export($singleMatch, TRUE) . '"). Please post your template to the bugtracker at forge.typo3.org.', 1225136013);
}
}
return $arrayToBuild;
} else {
throw new \TYPO3\Fluid\Core\Parser\Exception('This exception should never be thrown, there is most likely some error in the regular expressions. Please post your template to the bugtracker at forge.typo3.org.', 1225136013);
}
}
/**
* Text node handler
*
* @param \TYPO3\Fluid\Core\Parser\ParsingState $state
* @param string $text
* @return void
*/
protected function textHandler(\TYPO3\Fluid\Core\Parser\ParsingState $state, $text) {
$node = $this->objectManager->get('TYPO3\Fluid\Core\Parser\SyntaxTree\TextNode', $text);
$this->callInterceptor($node, \TYPO3\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_TEXT, $state);
$state->getNodeFromStack()->addChildNode($node);
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment