Completed
Push — master ( e7fdda...fc5ac3 )
by Marc
02:00
created

TemplateParser::extractEscapingModifier()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 1
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
1
<?php
2
namespace TYPO3Fluid\Fluid\Core\Parser;
3
4
/*
5
 * This file belongs to the package "TYPO3 Fluid".
6
 * See LICENSE.txt that was shipped with this package.
7
 */
8
9
use TYPO3Fluid\Fluid\Core\Compiler\StopCompilingException;
10
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NumericNode;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
15
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
16
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
17
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
18
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
19
20
/**
21
 * Template parser building up an object syntax tree
22
 */
23
class TemplateParser
24
{
25
26
    /**
27
     * The following two constants are used for tracking whether we are currently
28
     * parsing ViewHelper arguments or not. This is used to parse arrays only as
29
     * ViewHelper argument.
30
     */
31
    const CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS = 1;
32
    const CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS = 2;
33
34
    /**
35
     * Whether or not the escaping interceptors are active
36
     *
37
     * @var boolean
38
     */
39
    protected $escapingEnabled = true;
40
41
    /**
42
     * @var Configuration
43
     */
44
    protected $configuration;
45
46
    /**
47
     * @var array
48
     */
49
    protected $settings;
50
51
    /**
52
     * @var RenderingContextInterface
53
     */
54
    protected $renderingContext;
55
56
    /**
57
     * @var integer
58
     */
59
    protected $pointerLineNumber = 1;
60
61
    /**
62
     * @var integer
63
     */
64
    protected $pointerLineCharacter = 1;
65
66
    /**
67
     * @var string
68
     */
69
    protected $pointerTemplateCode = null;
70
71
    /**
72
     * @var ParsedTemplateInterface[]
73
     */
74
    protected $parsedTemplates = [];
75
76
    /**
77
     * @param RenderingContextInterface $renderingContext
78
     * @return void
79
     */
80
    public function setRenderingContext(RenderingContextInterface $renderingContext)
81
    {
82
        $this->renderingContext = $renderingContext;
83
        $this->configuration = $renderingContext->buildParserConfiguration();
84
    }
85
86
    /**
87
     * Returns an array of current line number, character in line and reference template code;
88
     * for extraction when catching parser-related Exceptions during parsing.
89
     *
90
     * @return array
91
     */
92
    public function getCurrentParsingPointers()
93
    {
94
        return [$this->pointerLineNumber, $this->pointerLineCharacter, $this->pointerTemplateCode];
95
    }
96
97
    /**
98
     * @return boolean
99
     */
100
    public function isEscapingEnabled()
101
    {
102
        return $this->escapingEnabled;
103
    }
104
105
    /**
106
     * @param boolean $escapingEnabled
107
     * @return void
108
     */
109
    public function setEscapingEnabled($escapingEnabled)
110
    {
111
        $this->escapingEnabled = (boolean) $escapingEnabled;
112
    }
113
114
    /**
115
     * Parses a given template string and returns a parsed template object.
116
     *
117
     * The resulting ParsedTemplate can then be rendered by calling evaluate() on it.
118
     *
119
     * Normally, you should use a subclass of AbstractTemplateView instead of calling the
120
     * TemplateParser directly.
121
     *
122
     * @param string $templateString The template to parse as a string
123
     * @param string|null $templateIdentifier If the template has an identifying string it can be passed here to improve error reporting.
124
     * @return ParsingState Parsed template
125
     * @throws Exception
126
     */
127
    public function parse($templateString, $templateIdentifier = null)
128
    {
129
        if (!is_string($templateString)) {
130
            throw new Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
131
        }
132
        try {
133
            $this->reset();
134
135
            $templateString = $this->preProcessTemplateSource($templateString);
136
137
            $splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
138
            $parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
139
        } catch (Exception $error) {
140
            throw $this->createParsingRelatedExceptionWithContext($error, $templateIdentifier);
141
        }
142
        $this->parsedTemplates[$templateIdentifier] = $parsingState;
143
        return $parsingState;
144
    }
145
146
    /**
147
     * @param \Exception $error
148
     * @param string $templateIdentifier
149
     * @throws \Exception
150
     */
151
    public function createParsingRelatedExceptionWithContext(\Exception $error, $templateIdentifier)
152
    {
153
        list ($line, $character, $templateCode) = $this->getCurrentParsingPointers();
154
        $exceptionClass = get_class($error);
155
        return new $exceptionClass(
156
            sprintf(
157
                'Fluid parse error in template %s, line %d at character %d. Error: %s (error code %d). Template source chunk: %s',
158
                $templateIdentifier,
159
                $line,
160
                $character,
161
                $error->getMessage(),
162
                $error->getCode(),
163
                $templateCode
164
            ),
165
            $error->getCode(),
166
            $error
167
        );
168
    }
169
170
    /**
171
     * @param string $templateIdentifier
172
     * @param \Closure $templateSourceClosure Closure which returns the template source if needed
173
     * @return ParsedTemplateInterface
174
     */
175
    public function getOrParseAndStoreTemplate($templateIdentifier, $templateSourceClosure)
176
    {
177
        $compiler = $this->renderingContext->getTemplateCompiler();
178
        if (isset($this->parsedTemplates[$templateIdentifier])) {
179
            $parsedTemplate = $this->parsedTemplates[$templateIdentifier];
180
        } elseif ($compiler->has($templateIdentifier)) {
181
            $parsedTemplate = $compiler->get($templateIdentifier);
182
        } else {
183
            $parsedTemplate = $this->parse(
184
                $templateSourceClosure($this, $this->renderingContext->getTemplatePaths()),
185
                $templateIdentifier
186
            );
187
            $parsedTemplate->setIdentifier($templateIdentifier);
188
            $this->parsedTemplates[$templateIdentifier] = $parsedTemplate;
189
            if ($parsedTemplate->isCompilable()) {
190
                try {
191
                    $compiler->store($templateIdentifier, $parsedTemplate);
192
                } catch (StopCompilingException $stop) {
193
                    $parsedTemplate->setCompilable(false);
194
                    return $parsedTemplate;
195
                }
196
            }
197
        }
198
        return $parsedTemplate;
199
    }
200
201
    /**
202
     * Pre-process the template source, making all registered TemplateProcessors
203
     * do what they need to do with the template source before it is parsed.
204
     *
205
     * @param string $templateSource
206
     * @return string
207
     */
208
    protected function preProcessTemplateSource($templateSource)
209
    {
210
        foreach ($this->renderingContext->getTemplateProcessors() as $templateProcessor) {
211
            $templateSource = $templateProcessor->preProcessSource($templateSource);
212
        }
213
        return $templateSource;
214
    }
215
216
    /**
217
     * Resets the parser to its default values.
218
     *
219
     * @return void
220
     */
221
    protected function reset()
222
    {
223
        $this->escapingEnabled = true;
224
        $this->pointerLineNumber = 1;
225
        $this->pointerLineCharacter = 1;
226
    }
227
228
    /**
229
     * Splits the template string on all dynamic tags found.
230
     *
231
     * @param string $templateString Template string to split.
232
     * @return array Splitted template
233
     */
234
    protected function splitTemplateAtDynamicTags($templateString)
235
    {
236
        return preg_split(Patterns::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
237
    }
238
239
    /**
240
     * Build object tree from the split template
241
     *
242
     * @param array $splitTemplate The split template, so that every tag with a namespace declaration is already a seperate array element.
243
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
244
     * @return ParsingState
245
     * @throws Exception
246
     */
247
    protected function buildObjectTree(array $splitTemplate, $context)
248
    {
249
        $state = $this->getParsingState();
250
        $previousBlock = '';
251
252
        foreach ($splitTemplate as $templateElement) {
253
            if ($context === self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS) {
254
                // Store a neat reference to the outermost chunk of Fluid template code.
255
                // Don't store the reference if parsing ViewHelper arguments object tree;
256
                // we want the reference code to contain *all* of the ViewHelper call.
257
                $this->pointerTemplateCode = $templateElement;
258
            }
259
            $this->pointerLineNumber += substr_count($templateElement, PHP_EOL);
260
            $this->pointerLineCharacter = strlen(substr($previousBlock, strrpos($previousBlock, PHP_EOL))) + 1;
261
            $previousBlock = $templateElement;
262
            $matchedVariables = [];
263
264
            if (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
265
                if ($this->openingViewHelperTagHandler(
266
                    $state,
267
                    $matchedVariables['NamespaceIdentifier'],
268
                    $matchedVariables['MethodIdentifier'],
269
                    $matchedVariables['Attributes'],
270
                    ($matchedVariables['Selfclosing'] === '' ? false : true),
271
                    $templateElement
272
                )) {
273
                    continue;
274
                }
275
            } elseif (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
276
                if ($this->closingViewHelperTagHandler(
277
                    $state,
278
                    $matchedVariables['NamespaceIdentifier'],
279
                    $matchedVariables['MethodIdentifier']
280
                )) {
281
                    continue;
282
                }
283
            }
284
            $this->textAndShorthandSyntaxHandler($state, $templateElement, $context);
285
        }
286
287
        if ($state->countNodeStack() !== 1) {
288
            throw new Exception('Not all tags were closed!', 1238169398);
289
        }
290
        return $state;
291
    }
292
    /**
293
     * Handles an opening or self-closing view helper tag.
294
     *
295
     * @param ParsingState $state Current parsing state
296
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
297
     * @param string $methodIdentifier Method identifier
298
     * @param string $arguments Arguments string, not yet parsed
299
     * @param boolean $selfclosing true, if the tag is a self-closing tag.
300
     * @param string $templateElement The template code containing the ViewHelper call
301
     * @return NodeInterface|null
302
     */
303
    protected function openingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing, $templateElement)
304
    {
305
        $viewHelperNode = $this->initializeViewHelperAndAddItToStack(
306
            $state,
307
            $namespaceIdentifier,
308
            $methodIdentifier,
309
            $this->parseArguments($arguments)
310
        );
311
312
        if ($viewHelperNode) {
313
            $viewHelperNode->setPointerTemplateCode($templateElement);
314
            if ($selfclosing === true) {
315
                $state->popNodeFromStack();
316
                $this->callInterceptor($viewHelperNode, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
317
                // This needs to be called here because closingViewHelperTagHandler() is not triggered for self-closing tags
318
                $state->getNodeFromStack()->addChildNode($viewHelperNode);
319
            }
320
        }
321
322
        return $viewHelperNode;
323
    }
324
325
    /**
326
     * Initialize the given ViewHelper and adds it to the current node and to
327
     * the stack.
328
     *
329
     * @param ParsingState $state Current parsing state
330
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
331
     * @param string $methodIdentifier Method identifier
332
     * @param array $argumentsObjectTree Arguments object tree
333
     * @return null|NodeInterface An instance of ViewHelperNode if identity was valid - NULL if the namespace/identity was not registered
334
     * @throws Exception
335
     */
336
    protected function initializeViewHelperAndAddItToStack(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree)
337
    {
338
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
339
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
340
            return null;
341
        }
342
        $currentViewHelperNode = new ViewHelperNode(
343
            $this->renderingContext,
344
            $namespaceIdentifier,
345
            $methodIdentifier,
346
            $argumentsObjectTree,
347
            $state
348
        );
349
350
        $this->callInterceptor($currentViewHelperNode, InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER, $state);
351
        $viewHelper = $currentViewHelperNode->getUninitializedViewHelper();
352
        $viewHelper::postParseEvent($currentViewHelperNode, $argumentsObjectTree, $state->getVariableContainer());
353
        $state->pushNodeToStack($currentViewHelperNode);
354
355
        return $currentViewHelperNode;
356
    }
357
358
    /**
359
     * Handles a closing view helper tag
360
     *
361
     * @param ParsingState $state The current parsing state
362
     * @param string $namespaceIdentifier Namespace identifier for the closing tag.
363
     * @param string $methodIdentifier Method identifier.
364
     * @return boolean whether the viewHelper was found and added to the stack or not
365
     * @throws Exception
366
     */
367
    protected function closingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier)
368
    {
369
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
370
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
371
            return false;
372
        }
373
        $lastStackElement = $state->popNodeFromStack();
374
        if (!($lastStackElement instanceof ViewHelperNode)) {
375
            throw new Exception('You closed a templating tag which you never opened!', 1224485838);
376
        }
377
        $actualViewHelperClassName = $viewHelperResolver->resolveViewHelperClassName($namespaceIdentifier, $methodIdentifier);
378
        $expectedViewHelperClassName = $lastStackElement->getViewHelperClassName();
379
        if ($actualViewHelperClassName !== $expectedViewHelperClassName) {
380
            throw new Exception(
381
                'Templating tags not properly nested. Expected: ' . $expectedViewHelperClassName . '; Actual: ' .
382
                $actualViewHelperClassName,
383
                1224485398
384
            );
385
        }
386
        $this->callInterceptor($lastStackElement, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
387
        $state->getNodeFromStack()->addChildNode($lastStackElement);
388
389
        return true;
390
    }
391
392
    /**
393
     * Handles the appearance of an object accessor (like {posts.author.email}).
394
     * Creates a new instance of \TYPO3Fluid\Fluid\ObjectAccessorNode.
395
     *
396
     * Handles ViewHelpers as well which are in the shorthand syntax.
397
     *
398
     * @param ParsingState $state The current parsing state
399
     * @param string $objectAccessorString String which identifies which objects to fetch
400
     * @param string $delimiter
401
     * @param string $viewHelperString
402
     * @param string $additionalViewHelpersString
403
     * @return void
404
     */
405
    protected function objectAccessorHandler(ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString)
406
    {
407
        $viewHelperString .= $additionalViewHelpersString;
408
        $numberOfViewHelpers = 0;
409
410
        // The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
411
        // Resolves bug #5107.
412
        if (strlen($delimiter) === 0 && strlen($viewHelperString) > 0) {
413
            $viewHelperString = $objectAccessorString . $viewHelperString;
414
            $objectAccessorString = '';
415
        }
416
417
        // ViewHelpers
418
        $matches = [];
419
        if (strlen($viewHelperString) > 0 && preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
420
            // The last ViewHelper has to be added first for correct chaining.
421
            foreach (array_reverse($matches) as $singleMatch) {
422
                if (strlen($singleMatch['ViewHelperArguments']) > 0) {
423
                    $arguments = $this->recursiveArrayHandler($singleMatch['ViewHelperArguments']);
424
                } else {
425
                    $arguments = [];
426
                }
427
                $viewHelperNode = $this->initializeViewHelperAndAddItToStack($state, $singleMatch['NamespaceIdentifier'], $singleMatch['MethodIdentifier'], $arguments);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $viewHelperNode is correct as $this->initializeViewHel...entifier'], $arguments) (which targets TYPO3Fluid\Fluid\Core\Pa...HelperAndAddItToStack()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
428
                if ($viewHelperNode) {
429
                    $numberOfViewHelpers++;
430
                }
431
            }
432
        }
433
434
        // Object Accessor
435
        if (strlen($objectAccessorString) > 0) {
436
            $node = new ObjectAccessorNode($objectAccessorString);
437
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
438
            $state->getNodeFromStack()->addChildNode($node);
439
        }
440
441
        // Close ViewHelper Tags if needed.
442
        for ($i = 0; $i < $numberOfViewHelpers; $i++) {
443
            $node = $state->popNodeFromStack();
444
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
445
            $state->getNodeFromStack()->addChildNode($node);
446
        }
447
    }
448
449
    /**
450
     * Call all interceptors registered for a given interception point.
451
     *
452
     * @param NodeInterface $node The syntax tree node which can be modified by the interceptors.
453
     * @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
454
     * @param ParsingState $state the parsing state
455
     * @return void
456
     */
457
    protected function callInterceptor(NodeInterface & $node, $interceptionPoint, ParsingState $state)
458
    {
459
        if ($this->configuration === null) {
460
            return;
461
        }
462
        if ($this->escapingEnabled) {
463
            /** @var $interceptor InterceptorInterface */
464
            foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) {
465
                $node = $interceptor->process($node, $interceptionPoint, $state);
466
            }
467
        }
468
469
        /** @var $interceptor InterceptorInterface */
470
        foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) {
471
            $node = $interceptor->process($node, $interceptionPoint, $state);
472
        }
473
    }
474
475
    /**
476
     * Parse arguments of a given tag, and build up the Arguments Object Tree
477
     * for each argument.
478
     * Returns an associative array, where the key is the name of the argument,
479
     * and the value is a single Argument Object Tree.
480
     *
481
     * @param string $argumentsString All arguments as string
482
     * @return array An associative array of objects, where the key is the argument name.
483
     */
484
    protected function parseArguments($argumentsString)
485
    {
486
        $argumentsObjectTree = [];
487
        $matches = [];
488
        if (preg_match_all(Patterns::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
489
            $escapingEnabledBackup = $this->escapingEnabled;
490
            $this->escapingEnabled = false;
491
            foreach ($matches as $singleMatch) {
492
                $argument = $singleMatch['Argument'];
493
                $value = $this->unquoteString($singleMatch['ValueQuoted']);
494
                $argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
495
            }
496
            $this->escapingEnabled = $escapingEnabledBackup;
497
        }
498
        return $argumentsObjectTree;
499
    }
500
501
    /**
502
     * Build up an argument object tree for the string in $argumentString.
503
     * This builds up the tree for a single argument value.
504
     *
505
     * This method also does some performance optimizations, so in case
506
     * no { or < is found, then we just return a TextNode.
507
     *
508
     * @param string $argumentString
509
     * @return SyntaxTree\NodeInterface the corresponding argument object tree.
510
     */
511
    protected function buildArgumentObjectTree($argumentString)
512
    {
513
        if (strpos($argumentString, '{') === false && strpos($argumentString, '<') === false) {
514
            if (is_numeric($argumentString)) {
515
                return new NumericNode($argumentString);
516
            }
517
            return new TextNode($argumentString);
518
        }
519
        $splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
520
        $rootNode = $this->buildObjectTree($splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS)->getRootNode();
521
        return $rootNode;
522
    }
523
524
    /**
525
     * Removes escapings from a given argument string and trims the outermost
526
     * quotes.
527
     *
528
     * This method is meant as a helper for regular expression results.
529
     *
530
     * @param string $quotedValue Value to unquote
531
     * @return string Unquoted value
532
     */
533
    public function unquoteString($quotedValue)
534
    {
535
        $value = $quotedValue;
536
        if ($quotedValue{0} === '"') {
537
            $value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
538
        } elseif ($quotedValue{0} === '\'') {
539
            $value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
540
        }
541
        return str_replace('\\\\', '\\', $value);
542
    }
543
544
    /**
545
     * Handler for everything which is not a ViewHelperNode.
546
     *
547
     * This includes Text, array syntax, and object accessor syntax.
548
     *
549
     * @param ParsingState $state Current parsing state
550
     * @param string $text Text to process
551
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
552
     * @return void
553
     */
554
    protected function textAndShorthandSyntaxHandler(ParsingState $state, $text, $context)
555
    {
556
        $sections = preg_split(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
557
        foreach ($sections as $section) {
558
            $matchedVariables = [];
559
            $expressionNode = null;
560
            if (preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
561
                $this->objectAccessorHandler(
562
                    $state,
563
                    $matchedVariables['Object'],
564
                    $matchedVariables['Delimiter'],
565
                    (isset($matchedVariables['ViewHelper']) ? $matchedVariables['ViewHelper'] : ''),
566
                    (isset($matchedVariables['AdditionalViewHelpers']) ? $matchedVariables['AdditionalViewHelpers'] : '')
567
                );
568
            } elseif ($context === self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS
569
                && preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0
570
            ) {
571
                // We only match arrays if we are INSIDE viewhelper arguments
572
                $this->arrayHandler($state, $this->recursiveArrayHandler($matchedVariables['Array']));
573
            } else {
574
                // We ask custom ExpressionNode instances from ViewHelperResolver
575
                // if any match our expression:
576
                foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
577
                    $detectionExpression = $expressionNodeTypeClassName::$detectionExpression;
578
                    $matchedVariables = [];
579
                    preg_match_all($detectionExpression, $section, $matchedVariables, PREG_SET_ORDER);
580
                    if (is_array($matchedVariables) === true) {
581
                        foreach ($matchedVariables as $matchedVariableSet) {
582
                            $expressionStartPosition = strpos($section, $matchedVariableSet[0]);
583
                            /** @var ExpressionNodeInterface $expressionNode */
584
                            $expressionNode = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $state);
585
                            if ($expressionStartPosition > 0) {
586
                                $state->getNodeFromStack()->addChildNode(new TextNode(substr($section, 0, $expressionStartPosition)));
587
                            }
588
                            $state->getNodeFromStack()->addChildNode($expressionNode);
589
                            $expressionEndPosition = $expressionStartPosition + strlen($matchedVariableSet[0]);
590
                            if ($expressionEndPosition < strlen($section)) {
591
                                $this->textAndShorthandSyntaxHandler($state, substr($section, $expressionEndPosition), $context);
592
                                break;
593
                            }
594
                        }
595
                    }
596
                }
597
598
                if ($expressionNode) {
599
                    // Trigger initial parse-time evaluation to allow the node to manipulate the rendering context.
600
                    $expressionNode->evaluate($this->renderingContext);
601
                } else {
602
                    // As fallback we simply render the expression back as template content.
603
                    $this->textHandler($state, $section);
604
                }
605
            }
606
        }
607
    }
608
609
    /**
610
     * Handler for array syntax. This creates the array object recursively and
611
     * adds it to the current node.
612
     *
613
     * @param ParsingState $state The current parsing state
614
     * @param NodeInterface[] $arrayText The array as string.
615
     * @return void
616
     */
617
    protected function arrayHandler(ParsingState $state, $arrayText)
618
    {
619
        $arrayNode = new ArrayNode($arrayText);
620
        $state->getNodeFromStack()->addChildNode($arrayNode);
621
    }
622
623
    /**
624
     * Recursive function which takes the string representation of an array and
625
     * builds an object tree from it.
626
     *
627
     * Deals with the following value types:
628
     * - Numbers (Integers and Floats)
629
     * - Strings
630
     * - Variables
631
     * - sub-arrays
632
     *
633
     * @param string $arrayText Array text
634
     * @return NodeInterface[] the array node built up
635
     * @throws Exception
636
     */
637
    protected function recursiveArrayHandler($arrayText)
638
    {
639
        $matches = [];
640
        $arrayToBuild = [];
641
        if (preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER)) {
642
            foreach ($matches as $singleMatch) {
643
                $arrayKey = $this->unquoteString($singleMatch['Key']);
644
                if (!empty($singleMatch['VariableIdentifier'])) {
645
                    $arrayToBuild[$arrayKey] = new ObjectAccessorNode($singleMatch['VariableIdentifier']);
646
                } elseif (array_key_exists('Number', $singleMatch) && (!empty($singleMatch['Number']) || $singleMatch['Number'] === '0')) {
647
                    $arrayToBuild[$arrayKey] = (float)$singleMatch['Number'];
648
                } elseif ((array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']))) {
649
                    $argumentString = $this->unquoteString($singleMatch['QuotedString']);
650
                    $arrayToBuild[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
651
                } elseif (array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray'])) {
652
                    $arrayToBuild[$arrayKey] = new ArrayNode($this->recursiveArrayHandler($singleMatch['Subarray']));
653
                }
654
            }
655
        }
656
        return $arrayToBuild;
657
    }
658
659
    /**
660
     * Text node handler
661
     *
662
     * @param ParsingState $state
663
     * @param string $text
664
     * @return void
665
     */
666
    protected function textHandler(ParsingState $state, $text)
667
    {
668
        $node = new TextNode($text);
669
        $this->callInterceptor($node, InterceptorInterface::INTERCEPT_TEXT, $state);
670
        $state->getNodeFromStack()->addChildNode($node);
671
    }
672
673
    /**
674
     * @return ParsingState
675
     */
676
    protected function getParsingState()
677
    {
678
        $rootNode = new RootNode();
679
        $variableProvider = $this->renderingContext->getVariableProvider();
680
        $state = new ParsingState();
681
        $state->setRootNode($rootNode);
682
        $state->pushNodeToStack($rootNode);
683
        $state->setVariableProvider($variableProvider->getScopeCopy($variableProvider->getAll()));
684
        return $state;
685
    }
686
}
687