Completed
Pull Request — master (#335)
by Claus
04:28 queued 02:03
created

TemplateParser::parseTemplateSource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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