<?php
/**
 * CPradoViewRenderer class file.
 *
 * @author Steve Heyns http://customgothic.com/
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright Copyright &copy; 2008-2011 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * CPradoViewRenderer implements a view renderer that allows users to use a template syntax similar to PRADO templates.
 *
 * To use CPradoViewRenderer, configure it as an application component named "viewRenderer" in the application configuration:
 * <pre>
 * array(
 *     'components'=>array(
 *         ......
 *         'viewRenderer'=>array(
 *             'class'=>'CPradoViewRenderer',
 *         ),
 *     ),
 * )
 * </pre>
 *
 * CPradoViewRenderer allows you to write view files with the following syntax:
 * <pre>
 * // PHP tags:
 * <%= expression %>
 * // <?php echo expression ?>
 * <% statement %>
 * // <?php statement ?></li>
 *
 * // component tags:
 * <com:WigetClass name1="value1" name2='value2' name3={value3} >
 * // <?php $this->beginWidget('WigetClass',
 * // array('name1'=>"value1", 'name2'=>'value2', 'name3'=>value3)); ?>
 * </com:WigetClass >
 * // <?php $this->endWidget('WigetClass'); ?>
 * <com:WigetClass name1="value1" name2='value2' name3={value3} />
 * // <?php $this->widget('WigetClass',
 * // array('name1'=>"value1", 'name2'=>'value2', 'name3'=>value3)); ?>
 *
 * // cache tags:
 * <cache:fragmentID name1="value1" name2='value2' name3={value3} >
 * // <?php if($this->beginCache('fragmentID',
 * // array('name1'=>"value1", 'name2'=>'value2', 'name3'=>value3))): ?>
 * </cache:fragmentID >
 * // <?php $this->endCache('fragmentID'); endif; ?>
 *
 * // clip tags:
 * <clip:clipID >
 * // <?php $this->beginClip('clipID'); ?>
 * </clip:clipID >
 * // <?php $this->endClip('clipID'); ?>
 *
 * // comment tags:
 * <!--- comments --->
 * // the whole tag will be stripped off
 * </pre>
 *
 * @author Steve Heyns http://customgothic.com/
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package system.web.renderers
 * @since 1.0
 */
class CPradoViewRenderer extends CViewRenderer
{
	private $_input;
	private $_output;
	private $_sourceFile;

	/**
	 * Parses the source view file and saves the results as another file.
	 * This method is required by the parent class.
	 * @param string $sourceFile the source view file path
	 * @param string $viewFile the resulting view file path
	 */
	protected function generateViewFile($sourceFile,$viewFile)
	{
		static $regexRules=array(
			'<%=?\s*(.*?)\s*%>',		// PHP statements or expressions
			'<\/?(com|cache|clip):([\w\.]+)\s*((?:\s*\w+\s*=\s*\'.*?(?<!\\\\)\'|\s*\w+\s*=\s*".*?(?<!\\\\)"|\s*\w+\s*=\s*\{.*?\})*)\s*\/?>', // component tags
			'<!---.*?--->',	// template comments
		);
		$this->_sourceFile=$sourceFile;
		$this->_input=file_get_contents($sourceFile);
		$n=preg_match_all('/'.implode('|',$regexRules).'/msS',$this->_input,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
		$textStart=0;
        $this->_output="<?php /* source file: $sourceFile */ ?>\n";
		for($i=0;$i<$n;++$i)
		{
			$match=&$matches[$i];
			$str=$match[0][0];
			$matchStart=$match[0][1];
			$matchEnd=$matchStart+strlen($str)-1;

			if($matchStart>$textStart)
				$this->_output.=substr($this->_input,$textStart,$matchStart-$textStart);
			$textStart=$matchEnd+1;

			if(strpos($str,'<com:')===0)	// opening component tag
			{
				$type=$match[3][0];
				if($str[strlen($str)-2]!=='/')  // open tag
					$this->_output.=$this->processBeginWidget($type,$match[4][0],$match[2][1]);
				else
					$this->_output.=$this->processWidget($type,$match[4][0],$match[2][1]);
			}
			else if(strpos($str,'</com:')===0)	// closing component tag
				$this->_output.=$this->processEndWidget($match[3][0],$match[2][1]);
			else if(strpos($str,'<cache:')===0)	// opening cache tag
			{
				$id=$match[3][0];
				if($str[strlen($str)-2]!=='/')  // open tag
					$this->_output.=$this->processBeginCache($id,$match[4][0],$match[2][1]);
				else
					$this->_output.=$this->processCache($id,$match[4][0],$match[2][1]);
			}
			else if(strpos($str,'</cache:')===0)	// closing cache tag
				$this->_output.=$this->processEndCache($match[3][0],$match[2][1]);
			else if(strpos($str,'<clip:')===0)	// opening clip tag
			{
				$id=$match[3][0];
				if($str[strlen($str)-2]!=='/')  // open tag
					$this->_output.=$this->processBeginClip($id,$match[4][0],$match[2][1]);
				else
					$this->_output.=$this->processClip($id,$match[4][0],$match[2][1]);
			}
			else if(strpos($str,'</clip:')===0)	// closing clip tag
				$this->_output.=$this->processEndClip($match[3][0],$match[2][1]);
			else if(strpos($str,'<%=')===0)	// expression
				$this->_output.=$this->processExpression($match[1][0],$match[1][1]);
			else if(strpos($str,'<%')===0)	// statement
				$this->_output.=$this->processStatement($match[1][0],$match[1][1]);
		}
		if($textStart<strlen($this->_input))
			$this->_output.=substr($this->_input,$textStart);

		file_put_contents($viewFile,$this->_output);
	}

	/*
	 * @param string $type type
	 * @param string $attributes attributes
	 * @param string $offset offset
	 */
	private function processWidget($type,$attributes,$offset)
	{
		$attrs=$this->processAttributes($attributes);
		if(empty($attrs))
			return $this->generatePhpCode("\$this->widget('$type');",$offset);
		else
			return $this->generatePhpCode("\$this->widget('$type', array($attrs));",$offset);
	}

	/*
	 * @param string $type type
	 * @param string $attributes attributes
	 * @param string $offset offset
	 */
	private function processBeginWidget($type,$attributes,$offset)
	{
		$attrs=$this->processAttributes($attributes);
		if(empty($attrs))
			return $this->generatePhpCode("\$this->beginWidget('$type');",$offset);
		else
			return $this->generatePhpCode("\$this->beginWidget('$type', array($attrs));",$offset);
	}

	/*
	 * @param string $type type
	 * @param string $offset offset
	 */
	private function processEndWidget($type,$offset)
	{
		return $this->generatePhpCode("\$this->endWidget('$type');",$offset);
	}

	/*
	 * @param string $id id
	 * @param string $attributes attributes
	 * @param string $offset offset
	 */
	private function processCache($id,$attributes,$offset)
	{
		return $this->processBeginCache($id,$attributes,$offset) . $this->processEndCache($id,$offset);
	}

	/*
	 * @param string $id id
	 * @param string $attributes attributes
	 * @param string $offset offset
	 */
	private function processBeginCache($id,$attributes,$offset)
	{
		$attrs=$this->processAttributes($attributes);
		if(empty($attrs))
			return $this->generatePhpCode("if(\$this->beginCache('$id')):",$offset);
		else
			return $this->generatePhpCode("if(\$this->beginCache('$id', array($attrs))):",$offset);
	}

	/*
	 * @param string $id id
	 * @param string $offset offset
	 */
	private function processEndCache($id,$offset)
	{
		return $this->generatePhpCode("\$this->endCache('$id'); endif;",$offset);
	}

	/*
	 * @param string $id id
	 * @param string $attributes attributes
	 * @param string $offset offset
	 */
	private function processClip($id,$attributes,$offset)
	{
		return $this->processBeginClip($id,$attributes,$offset) . $this->processEndClip($id,$offset);
	}

	/*
	 * @param string $id id
	 * @param string $attributes attributes
	 * @param string $offset offset
	 */
	private function processBeginClip($id,$attributes,$offset)
	{
		$attrs=$this->processAttributes($attributes);
		if(empty($attrs))
			return $this->generatePhpCode("\$this->beginClip('$id');",$offset);
		else
			return $this->generatePhpCode("\$this->beginClip('$id', array($attrs));",$offset);
	}

	/*
	 * @param string $id id
	 * @param string $offset offset
	 */
	private function processEndClip($id,$offset)
	{
		return $this->generatePhpCode("\$this->endClip('$id');",$offset);
	}

	/*
	 * @param string $expression expression
	 * @param string $offset offset
	 */
	private function processExpression($expression,$offset)
	{
		return $this->generatePhpCode('echo '.$expression,$offset);
	}

	/*
	 * @param string $statement statement
	 * @param string $offset offset
	 */
	private function processStatement($statement,$offset)
	{
		return $this->generatePhpCode($statement,$offset);
	}

	/*
	 * @param string $code code
	 * @param string $offset offset
	 */
	private function generatePhpCode($code,$offset)
	{
		$line=$this->getLineNumber($offset);
		$code=str_replace('__FILE__',var_export($this->_sourceFile,true),$code);
		return "<?php /* line $line */ $code ?>";
	}

	/*
	 * @param string $str str
	 */
	private function processAttributes($str)
	{
		static $pattern='/(\w+)\s*=\s*(\'.*?(?<!\\\\)\'|".*?(?<!\\\\)"|\{.*?\})/msS';
		$attributes=array();
		$n=preg_match_all($pattern,$str,$matches,PREG_SET_ORDER);
		for($i=0;$i<$n;++$i)
		{
			$match=&$matches[$i];
			$name=$match[1];
			$value=$match[2];
			if($value[0]==='{')
				$attributes[]="'$name'=>".str_replace('__FILE__',$this->_sourceFile,substr($value,1,-1));
			else
				$attributes[]="'$name'=>$value";
		}
		return implode(', ',$attributes);
	}

	/*
	 * @param string $offset offset
	 */
	private function getLineNumber($offset)
	{
		return count(explode("\n",substr($this->_input,0,$offset)));
	}
}
