Completed
Push — master ( b4adf1...7cbf62 )
by Marc
02:01
created

TemplateParser::parse()   A

Complexity

Conditions 3
Paths 6

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 6
nop 2
dl 0
loc 18
rs 9.4285
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\ExpressionException;
11
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NumericNode;
15
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
16
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
17
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
18
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
19
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
20
21
/**
22
 * Template parser building up an object syntax tree
23
 */
24
class TemplateParser
25
{
26
27
    /**
28
     * The following two constants are used for tracking whether we are currently
29
     * parsing ViewHelper arguments or not. This is used to parse arrays only as
30
     * ViewHelper argument.
31
     */
32
    const CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS = 1;
33
    const CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS = 2;
34
35
    /**
36
     * Whether or not the escaping interceptors are active
37
     *
38
     * @var boolean
39
     */
40
    protected $escapingEnabled = true;
41
42
    /**
43
     * @var Configuration
44
     */
45
    protected $configuration;
46
47
    /**
48
     * @var array
49
     */
50
    protected $settings;
51
52
    /**
53
     * @var RenderingContextInterface
54
     */
55
    protected $renderingContext;
56
57
    /**
58
     * @var integer
59
     */
60
    protected $pointerLineNumber = 1;
61
62
    /**
63
     * @var integer
64
     */
65
    protected $pointerLineCharacter = 1;
66
67
    /**
68
     * @var string
69
     */
70
    protected $pointerTemplateCode = null;
71
72
    /**
73
     * @var ParsedTemplateInterface[]
74
     */
75
    protected $parsedTemplates = [];
76
77
    /**
78
     * @param RenderingContextInterface $renderingContext
79
     * @return void
80
     */
81
    public function setRenderingContext(RenderingContextInterface $renderingContext)
82
    {
83
        $this->renderingContext = $renderingContext;
84
        $this->configuration = $renderingContext->buildParserConfiguration();
85
    }
86
87
    /**
88
     * Returns an array of current line number, character in line and reference template code;
89
     * for extraction when catching parser-related Exceptions during parsing.
90
     *
91
     * @return array
92
     */
93
    public function getCurrentParsingPointers()
94
    {
95
        return [$this->pointerLineNumber, $this->pointerLineCharacter, $this->pointerTemplateCode];
96
    }
97
98
    /**
99
     * @return boolean
100
     */
101
    public function isEscapingEnabled()
102
    {
103
        return $this->escapingEnabled;
104
    }
105
106
    /**
107
     * @param boolean $escapingEnabled
108
     * @return void
109
     */
110
    public function setEscapingEnabled($escapingEnabled)
111
    {
112
        $this->escapingEnabled = (boolean) $escapingEnabled;
113
    }
114
115
    /**
116
     * Parses a given template string and returns a parsed template object.
117
     *
118
     * The resulting ParsedTemplate can then be rendered by calling evaluate() on it.
119
     *
120
     * Normally, you should use a subclass of AbstractTemplateView instead of calling the
121
     * TemplateParser directly.
122
     *
123
     * @param string $templateString The template to parse as a string
124
     * @param string|null $templateIdentifier If the template has an identifying string it can be passed here to improve error reporting.
125
     * @return ParsingState Parsed template
126
     * @throws Exception
127
     */
128
    public function parse($templateString, $templateIdentifier = null)
129
    {
130
        if (!is_string($templateString)) {
131
            throw new Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
132
        }
133
        try {
134
            $this->reset();
135
136
            $templateString = $this->preProcessTemplateSource($templateString);
137
138
            $splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
139
            $parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
140
        } catch (Exception $error) {
141
            throw $this->createParsingRelatedExceptionWithContext($error, $templateIdentifier);
142
        }
143
        $this->parsedTemplates[$templateIdentifier] = $parsingState;
144
        return $parsingState;
145
    }
146
147
    /**
148
     * @param \Exception $error
149
     * @param string $templateIdentifier
150
     * @throws \Exception
151
     */
152
    public function createParsingRelatedExceptionWithContext(\Exception $error, $templateIdentifier)
153
    {
154
        list ($line, $character, $templateCode) = $this->getCurrentParsingPointers();
155
        $exceptionClass = get_class($error);
156
        return new $exceptionClass(
157
            sprintf(
158
                'Fluid parse error in template %s, line %d at character %d. Error: %s (error code %d). Template source chunk: %s',
159
                $templateIdentifier,
160
                $line,
161
                $character,
162
                $error->getMessage(),
163
                $error->getCode(),
164
                $templateCode
165
            ),
166
            $error->getCode(),
167
            $error
168
        );
169
    }
170
171
    /**
172
     * @param string $templateIdentifier
173
     * @param \Closure $templateSourceClosure Closure which returns the template source if needed
174
     * @return ParsedTemplateInterface
175
     */
176
    public function getOrParseAndStoreTemplate($templateIdentifier, $templateSourceClosure)
177
    {
178
        $compiler = $this->renderingContext->getTemplateCompiler();
179
        if (isset($this->parsedTemplates[$templateIdentifier])) {
180
            $parsedTemplate = $this->parsedTemplates[$templateIdentifier];
181
        } elseif ($compiler->has($templateIdentifier)) {
182
            $parsedTemplate = $compiler->get($templateIdentifier);
183
        } else {
184
            $parsedTemplate = $this->parse(
185
                $templateSourceClosure($this, $this->renderingContext->getTemplatePaths()),
186
                $templateIdentifier
187
            );
188
            $parsedTemplate->setIdentifier($templateIdentifier);
189
            $this->parsedTemplates[$templateIdentifier] = $parsedTemplate;
190
            if ($parsedTemplate->isCompilable()) {
191
                try {
192
                    $compiler->store($templateIdentifier, $parsedTemplate);
193
                } catch (StopCompilingException $stop) {
194
                    $this->renderingContext->getErrorHandler()->handleCompilerError($stop);
195
                    $parsedTemplate->setCompilable(false);
196
                    return $parsedTemplate;
197
                }
198
            }
199
        }
200
        return $parsedTemplate;
201
    }
202
203
    /**
204
     * Pre-process the template source, making all registered TemplateProcessors
205
     * do what they need to do with the template source before it is parsed.
206
     *
207
     * @param string $templateSource
208
     * @return string
209
     */
210
    protected function preProcessTemplateSource($templateSource)
211
    {
212
        foreach ($this->renderingContext->getTemplateProcessors() as $templateProcessor) {
213
            $templateSource = $templateProcessor->preProcessSource($templateSource);
214
        }
215
        return $templateSource;
216
    }
217
218
    /**
219
     * Resets the parser to its default values.
220
     *
221
     * @return void
222
     */
223
    protected function reset()
224
    {
225
        $this->escapingEnabled = true;
226
        $this->pointerLineNumber = 1;
227
        $this->pointerLineCharacter = 1;
228
    }
229
230
    /**
231
     * Splits the template string on all dynamic tags found.
232
     *
233
     * @param string $templateString Template string to split.
234
     * @return array Splitted template
235
     */
236
    protected function splitTemplateAtDynamicTags($templateString)
237
    {
238
        return preg_split(Patterns::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
239
    }
240
241
    /**
242
     * Build object tree from the split template
243
     *
244
     * @param array $splitTemplate The split template, so that every tag with a namespace declaration is already a seperate array element.
245
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
246
     * @return ParsingState
247
     * @throws Exception
248
     */
249
    protected function buildObjectTree(array $splitTemplate, $context)
250
    {
251
        $state = $this->getParsingState();
252
        $previousBlock = '';
253
254
        foreach ($splitTemplate as $templateElement) {
255
            if ($context === self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS) {
256
                // Store a neat reference to the outermost chunk of Fluid template code.
257
                // Don't store the reference if parsing ViewHelper arguments object tree;
258
                // we want the reference code to contain *all* of the ViewHelper call.
259
                $this->pointerTemplateCode = $templateElement;
260
            }
261
            $this->pointerLineNumber += substr_count($templateElement, PHP_EOL);
262
            $this->pointerLineCharacter = strlen(substr($previousBlock, strrpos($previousBlock, PHP_EOL))) + 1;
263
            $previousBlock = $templateElement;
264
            $matchedVariables = [];
265
266
            if (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
267
                if ($this->openingViewHelperTagHandler(
268
                    $state,
269
                    $matchedVariables['NamespaceIdentifier'],
270
                    $matchedVariables['MethodIdentifier'],
271
                    $matchedVariables['Attributes'],
272
                    ($matchedVariables['Selfclosing'] === '' ? false : true),
273
                    $templateElement
274
                )) {
275
                    continue;
276
                }
277
            } elseif (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
278
                if ($this->closingViewHelperTagHandler(
279
                    $state,
280
                    $matchedVariables['NamespaceIdentifier'],
281
                    $matchedVariables['MethodIdentifier']
282
                )) {
283
                    continue;
284
                }
285
            }
286
            $this->textAndShorthandSyntaxHandler($state, $templateElement, $context);
287
        }
288
289
        if ($state->countNodeStack() !== 1) {
290
            throw new Exception(
291
                'Not all tags were closed!',
292
                1238169398
293
            );
294
        }
295
        return $state;
296
    }
297
    /**
298
     * Handles an opening or self-closing view helper tag.
299
     *
300
     * @param ParsingState $state Current parsing state
301
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
302
     * @param string $methodIdentifier Method identifier
303
     * @param string $arguments Arguments string, not yet parsed
304
     * @param boolean $selfclosing true, if the tag is a self-closing tag.
305
     * @param string $templateElement The template code containing the ViewHelper call
306
     * @return NodeInterface|null
307
     */
308
    protected function openingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing, $templateElement)
309
    {
310
        $viewHelperNode = $this->initializeViewHelperAndAddItToStack(
311
            $state,
312
            $namespaceIdentifier,
313
            $methodIdentifier,
314
            $this->parseArguments($arguments)
315
        );
316
317
        if ($viewHelperNode) {
318
            $viewHelperNode->setPointerTemplateCode($templateElement);
319
            if ($selfclosing === true) {
320
                $state->popNodeFromStack();
321
                $this->callInterceptor($viewHelperNode, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
322
                // This needs to be called here because closingViewHelperTagHandler() is not triggered for self-closing tags
323
                $state->getNodeFromStack()->addChildNode($viewHelperNode);
324
            }
325
        }
326
327
        return $viewHelperNode;
328
    }
329
330
    /**
331
     * Initialize the given ViewHelper and adds it to the current node and to
332
     * the stack.
333
     *
334
     * @param ParsingState $state Current parsing state
335
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
336
     * @param string $methodIdentifier Method identifier
337
     * @param array $argumentsObjectTree Arguments object tree
338
     * @return null|NodeInterface An instance of ViewHelperNode if identity was valid - NULL if the namespace/identity was not registered
339
     * @throws Exception
340
     */
341
    protected function initializeViewHelperAndAddItToStack(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree)
342
    {
343
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
344
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
345
            return null;
346
        }
347
        try {
348
            $currentViewHelperNode = new ViewHelperNode(
349
                $this->renderingContext,
350
                $namespaceIdentifier,
351
                $methodIdentifier,
352
                $argumentsObjectTree,
353
                $state
354
            );
355
356
            $this->callInterceptor($currentViewHelperNode, InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER, $state);
357
            $viewHelper = $currentViewHelperNode->getUninitializedViewHelper();
358
            $viewHelper::postParseEvent($currentViewHelperNode, $argumentsObjectTree, $state->getVariableContainer());
359
            $state->pushNodeToStack($currentViewHelperNode);
360
            return $currentViewHelperNode;
361
        } catch (\TYPO3Fluid\Fluid\Core\ViewHelper\Exception $error) {
362
            $this->textHandler(
363
                $state,
364
                $this->renderingContext->getErrorHandler()->handleViewHelperError($error)
365
            );
366
        } catch (Exception $error) {
367
            $this->textHandler(
368
                $state,
369
                $this->renderingContext->getErrorHandler()->handleParserError($error)
370
            );
371
        }
372
        return null;
373
    }
374
375
    /**
376
     * Handles a closing view helper tag
377
     *
378
     * @param ParsingState $state The current parsing state
379
     * @param string $namespaceIdentifier Namespace identifier for the closing tag.
380
     * @param string $methodIdentifier Method identifier.
381
     * @return boolean whether the viewHelper was found and added to the stack or not
382
     * @throws Exception
383
     */
384
    protected function closingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier)
385
    {
386
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
387
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
388
            return false;
389
        }
390
        $lastStackElement = $state->popNodeFromStack();
391
        if (!($lastStackElement instanceof ViewHelperNode)) {
392
            throw new Exception('You closed a templating tag which you never opened!', 1224485838);
393
        }
394
        $actualViewHelperClassName = $viewHelperResolver->resolveViewHelperClassName($namespaceIdentifier, $methodIdentifier);
395
        $expectedViewHelperClassName = $lastStackElement->getViewHelperClassName();
396
        if ($actualViewHelperClassName !== $expectedViewHelperClassName) {
397
            throw new Exception(
398
                'Templating tags not properly nested. Expected: ' . $expectedViewHelperClassName . '; Actual: ' .
399
                $actualViewHelperClassName,
400
                1224485398
401
            );
402
        }
403
        $this->callInterceptor($lastStackElement, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
404
        $state->getNodeFromStack()->addChildNode($lastStackElement);
405
406
        return true;
407
    }
408
409
    /**
410
     * Handles the appearance of an object accessor (like {posts.author.email}).
411
     * Creates a new instance of \TYPO3Fluid\Fluid\ObjectAccessorNode.
412
     *
413
     * Handles ViewHelpers as well which are in the shorthand syntax.
414
     *
415
     * @param ParsingState $state The current parsing state
416
     * @param string $objectAccessorString String which identifies which objects to fetch
417
     * @param string $delimiter
418
     * @param string $viewHelperString
419
     * @param string $additionalViewHelpersString
420
     * @return void
421
     */
422
    protected function objectAccessorHandler(ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString)
423
    {
424
        $viewHelperString .= $additionalViewHelpersString;
425
        $numberOfViewHelpers = 0;
426
427
        // The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
428
        // Resolves bug #5107.
429
        if (strlen($delimiter) === 0 && strlen($viewHelperString) > 0) {
430
            $viewHelperString = $objectAccessorString . $viewHelperString;
431
            $objectAccessorString = '';
432
        }
433
434
        // ViewHelpers
435
        $matches = [];
436
        if (strlen($viewHelperString) > 0 && preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
437
            // The last ViewHelper has to be added first for correct chaining.
438
            foreach (array_reverse($matches) as $singleMatch) {
439
                if (strlen($singleMatch['ViewHelperArguments']) > 0) {
440
                    $arguments = $this->recursiveArrayHandler($singleMatch['ViewHelperArguments']);
441
                } else {
442
                    $arguments = [];
443
                }
444
                $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...
445
                if ($viewHelperNode) {
446
                    $numberOfViewHelpers++;
447
                }
448
            }
449
        }
450
451
        // Object Accessor
452
        if (strlen($objectAccessorString) > 0) {
453
            $node = new ObjectAccessorNode($objectAccessorString);
454
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
455
            $state->getNodeFromStack()->addChildNode($node);
456
        }
457
458
        // Close ViewHelper Tags if needed.
459
        for ($i = 0; $i < $numberOfViewHelpers; $i++) {
460
            $node = $state->popNodeFromStack();
461
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
462
            $state->getNodeFromStack()->addChildNode($node);
463
        }
464
    }
465
466
    /**
467
     * Call all interceptors registered for a given interception point.
468
     *
469
     * @param NodeInterface $node The syntax tree node which can be modified by the interceptors.
470
     * @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
471
     * @param ParsingState $state the parsing state
472
     * @return void
473
     */
474
    protected function callInterceptor(NodeInterface & $node, $interceptionPoint, ParsingState $state)
475
    {
476
        if ($this->configuration === null) {
477
            return;
478
        }
479
        if ($this->escapingEnabled) {
480
            /** @var $interceptor InterceptorInterface */
481
            foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) {
482
                $node = $interceptor->process($node, $interceptionPoint, $state);
483
            }
484
        }
485
486
        /** @var $interceptor InterceptorInterface */
487
        foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) {
488
            $node = $interceptor->process($node, $interceptionPoint, $state);
489
        }
490
    }
491
492
    /**
493
     * Parse arguments of a given tag, and build up the Arguments Object Tree
494
     * for each argument.
495
     * Returns an associative array, where the key is the name of the argument,
496
     * and the value is a single Argument Object Tree.
497
     *
498
     * @param string $argumentsString All arguments as string
499
     * @return array An associative array of objects, where the key is the argument name.
500
     */
501
    protected function parseArguments($argumentsString)
502
    {
503
        $argumentsObjectTree = [];
504
        $matches = [];
505
        if (preg_match_all(Patterns::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
506
            $escapingEnabledBackup = $this->escapingEnabled;
507
            $this->escapingEnabled = false;
508
            foreach ($matches as $singleMatch) {
509
                $argument = $singleMatch['Argument'];
510
                $value = $this->unquoteString($singleMatch['ValueQuoted']);
511
                $argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
512
            }
513
            $this->escapingEnabled = $escapingEnabledBackup;
514
        }
515
        return $argumentsObjectTree;
516
    }
517
518
    /**
519
     * Build up an argument object tree for the string in $argumentString.
520
     * This builds up the tree for a single argument value.
521
     *
522
     * This method also does some performance optimizations, so in case
523
     * no { or < is found, then we just return a TextNode.
524
     *
525
     * @param string $argumentString
526
     * @return SyntaxTree\NodeInterface the corresponding argument object tree.
527
     */
528
    protected function buildArgumentObjectTree($argumentString)
529
    {
530
        if (strpos($argumentString, '{') === false && strpos($argumentString, '<') === false) {
531
            if (is_numeric($argumentString)) {
532
                return new NumericNode($argumentString);
533
            }
534
            return new TextNode($argumentString);
535
        }
536
        $splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
537
        $rootNode = $this->buildObjectTree($splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS)->getRootNode();
538
        return $rootNode;
539
    }
540
541
    /**
542
     * Removes escapings from a given argument string and trims the outermost
543
     * quotes.
544
     *
545
     * This method is meant as a helper for regular expression results.
546
     *
547
     * @param string $quotedValue Value to unquote
548
     * @return string Unquoted value
549
     */
550
    public function unquoteString($quotedValue)
551
    {
552
        $value = $quotedValue;
553
        if ($quotedValue{0} === '"') {
554
            $value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
555
        } elseif ($quotedValue{0} === '\'') {
556
            $value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
557
        }
558
        return str_replace('\\\\', '\\', $value);
559
    }
560
561
    /**
562
     * Handler for everything which is not a ViewHelperNode.
563
     *
564
     * This includes Text, array syntax, and object accessor syntax.
565
     *
566
     * @param ParsingState $state Current parsing state
567
     * @param string $text Text to process
568
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
569
     * @return void
570
     */
571
    protected function textAndShorthandSyntaxHandler(ParsingState $state, $text, $context)
572
    {
573
        $sections = preg_split(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
574
        foreach ($sections as $section) {
575
            $matchedVariables = [];
576
            $expressionNode = null;
577
            if (preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
578
                $this->objectAccessorHandler(
579
                    $state,
580
                    $matchedVariables['Object'],
581
                    $matchedVariables['Delimiter'],
582
                    (isset($matchedVariables['ViewHelper']) ? $matchedVariables['ViewHelper'] : ''),
583
                    (isset($matchedVariables['AdditionalViewHelpers']) ? $matchedVariables['AdditionalViewHelpers'] : '')
584
                );
585
            } elseif ($context === self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS
586
                && preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0
587
            ) {
588
                // We only match arrays if we are INSIDE viewhelper arguments
589
                $this->arrayHandler($state, $this->recursiveArrayHandler($matchedVariables['Array']));
590
            } else {
591
                // We ask custom ExpressionNode instances from ViewHelperResolver
592
                // if any match our expression:
593
                foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
594
                    $detectionExpression = $expressionNodeTypeClassName::$detectionExpression;
595
                    $matchedVariables = [];
596
                    preg_match_all($detectionExpression, $section, $matchedVariables, PREG_SET_ORDER);
597
                    if (is_array($matchedVariables) === true) {
598
                        foreach ($matchedVariables as $matchedVariableSet) {
599
                            $expressionStartPosition = strpos($section, $matchedVariableSet[0]);
600
                            /** @var ExpressionNodeInterface $expressionNode */
601
                            $expressionNode = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $state);
602
                            try {
603
                                // Trigger initial parse-time evaluation to allow the node to manipulate the rendering context.
604
                                $expressionNode->evaluate($this->renderingContext);
605
606
                                if ($expressionStartPosition > 0) {
607
                                    $state->getNodeFromStack()->addChildNode(new TextNode(substr($section, 0, $expressionStartPosition)));
608
                                }
609
                                $state->getNodeFromStack()->addChildNode($expressionNode);
610
611
                                $expressionEndPosition = $expressionStartPosition + strlen($matchedVariableSet[0]);
612
                                if ($expressionEndPosition < strlen($section)) {
613
                                    $this->textAndShorthandSyntaxHandler($state, substr($section, $expressionEndPosition), $context);
614
                                    break;
615
                                }
616
                            } catch (ExpressionException $error) {
617
                                $this->textHandler(
618
                                    $state,
619
                                    $this->renderingContext->getErrorHandler()->handleExpressionError($error)
620
                                );
621
                            }
622
                        }
623
                    }
624
                }
625
626
                if (!$expressionNode) {
627
                    // As fallback we simply render the expression back as template content.
628
                    $this->textHandler($state, $section);
629
                }
630
            }
631
        }
632
    }
633
634
    /**
635
     * Handler for array syntax. This creates the array object recursively and
636
     * adds it to the current node.
637
     *
638
     * @param ParsingState $state The current parsing state
639
     * @param NodeInterface[] $arrayText The array as string.
640
     * @return void
641
     */
642
    protected function arrayHandler(ParsingState $state, $arrayText)
643
    {
644
        $arrayNode = new ArrayNode($arrayText);
645
        $state->getNodeFromStack()->addChildNode($arrayNode);
646
    }
647
648
    /**
649
     * Recursive function which takes the string representation of an array and
650
     * builds an object tree from it.
651
     *
652
     * Deals with the following value types:
653
     * - Numbers (Integers and Floats)
654
     * - Strings
655
     * - Variables
656
     * - sub-arrays
657
     *
658
     * @param string $arrayText Array text
659
     * @return NodeInterface[] the array node built up
660
     * @throws Exception
661
     */
662
    protected function recursiveArrayHandler($arrayText)
663
    {
664
        $matches = [];
665
        $arrayToBuild = [];
666
        if (preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER)) {
667
            foreach ($matches as $singleMatch) {
668
                $arrayKey = $this->unquoteString($singleMatch['Key']);
669
                if (!empty($singleMatch['VariableIdentifier'])) {
670
                    $arrayToBuild[$arrayKey] = new ObjectAccessorNode($singleMatch['VariableIdentifier']);
671
                } elseif (array_key_exists('Number', $singleMatch) && (!empty($singleMatch['Number']) || $singleMatch['Number'] === '0')) {
672
                    $arrayToBuild[$arrayKey] = (float)$singleMatch['Number'];
673
                } elseif ((array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']))) {
674
                    $argumentString = $this->unquoteString($singleMatch['QuotedString']);
675
                    $arrayToBuild[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
676
                } elseif (array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray'])) {
677
                    $arrayToBuild[$arrayKey] = new ArrayNode($this->recursiveArrayHandler($singleMatch['Subarray']));
678
                }
679
            }
680
        }
681
        return $arrayToBuild;
682
    }
683
684
    /**
685
     * Text node handler
686
     *
687
     * @param ParsingState $state
688
     * @param string $text
689
     * @return void
690
     */
691
    protected function textHandler(ParsingState $state, $text)
692
    {
693
        $node = new TextNode($text);
694
        $this->callInterceptor($node, InterceptorInterface::INTERCEPT_TEXT, $state);
695
        $state->getNodeFromStack()->addChildNode($node);
696
    }
697
698
    /**
699
     * @return ParsingState
700
     */
701
    protected function getParsingState()
702
    {
703
        $rootNode = new RootNode();
704
        $variableProvider = $this->renderingContext->getVariableProvider();
705
        $state = new ParsingState();
706
        $state->setRootNode($rootNode);
707
        $state->pushNodeToStack($rootNode);
708
        $state->setVariableProvider($variableProvider->getScopeCopy($variableProvider->getAll()));
709
        return $state;
710
    }
711
}
712