<?php /** * TTemplateManager and TTemplate class file * * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.pradosoft.com/ * @copyright Copyright © 2005 PradoSoft * @license http://www.pradosoft.com/license/ * @version $Revision: $ $Date: $ * @package System.Web.UI */ /** * TTemplateManager class * * TTemplateManager manages the loading and parsing of control templates. * * There are two ways of loading a template, either by the associated template * control class name, or the template file name. * The former is via calling {@link getTemplateByClassName}, which tries to * locate the corresponding template file under the directory containing * the class file. The name of the template file is the class name with * the extension '.tpl'. To load a template from a template file path, * call {@link getTemplateByFileName}. * * By default, TTemplateManager is registered with {@link TPageService} as the * template manager module that can be accessed via {@link TPageService::getTemplateManager()}. * * @author Qiang Xue <qiang.xue@gmail.com> * @version $Revision: $ $Date: $ * @package System.Web.UI * @since 3.0 */ class TTemplateManager extends TModule { /** * Template file extension */ const TEMPLATE_FILE_EXT='.tpl'; /** * Prefix of the cache variable name for storing parsed templates */ const TEMPLATE_CACHE_PREFIX='prado:template:'; /** * Initializes the module. * This method is required by IModule and is invoked by application. * It starts output buffer if it is enabled. * @param TXmlElement module configuration */ public function init($config) { $this->getService()->setTemplateManager($this); } /** * Loads the template corresponding to the specified class name. * @return ITemplate template for the class name, null if template doesn't exist. */ public function getTemplateByClassName($className) { $class=new ReflectionClass($className); $tplFile=dirname($class->getFileName()).'/'.$className.self::TEMPLATE_FILE_EXT; return $this->getTemplateByFileName($tplFile); } /** * Loads the template from the specified file. * @return ITemplate template parsed from the specified file, null if the file doesn't exist. */ public function getTemplateByFileName($fileName) { if(!is_null($fileName=$this->getLocalizedTemplate($fileName))) { Prado::trace("Loading template $fileName",'System.Web.UI.TTemplateManager'); if(($cache=$this->getApplication()->getCache())===null) return new TTemplate(file_get_contents($fileName),dirname($fileName),$fileName); else { $array=$cache->get(self::TEMPLATE_CACHE_PREFIX.$fileName); if(is_array($array)) { list($template,$timestamp)=$array; if(filemtime($fileName)<$timestamp) return $template; } $template=new TTemplate(file_get_contents($fileName),dirname($fileName),$fileName); $cache->set(self::TEMPLATE_CACHE_PREFIX.$fileName,array($template,time())); return $template; } } else return null; } /** * Finds a localized template file. * @param string template file. * @return string|null a localized template file if found, null otherwise. */ protected function getLocalizedTemplate($filename) { $app = $this->getApplication()->getGlobalization(); if(is_null($app)) return $filename; foreach($app->getLocalizedResource($filename) as $file) { if(($file=realpath($file))!==false && is_file($file)) return $file; } } } /** * TTemplate implements PRADO template parsing logic. * A TTemplate object represents a parsed PRADO control template. * It can instantiate the template as child controls of a specified control. * The template format is like HTML, with the following special tags introduced, * - component tags: a component tag represents the configuration of a component. * The tag name is in the format of com:ComponentType, where ComponentType is the component * class name. Component tags must be well-formed. Attributes of the component tag * are treated as either property initial values, event handler attachment, or regular * tag attributes. * - property tags: property tags are used to set large block of attribute values. * The property tag name is in the format of prop:AttributeName, where AttributeName * can be a property name, an event name or a regular tag attribute name. * - directive: directive specifies the property values for the template owner. * It is in the format of <% property name-value pairs %> * - expressions: expressions are shorthand of {@link TExpression} and {@link TStatements} * controls. They are in the formate of <= PHP expression > and < PHP statements > * - comments: There are two kinds of comments, regular HTML comments and special template comments. * The former is in the format of <!-- comments -->, which will be treated as text strings. * The latter is in the format of <%* comments %>, which will be stripped out. * * Tags other than the above are not required to be well-formed. * * A TTemplate object represents a parsed PRADO template. To instantiate the template * for a particular control, call {@link instantiateIn($control)}, which * will create and intialize all components specified in the template and * set their parent as $control. * * @author Qiang Xue <qiang.xue@gmail.com> * @version $Revision: $ $Date: $ * @package System.Web.UI * @since 3.0 */ class TTemplate extends TComponent implements ITemplate { /** * '<!.*?!>' - template comments * '<!--.*?-->' - HTML comments * '<\/?com:([\w\.]+)((?:\s*[\w\.]+=\'.*?\'|\s*[\w\.]+=".*?"|\s*[\w\.]+=<%.*?%>)*)\s*\/?>' - component tags * '<\/?prop:([\w\.]+)\s*>' - property tags * '<%@\s*((?:\s*[\w\.]+=\'.*?\'|\s*[\w\.]+=".*?")*)\s*%>' - directives * '<%[%#~\\$=\\[](.*?)%>' - expressions */ const REGEX_RULES='/<!.*?!>|<!--.*?-->|<\/?com:([\w\.]+)((?:\s*[\w\.]+=\'.*?\'|\s*[\w\.]+=".*?"|\s*[\w\.]+=<%.*?%>)*)\s*\/?>|<\/?prop:([\w\.]+)\s*>|<%@\s*((?:\s*[\w\.]+=\'.*?\'|\s*[\w\.]+=".*?")*)\s*%>|<%[%#~\\$=\\[](.*?)%>/msS'; /** * Different configurations of component property/event/attribute */ const CONFIG_DATABIND=0; const CONFIG_EXPRESSION=1; const CONFIG_ASSET=2; const CONFIG_PARAMETER=3; const CONFIG_LOCALIZATION=4; /** * @var array list of component tags and strings */ private $_tpl=array(); /** * @var array list of directive settings */ private $_directive=array(); /** * @var string context path */ private $_contextPath; /** * @var string template file path (if available) */ private $_tplFile=null; /** * @var TAssetManager asset manager */ private $_assetManager; /** * Constructor. * The template will be parsed after construction. * @param string the template string * @param string the template context directory * @param string the template file, null if no file */ public function __construct($template,$contextPath,$tplFile=null) { $this->_contextPath=$contextPath; $this->_tplFile=$tplFile; $this->parse($template); } /** * @return string context directory path */ public function getContextPath() { return $this->_contextPath; } /** * @return array name-value pairs declared in the directive */ public function getDirective() { return $this->_directive; } /** * @return array the parsed template */ public function &getItems() { return $this->_tpl; } /** * Instantiates the template. * Content in the template will be instantiated as components and text strings * and passed to the specified parent control. * @param TControl the parent control * @throws TTemplateRuntimeException if an error is encountered during the instantiation. */ public function instantiateIn($tplControl) { if(($page=$tplControl->getPage())===null) $page=$this->getService()->getRequestedPage(); $this->_assetManager=$this->getService()->getAssetManager(); $controls=array(); foreach($this->_tpl as $key=>$object) { if(isset($object[2])) // component { $component=Prado::createComponent($object[1]); if($component instanceof TControl) { $controls[$key]=$component; $component->setTemplateControl($tplControl); if(isset($object[2]['id'])) $tplControl->registerObject($object[2]['id'],$component); if(isset($object[2]['skinid'])) { $component->setSkinID($object[2]['skinid']); unset($object[2]['skinid']); } $component->applyStyleSheetSkin($page); // apply attributes foreach($object[2] as $name=>$value) $this->configureControl($component,$name,$value); $parent=isset($controls[$object[0]])?$controls[$object[0]]:$tplControl; $component->createdOnTemplate($parent); } else if($component instanceof TComponent) { if(isset($object[2]['id'])) { $tplControl->registerObject($object[2]['id'],$component); if(!$component->hasProperty('id')) unset($object[2]['id']); } foreach($object[2] as $name=>$value) $this->configureComponent($component,$name,$value); $parent=isset($controls[$object[0]])?$controls[$object[0]]:$tplControl; $parent->addParsedObject($component); } else throw new TTemplateRuntimeException('template_component_required',$object[1]); } else // string { if(isset($controls[$object[0]])) $controls[$object[0]]->addParsedObject($object[1]); else $tplControl->addParsedObject($object[1]); } } } /** * Configures a property/event of a control. * @param TControl control to be configured * @param string property name * @param mixed property initial value */ protected function configureControl($control,$name,$value) { if(is_string($value) && $control->hasEvent($name)) // is an event $this->configureEvent($control,$name,$value); else if(strpos($name,'.')===false) // is a simple property or custom attribute { if($control->hasProperty($name)) $this->configureProperty($control,$name,$value); else if($control->getAllowCustomAttributes()) $this->configureAttribute($control,$name,$value); else throw new TTemplateRuntimeException('template_property_undefined',get_class($control),$name); } else // is a subproperty { $this->configureSubProperty($control,$name,$value); } } /** * Configures a property of a non-control component. * @param TComponent component to be configured * @param string property name * @param mixed property initial value */ protected function configureComponent($component,$name,$value) { if(strpos($name,'.')===false) // is a simple property or custom attribute { if($component->hasProperty($name)) $this->configureProperty($component,$name,$value); else throw new TTemplateRuntimeException('template_property_undefined',get_class($component),$name); } else // is a subproperty { $this->configureSubProperty($component,$name,$value); } } /** * Configures an event for a control. * @param TControl control to be configured * @param string event name * @param string event handler */ protected function configureEvent($component,$name,$value) { $value=THttpUtility::htmlDecode($value); if(strpos($value,'.')===false) $component->attachEventHandler($name,array($component,'TemplateControl.'.$value)); else $component->attachEventHandler($name,array($component,$value)); } /** * Configures a simple property for a component. * @param TComponent component to be configured * @param string property name * @param mixed property initial value */ protected function configureProperty($component,$name,$value) { if($component->canSetProperty($name)) { $setter='set'.$name; if(is_array($value)) { $v=THttpUtility::htmlDecode($value[1]); switch($value[0]) { case self::CONFIG_DATABIND: $component->bindProperty($name,$v); break; case self::CONFIG_EXPRESSION: $component->$setter($component->evaluateExpression($v)); break; case self::CONFIG_ASSET: // asset URL $url=$this->_assetManager->publishFilePath($this->_contextPath.'/'.$v); $component->$setter($url); break; case self::CONFIG_PARAMETER: // application parameter $component->$setter($this->getApplication()->getParameters()->itemAt($v)); break; case self::CONFIG_LOCALIZATION: $component->$setter(localize($v)); break; default: // an error if reaching here break; } } else $component->$setter(THttpUtility::htmlDecode($value)); } else throw new TTemplateRuntimeException('template_property_readonly',get_class($component),$name); } /** * Configures a subproperty for a component. * @param TComponent component to be configured * @param string subproperty name * @param mixed subproperty initial value */ protected function configureSubProperty($component,$name,$value) { if(is_array($value)) { $v=THttpUtility::htmlDecode($value[1]); switch($value[0]) { case self::CONFIG_DATABIND: // databinding $component->bindProperty($name,$v); break; case self::CONFIG_EXPRESSION: // expression $component->setSubProperty($name,$component->evaluateExpression($v)); break; case self::CONFIG_ASSET: // asset URL $url=$this->_assetManager->publishFilePath($this->_contextPath.'/'.$v); $component->setSubProperty($name,$url); break; case self::CONFIG_PARAMETER: // application parameter $component->setSubProperty($name,$this->getApplication()->getParameters()->itemAt($v)); break; case self::CONFIG_LOCALIZATION: $component->setSubProperty($name,localize($v)); break; default: // an error if reaching here break; } } else $component->setSubProperty($name,THttpUtility::htmlDecode($value)); } /** * Configures a custom attribute for a control. * @param TControl control to be configured * @param string attribute name * @param mixed attribute initial value */ protected function configureAttribute($control,$name,$value) { if(is_array($value)) { switch($value[0]) { case self::CONFIG_DATABIND: // databinding throw new TTemplateRuntimeException('template_attribute_unbindable',get_class($control),$name); case self::CONFIG_EXPRESSION: $value=$control->evaluateExpression($value[1]); break; case self::CONFIG_ASSET: $value=$this->_assetManager->publishFilePath($this->_contextPath.'/'.ltrim($value[1],'/')); break; case self::CONFIG_PARAMETER: $value=$this->getApplication()->getParameters()->itemAt($value[1]); break; case self::CONFIG_LOCALIZATION: $value=localize($value[1]); break; default: break; } } $control->getAttributes()->add($name,$value); } /** * Parses a template string. * * This template parser recognizes five types of data: * regular string, well-formed component tags, well-formed property tags, directives, and expressions. * * The parsing result is returned as an array. Each array element can be of three types: * - a string, 0: container index; 1: string content; * - a component tag, 0: container index; 1: component type; 2: attributes (name=>value pairs) * If a directive is found in the template, it will be parsed and can be * retrieved via {@link getDirective}, which returns an array consisting of * name-value pairs in the directive. * * Note, attribute names are treated as case-insensitive and will be turned into lower cases. * Component and directive types are case-sensitive. * Container index is the index to the array element that stores the container object. * If an object has no container, its container index is -1. * * @param string the template string * @throws TTemplateParsingException if a parsing error is encountered */ protected function parse($input) { $tpl=&$this->_tpl; $n=preg_match_all(self::REGEX_RULES,$input,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE); $expectPropEnd=false; $textStart=0; $stack=array(); $container=-1; $c=0; for($i=0;$i<$n;++$i) { $match=&$matches[$i]; $str=$match[0][0]; $matchStart=$match[0][1]; $matchEnd=$matchStart+strlen($str)-1; if(strpos($str,'<com:')===0) // opening component tag { if($expectPropEnd) continue; if($matchStart>$textStart) $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart)); $textStart=$matchEnd+1; $type=$match[1][0]; $attributes=$this->parseAttributes($match[2][0]); $tpl[$c++]=array($container,$type,$attributes); if($str[strlen($str)-2]!=='/') // open tag { array_push($stack,$type); $container=$c-1; } } else if(strpos($str,'</com:')===0) // closing component tag { if($expectPropEnd) continue; if($matchStart>$textStart) $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart)); $textStart=$matchEnd+1; $type=$match[1][0]; if(empty($stack)) { $line=count(explode("\n",substr($input,0,$matchEnd+1))); if($this->_tplFile===null) throw new TTemplateParsingException('template_closingtag_unexpected',"Line $line","</com:$type>"); else throw new TTemplateParsingException('template_closingtag_unexpected',"{$this->_tplFile} (Line $line)","</com:$type>"); } $name=array_pop($stack); if($name!==$type) { if($name[0]==='@') $tag='</prop:'.substr($name,1).'>'; else $tag='</com:'.$name.'>'; $line=count(explode("\n",substr($input,0,$matchEnd+1))); if($this->_tplFile===null) throw new TTemplateParsingException('template_closingtag_expected',"Line $line",$tag); else throw new TTemplateParsingException('template_closingtag_expected',"{$this->_tplFile} (Line $line)",$tag); } $container=$tpl[$container][0]; } else if(strpos($str,'<%@')===0) // directive { if($expectPropEnd) continue; if($matchStart>$textStart) $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart)); $textStart=$matchEnd+1; if(isset($tpl[0])) { $line=count(explode("\n",substr($input,0,$matchEnd+1))); if($this->_tplFile===null) throw new TTemplateParsingException('template_directive_nonunique',"Line $line"); else throw new TTemplateParsingException('template_directive_nonunique',"{$this->_tplFile} (Line $line)"); } $this->_directive=$this->parseAttributes($match[4][0]); } else if(strpos($str,'<%')===0) // expression { if($expectPropEnd) continue; if($matchStart>$textStart) $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart)); $textStart=$matchEnd+1; if($str[2]==='=') // expression $tpl[$c++]=array($container,'TExpression',array('Expression'=>$match[5][0])); else if($str[2]==='%') // statements $tpl[$c++]=array($container,'TStatements',array('Statements'=>$match[5][0])); else $tpl[$c++]=array($container,'TLiteral',array('Text'=>$this->parseAttribute($str))); } else if(strpos($str,'<prop:')===0) // opening property { $prop=strtolower($match[3][0]); array_push($stack,'@'.$prop); if(!$expectPropEnd) { if($matchStart>$textStart) $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart)); $textStart=$matchEnd+1; $expectPropEnd=true; } } else if(strpos($str,'</prop:')===0) // closing property { $prop=strtolower($match[3][0]); if(empty($stack)) { $line=count(explode("\n",substr($input,0,$matchEnd+1))); if($this->_tplFile===null) throw new TTemplateParsingException('template_closingtag_unexpected',"Line $line","</prop:$prop>"); else throw new TTemplateParsingException('template_closingtag_unexpected',"{$this->_tplFile} (Line $line)","</prop:$prop>"); } $name=array_pop($stack); if($name!=='@'.$prop) { if($name[0]==='@') $tag='</prop:'.substr($name,1).'>'; else $tag='</com:'.$name.'>'; $line=count(explode("\n",substr($input,0,$matchEnd+1))); if($this->_tplFile===null) throw new TTemplateParsingException('template_closingtag_expected',"Line $line",$tag); else throw new TTemplateParsingException('template_closingtag_expected',"{$this->_tplFile} (Line $line)",$tag); } if(($last=count($stack))<1 || $stack[$last-1][0]!=='@') { if($matchStart>$textStart && $container>=0) { $value=substr($input,$textStart,$matchStart-$textStart); $tpl[$container][2][$prop]=$this->parseAttribute($value); $textStart=$matchEnd+1; } $expectPropEnd=false; } } else if(strpos($str,'<!--')===0) // HTML comments { $state=0; } else if(strpos($str,'<!')===0) // template comments { if($expectPropEnd) { $line=count(explode("\n",substr($input,0,$matchEnd+1))); if($this->_tplFile===null) throw new TTemplateParsingException('template_comments_forbidden',"Line $line"); else throw new TTemplateParsingException('template_comments_forbidden',"{$this->_tplFile} (Line $line)"); } if($matchStart>$textStart) $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart)); $textStart=$matchEnd+1; } else { $line=count(explode("\n",substr($input,0,$matchEnd+1))); if($this->_tplFile===null) throw new TTemplateParsingException('template_matching_unexpected',"Line $line",$match); else throw new TTemplateParsingException('template_matching_unexpected',"{$this->_tplFile} (Line $line)",$match); } } if(!empty($stack)) { $name=array_pop($stack); if($name[0]==='@') $tag='</prop:'.substr($name,1).'>'; else $tag='</com:'.$name.'>'; $line=count(explode("\n",substr($input,0,$matchEnd+1))); if($this->_tplFile===null) throw new TTemplateParsingException('template_closingtag_expected',"Line $line",$tag); else throw new TTemplateParsingException('template_closingtag_expected',"{$this->_tplFile} (Line $line)",$tag); } if($textStart<strlen($input)) $tpl[$c++]=array($container,substr($input,$textStart)); return $tpl; } /** * Parses the attributes of a tag from a string. * @param string the string to be parsed. * @return array attribute values indexed by names. */ protected function parseAttributes($str) { if($str==='') return array(); $pattern='/([\w\.]+)=(\'.*?\'|".*?"|<%.*?%>)/msS'; $attributes=array(); $n=preg_match_all($pattern,$str,$matches,PREG_SET_ORDER); for($i=0;$i<$n;++$i) { $name=strtolower($matches[$i][1]); $value=$matches[$i][2]; if($value[0]==='\'' || $value[0]==='"') $value=substr($value,1,strlen($value)-2); $attributes[$name]=$this->parseAttribute($value); } return $attributes; } /** * Parses a single attribute. * @param string the string to be parsed. * @return array attribute initialization */ protected function parseAttribute($value) { $matches=array(); if(!preg_match('/^(<%#.*?%>|<%=.*?%>|<%~.*?%>|<%\\$.*?%>|<%\\[.*?\\]%>)$/msS',$value,$matches)) return $value; $value=$matches[1]; if($value[2]==='#') // databind return array(self::CONFIG_DATABIND,substr($value,3,strlen($value)-5)); else if($value[2]==='=') // a dynamic initialization return array(self::CONFIG_EXPRESSION,substr($value,3,strlen($value)-5)); else if($value[2]==='~') // a URL return array(self::CONFIG_ASSET,trim(substr($value,3,strlen($value)-5))); else if($value[2]==='[') return array(self::CONFIG_LOCALIZATION,trim(substr($value,3,strlen($value)-6))); else if($value[2]==='$') return array(self::CONFIG_PARAMETER,trim(substr($value,3,strlen($value)-5))); } } ?>