Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/**
3
 * TTemplateManager and TTemplate class file
4
 *
5
 * @author Qiang Xue <qiang.xue@gmail.com>
6
 * @link http://www.pradosoft.com/
7
 * @copyright Copyright &copy; 2005-2008 PradoSoft
8
 * @license http://www.pradosoft.com/license/
9
 * @version $Id: TTemplateManager.php 2567 2008-11-12 16:53:52Z carlgmathisen $
10
 * @package System.Web.UI
11
 */
12
 
13
/**
14
 * Includes TOutputCache class file
15
 */
16
Prado::using('System.Web.UI.WebControls.TOutputCache');
17
 
18
/**
19
 * TTemplateManager class
20
 *
21
 * TTemplateManager manages the loading and parsing of control templates.
22
 *
23
 * There are two ways of loading a template, either by the associated template
24
 * control class name, or the template file name.
25
 * The former is via calling {@link getTemplateByClassName}, which tries to
26
 * locate the corresponding template file under the directory containing
27
 * the class file. The name of the template file is the class name with
28
 * the extension '.tpl'. To load a template from a template file path,
29
 * call {@link getTemplateByFileName}.
30
 *
31
 * By default, TTemplateManager is registered with {@link TPageService} as the
32
 * template manager module that can be accessed via {@link TPageService::getTemplateManager()}.
33
 *
34
 * @author Qiang Xue <qiang.xue@gmail.com>
35
 * @version $Id: TTemplateManager.php 2567 2008-11-12 16:53:52Z carlgmathisen $
36
 * @package System.Web.UI
37
 * @since 3.0
38
 */
39
class TTemplateManager extends TModule
40
{
41
	/**
42
	 * Template file extension
43
	 */
44
	const TEMPLATE_FILE_EXT='.tpl';
45
	/**
46
	 * Prefix of the cache variable name for storing parsed templates
47
	 */
48
	const TEMPLATE_CACHE_PREFIX='prado:template:';
49
 
50
	/**
51
	 * Initializes the module.
52
	 * This method is required by IModule and is invoked by application.
53
	 * It starts output buffer if it is enabled.
54
	 * @param TXmlElement module configuration
55
	 */
56
	public function init($config)
57
	{
58
		$this->getService()->setTemplateManager($this);
59
	}
60
 
61
	/**
62
	 * Loads the template corresponding to the specified class name.
63
	 * @return ITemplate template for the class name, null if template doesn't exist.
64
	 */
65
	public function getTemplateByClassName($className)
66
	{
67
		$class=new ReflectionClass($className);
68
		$tplFile=dirname($class->getFileName()).DIRECTORY_SEPARATOR.$className.self::TEMPLATE_FILE_EXT;
69
		return $this->getTemplateByFileName($tplFile);
70
	}
71
 
72
	/**
73
	 * Loads the template from the specified file.
74
	 * @return ITemplate template parsed from the specified file, null if the file doesn't exist.
75
	 */
76
	public function getTemplateByFileName($fileName)
77
	{
78
		if(($fileName=$this->getLocalizedTemplate($fileName))!==null)
79
		{
80
			Prado::trace("Loading template $fileName",'System.Web.UI.TTemplateManager');
81
			if(($cache=$this->getApplication()->getCache())===null)
82
				return new TTemplate(file_get_contents($fileName),dirname($fileName),$fileName);
83
			else
84
			{
85
				$array=$cache->get(self::TEMPLATE_CACHE_PREFIX.$fileName);
86
				if(is_array($array))
87
				{
88
					list($template,$timestamps)=$array;
89
					if($this->getApplication()->getMode()===TApplicationMode::Performance)
90
						return $template;
91
					$cacheValid=true;
92
					foreach($timestamps as $tplFile=>$timestamp)
93
					{
94
						if(!is_file($tplFile) || filemtime($tplFile)>$timestamp)
95
						{
96
							$cacheValid=false;
97
							break;
98
						}
99
					}
100
					if($cacheValid)
101
						return $template;
102
				}
103
				$template=new TTemplate(file_get_contents($fileName),dirname($fileName),$fileName);
104
				$includedFiles=$template->getIncludedFiles();
105
				$timestamps=array();
106
				$timestamps[$fileName]=filemtime($fileName);
107
				foreach($includedFiles as $includedFile)
108
					$timestamps[$includedFile]=filemtime($includedFile);
109
				$cache->set(self::TEMPLATE_CACHE_PREFIX.$fileName,array($template,$timestamps));
110
				return $template;
111
			}
112
		}
113
		else
114
			return null;
115
	}
116
 
117
	/**
118
	 * Finds a localized template file.
119
	 * @param string template file.
120
	 * @return string|null a localized template file if found, null otherwise.
121
	 */
122
	protected function getLocalizedTemplate($filename)
123
	{
124
		if(($app=$this->getApplication()->getGlobalization(false))===null)
125
			return is_file($filename)?$filename:null;
126
		foreach($app->getLocalizedResource($filename) as $file)
127
		{
128
			if(($file=realpath($file))!==false && is_file($file))
129
				return $file;
130
		}
131
		return null;
132
	}
133
}
134
 
135
/**
136
 * TTemplate implements PRADO template parsing logic.
137
 * A TTemplate object represents a parsed PRADO control template.
138
 * It can instantiate the template as child controls of a specified control.
139
 * The template format is like HTML, with the following special tags introduced,
140
 * - component tags: a component tag represents the configuration of a component.
141
 * The tag name is in the format of com:ComponentType, where ComponentType is the component
142
 * class name. Component tags must be well-formed. Attributes of the component tag
143
 * are treated as either property initial values, event handler attachment, or regular
144
 * tag attributes.
145
 * - property tags: property tags are used to set large block of attribute values.
146
 * The property tag name is in the format of <prop:AttributeName> where AttributeName
147
 * can be a property name, an event name or a regular tag attribute name.
148
 * - group subproperty tags: subproperties of a common property can be configured using
149
 * <prop:MainProperty SubProperty1="Value1" SubProperty2="Value2" .../>
150
 * - directive: directive specifies the property values for the template owner.
151
 * It is in the format of <%@ property name-value pairs %>;
152
 * - expressions: They are in the formate of <%= PHP expression %> and <%% PHP statements %>
153
 * - comments: There are two kinds of comments, regular HTML comments and special template comments.
154
 * The former is in the format of <!-- comments -->, which will be treated as text strings.
155
 * The latter is in the format of <!-- comments --!>, which will be stripped out.
156
 *
157
 * Tags other than the above are not required to be well-formed.
158
 *
159
 * A TTemplate object represents a parsed PRADO template. To instantiate the template
160
 * for a particular control, call {@link instantiateIn($control)}, which
161
 * will create and intialize all components specified in the template and
162
 * set their parent as $control.
163
 *
164
 * @author Qiang Xue <qiang.xue@gmail.com>
165
 * @version $Id: TTemplateManager.php 2567 2008-11-12 16:53:52Z carlgmathisen $
166
 * @package System.Web.UI
167
 * @since 3.0
168
 */
169
class TTemplate extends TApplicationComponent implements ITemplate
170
{
171
	/**
172
	 *  '<!--.*?--!>' - template comments
173
     *  '<!--.*?-->'  - HTML comments
174
	 *	'<\/?com:([\w\.]+)((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?"|\s*[\w\.]+\s*=\s*<%.*?%>)*)\s*\/?>' - component tags
175
	 *	'<\/?prop:([\w\.]+)\s*>'  - property tags
176
	 *	'<%@\s*((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?")*)\s*%>'  - directives
177
	 *	'<%[%#~\/\\$=\\[](.*?)%>'  - expressions
178
	 *  '<prop:([\w\.]+)((?:\s*[\w\.]+=\'.*?\'|\s*[\w\.]+=".*?"|\s*[\w\.]+=<%.*?%>)*)\s*\/>' - group subproperty tags
179
	 */
180
	const REGEX_RULES='/<!--.*?--!>|<!---.*?--->|<\/?com:([\w\.]+)((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?"|\s*[\w\.]+\s*=\s*<%.*?%>)*)\s*\/?>|<\/?prop:([\w\.]+)\s*>|<%@\s*((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?")*)\s*%>|<%[%#~\/\\$=\\[](.*?)%>|<prop:([\w\.]+)((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?"|\s*[\w\.]+\s*=\s*<%.*?%>)*)\s*\/>/msS';
181
 
182
	/**
183
	 * Different configurations of component property/event/attribute
184
	 */
185
	const CONFIG_DATABIND=0;
186
	const CONFIG_EXPRESSION=1;
187
	const CONFIG_ASSET=2;
188
	const CONFIG_PARAMETER=3;
189
	const CONFIG_LOCALIZATION=4;
190
	const CONFIG_TEMPLATE=5;
191
 
192
	/**
193
	 * @var array list of component tags and strings
194
	 */
195
	private $_tpl=array();
196
	/**
197
	 * @var array list of directive settings
198
	 */
199
	private $_directive=array();
200
	/**
201
	 * @var string context path
202
	 */
203
	private $_contextPath;
204
	/**
205
	 * @var string template file path (if available)
206
	 */
207
	private $_tplFile=null;
208
	/**
209
	 * @var integer the line number that parsing starts from (internal use)
210
	 */
211
	private $_startingLine=0;
212
	/**
213
	 * @var string template content to be parsed
214
	 */
215
	private $_content;
216
	/**
217
	 * @var boolean whether this template is a source template
218
	 */
219
	private $_sourceTemplate=true;
220
	/**
221
	 * @var string hash code of the template
222
	 */
223
	private $_hashCode='';
224
	private $_tplControl=null;
225
	private $_includedFiles=array();
226
	private $_includeAtLine=array();
227
	private $_includeLines=array();
228
 
229
 
230
	/**
231
	 * Constructor.
232
	 * The template will be parsed after construction.
233
	 * @param string the template string
234
	 * @param string the template context directory
235
	 * @param string the template file, null if no file
236
	 * @param integer the line number that parsing starts from (internal use)
237
	 * @param boolean whether this template is a source template, i.e., this template is loaded from
238
	 * some external storage rather than from within another template.
239
	 */
240
	public function __construct($template,$contextPath,$tplFile=null,$startingLine=0,$sourceTemplate=true)
241
	{
242
		$this->_sourceTemplate=$sourceTemplate;
243
		$this->_contextPath=$contextPath;
244
		$this->_tplFile=$tplFile;
245
		$this->_startingLine=$startingLine;
246
		$this->_content=$template;
247
		$this->_hashCode=md5($template);
248
		$this->parse($template);
249
		$this->_content=null; // reset to save memory
250
	}
251
 
252
	/**
253
	 * @return string  template file path if available, null otherwise.
254
	 */
255
	public function getTemplateFile()
256
	{
257
		return $this->_tplFile;
258
	}
259
 
260
	/**
261
	 * @return boolean whether this template is a source template, i.e., this template is loaded from
262
	 * some external storage rather than from within another template.
263
	 */
264
	public function getIsSourceTemplate()
265
	{
266
		return $this->_sourceTemplate;
267
	}
268
 
269
	/**
270
	 * @return string context directory path
271
	 */
272
	public function getContextPath()
273
	{
274
		return $this->_contextPath;
275
	}
276
 
277
	/**
278
	 * @return array name-value pairs declared in the directive
279
	 */
280
	public function getDirective()
281
	{
282
		return $this->_directive;
283
	}
284
 
285
	/**
286
	 * @return string hash code that can be used to identify the template
287
	 */
288
	public function getHashCode()
289
	{
290
		return $this->_hashCode;
291
	}
292
 
293
	/**
294
	 * @return array the parsed template
295
	 */
296
	public function &getItems()
297
	{
298
		return $this->_tpl;
299
	}
300
 
301
	/**
302
	 * Instantiates the template.
303
	 * Content in the template will be instantiated as components and text strings
304
	 * and passed to the specified parent control.
305
	 * @param TControl the control who owns the template
306
	 * @param TControl the control who will become the root parent of the controls on the template. If null, it uses the template control.
307
	 */
308
	public function instantiateIn($tplControl,$parentControl=null)
309
	{
310
		$this->_tplControl=$tplControl;
311
		if($parentControl===null)
312
			$parentControl=$tplControl;
313
		if(($page=$tplControl->getPage())===null)
314
			$page=$this->getService()->getRequestedPage();
315
		$controls=array();
316
		$directChildren=array();
317
		foreach($this->_tpl as $key=>$object)
318
		{
319
			if($object[0]===-1)
320
				$parent=$parentControl;
321
			else if(isset($controls[$object[0]]))
322
				$parent=$controls[$object[0]];
323
			else
324
				continue;
325
			if(isset($object[2]))	// component
326
			{
327
				$component=Prado::createComponent($object[1]);
328
				$properties=&$object[2];
329
				if($component instanceof TControl)
330
				{
331
					if($component instanceof TOutputCache)
332
						$component->setCacheKeyPrefix($this->_hashCode.$key);
333
					$component->setTemplateControl($tplControl);
334
					if(isset($properties['id']))
335
					{
336
						if(is_array($properties['id']))
337
							$properties['id']=$component->evaluateExpression($properties['id'][1]);
338
						$tplControl->registerObject($properties['id'],$component);
339
					}
340
					if(isset($properties['skinid']))
341
					{
342
						if(is_array($properties['skinid']))
343
							$component->setSkinID($component->evaluateExpression($properties['skinid'][1]));
344
						else
345
							$component->setSkinID($properties['skinid']);
346
						unset($properties['skinid']);
347
					}
348
 
349
					$component->trackViewState(false);
350
 
351
					$component->applyStyleSheetSkin($page);
352
					foreach($properties as $name=>$value)
353
						$this->configureControl($component,$name,$value);
354
 
355
					$component->trackViewState(true);
356
 
357
					if($parent===$parentControl)
358
						$directChildren[]=$component;
359
					else
360
						$component->createdOnTemplate($parent);
361
					if($component->getAllowChildControls())
362
						$controls[$key]=$component;
363
				}
364
				else if($component instanceof TComponent)
365
				{
366
					$controls[$key]=$component;
367
					if(isset($properties['id']))
368
					{
369
						if(is_array($properties['id']))
370
							$properties['id']=$component->evaluateExpression($properties['id'][1]);
371
						$tplControl->registerObject($properties['id'],$component);
372
						if(!$component->hasProperty('id'))
373
							unset($properties['id']);
374
					}
375
					foreach($properties as $name=>$value)
376
						$this->configureComponent($component,$name,$value);
377
					if($parent===$parentControl)
378
						$directChildren[]=$component;
379
					else
380
						$component->createdOnTemplate($parent);
381
				}
382
			}
383
			else
384
			{
385
				if($object[1] instanceof TCompositeLiteral)
386
				{
387
					// need to clone a new object because the one in template is reused
388
					$o=clone $object[1];
389
					$o->setContainer($tplControl);
390
					if($parent===$parentControl)
391
						$directChildren[]=$o;
392
					else
393
						$parent->addParsedObject($o);
394
				}
395
				else
396
				{
397
					if($parent===$parentControl)
398
						$directChildren[]=$object[1];
399
					else
400
						$parent->addParsedObject($object[1]);
401
				}
402
			}
403
		}
404
		// delay setting parent till now because the parent may cause
405
		// the child to do lifecycle catchup which may cause problem
406
		// if the child needs its own child controls.
407
		foreach($directChildren as $control)
408
		{
409
			if($control instanceof TComponent)
410
				$control->createdOnTemplate($parentControl);
411
			else
412
				$parentControl->addParsedObject($control);
413
		}
414
	}
415
 
416
	/**
417
	 * Configures a property/event of a control.
418
	 * @param TControl control to be configured
419
	 * @param string property name
420
	 * @param mixed property initial value
421
	 */
422
	protected function configureControl($control,$name,$value)
423
	{
424
		if(strncasecmp($name,'on',2)===0)		// is an event
425
			$this->configureEvent($control,$name,$value,$control);
426
		else if(($pos=strrpos($name,'.'))===false)	// is a simple property or custom attribute
427
			$this->configureProperty($control,$name,$value);
428
		else	// is a subproperty
429
			$this->configureSubProperty($control,$name,$value);
430
	}
431
 
432
	/**
433
	 * Configures a property of a non-control component.
434
	 * @param TComponent component to be configured
435
	 * @param string property name
436
	 * @param mixed property initial value
437
	 */
438
	protected function configureComponent($component,$name,$value)
439
	{
440
		if(strpos($name,'.')===false)	// is a simple property or custom attribute
441
			$this->configureProperty($component,$name,$value);
442
		else	// is a subproperty
443
			$this->configureSubProperty($component,$name,$value);
444
	}
445
 
446
	/**
447
	 * Configures an event for a control.
448
	 * @param TControl control to be configured
449
	 * @param string event name
450
	 * @param string event handler
451
	 * @param TControl context control
452
	 */
453
	protected function configureEvent($control,$name,$value,$contextControl)
454
	{
455
		if(strpos($value,'.')===false)
456
			$control->attachEventHandler($name,array($contextControl,'TemplateControl.'.$value));
457
		else
458
			$control->attachEventHandler($name,array($contextControl,$value));
459
	}
460
 
461
	/**
462
	 * Configures a simple property for a component.
463
	 * @param TComponent component to be configured
464
	 * @param string property name
465
	 * @param mixed property initial value
466
	 */
467
	protected function configureProperty($component,$name,$value)
468
	{
469
		if(is_array($value))
470
		{
471
			switch($value[0])
472
			{
473
				case self::CONFIG_DATABIND:
474
					$component->bindProperty($name,$value[1]);
475
					break;
476
				case self::CONFIG_EXPRESSION:
477
					if($component instanceof TControl)
478
						$component->autoBindProperty($name,$value[1]);
479
					else
480
					{
481
						$setter='set'.$name;
482
						$component->$setter($this->_tplControl->evaluateExpression($value[1]));
483
					}
484
					break;
485
				case self::CONFIG_TEMPLATE:
486
					$setter='set'.$name;
487
					$component->$setter($value[1]);
488
					break;
489
				case self::CONFIG_ASSET:		// asset URL
490
					$setter='set'.$name;
491
					$url=$this->publishFilePath($this->_contextPath.DIRECTORY_SEPARATOR.$value[1]);
492
					$component->$setter($url);
493
					break;
494
				case self::CONFIG_PARAMETER:		// application parameter
495
					$setter='set'.$name;
496
					$component->$setter($this->getApplication()->getParameters()->itemAt($value[1]));
497
					break;
498
				case self::CONFIG_LOCALIZATION:
499
					$setter='set'.$name;
500
					$component->$setter(Prado::localize($value[1]));
501
					break;
502
				default:	// an error if reaching here
503
					throw new TConfigurationException('template_tag_unexpected',$name,$value[1]);
504
					break;
505
			}
506
		}
507
		else
508
		{
509
			$setter='set'.$name;
510
			$component->$setter($value);
511
		}
512
	}
513
 
514
	/**
515
	 * Configures a subproperty for a component.
516
	 * @param TComponent component to be configured
517
	 * @param string subproperty name
518
	 * @param mixed subproperty initial value
519
	 */
520
	protected function configureSubProperty($component,$name,$value)
521
	{
522
		if(is_array($value))
523
		{
524
			switch($value[0])
525
			{
526
				case self::CONFIG_DATABIND:		// databinding
527
					$component->bindProperty($name,$value[1]);
528
					break;
529
				case self::CONFIG_EXPRESSION:		// expression
530
					if($component instanceof TControl)
531
						$component->autoBindProperty($name,$value[1]);
532
					else
533
						$component->setSubProperty($name,$this->_tplControl->evaluateExpression($value[1]));
534
					break;
535
				case self::CONFIG_TEMPLATE:
536
					$component->setSubProperty($name,$value[1]);
537
					break;
538
				case self::CONFIG_ASSET:		// asset URL
539
					$url=$this->publishFilePath($this->_contextPath.DIRECTORY_SEPARATOR.$value[1]);
540
					$component->setSubProperty($name,$url);
541
					break;
542
				case self::CONFIG_PARAMETER:		// application parameter
543
					$component->setSubProperty($name,$this->getApplication()->getParameters()->itemAt($value[1]));
544
					break;
545
				case self::CONFIG_LOCALIZATION:
546
					$component->setSubProperty($name,Prado::localize($value[1]));
547
					break;
548
				default:	// an error if reaching here
549
					throw new TConfigurationException('template_tag_unexpected',$name,$value[1]);
550
					break;
551
			}
552
		}
553
		else
554
			$component->setSubProperty($name,$value);
555
	}
556
 
557
	/**
558
	 * Parses a template string.
559
	 *
560
	 * This template parser recognizes five types of data:
561
	 * regular string, well-formed component tags, well-formed property tags, directives, and expressions.
562
	 *
563
	 * The parsing result is returned as an array. Each array element can be of three types:
564
	 * - a string, 0: container index; 1: string content;
565
	 * - a component tag, 0: container index; 1: component type; 2: attributes (name=>value pairs)
566
	 * If a directive is found in the template, it will be parsed and can be
567
	 * retrieved via {@link getDirective}, which returns an array consisting of
568
	 * name-value pairs in the directive.
569
	 *
570
	 * Note, attribute names are treated as case-insensitive and will be turned into lower cases.
571
	 * Component and directive types are case-sensitive.
572
	 * Container index is the index to the array element that stores the container object.
573
	 * If an object has no container, its container index is -1.
574
	 *
575
	 * @param string the template string
576
	 * @throws TConfigurationException if a parsing error is encountered
577
	 */
578
	protected function parse($input)
579
	{
580
		$input=$this->preprocess($input);
581
		$tpl=&$this->_tpl;
582
		$n=preg_match_all(self::REGEX_RULES,$input,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
583
		$expectPropEnd=false;
584
		$textStart=0;
585
        $stack=array();
586
		$container=-1;
587
		$matchEnd=0;
588
		$c=0;
589
		$this->_directive=null;
590
		try
591
		{
592
			for($i=0;$i<$n;++$i)
593
			{
594
				$match=&$matches[$i];
595
				$str=$match[0][0];
596
				$matchStart=$match[0][1];
597
				$matchEnd=$matchStart+strlen($str)-1;
598
				if(strpos($str,'<com:')===0)	// opening component tag
599
				{
600
					if($expectPropEnd)
601
						continue;
602
					if($matchStart>$textStart)
603
						$tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
604
					$textStart=$matchEnd+1;
605
					$type=$match[1][0];
606
					$attributes=$this->parseAttributes($match[2][0],$match[2][1]);
607
					$this->validateAttributes($type,$attributes);
608
					$tpl[$c++]=array($container,$type,$attributes);
609
					if($str[strlen($str)-2]!=='/')  // open tag
610
					{
611
						array_push($stack,$type);
612
						$container=$c-1;
613
					}
614
				}
615
				else if(strpos($str,'</com:')===0)	// closing component tag
616
				{
617
					if($expectPropEnd)
618
						continue;
619
					if($matchStart>$textStart)
620
						$tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
621
					$textStart=$matchEnd+1;
622
					$type=$match[1][0];
623
 
624
					if(empty($stack))
625
						throw new TConfigurationException('template_closingtag_unexpected',"</com:$type>");
626
 
627
					$name=array_pop($stack);
628
					if($name!==$type)
629
					{
630
						$tag=$name[0]==='@' ? '</prop:'.substr($name,1).'>' : "</com:$name>";
631
						throw new TConfigurationException('template_closingtag_expected',$tag);
632
					}
633
					$container=$tpl[$container][0];
634
				}
635
				else if(strpos($str,'<%@')===0)	// directive
636
				{
637
					if($expectPropEnd)
638
						continue;
639
					if($matchStart>$textStart)
640
						$tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
641
					$textStart=$matchEnd+1;
642
					if(isset($tpl[0]) || $this->_directive!==null)
643
						throw new TConfigurationException('template_directive_nonunique');
644
					$this->_directive=$this->parseAttributes($match[4][0],$match[4][1]);
645
				}
646
				else if(strpos($str,'<%')===0)	// expression
647
				{
648
					if($expectPropEnd)
649
						continue;
650
					if($matchStart>$textStart)
651
						$tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
652
					$textStart=$matchEnd+1;
653
					$literal=trim($match[5][0]);
654
					if($str[2]==='=')	// expression
655
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,$literal));
656
					else if($str[2]==='%')  // statements
657
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_STATEMENTS,$literal));
658
					else if($str[2]==='#')
659
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_DATABINDING,$literal));
660
					else if($str[2]==='$')
661
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"\$this->getApplication()->getParameters()->itemAt('$literal')"));
662
					else if($str[2]==='~')
663
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"\$this->publishFilePath('$this->_contextPath/$literal')"));
664
					else if($str[2]==='/')
665
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"dirname(\$this->getApplication()->getRequest()->getApplicationUrl()).'/$literal'"));
666
					else if($str[2]==='[')
667
					{
668
						$literal=strtr(trim(substr($literal,0,strlen($literal)-1)),array("'"=>"\'","\\"=>"\\\\"));
669
						$tpl[$c++]=array($container,array(TCompositeLiteral::TYPE_EXPRESSION,"Prado::localize('$literal')"));
670
					}
671
				}
672
				else if(strpos($str,'<prop:')===0)	// opening property
673
				{
674
					if(strrpos($str,'/>')===strlen($str)-2)  //subproperties
675
					{
676
						if($expectPropEnd)
677
							continue;
678
						if($matchStart>$textStart)
679
							$tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
680
						$textStart=$matchEnd+1;
681
						$prop=strtolower($match[6][0]);
682
						$attrs=$this->parseAttributes($match[7][0],$match[7][1]);
683
						$attributes=array();
684
						foreach($attrs as $name=>$value)
685
							$attributes[$prop.'.'.$name]=$value;
686
						$type=$tpl[$container][1];
687
						$this->validateAttributes($type,$attributes);
688
						foreach($attributes as $name=>$value)
689
						{
690
							if(isset($tpl[$container][2][$name]))
691
								throw new TConfigurationException('template_property_duplicated',$name);
692
							$tpl[$container][2][$name]=$value;
693
						}
694
					}
695
					else  // regular property
696
					{
697
						$prop=strtolower($match[3][0]);
698
						array_push($stack,'@'.$prop);
699
						if(!$expectPropEnd)
700
						{
701
							if($matchStart>$textStart)
702
								$tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
703
							$textStart=$matchEnd+1;
704
							$expectPropEnd=true;
705
						}
706
					}
707
				}
708
				else if(strpos($str,'</prop:')===0)	// closing property
709
				{
710
					$prop=strtolower($match[3][0]);
711
					if(empty($stack))
712
						throw new TConfigurationException('template_closingtag_unexpected',"</prop:$prop>");
713
					$name=array_pop($stack);
714
					if($name!=='@'.$prop)
715
					{
716
						$tag=$name[0]==='@' ? '</prop:'.substr($name,1).'>' : "</com:$name>";
717
						throw new TConfigurationException('template_closingtag_expected',$tag);
718
					}
719
					if(($last=count($stack))<1 || $stack[$last-1][0]!=='@')
720
					{
721
						if($matchStart>$textStart)
722
						{
723
							$value=substr($input,$textStart,$matchStart-$textStart);
724
							if(substr($prop,-8,8)==='template')
725
								$value=$this->parseTemplateProperty($value,$textStart);
726
							else
727
								$value=$this->parseAttribute($value);
728
							if($container>=0)
729
							{
730
								$type=$tpl[$container][1];
731
								$this->validateAttributes($type,array($prop=>$value));
732
								if(isset($tpl[$container][2][$prop]))
733
									throw new TConfigurationException('template_property_duplicated',$prop);
734
								$tpl[$container][2][$prop]=$value;
735
							}
736
							else	// a property for the template control
737
								$this->_directive[$prop]=$value;
738
							$textStart=$matchEnd+1;
739
						}
740
						$expectPropEnd=false;
741
					}
742
				}
743
				else if(strpos($str,'<!--')===0)	// comments
744
				{
745
					if($expectPropEnd)
746
						throw new TConfigurationException('template_comments_forbidden');
747
					if($matchStart>$textStart)
748
						$tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart));
749
					$textStart=$matchEnd+1;
750
				}
751
				else
752
					throw new TConfigurationException('template_matching_unexpected',$match);
753
			}
754
			if(!empty($stack))
755
			{
756
				$name=array_pop($stack);
757
				$tag=$name[0]==='@' ? '</prop:'.substr($name,1).'>' : "</com:$name>";
758
				throw new TConfigurationException('template_closingtag_expected',$tag);
759
			}
760
			if($textStart<strlen($input))
761
				$tpl[$c++]=array($container,substr($input,$textStart));
762
		}
763
		catch(Exception $e)
764
		{
765
			if(($e instanceof TException) && ($e instanceof TTemplateException))
766
				throw $e;
767
			if($matchEnd===0)
768
				$line=$this->_startingLine+1;
769
			else
770
				$line=$this->_startingLine+count(explode("\n",substr($input,0,$matchEnd+1)));
771
			$this->handleException($e,$line,$input);
772
		}
773
 
774
		if($this->_directive===null)
775
			$this->_directive=array();
776
 
777
		// optimization by merging consecutive strings, expressions, statements and bindings
778
		$objects=array();
779
		$parent=null;
780
		$merged=array();
781
		foreach($tpl as $id=>$object)
782
		{
783
			if(isset($object[2]) || $object[0]!==$parent)
784
			{
785
				if($parent!==null)
786
				{
787
					if(count($merged[1])===1 && is_string($merged[1][0]))
788
						$objects[$id-1]=array($merged[0],$merged[1][0]);
789
					else
790
						$objects[$id-1]=array($merged[0],new TCompositeLiteral($merged[1]));
791
				}
792
				if(isset($object[2]))
793
				{
794
					$parent=null;
795
					$objects[$id]=$object;
796
				}
797
				else
798
				{
799
					$parent=$object[0];
800
					$merged=array($parent,array($object[1]));
801
				}
802
			}
803
			else
804
				$merged[1][]=$object[1];
805
		}
806
		if($parent!==null)
807
		{
808
			if(count($merged[1])===1 && is_string($merged[1][0]))
809
				$objects[$id]=array($merged[0],$merged[1][0]);
810
			else
811
				$objects[$id]=array($merged[0],new TCompositeLiteral($merged[1]));
812
		}
813
		$tpl=$objects;
814
		return $objects;
815
	}
816
 
817
	/**
818
	 * Parses the attributes of a tag from a string.
819
	 * @param string the string to be parsed.
820
	 * @return array attribute values indexed by names.
821
	 */
822
	protected function parseAttributes($str,$offset)
823
	{
824
		if($str==='')
825
			return array();
826
		$pattern='/([\w\.]+)\s*=\s*(\'.*?\'|".*?"|<%.*?%>)/msS';
827
		$attributes=array();
828
		$n=preg_match_all($pattern,$str,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
829
		for($i=0;$i<$n;++$i)
830
		{
831
			$match=&$matches[$i];
832
			$name=strtolower($match[1][0]);
833
			if(isset($attributes[$name]))
834
				throw new TConfigurationException('template_property_duplicated',$name);
835
			$value=$match[2][0];
836
			if(substr($name,-8,8)==='template')
837
			{
838
				if($value[0]==='\'' || $value[0]==='"')
839
					$attributes[$name]=$this->parseTemplateProperty(substr($value,1,strlen($value)-2),$match[2][1]+1);
840
				else
841
					$attributes[$name]=$this->parseTemplateProperty($value,$match[2][1]);
842
			}
843
			else
844
			{
845
				if($value[0]==='\'' || $value[0]==='"')
846
					$attributes[$name]=$this->parseAttribute(substr($value,1,strlen($value)-2));
847
				else
848
					$attributes[$name]=$this->parseAttribute($value);
849
			}
850
		}
851
		return $attributes;
852
	}
853
 
854
	protected function parseTemplateProperty($content,$offset)
855
	{
856
		$line=$this->_startingLine+count(explode("\n",substr($this->_content,0,$offset)))-1;
857
		return array(self::CONFIG_TEMPLATE,new TTemplate($content,$this->_contextPath,$this->_tplFile,$line,false));
858
	}
859
 
860
	/**
861
	 * Parses a single attribute.
862
	 * @param string the string to be parsed.
863
	 * @return array attribute initialization
864
	 */
865
	protected function parseAttribute($value)
866
	{
867
		if(($n=preg_match_all('/<%[#=].*?%>/msS',$value,$matches,PREG_OFFSET_CAPTURE))>0)
868
		{
869
			$isDataBind=false;
870
			$textStart=0;
871
			$expr='';
872
			for($i=0;$i<$n;++$i)
873
			{
874
				$match=$matches[0][$i];
875
				$token=$match[0];
876
				$offset=$match[1];
877
				$length=strlen($token);
878
				if($token[2]==='#')
879
					$isDataBind=true;
880
				if($offset>$textStart)
881
					$expr.=".'".strtr(substr($value,$textStart,$offset-$textStart),array("'"=>"\\'","\\"=>"\\\\"))."'";
882
				$expr.='.('.substr($token,3,$length-5).')';
883
				$textStart=$offset+$length;
884
			}
885
			$length=strlen($value);
886
			if($length>$textStart)
887
				$expr.=".'".strtr(substr($value,$textStart,$length-$textStart),array("'"=>"\\'","\\"=>"\\\\"))."'";
888
			if($isDataBind)
889
				return array(self::CONFIG_DATABIND,ltrim($expr,'.'));
890
			else
891
				return array(self::CONFIG_EXPRESSION,ltrim($expr,'.'));
892
		}
893
		else if(preg_match('/\\s*(<%~.*?%>|<%\\$.*?%>|<%\\[.*?\\]%>)\\s*/msS',$value,$matches) && $matches[0]===$value)
894
		{
895
			$value=$matches[1];
896
			if($value[2]==='~') // a URL
897
				return array(self::CONFIG_ASSET,trim(substr($value,3,strlen($value)-5)));
898
			else if($value[2]==='[')
899
				return array(self::CONFIG_LOCALIZATION,trim(substr($value,3,strlen($value)-6)));
900
			else if($value[2]==='$')
901
				return array(self::CONFIG_PARAMETER,trim(substr($value,3,strlen($value)-5)));
902
		}
903
		else
904
			return $value;
905
	}
906
 
907
	protected function validateAttributes($type,$attributes)
908
	{
909
		Prado::using($type);
910
		if(($pos=strrpos($type,'.'))!==false)
911
			$className=substr($type,$pos+1);
912
		else
913
			$className=$type;
914
		$class=new TReflectionClass($className);
915
		if(is_subclass_of($className,'TControl') || $className==='TControl')
916
		{
917
			foreach($attributes as $name=>$att)
918
			{
919
				if(($pos=strpos($name,'.'))!==false)
920
				{
921
					// a subproperty, so the first segment must be readable
922
					$subname=substr($name,0,$pos);
923
					if(!$class->hasMethod('get'.$subname))
924
						throw new TConfigurationException('template_property_unknown',$type,$subname);
925
				}
926
				else if(strncasecmp($name,'on',2)===0)
927
				{
928
					// an event
929
					if(!$class->hasMethod($name))
930
						throw new TConfigurationException('template_event_unknown',$type,$name);
931
					else if(!is_string($att))
932
						throw new TConfigurationException('template_eventhandler_invalid',$type,$name);
933
				}
934
				else
935
				{
936
					// a simple property
937
					if(!$class->hasMethod('set'.$name))
938
					{
939
						if($class->hasMethod('get'.$name))
940
							throw new TConfigurationException('template_property_readonly',$type,$name);
941
						else
942
							throw new TConfigurationException('template_property_unknown',$type,$name);
943
					}
944
					else if(is_array($att) && $att[0]!==self::CONFIG_EXPRESSION)
945
					{
946
						if(strcasecmp($name,'id')===0)
947
							throw new TConfigurationException('template_controlid_invalid',$type);
948
						else if(strcasecmp($name,'skinid')===0)
949
							throw new TConfigurationException('template_controlskinid_invalid',$type);
950
					}
951
				}
952
			}
953
		}
954
		else if(is_subclass_of($className,'TComponent') || $className==='TComponent')
955
		{
956
			foreach($attributes as $name=>$att)
957
			{
958
				if(is_array($att) && ($att[0]===self::CONFIG_DATABIND))
959
					throw new TConfigurationException('template_databind_forbidden',$type,$name);
960
				if(($pos=strpos($name,'.'))!==false)
961
				{
962
					// a subproperty, so the first segment must be readable
963
					$subname=substr($name,0,$pos);
964
					if(!$class->hasMethod('get'.$subname))
965
						throw new TConfigurationException('template_property_unknown',$type,$subname);
966
				}
967
				else if(strncasecmp($name,'on',2)===0)
968
					throw new TConfigurationException('template_event_forbidden',$type,$name);
969
				else
970
				{
971
					// id is still alowed for TComponent, even if id property doesn't exist
972
					if(strcasecmp($name,'id')!==0 && !$class->hasMethod('set'.$name))
973
					{
974
						if($class->hasMethod('get'.$name))
975
							throw new TConfigurationException('template_property_readonly',$type,$name);
976
						else
977
							throw new TConfigurationException('template_property_unknown',$type,$name);
978
					}
979
				}
980
			}
981
		}
982
		else
983
			throw new TConfigurationException('template_component_required',$type);
984
	}
985
 
986
	/**
987
	 * @return array list of included external template files
988
	 */
989
	public function getIncludedFiles()
990
	{
991
		return $this->_includedFiles;
992
	}
993
 
994
	/**
995
	 * Handles template parsing exception.
996
	 * This method rethrows the exception caught during template parsing.
997
	 * It adjusts the error location by giving out correct error line number and source file.
998
	 * @param Exception template exception
999
	 * @param int line number
1000
	 * @param string template string if no source file is used
1001
	 */
1002
	protected function handleException($e,$line,$input=null)
1003
	{
1004
		$srcFile=$this->_tplFile;
1005
 
1006
		if(($n=count($this->_includedFiles))>0) // need to adjust error row number and file name
1007
		{
1008
			for($i=$n-1;$i>=0;--$i)
1009
			{
1010
				if($this->_includeAtLine[$i]<=$line)
1011
				{
1012
					if($line<$this->_includeAtLine[$i]+$this->_includeLines[$i])
1013
					{
1014
						$line=$line-$this->_includeAtLine[$i]+1;
1015
						$srcFile=$this->_includedFiles[$i];
1016
						break;
1017
					}
1018
					else
1019
						$line=$line-$this->_includeLines[$i]+1;
1020
				}
1021
			}
1022
		}
1023
		$exception=new TTemplateException('template_format_invalid',$e->getMessage());
1024
		$exception->setLineNumber($line);
1025
		if(!empty($srcFile))
1026
			$exception->setTemplateFile($srcFile);
1027
		else
1028
			$exception->setTemplateSource($input);
1029
		throw $exception;
1030
	}
1031
 
1032
	/**
1033
	 * Preprocesses the template string by including external templates
1034
	 * @param string template string
1035
	 * @return string expanded template string
1036
	 */
1037
	protected function preprocess($input)
1038
	{
1039
		if($n=preg_match_all('/<%include(.*?)%>/',$input,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE))
1040
		{
1041
			for($i=0;$i<$n;++$i)
1042
			{
1043
				$filePath=Prado::getPathOfNamespace(trim($matches[$i][1][0]),TTemplateManager::TEMPLATE_FILE_EXT);
1044
				if($filePath!==null && is_file($filePath))
1045
					$this->_includedFiles[]=$filePath;
1046
				else
1047
				{
1048
					$errorLine=count(explode("\n",substr($input,0,$matches[$i][0][1]+1)));
1049
					$this->handleException(new TConfigurationException('template_include_invalid',trim($matches[$i][1][0])),$errorLine,$input);
1050
				}
1051
			}
1052
			$base=0;
1053
			for($i=0;$i<$n;++$i)
1054
			{
1055
				$ext=file_get_contents($this->_includedFiles[$i]);
1056
				$length=strlen($matches[$i][0][0]);
1057
				$offset=$base+$matches[$i][0][1];
1058
				$this->_includeAtLine[$i]=count(explode("\n",substr($input,0,$offset)));
1059
				$this->_includeLines[$i]=count(explode("\n",$ext));
1060
				$input=substr_replace($input,$ext,$offset,$length);
1061
				$base+=strlen($ext)-$length;
1062
			}
1063
		}
1064
 
1065
		return $input;
1066
	}
1067
}
1068