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