Completed
Push — master ( 020da6...b36c00 )
by Marc
02:33
created

TemplateParser::unquoteString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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