970bc2c293968e2bbcda06a3ddc1281779bacc08
[Packages/TYPO3.CMS.git] / typo3 / sysext / fluid / Classes / Core / Parser / TemplateParser.php
1 <?php
2
3 /* *
4 * This script belongs to the FLOW3 package "Fluid". *
5 * *
6 * It is free software; you can redistribute it and/or modify it under *
7 * the terms of the GNU Lesser General Public License as published by the *
8 * Free Software Foundation, either version 3 of the License, or (at your *
9 * option) any later version. *
10 * *
11 * This script is distributed in the hope that it will be useful, but *
12 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHAN- *
13 * TABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser *
14 * General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU Lesser General Public *
17 * License along with the script. *
18 * If not, see http://www.gnu.org/licenses/lgpl.html *
19 * *
20 * The TYPO3 project - inspiring people to share! *
21 * */
22
23 /**
24 * Template parser building up an object syntax tree
25 *
26 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 or later
27 */
28 class Tx_Fluid_Core_Parser_TemplateParser {
29
30 public static $SCAN_PATTERN_NAMESPACEDECLARATION = '/(?<!\\\\){namespace\s*([a-zA-Z]+[a-zA-Z0-9]*)\s*=\s*((?:F3|Tx)(?:FLUID_NAMESPACE_SEPARATOR\w+)+)\s*}/m';
31
32 /**
33 * This regular expression splits the input string at all dynamic tags, AND
34 * on all <![CDATA[...]]> sections.
35 *
36 * @author Sebastian Kurfürst <sebastian@typo3.org>
37 */
38 public static $SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS = '/
39 (
40 (?: <\/? # Start dynamic tags
41 (?:(?:NAMESPACE):[a-zA-Z0-9\\.]+) # A tag consists of the namespace prefix and word characters
42 (?: # Begin tag arguments
43 \s*[a-zA-Z0-9:]+ # Argument Keys
44 = # =
45 (?: # either...
46 "(?:\\\"|[^"])*" # a double-quoted string
47 |\'(?:\\\\\'|[^\'])*\' # or a single quoted string
48 )\s* #
49 )* # Tag arguments can be replaced many times.
50 \s*
51 \/?> # Closing tag
52 )
53 |(?: # Start match CDATA section
54 <!\[CDATA\[.*?\]\]>
55 )
56 )/xs';
57
58 /**
59 * This regular expression scans if the input string is a ViewHelper tag
60 *
61 * @author Sebastian Kurfürst <sebastian@typo3.org>
62 */
63 public static $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG = '/
64 ^< # A Tag begins with <
65 (?P<NamespaceIdentifier>NAMESPACE): # Then comes the Namespace prefix followed by a :
66 (?P<MethodIdentifier> # Now comes the Name of the ViewHelper
67 [a-zA-Z0-9\\.]+
68 )
69 (?P<Attributes> # Begin Tag Attributes
70 (?: # A tag might have multiple attributes
71 \s*
72 [a-zA-Z0-9:]+ # The attribute name
73 = # =
74 (?: # either...
75 "(?:\\\"|[^"])*" # a double-quoted string
76 |\'(?:\\\\\'|[^\'])*\' # or a single quoted string
77 ) #
78 \s*
79 )* # A tag might have multiple attributes
80 ) # End Tag Attributes
81 \s*
82 (?P<Selfclosing>\/?) # A tag might be selfclosing
83 >$/x';
84
85 /**
86 * This regular expression scans if the input string is a closing ViewHelper
87 * tag.
88 *
89 * @author Sebastian Kurfürst <sebastian@typo3.org>
90 */
91 public static $SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG = '/^<\/(?P<NamespaceIdentifier>NAMESPACE):(?P<MethodIdentifier>[a-zA-Z0-9\\.]+)\s*>$/';
92
93 /**
94 * This regular expression splits the tag arguments into its parts
95 *
96 * @author Sebastian Kurfürst <sebastian@typo3.org>
97 */
98 public static $SPLIT_PATTERN_TAGARGUMENTS = '/
99 (?: #
100 \s* #
101 (?P<Argument> # The attribute name
102 [a-zA-Z0-9:]+ #
103 ) #
104 = # =
105 (?: # obsolete I guess
106 (?P<ValueQuoted> # either...
107 (?:"(?:\\\"|[^"])*") # a double-quoted string
108 |(?:\'(?:\\\\\'|[^\'])*\') # or a single quoted string
109 )
110 )\s*
111 )
112 /xs';
113
114 /**
115 * This pattern detects CDATA sections and outputs the text between opening
116 * and closing CDATA.
117 *
118 * @author Sebastian Kurfürst <sebastian@typo3.org>
119 */
120 public static $SCAN_PATTERN_CDATA = '/^<!\[CDATA\[(.*?)\]\]>$/s';
121
122 /**
123 * Pattern which splits the shorthand syntax into different tokens. The
124 * "shorthand syntax" is everything like {...}
125 *
126 * @author Sebastian Kurfürst <sebastian@typo3.org>
127 */
128 public static $SPLIT_PATTERN_SHORTHANDSYNTAX = '/
129 (
130 { # Start of shorthand syntax
131 (?: # Shorthand syntax is either composed of...
132 [a-zA-Z0-9\->_:,.()] # Various characters
133 |"(?:\\\"|[^"])*" # Double-quoted strings
134 |\'(?:\\\\\'|[^\'])*\' # Single-quoted strings
135 |(?R) # Other shorthand syntaxes inside, albeit not in a quoted string
136 |\s+ # Spaces
137 )+
138 } # End of shorthand syntax
139 )/x';
140
141 /**
142 * Pattern which detects the object accessor syntax:
143 * {object.some.value}, additionally it detects ViewHelpers like
144 * {f:for(param1:bla)} and chaining like
145 * {object.some.value->f:bla.blubb()->f:bla.blubb2()}
146 *
147 * THIS IS ALMOST THE SAME AS IN $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
148 *
149 * @author Sebastian Kurfürst <sebastian@typo3.org>
150 */
151 public static $SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS = '/
152 ^{ # Start of shorthand syntax
153 # A shorthand syntax is either...
154 (?P<Object>[a-zA-Z0-9\-_.]*) # ... an object accessor
155 \s*(?P<Delimiter>(?:->)?)\s*
156
157 (?P<ViewHelper> # ... a ViewHelper
158 [a-zA-Z0-9]+ # Namespace prefix of ViewHelper (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
159 :
160 [a-zA-Z0-9\\.]+ # Method Identifier (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
161 \( # Opening parameter brackets of ViewHelper
162 (?P<ViewHelperArguments> # Start submatch for ViewHelper arguments. This is taken from $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
163 (?:
164 \s*[a-zA-Z0-9\-_]+ # The keys of the array
165 \s*:\s* # Key|Value delimiter :
166 (?: # Possible value options:
167 "(?:\\\"|[^"])*" # Double qouoted string
168 |\'(?:\\\\\'|[^\'])*\' # Single quoted string
169 |[a-zA-Z0-9\-_.]+ # variable identifiers
170 |{(?P>ViewHelperArguments)} # Another sub-array
171 ) # END possible value options
172 \s*,? # There might be a , to seperate different parts of the array
173 )* # The above cycle is repeated for all array elements
174 ) # End ViewHelper Arguments submatch
175 \) # Closing parameter brackets of ViewHelper
176 )?
177 (?P<AdditionalViewHelpers> # There can be more than one ViewHelper chained, by adding more -> and the ViewHelper (recursively)
178 (?:
179 \s*->\s*
180 (?P>ViewHelper)
181 )*
182 )
183 }$/x';
184
185 /**
186 * THIS IS ALMOST THE SAME AS $SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS
187 *
188 * @author Sebastian Kurfürst <sebastian@typo3.org>
189 */
190 public static $SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER = '/
191
192 (?P<NamespaceIdentifier>[a-zA-Z0-9]+) # Namespace prefix of ViewHelper (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
193 :
194 (?P<MethodIdentifier>[a-zA-Z0-9\\.]+)
195 \( # Opening parameter brackets of ViewHelper
196 (?P<ViewHelperArguments> # Start submatch for ViewHelper arguments. This is taken from $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
197 (?:
198 \s*[a-zA-Z0-9\-_]+ # The keys of the array
199 \s*:\s* # Key|Value delimiter :
200 (?: # Possible value options:
201 "(?:\\\"|[^"])*" # Double qouoted string
202 |\'(?:\\\\\'|[^\'])*\' # Single quoted string
203 |[a-zA-Z0-9\-_.]+ # variable identifiers
204 |{(?P>ViewHelperArguments)} # Another sub-array
205 ) # END possible value options
206 \s*,? # There might be a , to seperate different parts of the array
207 )* # The above cycle is repeated for all array elements
208 ) # End ViewHelper Arguments submatch
209 \) # Closing parameter brackets of ViewHelper
210 /x';
211
212 /**
213 * Pattern which detects the array/object syntax like in JavaScript, so it
214 * detects strings like:
215 * {object: value, object2: {nested: array}, object3: "Some string"}
216 *
217 * THIS IS ALMOST THE SAME AS IN SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS
218 *
219 * @author Sebastian Kurfürst <sebastian@typo3.org>
220 */
221 public static $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS = '/^
222 (?P<Recursion> # Start the recursive part of the regular expression - describing the array syntax
223 { # Each array needs to start with {
224 (?P<Array> # Start submatch
225 (?:
226 \s*[a-zA-Z0-9\-_]+ # The keys of the array
227 \s*:\s* # Key|Value delimiter :
228 (?: # Possible value options:
229 "(?:\\\"|[^"])*" # Double qouoted string
230 |\'(?:\\\\\'|[^\'])*\' # Single quoted string
231 |[a-zA-Z0-9\-_.]+ # variable identifiers
232 |(?P>Recursion) # Another sub-array
233 ) # END possible value options
234 \s*,? # There might be a , to seperate different parts of the array
235 )* # The above cycle is repeated for all array elements
236 ) # End array submatch
237 } # Each array ends with }
238 )$/x';
239
240 /**
241 * This pattern splits an array into its parts. It is quite similar to the
242 * pattern above.
243 *
244 * @author Sebastian Kurfürst <sebastian@typo3.org>
245 */
246 public static $SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS = '/
247 (?P<ArrayPart> # Start submatch
248 (?P<Key>[a-zA-Z0-9\-_]+) # The keys of the array
249 \s*:\s* # Key|Value delimiter :
250 (?: # Possible value options:
251 (?P<QuotedString> # Quoted string
252 (?:"(?:\\\"|[^"])*")
253 |(?:\'(?:\\\\\'|[^\'])*\')
254 )
255 |(?P<VariableIdentifier>[a-zA-Z][a-zA-Z0-9\-_.]*) # variable identifiers have to start with a letter
256 |(?P<Number>[0-9.]+) # Number
257 |{\s*(?P<Subarray>(?:(?P>ArrayPart)\s*,?\s*)+)\s*} # Another sub-array
258 ) # END possible value options
259 ) # End array part submatch
260 /x';
261
262 /**
263 * Namespace identifiers and their component name prefix (Associative array).
264 * @var array
265 */
266 protected $namespaces = array(
267 'f' => 'Tx_Fluid_ViewHelpers'
268 );
269
270 /**
271 * @var Tx_Fluid_Compatibility_ObjectManager
272 */
273 protected $objectManager;
274
275 /**
276 * @var Tx_Fluid_Core_Parser_Configuration
277 */
278 protected $configuration;
279
280 /**
281 * Constructor. Preprocesses the $SCAN_PATTERN_NAMESPACEDECLARATION by
282 * inserting the correct namespace separator.
283 *
284 * @author Sebastian Kurfürst <sebastian@typo3.org>
285 */
286 public function __construct() {
287 self::$SCAN_PATTERN_NAMESPACEDECLARATION = str_replace('FLUID_NAMESPACE_SEPARATOR', preg_quote(Tx_Fluid_Fluid::NAMESPACE_SEPARATOR), self::$SCAN_PATTERN_NAMESPACEDECLARATION);
288 }
289
290 /**
291 * Inject object factory
292 *
293 * @param Tx_Fluid_Compatibility_ObjectManager $objectManager
294 * @return void
295 * @author Sebastian Kurfürst <sebastian@typo3.org>
296 */
297 public function injectObjectManager(Tx_Fluid_Compatibility_ObjectManager $objectManager) {
298 $this->objectManager = $objectManager;
299 }
300
301 /**
302 * Set the configuration for the parser.
303 *
304 * @param Tx_Fluid_Core_Parser_Configuration $configuration
305 * @return void
306 * @author Karsten Dambekalns <karsten@typo3.org>
307 */
308 public function setConfiguration(Tx_Fluid_Core_Parser_Configuration $configuration = NULL) {
309 $this->configuration = $configuration;
310 }
311
312 /**
313 * Parses a given template and returns a parsed template object.
314 *
315 * @param string $templateString The template to parse as a string
316 * @return Tx_Fluid_Core_Parser_ParsedTemplateInterface Parsed template
317 * @author Sebastian Kurfürst <sebastian@typo3.org>
318 * @todo Refine doc comment
319 */
320 public function parse($templateString) {
321 if (!is_string($templateString)) throw new Tx_Fluid_Core_Parser_Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
322
323 $this->reset();
324
325 $templateString = $this->extractNamespaceDefinitions($templateString);
326 $splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
327 $parsingState = $this->buildObjectTree($splitTemplate);
328
329 return $parsingState;
330 }
331
332 /**
333 * Gets the namespace definitions found.
334 *
335 * @return array Namespace identifiers and their component name prefix
336 * @author Sebastian Kurfürst <sebastian@typo3.org>
337 */
338 public function getNamespaces() {
339 return $this->namespaces;
340 }
341
342 /**
343 * Resets the parser to its default values.
344 *
345 * @return void
346 * @author Sebastian Kurfürst <sebastian@typo3.org>
347 */
348 protected function reset() {
349 $this->namespaces = array(
350 'f' => 'Tx_Fluid_ViewHelpers'
351 );
352 }
353
354 /**
355 * Extracts namespace definitions out of the given template string and sets
356 * $this->namespaces.
357 *
358 * @param string $templateString Template string to extract the namespaces from
359 * @return string The updated template string without namespace declarations inside
360 * @author Sebastian Kurfürst <sebastian@typo3.org>
361 */
362 protected function extractNamespaceDefinitions($templateString) {
363 $matchedVariables = array();
364 if (preg_match_all(self::$SCAN_PATTERN_NAMESPACEDECLARATION, $templateString, $matchedVariables) > 0) {
365 foreach (array_keys($matchedVariables[0]) as $index) {
366 $namespaceIdentifier = $matchedVariables[1][$index];
367 $fullyQualifiedNamespace = $matchedVariables[2][$index];
368 if (key_exists($namespaceIdentifier, $this->namespaces)) {
369 throw new Tx_Fluid_Core_Parser_Exception('Namespace identifier "' . $namespaceIdentifier . '" is already registered. Do not redeclare namespaces!', 1224241246);
370 }
371 $this->namespaces[$namespaceIdentifier] = $fullyQualifiedNamespace;
372 }
373
374 $templateString = preg_replace(self::$SCAN_PATTERN_NAMESPACEDECLARATION, '', $templateString);
375 }
376 return $templateString;
377 }
378
379 /**
380 * Splits the template string on all dynamic tags found.
381 *
382 * @param string $templateString Template string to split.
383 * @return array Splitted template
384 * @author Sebastian Kurfürst <sebastian@typo3.org>
385 */
386 protected function splitTemplateAtDynamicTags($templateString) {
387 $regularExpression = $this->prepareTemplateRegularExpression(self::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS);
388 return preg_split($regularExpression, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
389 }
390
391 /**
392 * Build object tree from the split template
393 *
394 * @param array $splitTemplate The split template, so that every tag with a namespace declaration is already a seperate array element.
395 * @return Tx_Fluid_Core_Parser_ParsingState
396 * @author Sebastian Kurfürst <sebastian@typo3.org>
397 */
398 protected function buildObjectTree($splitTemplate) {
399 $regularExpression_openingViewHelperTag = $this->prepareTemplateRegularExpression(self::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG);
400 $regularExpression_closingViewHelperTag = $this->prepareTemplateRegularExpression(self::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG);
401
402 $state = $this->objectManager->create('Tx_Fluid_Core_Parser_ParsingState');
403 $rootNode = $this->objectManager->create('Tx_Fluid_Core_Parser_SyntaxTree_RootNode');
404 $state->setRootNode($rootNode);
405 $state->pushNodeToStack($rootNode);
406
407 foreach ($splitTemplate as $templateElement) {
408 $matchedVariables = array();
409 if (preg_match(self::$SCAN_PATTERN_CDATA, $templateElement, $matchedVariables) > 0) {
410 $this->textHandler($state, $matchedVariables[1]);
411 } elseif (preg_match($regularExpression_openingViewHelperTag, $templateElement, $matchedVariables) > 0) {
412 $this->openingViewHelperTagHandler($state, $matchedVariables['NamespaceIdentifier'], $matchedVariables['MethodIdentifier'], $matchedVariables['Attributes'], ($matchedVariables['Selfclosing'] === '' ? FALSE : TRUE));
413 } elseif (preg_match($regularExpression_closingViewHelperTag, $templateElement, $matchedVariables) > 0) {
414 $this->closingViewHelperTagHandler($state, $matchedVariables['NamespaceIdentifier'], $matchedVariables['MethodIdentifier']);
415 } else {
416 $this->textAndShorthandSyntaxHandler($state, $templateElement);
417 }
418 }
419
420 if ($state->countNodeStack() !== 1) {
421 throw new Tx_Fluid_Core_Parser_Exception('Not all tags were closed!', 1238169398);
422 }
423 return $state;
424 }
425
426 /**
427 * Handles an opening or self-closing view helper tag.
428 *
429 * @param Tx_Fluid_Core_Parser_ParsingState $state Current parsing state
430 * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
431 * @param string $methodIdentifier Method identifier
432 * @param string $arguments Arguments string, not yet parsed
433 * @param boolean $selfclosing true, if the tag is a self-closing tag.
434 * @return void
435 * @author Sebastian Kurfürst <sebastian@typo3.org>
436 */
437 protected function openingViewHelperTagHandler(Tx_Fluid_Core_Parser_ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing) {
438 $argumentsObjectTree = $this->parseArguments($arguments);
439 $this->initializeViewHelperAndAddItToStack($state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree);
440
441 if ($selfclosing) {
442 $node = $state->popNodeFromStack();
443 $this->callInterceptor($node, Tx_Fluid_Core_Parser_InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER);
444 }
445 }
446
447 /**
448 * Initialize the given ViewHelper and adds it to the current node and to
449 * the stack.
450 *
451 * @param Tx_Fluid_Core_Parser_ParsingState $state Current parsing state
452 * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
453 * @param string $methodIdentifier Method identifier
454 * @param array $argumentsObjectTree Arguments object tree
455 * @return void
456 * @author Sebastian Kurfürst <sebastian@typo3.org>
457 * @author Karsten Dambekalns <karsten@typo3.org>
458 */
459 protected function initializeViewHelperAndAddItToStack(Tx_Fluid_Core_Parser_ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree) {
460 if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
461 throw new Tx_Fluid_Core_Parser_Exception('Namespace could not be resolved. This exception should never be thrown!', 1224254792);
462 }
463
464 $viewHelper = $this->objectManager->create($this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier));
465 $expectedViewHelperArguments = $viewHelper->prepareArguments();
466 $this->abortIfUnregisteredArgumentsExist($expectedViewHelperArguments, $argumentsObjectTree);
467 $this->abortIfRequiredArgumentsAreMissing($expectedViewHelperArguments, $argumentsObjectTree);
468
469 $currentDynamicNode = $this->objectManager->create('Tx_Fluid_Core_Parser_SyntaxTree_ViewHelperNode', $viewHelper, $argumentsObjectTree);
470
471 $state->getNodeFromStack()->addChildNode($currentDynamicNode);
472
473 // PostParse Facet
474 if ($viewHelper instanceof Tx_Fluid_Core_ViewHelper_Facets_PostParseInterface) {
475 // Don't just use $viewHelper::postParseEvent(...),
476 // as this will break with PHP < 5.3.
477 call_user_func(array($viewHelper, 'postParseEvent'), $currentDynamicNode, $argumentsObjectTree, $state->getVariableContainer());
478 }
479
480 $this->callInterceptor($currentDynamicNode, Tx_Fluid_Core_Parser_InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER);
481
482 $state->pushNodeToStack($currentDynamicNode);
483 }
484
485 /**
486 * Throw an exception if there are arguments which were not registered
487 * before.
488 *
489 * @param array $expectedArguments Array of Tx_Fluid_Core_ViewHelper_ArgumentDefinition of all expected arguments
490 * @param array $actualArguments Actual arguments
491 * @throws Tx_Fluid_Core_Parser_Exception
492 * @author Sebastian Kurfürst <sebastian@typo3.org>
493 */
494 protected function abortIfUnregisteredArgumentsExist($expectedArguments, $actualArguments) {
495 $expectedArgumentNames = array();
496 foreach ($expectedArguments as $expectedArgument) {
497 $expectedArgumentNames[] = $expectedArgument->getName();
498 }
499
500 foreach (array_keys($actualArguments) as $argumentName) {
501 if (!in_array($argumentName, $expectedArgumentNames)) {
502 throw new Tx_Fluid_Core_Parser_Exception('Argument "' . $argumentName . '" was not registered.', 1237823695);
503 }
504 }
505 }
506
507 /**
508 * Throw an exception if required arguments are missing
509 *
510 * @param array $expectedArguments Array of Tx_Fluid_Core_ViewHelper_ArgumentDefinition of all expected arguments
511 * @param array $actualArguments Actual arguments
512 * @throws Tx_Fluid_Core_Parser_Exception
513 * @author Sebastian Kurfürst <sebastian@typo3.org>
514 */
515 protected function abortIfRequiredArgumentsAreMissing($expectedArguments, $actualArguments) {
516 $actualArgumentNames = array_keys($actualArguments);
517 foreach ($expectedArguments as $expectedArgument) {
518 if ($expectedArgument->isRequired() && !in_array($expectedArgument->getName(), $actualArgumentNames)) {
519 throw new Tx_Fluid_Core_Parser_Exception('Required argument "' . $expectedArgument->getName() . '" was not supplied.', 1237823699);
520 }
521 }
522 }
523
524 /**
525 * Resolve a viewhelper name.
526 *
527 * @param string $namespaceIdentifier Namespace identifier for the view helper.
528 * @param string $methodIdentifier Method identifier, might be hierarchical like "link.url"
529 * @return string The fully qualified class name of the viewhelper
530 * @author Sebastian Kurfürst <sebastian@typo3.org>
531 */
532 protected function resolveViewHelperName($namespaceIdentifier, $methodIdentifier) {
533 $explodedViewHelperName = explode('.', $methodIdentifier);
534 $className = '';
535 if (count($explodedViewHelperName) > 1) {
536 $className = implode(Tx_Fluid_Fluid::NAMESPACE_SEPARATOR, array_map('ucfirst', $explodedViewHelperName));
537 } else {
538 $className = ucfirst($explodedViewHelperName[0]);
539 }
540 $className .= 'ViewHelper';
541
542 $name = $this->namespaces[$namespaceIdentifier] . Tx_Fluid_Fluid::NAMESPACE_SEPARATOR . $className;
543
544 return $name;
545 }
546
547 /**
548 * Handles a closing view helper tag
549 *
550 * @param Tx_Fluid_Core_Parser_ParsingState $state The current parsing state
551 * @param string $namespaceIdentifier Namespace identifier for the closing tag.
552 * @param string $methodIdentifier Method identifier.
553 * @return void
554 * @throws Tx_Fluid_Core_Parser_Exception
555 * @author Sebastian Kurfürst <sebastian@typo3.org>
556 */
557 protected function closingViewHelperTagHandler(Tx_Fluid_Core_Parser_ParsingState $state, $namespaceIdentifier, $methodIdentifier) {
558 if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
559 throw new Tx_Fluid_Core_Parser_Exception('Namespace could not be resolved. This exception should never be thrown!', 1224256186);
560 }
561 $lastStackElement = $state->popNodeFromStack();
562 if (!($lastStackElement instanceof Tx_Fluid_Core_Parser_SyntaxTree_ViewHelperNode)) {
563 throw new Tx_Fluid_Core_Parser_Exception('You closed a templating tag which you never opened!', 1224485838);
564 }
565 if ($lastStackElement->getViewHelperClassName() != $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier)) {
566 throw new Tx_Fluid_Core_Parser_Exception('Templating tags not properly nested. Expected: ' . $lastStackElement->getViewHelperClassName() . '; Actual: ' . $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier), 1224485398);
567 }
568 $this->callInterceptor($lastStackElement, Tx_Fluid_Core_Parser_InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER);
569 }
570
571 /**
572 * Handles the appearance of an object accessor (like {posts.author.email}).
573 * Creates a new instance of Tx_Fluid_ObjectAccessorNode.
574 *
575 * Handles ViewHelpers as well which are in the shorthand syntax.
576 *
577 * @param Tx_Fluid_Core_Parser_ParsingState $state The current parsing state
578 * @param string $objectAccessorString String which identifies which objects to fetch
579 * @param string $delimiter
580 * @param string $viewHelperString
581 * @param string $additionalViewHelpersString
582 * @return void
583 * @author Sebastian Kurfürst <sebastian@typo3.org>
584 */
585 protected function objectAccessorHandler(Tx_Fluid_Core_Parser_ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString) {
586 $viewHelperString .= $additionalViewHelpersString;
587 $numberOfViewHelpers = 0;
588
589 // The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
590 // Resolves bug #5107.
591 if (strlen($delimiter) === 0 && strlen($viewHelperString) > 0) {
592 $viewHelperString = $objectAccessorString . $viewHelperString;
593 $objectAccessorString = '';
594 }
595
596 // ViewHelpers
597 $matches = array();
598 if (strlen($viewHelperString) > 0 && preg_match_all(self::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
599 // The last ViewHelper has to be added first for correct chaining.
600 foreach (array_reverse($matches) as $singleMatch) {
601 if (strlen($singleMatch['ViewHelperArguments']) > 0) {
602 $arguments = $this->postProcessArgumentsForObjectAccessor(
603 $this->recursiveArrayHandler($singleMatch['ViewHelperArguments'])
604 );
605 } else {
606 $arguments = array();
607 }
608 $this->initializeViewHelperAndAddItToStack($state, $singleMatch['NamespaceIdentifier'], $singleMatch['MethodIdentifier'], $arguments);
609 $numberOfViewHelpers++;
610 }
611 }
612
613 // Object Accessor
614 if (strlen($objectAccessorString) > 0) {
615
616 $node = $this->objectManager->create('Tx_Fluid_Core_Parser_SyntaxTree_ObjectAccessorNode', $objectAccessorString);
617 $this->callInterceptor($node, Tx_Fluid_Core_Parser_InterceptorInterface::INTERCEPT_OBJECTACCESSOR);
618
619 $state->getNodeFromStack()->addChildNode($node);
620 }
621
622 // Close ViewHelper Tags if needed.
623 for ($i=0; $i<$numberOfViewHelpers; $i++) {
624 $node = $state->popNodeFromStack();
625 $this->callInterceptor($node, Tx_Fluid_Core_Parser_InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER);
626 }
627 }
628
629 /**
630 * Call all interceptors registered for a given interception point.
631 *
632 * @param Tx_Fluid_Core_Parser_SyntaxTree_NodeInterface $node The syntax tree node which can be modified by the interceptors.
633 * @param integer $interceptionPoint the interception point. One of the Tx_Fluid_Core_Parser_InterceptorInterface::INTERCEPT_* constants.
634 * @return void
635 * @author Sebastian Kurfürst <sebastian@typo3.org>
636 */
637 protected function callInterceptor(Tx_Fluid_Core_Parser_SyntaxTree_NodeInterface &$node, $interceptionPoint) {
638 if ($this->configuration !== NULL) {
639 // $this->configuration is UNSET inside the arguments of a ViewHelper.
640 // That's why the interceptors are only called if the object accesor is not inside a ViewHelper Argument
641 // This could be a problem if We have a ViewHelper as an argument to another ViewHelper, and an ObjectAccessor nested inside there.
642 // TODO: Clean up this.
643 $interceptors = $this->configuration->getInterceptors($interceptionPoint);
644 if (count($interceptors) > 0) {
645 foreach($interceptors as $interceptor) {
646 $node = $interceptor->process($node, $interceptionPoint);
647 }
648 }
649 }
650 }
651
652 /**
653 * Post process the arguments for the ViewHelpers in the object accessor
654 * syntax. We need to convert an array into an array of (only) nodes
655 *
656 * @param array $arguments The arguments to be processed
657 * @return array the processed array
658 * @author Sebastian Kurfürst <sebastian@typo3.org>
659 * @todo This method should become superflous once the rest has been refactored, so that this code is not needed.
660 */
661 protected function postProcessArgumentsForObjectAccessor(array $arguments) {
662 foreach ($arguments as $argumentName => $argumentValue) {
663 if (!($argumentValue instanceof Tx_Fluid_Core_Parser_SyntaxTree_AbstractNode)) {
664 $arguments[$argumentName] = $this->objectManager->create('Tx_Fluid_Core_Parser_SyntaxTree_TextNode', (string)$argumentValue);
665 }
666 }
667 return $arguments;
668 }
669
670 /**
671 * Parse arguments of a given tag, and build up the Arguments Object Tree
672 * for each argument.
673 * Returns an associative array, where the key is the name of the argument,
674 * and the value is a single Argument Object Tree.
675 *
676 * @param string $argumentsString All arguments as string
677 * @return array An associative array of objects, where the key is the argument name.
678 * @author Sebastian Kurfürst <sebastian@typo3.org>
679 */
680 protected function parseArguments($argumentsString) {
681 $argumentsObjectTree = array();
682 $matches = array();
683 if (preg_match_all(self::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
684 $configurationBackup = $this->configuration;
685 $this->configuration = NULL;
686 foreach ($matches as $singleMatch) {
687 $argument = $singleMatch['Argument'];
688 $value = $this->unquoteString($singleMatch['ValueQuoted']);
689 $argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
690 }
691 $this->configuration = $configurationBackup;
692 }
693 return $argumentsObjectTree;
694 }
695
696 /**
697 * Build up an argument object tree for the string in $argumentString.
698 * This builds up the tree for a single argument value.
699 *
700 * This method also does some performance optimizations, so in case
701 * no { or < is found, then we just return a TextNode.
702 *
703 * @param string $argumentString
704 * @return ArgumentObject the corresponding argument object tree.
705 * @author Sebastian Kurfürst <sebastian@typo3.org>
706 */
707 protected function buildArgumentObjectTree($argumentString) {
708 if (strstr($argumentString, '{') === FALSE && strstr($argumentString, '<') === FALSE) {
709 return $this->objectManager->create('Tx_Fluid_Core_Parser_SyntaxTree_TextNode', $argumentString);
710 }
711 $splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
712 $rootNode = $this->buildObjectTree($splitArgument)->getRootNode();
713 return $rootNode;
714 }
715
716 /**
717 * Removes escapings from a given argument string and trims the outermost
718 * quotes.
719 *
720 * This method is meant as a helper for regular expression results.
721 *
722 * @param string $quotedValue Value to unquote
723 * @return string Unquoted value
724 * @author Sebastian Kurfürst <sebastian@typo3.org>
725 * @author Karsten Dambekalns <karsten@typo3.org>
726 */
727 protected function unquoteString($quotedValue) {
728 switch ($quotedValue[0]) {
729 case '"':
730 $value = str_replace('\"', '"', trim($quotedValue, '"'));
731 break;
732 case "'":
733 $value = str_replace("\'", "'", trim($quotedValue, "'"));
734 break;
735 }
736 return str_replace('\\\\', '\\', $value);
737 }
738
739 /**
740 * Takes a regular expression template and replaces "NAMESPACE" with the
741 * currently registered namespace identifiers. Returns a regular expression
742 * which is ready to use.
743 *
744 * @param string $regularExpression Regular expression template
745 * @return string Regular expression ready to be used
746 * @author Sebastian Kurfürst <sebastian@typo3.org>
747 */
748 protected function prepareTemplateRegularExpression($regularExpression) {
749 return str_replace('NAMESPACE', implode('|', array_keys($this->namespaces)), $regularExpression);
750 }
751
752 /**
753 * Handler for everything which is not a ViewHelperNode.
754 *
755 * This includes Text, array syntax, and object accessor syntax.
756 *
757 * @param Tx_Fluid_Core_Parser_ParsingState $state Current parsing state
758 * @param string $text Text to process
759 * @return void
760 * @author Sebastian Kurfürst <sebastian@typo3.org>
761 */
762 protected function textAndShorthandSyntaxHandler(Tx_Fluid_Core_Parser_ParsingState $state, $text) {
763 $sections = preg_split($this->prepareTemplateRegularExpression(self::$SPLIT_PATTERN_SHORTHANDSYNTAX), $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
764
765 foreach ($sections as $section) {
766 $matchedVariables = array();
767 if (preg_match(self::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
768 $this->objectAccessorHandler($state, $matchedVariables['Object'], $matchedVariables['Delimiter'], (isset($matchedVariables['ViewHelper'])?$matchedVariables['ViewHelper']:''), (isset($matchedVariables['AdditionalViewHelpers'])?$matchedVariables['AdditionalViewHelpers']:''));
769 } elseif (preg_match(self::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0) {
770 $this->arrayHandler($state, $matchedVariables['Array']);
771 } else {
772 $this->textHandler($state, $section);
773 }
774 }
775 }
776
777 /**
778 * Handler for array syntax. This creates the array object recursively and
779 * adds it to the current node.
780 *
781 * @param Tx_Fluid_Core_Parser_ParsingState $state The current parsing state
782 * @param string $arrayText The array as string.
783 * @return void
784 * @author Sebastian Kurfürst <sebastian@typo3.org>
785 */
786 protected function arrayHandler(Tx_Fluid_Core_Parser_ParsingState $state, $arrayText) {
787 $state->getNodeFromStack()->addChildNode(
788 $this->objectManager->create('Tx_Fluid_Core_Parser_SyntaxTree_ArrayNode', $this->recursiveArrayHandler($arrayText))
789 );
790 }
791
792 /**
793 * Recursive function which takes the string representation of an array and
794 * builds an object tree from it.
795 *
796 * Deals with the following value types:
797 * - Numbers (Integers and Floats)
798 * - Strings
799 * - Variables
800 * - sub-arrays
801 *
802 * @param string $arrayText Array text
803 * @return Tx_Fluid_ArrayNode the array node built up
804 * @author Sebastian Kurfürst <sebastian@typo3.org>
805 */
806 protected function recursiveArrayHandler($arrayText) {
807 $matches = array();
808 if (preg_match_all(self::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER) > 0) {
809 $arrayToBuild = array();
810 foreach ($matches as $singleMatch) {
811 $arrayKey = $singleMatch['Key'];
812 if (!empty($singleMatch['VariableIdentifier'])) {
813 $arrayToBuild[$arrayKey] = $this->objectManager->create('Tx_Fluid_Core_Parser_SyntaxTree_ObjectAccessorNode', $singleMatch['VariableIdentifier']);
814 } elseif (array_key_exists('Number', $singleMatch) && ( !empty($singleMatch['Number']) || $singleMatch['Number'] === '0' ) ) {
815 $arrayToBuild[$arrayKey] = floatval($singleMatch['Number']);
816 } elseif ( ( array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']) ) ) {
817 $argumentString = $this->unquoteString($singleMatch['QuotedString']);
818 $arrayToBuild[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
819 } elseif ( array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray'])) {
820 $arrayToBuild[$arrayKey] = $this->objectManager->create('Tx_Fluid_Core_Parser_SyntaxTree_ArrayNode', $this->recursiveArrayHandler($singleMatch['Subarray']));
821 } else {
822 throw new Tx_Fluid_Core_Parser_Exception('This exception should never be thrown, as the array value has to be of some type (Value given: "' . var_export($singleMatch, TRUE) . '"). Please post your template to the bugtracker at forge.typo3.org.', 1225136013);
823 }
824 }
825 return $arrayToBuild;
826 } else {
827 throw new Tx_Fluid_Core_Parser_Exception('This exception should never be thrown, there is most likely some error in the regular expressions. Please post your template to the bugtracker at forge.typo3.org.', 1225136013);
828 }
829 }
830
831 /**
832 * Text node handler
833 *
834 * @param Tx_Fluid_Core_Parser_ParsingState $state
835 * @param string $text
836 * @return void
837 * @author Sebastian Kurfürst <sebastian@typo3.org>
838 * @author Karsten Dambekalns <karsten@typo3.org>
839 */
840 protected function textHandler(Tx_Fluid_Core_Parser_ParsingState $state, $text) {
841 $node = $this->objectManager->create('Tx_Fluid_Core_Parser_SyntaxTree_TextNode', $text);
842 $this->callInterceptor($node, Tx_Fluid_Core_Parser_InterceptorInterface::INTERCEPT_TEXT);
843
844 $state->getNodeFromStack()->addChildNode($node);
845 }
846
847 }
848 ?>