Completed
Pull Request — master (#446)
by Claus
02:04
created

TemplateParser::buildObjectTree()   B

Complexity

Conditions 9
Paths 22

Size

Total Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 22
nop 2
dl 0
loc 48
rs 7.5789
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\ArrayNode;
11
use TYPO3Fluid\Fluid\Core\Compiler\UncompilableTemplateInterface;
12
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionException;
13
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface;
14
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ParseTimeEvaluatedExpressionNodeInterface;
15
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
16
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NumericNode;
17
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
18
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
19
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
20
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
21
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
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
     * @return boolean
102
     */
103
    public function isEscapingEnabled()
104
    {
105
        return $this->escapingEnabled;
106
    }
107
108
    /**
109
     * @param boolean $escapingEnabled
110
     * @return void
111
     */
112
    public function setEscapingEnabled($escapingEnabled)
113
    {
114
        $this->escapingEnabled = (boolean) $escapingEnabled;
115
    }
116
117
    /**
118
     * Parses a given template string and returns a parsed template object.
119
     *
120
     * The resulting ParsedTemplate can then be rendered by calling evaluate() on it.
121
     *
122
     * Normally, you should use a subclass of AbstractTemplateView instead of calling the
123
     * TemplateParser directly.
124
     *
125
     * @param string $templateString The template to parse as a string
126
     * @param string|null $templateIdentifier If the template has an identifying string it can be passed here to improve error reporting.
127
     * @return ParsingState Parsed template
128
     * @throws Exception
129
     */
130
    public function parse($templateString, $templateIdentifier = null)
131
    {
132
        if (!is_string($templateString)) {
133
            throw new Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
134
        }
135
        try {
136
            $this->reset();
137
138
            $templateString = $this->preProcessTemplateSource($templateString);
139
140
            $splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
141
            $parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
142
        } catch (Exception $error) {
143
            throw $this->createParsingRelatedExceptionWithContext($error, $templateIdentifier);
144
        }
145
        $this->parsedTemplates[$templateIdentifier] = $parsingState;
146
        return $parsingState;
147
    }
148
149
    /**
150
     * @param \Exception $error
151
     * @param string $templateIdentifier
152
     * @throws \Exception
153
     */
154
    public function createParsingRelatedExceptionWithContext(\Exception $error, $templateIdentifier)
155
    {
156
        list ($line, $character, $templateCode) = $this->getCurrentParsingPointers();
157
        $exceptionClass = get_class($error);
158
        return new $exceptionClass(
159
            sprintf(
160
                'Fluid parse error in template %s, line %d at character %d. Error: %s (error code %d). Template source chunk: %s',
161
                $templateIdentifier,
162
                $line,
163
                $character,
164
                $error->getMessage(),
165
                $error->getCode(),
166
                $templateCode
167
            ),
168
            $error->getCode(),
169
            $error
170
        );
171
    }
172
173
    /**
174
     * @param string $templateIdentifier
175
     * @param \Closure $templateSourceClosure Closure which returns the template source if needed
176
     * @return ParsedTemplateInterface
177
     */
178
    public function getOrParseAndStoreTemplate($templateIdentifier, $templateSourceClosure)
179
    {
180
        $compiler = $this->renderingContext->getTemplateCompiler();
181
        if (isset($this->parsedTemplates[$templateIdentifier])) {
182
            $parsedTemplate = $this->parsedTemplates[$templateIdentifier];
183
        } elseif ($compiler->has($templateIdentifier)) {
184
            $parsedTemplate = $compiler->get($templateIdentifier);
185
            if ($parsedTemplate instanceof UncompilableTemplateInterface) {
186
                $parsedTemplate = $this->parseTemplateSource($templateIdentifier, $templateSourceClosure);
187
            }
188
        } else {
189
            $parsedTemplate = $this->parseTemplateSource($templateIdentifier, $templateSourceClosure);
190
            try {
191
                $compiler->store($templateIdentifier, $parsedTemplate);
0 ignored issues
show
Compatibility introduced by
$parsedTemplate of type object<TYPO3Fluid\Fluid\...arsedTemplateInterface> is not a sub-type of object<TYPO3Fluid\Fluid\Core\Parser\ParsingState>. It seems like you assume a concrete implementation of the interface TYPO3Fluid\Fluid\Core\Pa...ParsedTemplateInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
192
            } catch (StopCompilingException $stop) {
193
                $this->renderingContext->getErrorHandler()->handleCompilerError($stop);
194
                $parsedTemplate->setCompilable(false);
195
                $compiler->store($templateIdentifier, $parsedTemplate);
0 ignored issues
show
Compatibility introduced by
$parsedTemplate of type object<TYPO3Fluid\Fluid\...arsedTemplateInterface> is not a sub-type of object<TYPO3Fluid\Fluid\Core\Parser\ParsingState>. It seems like you assume a concrete implementation of the interface TYPO3Fluid\Fluid\Core\Pa...ParsedTemplateInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
196
            }
197
        }
198
        return $parsedTemplate;
199
    }
200
201
    /**
202
     * @param string $templateIdentifier
203
     * @param \Closure $templateSourceClosure
204
     * @return ParsedTemplateInterface
205
     */
206
    protected function parseTemplateSource($templateIdentifier, $templateSourceClosure)
207
    {
208
        $parsedTemplate = $this->parse(
209
            $templateSourceClosure($this, $this->renderingContext->getTemplatePaths()),
210
            $templateIdentifier
211
        );
212
        $parsedTemplate->setIdentifier($templateIdentifier);
213
        $this->parsedTemplates[$templateIdentifier] = $parsedTemplate;
214
        return $parsedTemplate;
215
    }
216
217
    /**
218
     * Pre-process the template source, making all registered TemplateProcessors
219
     * do what they need to do with the template source before it is parsed.
220
     *
221
     * @param string $templateSource
222
     * @return string
223
     */
224
    protected function preProcessTemplateSource($templateSource)
225
    {
226
        foreach ($this->renderingContext->getTemplateProcessors() as $templateProcessor) {
227
            $templateSource = $templateProcessor->preProcessSource($templateSource);
228
        }
229
        return $templateSource;
230
    }
231
232
    /**
233
     * Resets the parser to its default values.
234
     *
235
     * @return void
236
     */
237
    protected function reset()
238
    {
239
        $this->escapingEnabled = true;
240
        $this->pointerLineNumber = 1;
241
        $this->pointerLineCharacter = 1;
242
    }
243
244
    /**
245
     * Splits the template string on all dynamic tags found.
246
     *
247
     * @param string $templateString Template string to split.
248
     * @return array Splitted template
249
     */
250
    protected function splitTemplateAtDynamicTags($templateString)
251
    {
252
        return preg_split(Patterns::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
253
    }
254
255
    /**
256
     * Build object tree from the split template
257
     *
258
     * @param array $splitTemplate The split template, so that every tag with a namespace declaration is already a seperate array element.
259
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
260
     * @return ParsingState
261
     * @throws Exception
262
     */
263
    protected function buildObjectTree(array $splitTemplate, $context)
264
    {
265
        $state = $this->getParsingState();
266
        $previousBlock = '';
267
268
        foreach ($splitTemplate as $templateElement) {
269
            if ($context === self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS) {
270
                // Store a neat reference to the outermost chunk of Fluid template code.
271
                // Don't store the reference if parsing ViewHelper arguments object tree;
272
                // we want the reference code to contain *all* of the ViewHelper call.
273
                $this->pointerTemplateCode = $templateElement;
274
            }
275
            $this->pointerLineNumber += substr_count($templateElement, PHP_EOL);
276
            $this->pointerLineCharacter = strlen(substr($previousBlock, strrpos($previousBlock, PHP_EOL))) + 1;
277
            $previousBlock = $templateElement;
278
            $matchedVariables = [];
279
280
            if (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
281
                if ($this->openingViewHelperTagHandler(
282
                    $state,
283
                    $matchedVariables['NamespaceIdentifier'],
284
                    $matchedVariables['MethodIdentifier'],
285
                    $matchedVariables['Attributes'],
286
                    ($matchedVariables['Selfclosing'] === '' ? false : true),
287
                    $templateElement
288
                )) {
289
                    continue;
290
                }
291
            } elseif (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
292
                if ($this->closingViewHelperTagHandler(
293
                    $state,
294
                    $matchedVariables['NamespaceIdentifier'],
295
                    $matchedVariables['MethodIdentifier']
296
                )) {
297
                    continue;
298
                }
299
            }
300
            $this->textAndShorthandSyntaxHandler($state, $templateElement, $context);
301
        }
302
303
        if ($state->countNodeStack() !== 1) {
304
            throw new Exception(
305
                'Not all tags were closed!',
306
                1238169398
307
            );
308
        }
309
        return $state;
310
    }
311
    /**
312
     * Handles an opening or self-closing view helper tag.
313
     *
314
     * @param ParsingState $state Current parsing state
315
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
316
     * @param string $methodIdentifier Method identifier
317
     * @param string $arguments Arguments string, not yet parsed
318
     * @param boolean $selfclosing true, if the tag is a self-closing tag.
319
     * @param string $templateElement The template code containing the ViewHelper call
320
     * @return NodeInterface|null
321
     */
322
    protected function openingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing, $templateElement)
323
    {
324
        $viewHelperNode = $this->initializeViewHelperAndAddItToStack(
325
            $state,
326
            $namespaceIdentifier,
327
            $methodIdentifier,
328
            $this->parseArguments($arguments)
329
        );
330
331
        if ($viewHelperNode) {
332
            $viewHelperNode->setPointerTemplateCode($templateElement);
333
            if ($selfclosing === true) {
334
                $state->popNodeFromStack();
335
                $this->callInterceptor($viewHelperNode, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
336
                // This needs to be called here because closingViewHelperTagHandler() is not triggered for self-closing tags
337
                $this->addChildToNode($state->getNodeFromStack(), $viewHelperNode);
338
            }
339
        }
340
341
        return $viewHelperNode;
342
    }
343
344
    /**
345
     * Initialize the given ViewHelper and adds it to the current node and to
346
     * the stack.
347
     *
348
     * @param ParsingState $state Current parsing state
349
     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
350
     * @param string $methodIdentifier Method identifier
351
     * @param array $argumentsObjectTree Arguments object tree
352
     * @return null|NodeInterface An instance of ViewHelperNode if identity was valid - NULL if the namespace/identity was not registered
353
     * @throws Exception
354
     */
355
    protected function initializeViewHelperAndAddItToStack(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree)
356
    {
357
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
358
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
359
            return null;
360
        }
361
        try {
362
            $currentViewHelperNode = new ViewHelperNode(
363
                $this->renderingContext,
364
                $namespaceIdentifier,
365
                $methodIdentifier,
366
                $argumentsObjectTree,
367
                $state
368
            );
369
370
            $this->callInterceptor($currentViewHelperNode, InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER, $state);
371
            $viewHelper = $currentViewHelperNode->getUninitializedViewHelper();
372
            $viewHelper::postParseEvent($currentViewHelperNode, $argumentsObjectTree, $state->getVariableContainer());
373
            $state->pushNodeToStack($currentViewHelperNode);
374
            return $currentViewHelperNode;
375
        } catch (\TYPO3Fluid\Fluid\Core\ViewHelper\Exception $error) {
376
            $this->textHandler(
377
                $state,
378
                $this->renderingContext->getErrorHandler()->handleViewHelperError($error)
379
            );
380
        } catch (Exception $error) {
381
            $this->textHandler(
382
                $state,
383
                $this->renderingContext->getErrorHandler()->handleParserError($error)
384
            );
385
        }
386
        return null;
387
    }
388
389
    /**
390
     * Handles a closing view helper tag
391
     *
392
     * @param ParsingState $state The current parsing state
393
     * @param string $namespaceIdentifier Namespace identifier for the closing tag.
394
     * @param string $methodIdentifier Method identifier.
395
     * @return boolean whether the viewHelper was found and added to the stack or not
396
     * @throws Exception
397
     */
398
    protected function closingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier)
399
    {
400
        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
401
        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
402
            return false;
403
        }
404
        $lastStackElement = $state->popNodeFromStack();
405
        if (!($lastStackElement instanceof ViewHelperNode)) {
406
            throw new Exception('You closed a templating tag which you never opened!', 1224485838);
407
        }
408
        $actualViewHelperClassName = $viewHelperResolver->resolveViewHelperClassName($namespaceIdentifier, $methodIdentifier);
409
        $expectedViewHelperClassName = $lastStackElement->getViewHelperClassName();
410
        if ($actualViewHelperClassName !== $expectedViewHelperClassName) {
411
            throw new Exception(
412
                'Templating tags not properly nested. Expected: ' . $expectedViewHelperClassName . '; Actual: ' .
413
                $actualViewHelperClassName,
414
                1224485398
415
            );
416
        }
417
        $this->callInterceptor($lastStackElement, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
418
        $this->addChildToNode($state->getNodeFromStack(), $lastStackElement);
419
420
        return true;
421
    }
422
423
    /**
424
     * Handles the appearance of an object accessor (like {posts.author.email}).
425
     * Creates a new instance of \TYPO3Fluid\Fluid\ObjectAccessorNode.
426
     *
427
     * Handles ViewHelpers as well which are in the shorthand syntax.
428
     *
429
     * @param ParsingState $state The current parsing state
430
     * @param string $objectAccessorString String which identifies which objects to fetch
431
     * @param string $delimiter
432
     * @param string $viewHelperString
433
     * @param string $additionalViewHelpersString
434
     * @return void
435
     */
436
    protected function objectAccessorHandler(ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString)
437
    {
438
        $viewHelperString .= $additionalViewHelpersString;
439
        $numberOfViewHelpers = 0;
440
441
        // The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
442
        // Resolves bug #5107.
443
        if (strlen($delimiter) === 0 && strlen($viewHelperString) > 0) {
444
            $viewHelperString = $objectAccessorString . $viewHelperString;
445
            $objectAccessorString = '';
446
        }
447
448
        // ViewHelpers
449
        $matches = [];
450
        if (strlen($viewHelperString) > 0 && preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
451
            // The last ViewHelper has to be added first for correct chaining.
452
            foreach (array_reverse($matches) as $singleMatch) {
453
                if (strlen($singleMatch['ViewHelperArguments']) > 0) {
454
                    $arguments = $this->recursiveArrayHandler($singleMatch['ViewHelperArguments']);
455
                } else {
456
                    $arguments = [];
457
                }
458
                $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...
459
                if ($viewHelperNode) {
460
                    $numberOfViewHelpers++;
461
                }
462
            }
463
        }
464
465
        // Object Accessor
466
        if (strlen($objectAccessorString) > 0) {
467
            $node = new ObjectAccessorNode($objectAccessorString);
468
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
469
            $this->addChildToNode($state->getNodeFromStack(), $node);
470
        }
471
472
        // Close ViewHelper Tags if needed.
473
        for ($i = 0; $i < $numberOfViewHelpers; $i++) {
474
            $node = $state->popNodeFromStack();
475
            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
476
            $this->addChildToNode($state->getNodeFromStack(), $node);
477
        }
478
    }
479
480
    /**
481
     * Call all interceptors registered for a given interception point.
482
     *
483
     * @param NodeInterface $node The syntax tree node which can be modified by the interceptors.
484
     * @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
485
     * @param ParsingState $state the parsing state
486
     * @return void
487
     */
488
    protected function callInterceptor(NodeInterface & $node, $interceptionPoint, ParsingState $state)
489
    {
490
        if ($this->configuration === null) {
491
            return;
492
        }
493
        if ($this->escapingEnabled) {
494
            /** @var $interceptor InterceptorInterface */
495
            foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) {
496
                $node = $interceptor->process($node, $interceptionPoint, $state);
497
            }
498
        }
499
500
        /** @var $interceptor InterceptorInterface */
501
        foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) {
502
            $node = $interceptor->process($node, $interceptionPoint, $state);
503
        }
504
    }
505
506
    /**
507
     * Parse arguments of a given tag, and build up the Arguments Object Tree
508
     * for each argument.
509
     * Returns an associative array, where the key is the name of the argument,
510
     * and the value is a single Argument Object Tree.
511
     *
512
     * @param string $argumentsString All arguments as string
513
     * @return array An associative array of objects, where the key is the argument name.
514
     */
515
    protected function parseArguments($argumentsString)
516
    {
517
        $argumentsObjectTree = [];
518
        $matches = [];
519
        if (preg_match_all(Patterns::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
520
            $escapingEnabledBackup = $this->escapingEnabled;
521
            $this->escapingEnabled = false;
522
            foreach ($matches as $singleMatch) {
523
                $argument = $singleMatch['Argument'];
524
                $value = $this->unquoteString($singleMatch['ValueQuoted']);
525
                $argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
526
            }
527
            $this->escapingEnabled = $escapingEnabledBackup;
528
        }
529
        return $argumentsObjectTree;
530
    }
531
532
    /**
533
     * Build up an argument object tree for the string in $argumentString.
534
     * This builds up the tree for a single argument value.
535
     *
536
     * This method also does some performance optimizations, so in case
537
     * no { or < is found, then we just return a TextNode.
538
     *
539
     * @param string $argumentString
540
     * @return SyntaxTree\NodeInterface the corresponding argument object tree.
541
     */
542
    protected function buildArgumentObjectTree($argumentString)
543
    {
544
        if (strpos($argumentString, '{') === false && strpos($argumentString, '<') === false) {
545
            if (is_numeric($argumentString)) {
546
                return new NumericNode($argumentString);
547
            }
548
            return new TextNode($argumentString);
549
        }
550
        $splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
551
        $rootNode = $this->buildObjectTree($splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS)->getRootNode();
552
        return $rootNode;
553
    }
554
555
    /**
556
     * Removes escapings from a given argument string and trims the outermost
557
     * quotes.
558
     *
559
     * This method is meant as a helper for regular expression results.
560
     *
561
     * @param string $quotedValue Value to unquote
562
     * @return string Unquoted value
563
     */
564
    public function unquoteString($quotedValue)
565
    {
566
        $value = $quotedValue;
567
        if ($value === '') {
568
            return $value;
569
        }
570
        if ($quotedValue{0} === '"') {
571
            $value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
572
        } elseif ($quotedValue{0} === '\'') {
573
            $value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
574
        }
575
        return str_replace('\\\\', '\\', $value);
576
    }
577
578
    /**
579
     * Handler for everything which is not a ViewHelperNode.
580
     *
581
     * This includes Text, array syntax, and object accessor syntax.
582
     *
583
     * @param ParsingState $state Current parsing state
584
     * @param string $text Text to process
585
     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
586
     * @return void
587
     */
588
    protected function textAndShorthandSyntaxHandler(ParsingState $state, $text, $context)
589
    {
590
        $sections = preg_split(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
591
        foreach ($sections as $section) {
592
            $matchedVariables = [];
593
            $expressionNode = null;
594
            if (preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
595
                $this->objectAccessorHandler(
596
                    $state,
597
                    $matchedVariables['Object'],
598
                    $matchedVariables['Delimiter'],
599
                    (isset($matchedVariables['ViewHelper']) ? $matchedVariables['ViewHelper'] : ''),
600
                    (isset($matchedVariables['AdditionalViewHelpers']) ? $matchedVariables['AdditionalViewHelpers'] : '')
601
                );
602
            } elseif ($context === self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS
603
                && preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0
604
            ) {
605
                // We only match arrays if we are INSIDE viewhelper arguments
606
                $this->arrayHandler($state, $this->recursiveArrayHandler($matchedVariables['Array']));
607
            } else {
608
                // We ask custom ExpressionNode instances from ViewHelperResolver
609
                // if any match our expression:
610
                foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
611
                    $detectionExpression = $expressionNodeTypeClassName::$detectionExpression;
612
                    $matchedVariables = [];
613
                    preg_match_all($detectionExpression, $section, $matchedVariables, PREG_SET_ORDER);
614
                    if (is_array($matchedVariables) === true) {
615
                        foreach ($matchedVariables as $matchedVariableSet) {
616
                            $expressionStartPosition = strpos($section, $matchedVariableSet[0]);
617
                            /** @var ExpressionNodeInterface $expressionNode */
618
                            $expressionNode = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $state);
619
                            try {
620
                                // Trigger initial parse-time evaluation to allow the node to manipulate the rendering context.
621
                                if ($expressionNode instanceof ParseTimeEvaluatedExpressionNodeInterface) {
622
                                    $expressionNode->evaluate($this->renderingContext);
623
                                }
624
625
                                if ($expressionStartPosition > 0) {
626
                                    $this->addChildToNode($state->getNodeFromStack(), new TextNode(substr($section, 0, $expressionStartPosition)));
627
                                }
628
                                $this->addChildToNode($state->getNodeFromStack(), $expressionNode);
629
630
                                $expressionEndPosition = $expressionStartPosition + strlen($matchedVariableSet[0]);
631
                                if ($expressionEndPosition < strlen($section)) {
632
                                    $this->textAndShorthandSyntaxHandler($state, substr($section, $expressionEndPosition), $context);
633
                                    break;
634
                                }
635
                            } catch (ExpressionException $error) {
636
                                $this->textHandler(
637
                                    $state,
638
                                    $this->renderingContext->getErrorHandler()->handleExpressionError($error)
639
                                );
640
                            }
641
                        }
642
                    }
643
                }
644
645
                if (!$expressionNode) {
646
                    // As fallback we simply render the expression back as template content.
647
                    $this->textHandler($state, $section);
648
                }
649
            }
650
        }
651
    }
652
653
    /**
654
     * Handler for array syntax. This creates the array object recursively and
655
     * adds it to the current node.
656
     *
657
     * @param ParsingState $state The current parsing state
658
     * @param NodeInterface[] $arrayText The array as string.
659
     * @return void
660
     */
661
    protected function arrayHandler(ParsingState $state, $arrayText)
662
    {
663
        $arrayNode = new ArrayNode($arrayText);
664
        $this->addChildToNode($state->getNodeFromStack(), $arrayNode);
665
    }
666
667
    /**
668
     * Recursive function which takes the string representation of an array and
669
     * builds an object tree from it.
670
     *
671
     * Deals with the following value types:
672
     * - Numbers (Integers and Floats)
673
     * - Strings
674
     * - Variables
675
     * - sub-arrays
676
     *
677
     * @param string $arrayText Array text
678
     * @return NodeInterface[] the array node built up
679
     * @throws Exception
680
     */
681
    protected function recursiveArrayHandler($arrayText)
682
    {
683
        $matches = [];
684
        $arrayToBuild = [];
685
        if (preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER)) {
686
            foreach ($matches as $singleMatch) {
687
                $arrayKey = $this->unquoteString($singleMatch['Key']);
688
                if (isset($singleMatch['Unquoted'])) {
689
                    if (is_numeric($singleMatch['Unquoted'])) {
690
                        $arrayToBuild[$arrayKey] = new NumericNode($singleMatch['Unquoted']);
691
                    } else {
692
                        $arrayToBuild[$arrayKey] = new ObjectAccessorNode($singleMatch['Unquoted']);
693
                    }
694
                } elseif ((array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']))) {
695
                    $argumentString = $this->unquoteString($singleMatch['QuotedString']);
696
                    $arrayToBuild[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
697
                } elseif (array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray'])) {
698
                    $arrayToBuild[$arrayKey] = new ArrayNode($this->recursiveArrayHandler($singleMatch['Subarray']));
699
                }
700
            }
701
        }
702
        return $arrayToBuild;
703
    }
704
705
    /**
706
     * Text node handler
707
     *
708
     * @param ParsingState $state
709
     * @param string $text
710
     * @return void
711
     */
712
    protected function textHandler(ParsingState $state, $text)
713
    {
714
        $node = new TextNode($text);
715
        $this->callInterceptor($node, InterceptorInterface::INTERCEPT_TEXT, $state);
716
        $this->addChildToNode($state->getNodeFromStack(), $node);
717
    }
718
719
    /**
720
     * @param NodeInterface $parent
721
     * @param NodeInterface $child
722
     */
723
    protected function addChildToNode(NodeInterface $parent, NodeInterface $child)
724
    {
725
        if (!$parent instanceof NodeFilterInterface || $parent->allowsChildNodeType($child)) {
726
            $parent->addChildNode($child);
727
        }
728
    }
729
730
    /**
731
     * @return ParsingState
732
     */
733
    protected function getParsingState()
734
    {
735
        $rootNode = new RootNode();
736
        $variableProvider = $this->renderingContext->getVariableProvider();
737
        $state = new ParsingState();
738
        $state->setRootNode($rootNode);
739
        $state->pushNodeToStack($rootNode);
740
        $state->setVariableProvider($variableProvider->getScopeCopy($variableProvider->getAll()));
741
        return $state;
742
    }
743
}
744