1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace TYPO3Fluid\Fluid\Core\Parser; |
5
|
|
|
|
6
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode; |
7
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionException; |
8
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface; |
9
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode; |
10
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode; |
11
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode; |
12
|
|
|
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; |
13
|
|
|
use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition; |
14
|
|
|
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface; |
15
|
|
|
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolver; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Sequencer for Fluid syntax |
19
|
|
|
* |
20
|
|
|
* Uses a NoRewindIterator around a sequence of byte values to |
21
|
|
|
* iterate over each syntax-relevant character and determine |
22
|
|
|
* which nodes to create. |
23
|
|
|
* |
24
|
|
|
* Passes the outer iterator between functions that perform the |
25
|
|
|
* iterations. Since the iterator is a NoRewindIterator it will |
26
|
|
|
* not be reset before the start of each loop - meaning that |
27
|
|
|
* when it is passed to a new function, that function continues |
28
|
|
|
* from the last touched index in the byte sequence. |
29
|
|
|
* |
30
|
|
|
* The circumstances around "break or return" in the switches is |
31
|
|
|
* very, very important to understand in context of how iterators |
32
|
|
|
* work. Returning does not advance the iterator like breaking |
33
|
|
|
* would and this causes a different position in the byte sequence |
34
|
|
|
* to be experienced in the method that uses the return value (it |
35
|
|
|
* sees the index of the symbol which terminated the expression, |
36
|
|
|
* not the next symbol after that). |
37
|
|
|
*/ |
38
|
|
|
class Sequencer |
39
|
|
|
{ |
40
|
|
|
/** |
41
|
|
|
* @var RenderingContextInterface |
42
|
|
|
*/ |
43
|
|
|
protected $renderingContext; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var ParsingState |
47
|
|
|
*/ |
48
|
|
|
protected $state; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var Contexts |
52
|
|
|
*/ |
53
|
|
|
protected $contexts; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var Source |
57
|
|
|
*/ |
58
|
|
|
protected $source; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var Splitter |
62
|
|
|
*/ |
63
|
|
|
protected $splitter; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var Configuration |
67
|
|
|
*/ |
68
|
|
|
protected $configuration; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @var ViewHelperResolver |
72
|
|
|
*/ |
73
|
|
|
protected $resolver; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Whether or not the escaping interceptors are active |
77
|
|
|
* |
78
|
|
|
* @var boolean |
79
|
|
|
*/ |
80
|
|
|
protected $escapingEnabled = true; |
81
|
|
|
|
82
|
|
|
public function __construct( |
83
|
|
|
RenderingContextInterface $renderingContext, |
84
|
|
|
ParsingState $state, |
85
|
|
|
Contexts $contexts, |
86
|
|
|
Source $source |
87
|
|
|
) { |
88
|
|
|
$this->renderingContext = $renderingContext; |
89
|
|
|
$this->resolver = $renderingContext->getViewHelperResolver(); |
90
|
|
|
$this->configuration = $renderingContext->buildParserConfiguration(); |
91
|
|
|
$this->state = clone $state; |
92
|
|
|
$this->contexts = $contexts; |
93
|
|
|
$this->source = $source; |
94
|
|
|
$this->splitter = new Splitter($this->source, $this->contexts); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
public function sequence(): ParsingState |
98
|
|
|
{ |
99
|
|
|
#$sequence = $this->splitter->parse(); |
100
|
|
|
|
101
|
|
|
// Please note: repeated calls to $this->state->getTopmostNodeFromStack() are indeed intentional. That method may |
102
|
|
|
// return different nodes at different times depending on what has occurred in other methods! Only the places |
103
|
|
|
// where $node is actually extracted is it (by design) safe to do so. DO NOT REFACTOR! |
104
|
|
|
// It is *also* intentional that this switch has no default case. The root context is very specific and will |
105
|
|
|
// only apply when the splitter is actually in root, which means there is no chance of it yielding an unexpected |
106
|
|
|
// character (because that implies a method called by this method already threw a SequencingException). |
107
|
|
|
foreach ($this->splitter->sequence as $symbol => $captured) { |
108
|
|
|
switch ($symbol) { |
109
|
|
|
case Splitter::BYTE_INLINE: |
110
|
|
|
$node = $this->state->getNodeFromStack(); |
111
|
|
|
if ($this->splitter->index > 1 && $this->source->bytes[$this->splitter->index - 1] === Splitter::BYTE_BACKSLASH) { |
112
|
|
|
$node->addChildNode(new TextNode(substr($captured, 0, -1) . '{')); |
113
|
|
|
break; |
114
|
|
|
} |
115
|
|
|
if ($captured !== null) { |
116
|
|
|
$node->addChildNode(new TextNode($captured)); |
117
|
|
|
} |
118
|
|
|
$node->addChildNode($this->sequenceInlineNodes(false)); |
119
|
|
|
$this->splitter->switch($this->contexts->root); |
120
|
|
|
break; |
121
|
|
|
|
122
|
|
|
case Splitter::BYTE_TAG: |
123
|
|
|
if ($captured !== null) { |
124
|
|
|
$this->state->getNodeFromStack()->addChildNode(new TextNode($captured)); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
$childNode = $this->sequenceTagNode(); |
128
|
|
|
$this->splitter->switch($this->contexts->root); |
129
|
|
|
if ($childNode) { |
130
|
|
|
$this->state->getNodeFromStack()->addChildNode($childNode); |
131
|
|
|
} |
132
|
|
|
break; |
133
|
|
|
|
134
|
|
|
case Splitter::BYTE_NULL: |
135
|
|
|
if ($captured !== null) { |
136
|
|
|
$this->state->getNodeFromStack()->addChildNode(new TextNode($captured)); |
137
|
|
|
} |
138
|
|
|
break; |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
return $this->state; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* @return NodeInterface|null |
147
|
|
|
*/ |
148
|
|
|
protected function sequenceTagNode(): ?NodeInterface |
149
|
|
|
{ |
150
|
|
|
$arguments = []; |
151
|
|
|
$definitions = null; |
152
|
|
|
$text = '<'; |
153
|
|
|
$namespace = null; |
154
|
|
|
$method = null; |
155
|
|
|
$bytes = &$this->source->bytes; |
156
|
|
|
$node = new RootNode(); |
157
|
|
|
$selfClosing = false; |
158
|
|
|
$closing = false; |
159
|
|
|
#$escapingEnabledBackup = $this->escapingEnabled; |
160
|
|
|
|
161
|
|
|
$interceptionPoint = InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER; |
162
|
|
|
|
163
|
|
|
$this->splitter->switch($this->contexts->tag); |
164
|
|
|
$this->splitter->sequence->next(); |
165
|
|
|
foreach ($this->splitter->sequence as $symbol => $captured) { |
166
|
|
|
$text .= $captured; |
167
|
|
|
switch ($symbol) { |
168
|
|
|
case Splitter::BYTE_INLINE: |
169
|
|
|
$contextBefore = $this->splitter->context; |
170
|
|
|
$collected = $this->sequenceInlineNodes(isset($namespace) && isset($method)); |
171
|
|
|
$node->addChildNode(new TextNode($text)); |
172
|
|
|
$node->addChildNode($collected); |
173
|
|
|
$text = ''; |
174
|
|
|
$this->splitter->switch($contextBefore); |
175
|
|
|
break; |
176
|
|
|
|
177
|
|
|
case Splitter::BYTE_SEPARATOR_EQUALS: |
178
|
|
|
$key = $captured; |
179
|
|
|
if ($key === null) { |
180
|
|
|
throw $this->splitter->createErrorAtPosition('Unexpected equals sign without preceding attribute/key name', 1561039838); |
181
|
|
|
} elseif ($definitions !== null && !isset($definitions[$key])) { |
182
|
|
|
throw $this->splitter->createUnsupportedArgumentError($key, $definitions); |
183
|
|
|
} |
184
|
|
|
break; |
185
|
|
|
|
186
|
|
|
case Splitter::BYTE_QUOTE_DOUBLE: |
187
|
|
|
case Splitter::BYTE_QUOTE_SINGLE: |
188
|
|
|
$text .= chr($symbol); |
189
|
|
|
if (!isset($key)) { |
190
|
|
|
throw $this->splitter->createErrorAtPosition('Quoted value without a key is not allowed in tags', 1558952412); |
191
|
|
|
} else { |
192
|
|
|
$arguments[$key] = $this->sequenceQuotedNode(0, isset($namespace) && isset($method))->flatten(true); |
193
|
|
|
$key = null; |
194
|
|
|
} |
195
|
|
|
break; |
196
|
|
|
|
197
|
|
|
case Splitter::BYTE_TAG_CLOSE: |
198
|
|
|
$method = $method ?? $captured; |
199
|
|
|
$text .= '/'; |
200
|
|
|
$closing = true; |
201
|
|
|
$selfClosing = $bytes[$this->splitter->index - 1] !== Splitter::BYTE_TAG; |
202
|
|
|
$interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER; |
203
|
|
|
if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES && $captured !== null) { |
204
|
|
|
// We are still capturing arguments and the last yield contained a value. Null-coalesce key |
205
|
|
|
// with captured string so object accessor becomes key name (ECMA shorthand literal) |
206
|
|
|
$arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured); |
207
|
|
|
$key = null; |
208
|
|
|
} |
209
|
|
|
break; |
210
|
|
|
|
211
|
|
|
case Splitter::BYTE_SEPARATOR_COLON: |
212
|
|
|
$text .= ':'; |
213
|
|
|
$namespace = $namespace ?? $captured; |
214
|
|
|
break; |
215
|
|
|
|
216
|
|
|
case Splitter::BYTE_TAG_END: |
217
|
|
|
$text .= '>'; |
218
|
|
|
$method = $method ?? $captured; |
219
|
|
|
|
220
|
|
|
if (!isset($namespace) || !isset($method) || $this->splitter->context->context === Context::CONTEXT_DEAD || $this->resolver->isNamespaceIgnored($namespace)) { |
221
|
|
|
return $node->addChildNode(new TextNode($text))->flatten(); |
|
|
|
|
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
try { |
225
|
|
|
$expectedClass = $this->resolver->resolveViewHelperClassName($namespace, $method); |
226
|
|
|
} catch (\TYPO3Fluid\Fluid\Core\Exception $exception) { |
227
|
|
|
throw $this->splitter->createErrorAtPosition($exception->getMessage(), $exception->getCode()); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
if ($closing && !$selfClosing) { |
231
|
|
|
// Closing byte was more than two bytes back, meaning the tag is NOT self-closing, but is a |
232
|
|
|
// closing tag for a previously opened+stacked node. Finalize the node now. |
233
|
|
|
$closesNode = $this->state->popNodeFromStack(); |
234
|
|
|
if ($closesNode instanceof $expectedClass) { |
235
|
|
|
$arguments = $closesNode->getParsedArguments(); |
236
|
|
|
$viewHelperNode = $closesNode; |
237
|
|
|
} else { |
238
|
|
|
throw $this->splitter->createErrorAtPosition( |
239
|
|
|
sprintf( |
240
|
|
|
'Mismatched closing tag. Expecting: %s:%s (%s). Found: (%s).', |
241
|
|
|
$namespace, |
242
|
|
|
$method, |
243
|
|
|
$expectedClass, |
244
|
|
|
get_class($closesNode) |
245
|
|
|
), |
246
|
|
|
1557700789 |
247
|
|
|
); |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES && $captured !== null) { |
252
|
|
|
// We are still capturing arguments and the last yield contained a value. Null-coalesce key |
253
|
|
|
// with captured string so object accessor becomes key name (ECMA shorthand literal) |
254
|
|
|
$arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
$viewHelperNode = $viewHelperNode ?? $this->resolver->createViewHelperInstanceFromClassName($expectedClass); |
258
|
|
|
#$this->escapingEnabled = $escapingEnabledBackup; |
259
|
|
|
|
260
|
|
|
if (!$closing) { |
261
|
|
|
$this->callInterceptor($viewHelperNode, $interceptionPoint); |
262
|
|
|
$viewHelperNode->setParsedArguments($arguments); |
|
|
|
|
263
|
|
|
$this->state->pushNodeToStack($viewHelperNode); |
264
|
|
|
return null; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
$viewHelperNode = $viewHelperNode->postParse($arguments, $this->state, $this->renderingContext); |
268
|
|
|
|
269
|
|
|
return $viewHelperNode; |
270
|
|
|
|
271
|
|
|
case Splitter::BYTE_WHITESPACE_TAB: |
272
|
|
|
case Splitter::BYTE_WHITESPACE_RETURN: |
273
|
|
|
case Splitter::BYTE_WHITESPACE_EOL: |
274
|
|
|
case Splitter::BYTE_WHITESPACE_SPACE: |
275
|
|
|
if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) { |
276
|
|
|
if ($captured !== null) { |
277
|
|
|
$arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured); |
278
|
|
|
$key = null; |
279
|
|
|
} |
280
|
|
|
} else { |
281
|
|
|
$text .= chr($symbol); |
282
|
|
|
if (isset($namespace)) { |
283
|
|
|
$method = $captured; |
284
|
|
|
|
285
|
|
|
$this->escapingEnabled = false; |
286
|
|
|
$viewHelperNode = $this->resolver->createViewHelperInstance($namespace, $method); |
287
|
|
|
$definitions = $viewHelperNode->prepareArguments(); |
288
|
|
|
|
289
|
|
|
// A whitespace character, in tag context, means the beginning of an array sequence (which may |
290
|
|
|
// or may not contain any items; the next symbol may be a tag end or tag close). We sequence the |
291
|
|
|
// arguments array and create a ViewHelper node. |
292
|
|
|
$this->splitter->switch($this->contexts->attributes); |
293
|
|
|
break; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
// A whitespace before a colon means the tag is not a namespaced tag. We will ignore everything |
297
|
|
|
// inside this tag, except for inline syntax, until the tag ends. For this we use a special, |
298
|
|
|
// limited variant of the root context where instead of scanning for "<" we scan for ">". |
299
|
|
|
// We continue in this same loop because it still matches the potential symbols being yielded. |
300
|
|
|
// Most importantly: this new reduced context will NOT match a colon which is the trigger symbol |
301
|
|
|
// for a ViewHelper tag. |
302
|
|
|
$this->splitter->switch($this->contexts->dead); |
303
|
|
|
} |
304
|
|
|
break; |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
// This case on the surface of it, belongs as "default" case in the switch above. However, the only case that |
309
|
|
|
// would *actually* produce this error, is if the splitter reaches EOF (null byte) symbol before the tag was |
310
|
|
|
// closed. Literally every other possible error type will be thrown as more specific exceptions (e.g. invalid |
311
|
|
|
// argument, missing key, wrong quotes, bad inline and *everything* else with the exception of EOF). Even a |
312
|
|
|
// stray null byte would not be caught here as null byte is not part of the symbol collection for "tag" context. |
313
|
|
|
throw $this->splitter->createErrorAtPosition('Unexpected token in tag sequencing', 1557700786); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* @param bool $allowArray |
318
|
|
|
* @return NodeInterface |
319
|
|
|
*/ |
320
|
|
|
protected function sequenceInlineNodes(bool $allowArray = true): NodeInterface |
321
|
|
|
{ |
322
|
|
|
$text = '{'; |
323
|
|
|
$node = null; |
324
|
|
|
$key = null; |
325
|
|
|
$namespace = null; |
326
|
|
|
$method = null; |
327
|
|
|
$potentialAccessor = null; |
328
|
|
|
$callDetected = false; |
329
|
|
|
$hasPass = false; |
330
|
|
|
$hasColon = null; |
331
|
|
|
$hasWhitespace = false; |
332
|
|
|
$isArray = false; |
333
|
|
|
$array = []; |
334
|
|
|
$arguments = []; |
335
|
|
|
$ignoredEndingBraces = 0; |
336
|
|
|
$countedEscapes = 0; |
337
|
|
|
|
338
|
|
|
$this->splitter->switch($this->contexts->inline); |
339
|
|
|
$this->splitter->sequence->next(); |
340
|
|
|
foreach ($this->splitter->sequence as $symbol => $captured) { |
341
|
|
|
$text .= $captured; |
342
|
|
|
switch ($symbol) { |
343
|
|
|
case Splitter::BYTE_BACKSLASH: |
344
|
|
|
// Increase the number of counted escapes (is passed to sequenceNode() in the "QUOTE" cases and reset |
345
|
|
|
// after the quoted string is extracted). |
346
|
|
|
++$countedEscapes; |
347
|
|
|
break; |
348
|
|
|
|
349
|
|
|
case Splitter::BYTE_ARRAY_START: |
350
|
|
|
|
351
|
|
|
$text .= chr($symbol); |
352
|
|
|
$isArray = $allowArray; |
353
|
|
|
|
354
|
|
|
#ArrayStart: |
355
|
|
|
// Sequence the node. Pass the "use numeric keys?" boolean based on the current byte. Only array |
356
|
|
|
// start creates numeric keys. Inline start with keyless values creates ECMA style {foo:foo, bar:bar} |
357
|
|
|
// from {foo, bar}. |
358
|
|
|
$array[$key ?? $captured ?? 0] = $node = $this->sequenceArrayNode(null, $symbol === Splitter::BYTE_ARRAY_START); |
359
|
|
|
$this->splitter->switch($this->contexts->inline); |
360
|
|
|
unset($key); |
361
|
|
|
break; |
362
|
|
|
|
363
|
|
|
case Splitter::BYTE_INLINE: |
364
|
|
|
// Encountering this case can mean different things: sub-syntax like {foo.{index}} or array, depending |
365
|
|
|
// on presence of either a colon or comma before the inline. In protected mode it is simply added. |
366
|
|
|
$text .= '{'; |
367
|
|
|
if (!$hasWhitespace && $text !== '{{') { |
368
|
|
|
// Most likely, a nested object accessor syntax e.g. {foo.{bar}} - enter protected context since |
369
|
|
|
// these accessors do not allow anything other than additional nested accessors. |
370
|
|
|
$this->splitter->switch($this->contexts->accessor); |
371
|
|
|
++$ignoredEndingBraces; |
372
|
|
|
} elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED) { |
373
|
|
|
// Ignore one ending additional curly brace. Subtracted in the BYTE_INLINE_END case below. |
374
|
|
|
// The expression in this case looks like {{inline}.....} and we capture the curlies. |
375
|
|
|
$potentialAccessor .= $captured; |
376
|
|
|
++$ignoredEndingBraces; |
377
|
|
|
} elseif ($allowArray || $isArray) { |
378
|
|
|
$isArray = true; |
379
|
|
|
$captured = $key ?? $captured ?? $potentialAccessor; |
380
|
|
|
// This is a sub-syntax following a colon - meaning it is an array. |
381
|
|
|
if ($captured !== null) { |
382
|
|
|
#goto ArrayStart; |
383
|
|
|
$array[$key ?? $captured ?? 0] = $node = $this->sequenceArrayNode(null, $symbol === Splitter::BYTE_ARRAY_START); |
384
|
|
|
$this->splitter->switch($this->contexts->inline); |
385
|
|
|
} |
386
|
|
|
} else { |
387
|
|
|
$childNodeToAdd = $this->sequenceInlineNodes($allowArray); |
388
|
|
|
$node = isset($node) ? $node->addChildNode($childNodeToAdd) : (new RootNode())->addChildNode($childNodeToAdd); |
389
|
|
|
} |
390
|
|
|
break; |
391
|
|
|
|
392
|
|
|
case Splitter::BYTE_MINUS: |
393
|
|
|
$text .= '-'; |
394
|
|
|
break; |
395
|
|
|
|
396
|
|
|
// Backtick may be encountered in two different contexts: normal inline context, in which case it has |
397
|
|
|
// the same meaning as any quote and causes sequencing of a quoted string. Or protected context, in |
398
|
|
|
// which case it also sequences a quoted node but appends the result instead of assigning to array. |
399
|
|
|
// Note that backticks do not support escapes (they are a new feature that does not require escaping). |
400
|
|
|
case Splitter::BYTE_BACKTICK: |
401
|
|
|
if ($this->splitter->context->context === Context::CONTEXT_PROTECTED) { |
402
|
|
|
$node->addChildNode(new TextNode($text)); |
403
|
|
|
$node->addChildNode($this->sequenceQuotedNode()->flatten()); |
404
|
|
|
$text = ''; |
405
|
|
|
break; |
406
|
|
|
} |
407
|
|
|
// Fallthrough is intentional: if not in protected context, consider the backtick a normal quote. |
408
|
|
|
|
409
|
|
|
// Case not normally countered in straight up "inline" context, but when encountered, means we have |
410
|
|
|
// explicitly found a quoted array key - and we extract it. |
411
|
|
|
case Splitter::BYTE_QUOTE_SINGLE: |
412
|
|
|
case Splitter::BYTE_QUOTE_DOUBLE: |
413
|
|
|
if (!$allowArray) { |
414
|
|
|
$text .= chr($symbol); |
415
|
|
|
break; |
416
|
|
|
} |
417
|
|
|
if (isset($key)) { |
418
|
|
|
$array[$key] = $this->sequenceQuotedNode($countedEscapes)->flatten(true); |
419
|
|
|
$key = null; |
420
|
|
|
} else { |
421
|
|
|
$key = $this->sequenceQuotedNode($countedEscapes)->flatten(true); |
422
|
|
|
} |
423
|
|
|
$countedEscapes = 0; |
424
|
|
|
$isArray = $allowArray; |
425
|
|
|
break; |
426
|
|
|
|
427
|
|
|
case Splitter::BYTE_SEPARATOR_COMMA: |
428
|
|
|
if (!$allowArray) { |
429
|
|
|
$text .= ','; |
430
|
|
|
break; |
431
|
|
|
} |
432
|
|
|
if (isset($captured)) { |
433
|
|
|
$array[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured); |
434
|
|
|
} |
435
|
|
|
$key = null; |
436
|
|
|
$isArray = $allowArray; |
437
|
|
|
break; |
438
|
|
|
|
439
|
|
|
case Splitter::BYTE_SEPARATOR_EQUALS: |
440
|
|
|
$text .= '='; |
441
|
|
|
if (!$allowArray) { |
442
|
|
|
$node = new RootNode(); |
443
|
|
|
$this->splitter->switch($this->contexts->protected); |
444
|
|
|
break; |
445
|
|
|
} |
446
|
|
|
$key = $captured; |
447
|
|
|
$isArray = $allowArray; |
448
|
|
|
break; |
449
|
|
|
|
450
|
|
|
case Splitter::BYTE_SEPARATOR_COLON: |
451
|
|
|
$text .= ':'; |
452
|
|
|
$hasColon = true; |
453
|
|
|
$namespace = $captured; |
454
|
|
|
$key = $key ?? $captured; |
455
|
|
|
$isArray = $isArray || ($allowArray && is_numeric($key)); |
456
|
|
|
break; |
457
|
|
|
|
458
|
|
|
case Splitter::BYTE_WHITESPACE_SPACE: |
459
|
|
|
case Splitter::BYTE_WHITESPACE_EOL: |
460
|
|
|
case Splitter::BYTE_WHITESPACE_RETURN: |
461
|
|
|
case Splitter::BYTE_WHITESPACE_TAB: |
462
|
|
|
// If we already collected some whitespace we must enter protected context. |
463
|
|
|
$text .= $this->source->source[$this->splitter->index - 1]; |
464
|
|
|
if ($hasWhitespace && !$hasPass && !$allowArray) { |
465
|
|
|
// Protection mode: this very limited context does not allow tags or inline syntax, and will |
466
|
|
|
// protect things like CSS and JS - and will only enter a more reactive context if encountering |
467
|
|
|
// the backtick character, meaning a quoted string will be sequenced. This backtick-quoted |
468
|
|
|
// string can then contain inline syntax like variable accessors. |
469
|
|
|
$node = $node ?? new RootNode(); |
470
|
|
|
$this->splitter->switch($this->contexts->protected); |
471
|
|
|
break; |
472
|
|
|
} |
473
|
|
|
$key = $key ?? $captured; |
474
|
|
|
$hasWhitespace = true; |
475
|
|
|
$isArray = $allowArray && ($hasColon ?? $isArray ?? is_numeric($captured)); |
476
|
|
|
$potentialAccessor = ($potentialAccessor ?? $captured); |
477
|
|
|
break; |
478
|
|
|
|
479
|
|
|
case Splitter::BYTE_TAG_END: |
480
|
|
|
case Splitter::BYTE_PIPE: |
481
|
|
|
// If there is an accessor on the left side of the pipe and $node is not defined, we create $node |
482
|
|
|
// as an object accessor. If $node already exists we do nothing (and expect the VH trigger, the |
483
|
|
|
// parenthesis start case below, to add $node as childnode and create a new $node). |
484
|
|
|
$hasPass = true; |
485
|
|
|
$isArray = $allowArray; |
486
|
|
|
$callDetected = false; |
487
|
|
|
$potentialAccessor = $potentialAccessor ?? $captured; |
488
|
|
|
$text .= $this->source->source[$this->splitter->index - 1]; |
489
|
|
|
if ($node instanceof ViewHelperInterface) { |
490
|
|
|
$node->postParse($arguments, $this->state, $this->renderingContext); |
491
|
|
|
} |
492
|
|
|
if (isset($potentialAccessor)) { |
493
|
|
|
$childNodeToAdd = new ObjectAccessorNode($potentialAccessor); |
494
|
|
|
$node = isset($node) ? $node->addChildNode($childNodeToAdd) : $childNodeToAdd; //$node ?? (is_numeric($potentialAccessor) ? $potentialAccessor + 0 : new ObjectAccessorNode($potentialAccessor)); |
495
|
|
|
} |
496
|
|
|
unset($namespace, $method, $potentialAccessor, $key); |
497
|
|
|
break; |
498
|
|
|
|
499
|
|
|
case Splitter::BYTE_PARENTHESIS_START: |
500
|
|
|
$isArray = false; |
501
|
|
|
// Special case: if a parenthesis start was preceded by whitespace but had no pass operator we are |
502
|
|
|
// not dealing with a ViewHelper call and will continue the sequencing, grabbing the parenthesis as |
503
|
|
|
// part of the expression. |
504
|
|
|
$text .= '('; |
505
|
|
|
if (!$hasColon || ($hasWhitespace && !$hasPass)) { |
|
|
|
|
506
|
|
|
$this->splitter->switch($this->contexts->protected); |
507
|
|
|
unset($namespace, $method); |
508
|
|
|
break; |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
$callDetected = true; |
512
|
|
|
$method = $captured; |
513
|
|
|
$childNodeToAdd = $node; |
514
|
|
|
try { |
515
|
|
|
$node = $this->resolver->createViewHelperInstance($namespace, $method); |
516
|
|
|
$definitions = $node->prepareArguments(); |
517
|
|
|
} catch (\TYPO3Fluid\Fluid\Core\Exception $exception) { |
518
|
|
|
throw $this->splitter->createErrorAtPosition($exception->getMessage(), $exception->getCode()); |
519
|
|
|
} |
520
|
|
|
$this->splitter->switch($this->contexts->array); |
521
|
|
|
$arguments = $this->sequenceArrayNode($definitions)->getInternalArray(); |
522
|
|
|
$this->splitter->switch($this->contexts->inline); |
523
|
|
|
if ($childNodeToAdd) { |
524
|
|
|
$escapingEnabledBackup = $this->escapingEnabled; |
525
|
|
|
$this->escapingEnabled = (bool)$node->isChildrenEscapingEnabled(); |
526
|
|
|
if ($childNodeToAdd instanceof ObjectAccessorNode) { |
527
|
|
|
$this->callInterceptor($childNodeToAdd, InterceptorInterface::INTERCEPT_OBJECTACCESSOR); |
528
|
|
|
} |
529
|
|
|
$this->escapingEnabled = $escapingEnabledBackup; |
530
|
|
|
$node->addChildNode($childNodeToAdd); |
531
|
|
|
} |
532
|
|
|
$text .= ')'; |
533
|
|
|
unset($potentialAccessor); |
534
|
|
|
break; |
535
|
|
|
|
536
|
|
|
case Splitter::BYTE_INLINE_END: |
537
|
|
|
$text .= '}'; |
538
|
|
|
if (--$ignoredEndingBraces >= 0) { |
539
|
|
|
break; |
540
|
|
|
} |
541
|
|
|
$isArray = $allowArray && ($isArray ?: ($hasColon && !$hasPass && !$callDetected)); |
542
|
|
|
$potentialAccessor = $potentialAccessor ?? $captured; |
543
|
|
|
|
544
|
|
|
// Decision: if we did not detect a ViewHelper we match the *entire* expression, from the cached |
545
|
|
|
// starting index, to see if it matches a known type of expression. If it does, we must return the |
546
|
|
|
// appropriate type of ExpressionNode. |
547
|
|
|
if ($isArray) { |
548
|
|
|
if ($captured !== null) { |
549
|
|
|
$array[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured); |
550
|
|
|
} |
551
|
|
|
return new ArrayNode($array); |
552
|
|
|
} elseif ($callDetected) { |
553
|
|
|
// The first-priority check is for a ViewHelper used right before the inline expression ends, |
554
|
|
|
// in which case there is no further syntax to come. |
555
|
|
|
$node = $node->postParse($arguments, $this->state, $this->renderingContext); |
556
|
|
|
$interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER; |
557
|
|
|
} elseif ($this->splitter->context->context === Context::CONTEXT_ACCESSOR) { |
558
|
|
|
// If we are currently in "accessor" context we can now add the accessor by stripping the collected text. |
559
|
|
|
$node = new ObjectAccessorNode(substr($text, 1, -1)); |
560
|
|
|
$interceptionPoint = InterceptorInterface::INTERCEPT_OBJECTACCESSOR; |
561
|
|
|
} elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED || ($hasWhitespace && !$callDetected && !$hasPass)) { |
562
|
|
|
// In order to qualify for potentially being an expression, the entire inline node must contain |
563
|
|
|
// whitespace, must not contain parenthesis, must not contain a colon and must not contain an |
564
|
|
|
// inline pass operand. This significantly limits the number of times this (expensive) routine |
565
|
|
|
// has to be executed. |
566
|
|
|
$interceptionPoint = InterceptorInterface::INTERCEPT_TEXT; |
567
|
|
|
$childNodeToAdd = new TextNode($text); |
568
|
|
|
foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) { |
569
|
|
|
$matchedVariables = []; |
570
|
|
|
// TODO: rewrite expression nodes to receive a sub-Splitter that lets the expression node |
571
|
|
|
// consume a symbol+capture sequence and either match or ignore it; then use the already |
572
|
|
|
// consumed (possibly halted mid-way through iterator!) sequence to achieve desired behavior. |
573
|
|
|
preg_match_all($expressionNodeTypeClassName::$detectionExpression, $text, $matchedVariables, PREG_SET_ORDER); |
574
|
|
|
foreach ($matchedVariables as $matchedVariableSet) { |
|
|
|
|
575
|
|
|
try { |
576
|
|
|
$childNodeToAdd = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $this->state); |
577
|
|
|
$interceptionPoint = InterceptorInterface::INTERCEPT_EXPRESSION; |
578
|
|
|
} catch (ExpressionException $error) { |
579
|
|
|
$childNodeToAdd = new TextNode($this->renderingContext->getErrorHandler()->handleExpressionError($error)); |
580
|
|
|
} |
581
|
|
|
break; |
582
|
|
|
} |
583
|
|
|
} |
584
|
|
|
$node = isset($node) ? $node->addChildNode($childNodeToAdd) : $childNodeToAdd; |
585
|
|
|
} elseif (!$hasPass && !$callDetected) { |
586
|
|
|
// Third priority check is if there was no pass syntax and no ViewHelper, in which case we |
587
|
|
|
// create a standard ObjectAccessorNode; alternatively, if nothing was captured (expression |
588
|
|
|
// was empty, e.g. {} was used) we create a TextNode with the captured text to output "{}". |
589
|
|
|
if (isset($potentialAccessor)) { |
590
|
|
|
// If the accessor is set we can trust it is not a numeric value, since this will have |
591
|
|
|
// set $isArray to TRUE if nothing else already did so. |
592
|
|
|
$node = is_numeric($potentialAccessor) ? $potentialAccessor + 0 : new ObjectAccessorNode($potentialAccessor); |
593
|
|
|
$interceptionPoint = InterceptorInterface::INTERCEPT_OBJECTACCESSOR; |
594
|
|
|
} else { |
595
|
|
|
$node = new TextNode($text); |
596
|
|
|
$interceptionPoint = InterceptorInterface::INTERCEPT_TEXT; |
597
|
|
|
} |
598
|
|
|
} elseif ($hasPass && $this->resolver->isAliasRegistered((string)$potentialAccessor)) { |
599
|
|
|
// Fourth priority check is for a pass to a ViewHelper alias, e.g. "{value | raw}" in which case |
600
|
|
|
// we look for the alias used and create a ViewHelperNode with no arguments. |
601
|
|
|
$childNodeToAdd = $node; |
602
|
|
|
$node = $this->resolver->createViewHelperInstance(null, $potentialAccessor); |
603
|
|
|
$node->addChildNode($childNodeToAdd); |
604
|
|
|
$node = $node->postParse($arguments, $this->state, $this->renderingContext); |
605
|
|
|
$interceptionPoint = InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER; |
606
|
|
|
} else { |
607
|
|
|
# TODO: should this be an error case, or should it result in a TextNode? |
608
|
|
|
throw $this->splitter->createErrorAtPosition( |
609
|
|
|
'Invalid inline syntax - not accessor, not expression, not array, not ViewHelper, but ' . |
610
|
|
|
'contains the tokens used by these in a sequence that is not valid Fluid syntax. You can ' . |
611
|
|
|
'most likely avoid this by adding whitespace inside the curly braces before the first ' . |
612
|
|
|
'Fluid-like symbol in the expression. Symbols recognized as Fluid are: "' . |
613
|
|
|
addslashes(implode('","', array_map('chr', $this->contexts->inline->bytes))) . '"', |
614
|
|
|
1558782228 |
615
|
|
|
); |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
$escapingEnabledBackup = $this->escapingEnabled; |
619
|
|
|
$this->escapingEnabled = (bool)((isset($viewHelper) && $node->isOutputEscapingEnabled()) || $escapingEnabledBackup); |
|
|
|
|
620
|
|
|
$this->callInterceptor($node, $interceptionPoint); |
621
|
|
|
$this->escapingEnabled = $escapingEnabledBackup; |
622
|
|
|
return $node; |
623
|
|
|
} |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
// See note in sequenceTagNode() end of method body. TL;DR: this is intentionally here instead of as "default" |
627
|
|
|
// case in the switch above for a very specific reason: the case is only encountered if seeing EOF before the |
628
|
|
|
// inline expression was closed. |
629
|
|
|
throw $this->splitter->createErrorAtPosition('Unterminated inline syntax', 1557838506); |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
/** |
633
|
|
|
* @param ArgumentDefinition[] $definitions |
634
|
|
|
* @param bool $numeric |
635
|
|
|
* @return ArrayNode |
636
|
|
|
*/ |
637
|
|
|
protected function sequenceArrayNode(array $definitions = null, bool $numeric = false): ArrayNode |
638
|
|
|
{ |
639
|
|
|
$array = []; |
640
|
|
|
|
641
|
|
|
$keyOrValue = null; |
642
|
|
|
$key = null; |
643
|
|
|
$escapingEnabledBackup = $this->escapingEnabled; |
644
|
|
|
$this->escapingEnabled = false; |
645
|
|
|
$itemCount = -1; |
646
|
|
|
$countedEscapes = 0; |
647
|
|
|
|
648
|
|
|
$this->splitter->sequence->next(); |
649
|
|
|
foreach ($this->splitter->sequence as $symbol => $captured) { |
650
|
|
|
switch ($symbol) { |
651
|
|
|
case Splitter::BYTE_SEPARATOR_COLON: |
652
|
|
|
case Splitter::BYTE_SEPARATOR_EQUALS: |
653
|
|
|
// Colon or equals has same meaning (which allows tag syntax as argument syntax). Encountering this |
654
|
|
|
// byte always means the preceding byte was a key. However, if nothing was captured before this, |
655
|
|
|
// it means colon or equals was used without a key which is a syntax error. |
656
|
|
|
$key = $key ?? $captured ?? (isset($keyOrValue) ? $keyOrValue->flatten(true) : null); |
657
|
|
|
if (!isset($key)) { |
658
|
|
|
throw $this->splitter->createErrorAtPosition('Unexpected colon or equals sign, no preceding key', 1559250839); |
659
|
|
|
} |
660
|
|
|
if ($definitions !== null && !$numeric && !isset($definitions[$key])) { |
661
|
|
|
throw $this->splitter->createUnsupportedArgumentError((string)$key, $definitions); |
662
|
|
|
} |
663
|
|
|
break; |
664
|
|
|
|
665
|
|
|
case Splitter::BYTE_ARRAY_START: |
666
|
|
|
case Splitter::BYTE_INLINE: |
667
|
|
|
// Minimal safeguards to improve error feedback. Theoretically such "garbage" could simply be ignored |
668
|
|
|
// without causing problems to the parser, but it is probably best to report it as it could indicate |
669
|
|
|
// the user expected X value but gets Y and doesn't notice why. |
670
|
|
|
if ($captured !== null) { |
671
|
|
|
throw $this->splitter->createErrorAtPosition('Unexpected content before array/inline start in associative array, ASCII: ' . ord($captured), 1559131849); |
672
|
|
|
} |
673
|
|
|
if (!isset($key) && !$numeric) { |
674
|
|
|
throw $this->splitter->createErrorAtPosition('Unexpected array/inline start in associative array without preceding key', 1559131848); |
675
|
|
|
} |
676
|
|
|
|
677
|
|
|
// Encountering a curly brace or square bracket start byte will both cause a sub-array to be sequenced, |
678
|
|
|
// the difference being that only the square bracket will cause third parameter ($numeric) passed to |
679
|
|
|
// sequenceArrayNode() to be true, which in turn causes key-less items to be added with numeric indexes. |
680
|
|
|
$key = $key ?? ++$itemCount; |
681
|
|
|
$array[$key] = $this->sequenceArrayNode(null, $symbol === Splitter::BYTE_ARRAY_START); |
682
|
|
|
$keyOrValue = null; |
683
|
|
|
$key = null; |
684
|
|
|
break; |
685
|
|
|
|
686
|
|
|
case Splitter::BYTE_QUOTE_SINGLE: |
687
|
|
|
case Splitter::BYTE_QUOTE_DOUBLE: |
688
|
|
|
// Safeguard: if anything is captured before a quote this indicates garbage leading content. As with |
689
|
|
|
// the garbage safeguards above, this one could theoretically be ignored in favor of silently making |
690
|
|
|
// the odd syntax "just work". |
691
|
|
|
if ($captured !== null) { |
692
|
|
|
throw $this->splitter->createErrorAtPosition('Unexpected content before quote start in associative array, ASCII: ' . ord($captured), 1559145560); |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
// Quotes will always cause sequencing of the quoted string, but differs in behavior based on whether |
696
|
|
|
// or not the $key is set. If $key is set, we know for sure we can assign a value. If it is not set |
697
|
|
|
// we instead leave $keyOrValue defined so this will be processed by one of the next iterations. |
698
|
|
|
$keyOrValue = $this->sequenceQuotedNode($countedEscapes); |
699
|
|
|
if (isset($key)) { |
700
|
|
|
$array[$key] = $keyOrValue->flatten(true); |
701
|
|
|
$keyOrValue = null; |
702
|
|
|
$key = null; |
703
|
|
|
$countedEscapes = 0; |
704
|
|
|
} |
705
|
|
|
break; |
706
|
|
|
|
707
|
|
|
case Splitter::BYTE_SEPARATOR_COMMA: |
708
|
|
|
// Comma separator: if we've collected a key or value, use it. Otherwise, use captured string. |
709
|
|
|
// If neither key nor value nor captured string exists, ignore the comma (likely a tailing comma). |
710
|
|
|
if (isset($keyOrValue)) { |
711
|
|
|
// Key or value came as quoted string and exists in $keyOrValue |
712
|
|
|
$potentialValue = $keyOrValue->flatten(true); |
713
|
|
|
$key = $numeric ? ++$itemCount : $potentialValue; |
714
|
|
|
$array[$key] = $numeric ? $potentialValue : (is_numeric($key) ? $key + 0 : new ObjectAccessorNode($key)); |
|
|
|
|
715
|
|
|
} elseif (isset($captured)) { |
716
|
|
|
$key = $key ?? ($numeric ? ++$itemCount : $captured); |
717
|
|
|
if (!$numeric && isset($definitions) && !isset($definitions[$key])) { |
718
|
|
|
throw $this->splitter->createUnsupportedArgumentError((string)$key, $definitions); |
719
|
|
|
} |
720
|
|
|
$array[$key] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured); |
721
|
|
|
} |
722
|
|
|
$keyOrValue = null; |
723
|
|
|
$key = null; |
724
|
|
|
break; |
725
|
|
|
|
726
|
|
|
case Splitter::BYTE_WHITESPACE_TAB: |
727
|
|
|
case Splitter::BYTE_WHITESPACE_RETURN: |
728
|
|
|
case Splitter::BYTE_WHITESPACE_EOL: |
729
|
|
|
case Splitter::BYTE_WHITESPACE_SPACE: |
730
|
|
|
// Any whitespace attempts to set the key, if not already set. The captured string may be null as |
731
|
|
|
// well, leaving the $key variable still null and able to be coalesced. |
732
|
|
|
$key = $key ?? $captured; |
733
|
|
|
break; |
734
|
|
|
|
735
|
|
|
case Splitter::BYTE_BACKSLASH: |
736
|
|
|
// Escapes are simply counted and passed to the sequenceQuotedNode() method, causing that method |
737
|
|
|
// to ignore exactly this number of backslashes before a matching quote is seen as closing quote. |
738
|
|
|
++$countedEscapes; |
739
|
|
|
break; |
740
|
|
|
|
741
|
|
|
case Splitter::BYTE_INLINE_END: |
742
|
|
|
case Splitter::BYTE_ARRAY_END: |
743
|
|
|
case Splitter::BYTE_PARENTHESIS_END: |
744
|
|
|
// Array end indication. Check if anything was collected previously or was captured currently, |
745
|
|
|
// assign that to the array and return an ArrayNode with the full array inside. |
746
|
|
|
$captured = $captured ?? (isset($keyOrValue) ? $keyOrValue->flatten(true) : null); |
747
|
|
|
$key = $key ?? ($numeric ? ++$itemCount : $captured); |
748
|
|
|
if (isset($captured, $key)) { |
749
|
|
|
if (is_numeric($captured)) { |
750
|
|
|
$array[$key] = $captured + 0; |
751
|
|
|
} elseif (isset($keyOrValue)) { |
752
|
|
|
$array[$key] = $keyOrValue->flatten(); |
753
|
|
|
} else { |
754
|
|
|
$array[$key] = new ObjectAccessorNode($captured ?? $key); |
755
|
|
|
} |
756
|
|
|
} |
757
|
|
|
if (!$numeric && isset($key, $definitions) && !isset($definitions[$key])) { |
758
|
|
|
throw $this->splitter->createUnsupportedArgumentError((string)$key, $definitions); |
759
|
|
|
} |
760
|
|
|
$this->escapingEnabled = $escapingEnabledBackup; |
761
|
|
|
return new ArrayNode($array); |
762
|
|
|
} |
763
|
|
|
} |
764
|
|
|
|
765
|
|
|
throw $this->splitter->createErrorAtPosition( |
766
|
|
|
'Unterminated array', |
767
|
|
|
1557748574 |
768
|
|
|
); |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
/** |
772
|
|
|
* Sequence a quoted value |
773
|
|
|
* |
774
|
|
|
* The return can be either of: |
775
|
|
|
* |
776
|
|
|
* 1. A string value if source was for example "string" |
777
|
|
|
* 2. An integer if source was for example "1" |
778
|
|
|
* 3. A float if source was for example "1.25" |
779
|
|
|
* 4. A RootNode instance with multiple child nodes if source was for example "string {var}" |
780
|
|
|
* |
781
|
|
|
* The idea is to return the raw value if there is no reason for it to |
782
|
|
|
* be a node as such - which is only necessary if the quoted expression |
783
|
|
|
* contains other (dynamic) values like an inline syntax. |
784
|
|
|
* |
785
|
|
|
* @param int $leadingEscapes A backwards compatibility measure: when passed, this number of escapes must precede a closing quote for it to trigger node closing. |
786
|
|
|
* @param bool $allowArray |
787
|
|
|
* @return RootNode |
788
|
|
|
*/ |
789
|
|
|
protected function sequenceQuotedNode(int $leadingEscapes = 0, $allowArray = true): RootNode |
790
|
|
|
{ |
791
|
|
|
$startingByte = $this->source->bytes[$this->splitter->index]; |
792
|
|
|
$contextToRestore = $this->splitter->switch($this->contexts->quoted); |
793
|
|
|
$node = new RootNode(); |
794
|
|
|
$this->splitter->sequence->next(); |
795
|
|
|
$countedEscapes = 0; |
796
|
|
|
|
797
|
|
|
foreach ($this->splitter->sequence as $symbol => $captured) { |
798
|
|
|
switch ($symbol) { |
799
|
|
|
|
800
|
|
|
case Splitter::BYTE_ARRAY_START: |
801
|
|
|
$countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored) |
802
|
|
|
if ($captured === null) { |
803
|
|
|
// Array start "[" only triggers array sequencing if it is the very first byte in the quoted |
804
|
|
|
// string - otherwise, it is added as part of the text. |
805
|
|
|
$this->splitter->switch($this->contexts->array); |
806
|
|
|
$node->addChildNode($this->sequenceArrayNode(null, $allowArray)); |
807
|
|
|
$this->splitter->switch($this->contexts->quoted); |
808
|
|
|
} else { |
809
|
|
|
$node->addChildNode(new TextNode($captured . '[')); |
810
|
|
|
} |
811
|
|
|
break; |
812
|
|
|
|
813
|
|
|
case Splitter::BYTE_INLINE: |
814
|
|
|
$countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored) |
815
|
|
|
// The quoted string contains a sub-expression. We extract the captured content so far and if it |
816
|
|
|
// is not an empty string, add it as a child of the RootNode we're building, then we add the inline |
817
|
|
|
// expression as next sibling and continue the loop. |
818
|
|
|
if ($captured !== null) { |
819
|
|
|
$childNode = new TextNode($captured); |
820
|
|
|
$this->callInterceptor($childNode, InterceptorInterface::INTERCEPT_TEXT); |
821
|
|
|
$node->addChildNode($childNode); |
822
|
|
|
} |
823
|
|
|
|
824
|
|
|
$node->addChildNode($this->sequenceInlineNodes()); |
825
|
|
|
$this->splitter->switch($this->contexts->quoted); |
826
|
|
|
break; |
827
|
|
|
|
828
|
|
|
case Splitter::BYTE_BACKSLASH: |
829
|
|
|
$next = $this->source->bytes[$this->splitter->index + 1] ?? null; |
830
|
|
|
++$countedEscapes; |
831
|
|
|
if ($next === $startingByte || $next === Splitter::BYTE_BACKSLASH) { |
832
|
|
|
if ($captured !== null) { |
833
|
|
|
$node->addChildNode(new TextNode($captured)); |
834
|
|
|
} |
835
|
|
|
} else { |
836
|
|
|
$node->addChildNode(new TextNode($captured . str_repeat('\\', $countedEscapes))); |
837
|
|
|
$countedEscapes = 0; |
838
|
|
|
} |
839
|
|
|
break; |
840
|
|
|
|
841
|
|
|
// Note: although "case $startingByte:" could have been used here, it would not compile the switch |
842
|
|
|
// as a hash map and thus would not perform as well overall - when called frequently as it will be. |
843
|
|
|
// Backtick will only be encountered if the context is "protected" (insensitive inline sequencing) |
844
|
|
|
case Splitter::BYTE_QUOTE_SINGLE: |
845
|
|
|
case Splitter::BYTE_QUOTE_DOUBLE: |
846
|
|
|
case Splitter::BYTE_BACKTICK: |
847
|
|
|
if ($symbol !== $startingByte || $countedEscapes !== $leadingEscapes) { |
848
|
|
|
$node->addChildNode(new TextNode($captured . chr($symbol))); |
849
|
|
|
$countedEscapes = 0; // If number of escapes do not match expected, reset the counter |
850
|
|
|
break; |
851
|
|
|
} |
852
|
|
|
if ($captured !== null) { |
853
|
|
|
$node->addChildNode(new TextNode($captured)); |
854
|
|
|
} |
855
|
|
|
$this->splitter->switch($contextToRestore); |
856
|
|
|
return $node; |
857
|
|
|
} |
858
|
|
|
} |
859
|
|
|
|
860
|
|
|
throw $this->splitter->createErrorAtPosition('Unterminated expression inside quotes', 1557700793); |
861
|
|
|
} |
862
|
|
|
|
863
|
|
|
/** |
864
|
|
|
* Call all interceptors registered for a given interception point. |
865
|
|
|
* |
866
|
|
|
* @param NodeInterface $node The syntax tree node which can be modified by the interceptors. |
867
|
|
|
* @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants. |
868
|
|
|
* @return void |
869
|
|
|
*/ |
870
|
|
|
protected function callInterceptor(NodeInterface &$node, $interceptionPoint) |
871
|
|
|
{ |
872
|
|
|
if ($this->escapingEnabled) { |
873
|
|
|
/** @var $interceptor InterceptorInterface */ |
874
|
|
|
foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) { |
875
|
|
|
$node = $interceptor->process($node, $interceptionPoint, $this->state); |
876
|
|
|
} |
877
|
|
|
} |
878
|
|
|
|
879
|
|
|
/** @var $interceptor InterceptorInterface */ |
880
|
|
|
foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) { |
881
|
|
|
$node = $interceptor->process($node, $interceptionPoint, $this->state); |
882
|
|
|
} |
883
|
|
|
} |
884
|
|
|
} |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.