* @author Kornel LesiĆski
* @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
* @version SVN: $Id: CodeWriter.php 974 2010-06-30 12:52:12Z kornel $
* @link http://phptal.org/
*/
/**
* Helps generate php representation of a template.
*
* @package PHPTAL
* @subpackage Php
* @author Laurent Bedubourg
*/
class PHPTAL_Php_CodeWriter
{
/**
* max id of variable to give as temp
*/
private $temp_var_counter=0;
/**
* stack with free'd variables
*/
private $temp_recycling=array();
/**
* keeps track of seen functions for function_exists
*/
private $known_functions = array();
public function __construct(PHPTAL_Php_State $state)
{
$this->_state = $state;
}
public function createTempVariable()
{
if (count($this->temp_recycling)) return array_shift($this->temp_recycling);
return '$_tmp_'.(++$this->temp_var_counter);
}
public function recycleTempVariable($var)
{
if (substr($var, 0, 6)!=='$_tmp_') throw new PHPTAL_Exception("Invalid variable recycled");
$this->temp_recycling[] = $var;
}
public function getCacheFilesBaseName()
{
return $this->_state->getCacheFilesBaseName();
}
public function getResult()
{
$this->flush();
if (version_compare(PHP_VERSION, '5.3', '>=') && __NAMESPACE__) {
return ''.trim($this->_result);
} else {
return trim($this->_result);
}
}
/**
* set full '' string to output later
*
* @param string $dt
*
* @return void
*/
public function setDocType($dt)
{
$this->_doctype = $dt;
}
/**
* set full '' string to output later
*
* @param string $dt
*
* @return void
*/
public function setXmlDeclaration($dt)
{
$this->_xmldeclaration = $dt;
}
/**
* functions later generated and checked for existence will have this prefix added
* (poor man's namespace)
*
* @param string $prefix
*
* @return void
*/
public function setFunctionPrefix($prefix)
{
$this->_functionPrefix = $prefix;
}
/**
* @return string
*/
public function getFunctionPrefix()
{
return $this->_functionPrefix;
}
/**
* @see PHPTAL_Php_State::setTalesMode()
*
* @param string $mode
*
* @return string
*/
public function setTalesMode($mode)
{
return $this->_state->setTalesMode($mode);
}
public function splitExpression($src)
{
preg_match_all('/(?:[^;]+|;;)+/sm', $src, $array);
$array = $array[0];
foreach ($array as &$a) $a = str_replace(';;', ';', $a);
return $array;
}
public function evaluateExpression($src)
{
return $this->_state->evaluateExpression($src);
}
public function indent()
{
$this->_indentation ++;
}
public function unindent()
{
$this->_indentation --;
}
public function flush()
{
$this->flushCode();
$this->flushHtml();
}
public function noThrow($bool)
{
if ($bool) {
$this->pushCode('$ctx->noThrow(true)');
} else {
$this->pushCode('$ctx->noThrow(false)');
}
}
public function flushCode()
{
if (count($this->_codeBuffer) == 0) return;
// special treatment for one code line
if (count($this->_codeBuffer) == 1) {
$codeLine = $this->_codeBuffer[0];
// avoid adding ; after } and {
if (!preg_match('/\}\s*$|\{\s*$/', $codeLine))
$this->_result .= '\n"; // PHP consumes newline
else
$this->_result .= '\n"; // PHP consumes newline
$this->_codeBuffer = array();
return;
}
$this->_result .= '_codeBuffer as $codeLine) {
// avoid adding ; after } and {
if (!preg_match('/\}\s*$|\{\s*$/', $codeLine))
$this->_result .= $codeLine . ' ;'."\n";
else
$this->_result .= $codeLine;
}
$this->_result .= "?>\n";// PHP consumes newline
$this->_codeBuffer = array();
}
public function flushHtml()
{
if (count($this->_htmlBuffer) == 0) return;
$this->_result .= implode('', $this->_htmlBuffer);
$this->_htmlBuffer = array();
}
/**
* Generate code for setting DOCTYPE
*
* @param bool $called_from_macro for error checking: unbuffered output doesn't support that
*/
public function doDoctype($called_from_macro = false)
{
if ($this->_doctype) {
$code = '$ctx->setDocType('.$this->str($this->_doctype).','.($called_from_macro?'true':'false').')';
$this->pushCode($code);
}
}
/**
* Generate XML declaration
*
* @param bool $called_from_macro for error checking: unbuffered output doesn't support that
*/
public function doXmlDeclaration($called_from_macro = false)
{
if ($this->_xmldeclaration && $this->getOutputMode() !== PHPTAL::HTML5) {
$code = '$ctx->setXmlDeclaration('.$this->str($this->_xmldeclaration).','.($called_from_macro?'true':'false').')';
$this->pushCode($code);
}
}
public function functionExists($name)
{
return isset($this->known_functions[$this->_functionPrefix . $name]);
}
public function doTemplateFile($functionName, PHPTAL_Dom_Element $treeGen)
{
$this->doComment("\n*** DO NOT EDIT THIS FILE ***\n\nGenerated by PHPTAL from ".$treeGen->getSourceFile()." (edit that file instead)");
$this->doFunction($functionName, 'PHPTAL $tpl, PHPTAL_Context $ctx');
$this->setFunctionPrefix($functionName . "_");
$this->doSetVar('$_thistpl', '$tpl');
$this->doSetVar('$_translator', '$tpl->getTranslator()');
$treeGen->generateCode($this);
$this->doComment("end");
$this->doEnd('function');
}
public function doFunction($name, $params)
{
$name = $this->_functionPrefix . $name;
$this->known_functions[$name] = true;
$this->pushCodeWriterContext();
$this->pushCode("function $name($params) {\n");
$this->indent();
$this->_segments[] = 'function';
}
public function doComment($comment)
{
$comment = str_replace('*/', '* /', $comment);
$this->pushCode("/* $comment */");
}
public function doEval($code)
{
$this->pushCode($code);
}
public function doForeach($out, $source)
{
$this->_segments[] = 'foreach';
$this->pushCode("foreach ($source as $out):");
$this->indent();
}
public function doEnd($expects = null)
{
if (!count($this->_segments)) {
if (!$expects) $expects = 'anything';
throw new PHPTAL_Exception("Bug: CodeWriter generated end of block without $expects open");
}
$segment = array_pop($this->_segments);
if ($expects !== null && $segment !== $expects) {
throw new PHPTAL_Exception("Bug: CodeWriter generated end of $expects, but needs to close $segment");
}
$this->unindent();
if ($segment == 'function') {
$this->pushCode("\n}\n\n");
$this->flush();
$functionCode = $this->_result;
$this->popCodeWriterContext();
$this->_result = $functionCode . $this->_result;
} elseif ($segment == 'try')
$this->pushCode('}');
elseif ($segment == 'catch')
$this->pushCode('}');
else
$this->pushCode("end$segment");
}
public function doTry()
{
$this->_segments[] = 'try';
$this->pushCode('try {');
$this->indent();
}
public function doSetVar($varname, $code)
{
$this->pushCode($varname.' = '.$code);
}
public function doCatch($catch)
{
$this->doEnd('try');
$this->_segments[] = 'catch';
$this->pushCode('catch('.$catch.') {');
$this->indent();
}
public function doIf($condition)
{
$this->_segments[] = 'if';
$this->pushCode('if ('.$condition.'): ');
$this->indent();
}
public function doElseIf($condition)
{
if (end($this->_segments) !== 'if') {
throw new PHPTAL_Exception("Bug: CodeWriter generated elseif without if");
}
$this->unindent();
$this->pushCode('elseif ('.$condition.'): ');
$this->indent();
}
public function doElse()
{
if (end($this->_segments) !== 'if') {
throw new PHPTAL_Exception("Bug: CodeWriter generated else without if");
}
$this->unindent();
$this->pushCode('else: ');
$this->indent();
}
public function doEcho($code)
{
if ($code === "''") return;
$this->flush();
$this->pushCode('echo '.$this->escapeCode($code));
}
public function doEchoRaw($code)
{
if ($code === "''") return;
$this->pushCode('echo '.$this->stringifyCode($this->interpolateHTML($code)));
}
public function interpolateHTML($html)
{
return $this->_state->interpolateTalesVarsInHtml($html);
}
public function interpolateCDATA($str)
{
return $this->_state->interpolateTalesVarsInCDATA($str);
}
public function pushHTML($html)
{
if ($html === "") return;
$this->flushCode();
$this->_htmlBuffer[] = $html;
}
public function pushCode($codeLine)
{
$this->flushHtml();
$codeLine = $this->indentSpaces() . $codeLine;
$this->_codeBuffer[] = $codeLine;
}
/**
* php string with escaped text
*/
public function str($string)
{
return '\''.str_replace('\'', '\\\'', $string).'\'';
}
public function escapeCode($code)
{
return $this->_state->htmlchars($code);
}
public function stringifyCode($code)
{
return $this->_state->stringify($code);
}
public function getEncoding()
{
return $this->_state->getEncoding();
}
public function interpolateTalesVarsInString($src)
{
return $this->_state->interpolateTalesVarsInString($src);
}
public function setDebug($bool)
{
return $this->_state->setDebug($bool);
}
public function isDebugOn()
{
return $this->_state->isDebugOn();
}
public function getOutputMode()
{
return $this->_state->getOutputMode();
}
public function quoteAttributeValue($value)
{
// FIXME: interpolation is done _after_ that function, so ${} must be forbidden for now
if ($this->getEncoding() == 'UTF-8') // HTML 5: 8.1.2.3 Attributes ; http://code.google.com/p/html5lib/issues/detail?id=93
{
// regex excludes unicode control characters, all kinds of whitespace and unsafe characters
// and trailing / to avoid confusion with self-closing syntax
$unsafe_attr_regex = '/^$|[&=\'"><\s`\pM\pC\pZ\p{Pc}\p{Sk}]|\/$|\${/u';
} else {
$unsafe_attr_regex = '/^$|[&=\'"><\s`\0177-\377]|\/$|\${/';
}
if ($this->getOutputMode() == PHPTAL::HTML5 && !preg_match($unsafe_attr_regex, $value)) {
return $value;
} else {
return '"'.$value.'"';
}
}
public function pushContext()
{
$this->doSetVar('$ctx', '$tpl->pushContext()');
}
public function popContext()
{
$this->doSetVar('$ctx', '$tpl->popContext()');
}
// ~~~~~ Private members ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private function indentSpaces()
{
return str_repeat("\t", $this->_indent);
}
private function pushCodeWriterContext()
{
$this->_contexts[] = clone $this;
$this->_result = "";
$this->_indent = 0;
$this->_codeBuffer = array();
$this->_htmlBuffer = array();
$this->_segments = array();
}
private function popCodeWriterContext()
{
$oldContext = array_pop($this->_contexts);
$this->_result = $oldContext->_result;
$this->_indent = $oldContext->_indent;
$this->_codeBuffer = $oldContext->_codeBuffer;
$this->_htmlBuffer = $oldContext->_htmlBuffer;
$this->_segments = $oldContext->_segments;
}
private $_state;
private $_result = "";
private $_indent = 0;
private $_codeBuffer = array();
private $_htmlBuffer = array();
private $_segments = array();
private $_contexts = array();
private $_functionPrefix = "";
private $_doctype = "";
private $_xmldeclaration = "";
}