Completed
Pull Request — master (#256)
by Claus
03:49
created

TemplateParser::recursiveArrayHandler()   C

Complexity

Conditions 11
Paths 2

Size

Total Lines 21
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 16
nc 2
nop 1
dl 0
loc 21
rs 6.4715
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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