<?php
/*
* OPEN POWER LIBS <http://www.invenzzia.org>
*
* This file is subject to the new BSD license that is bundled
* with this package in the file LICENSE. It is also available through
* WWW at this URL: <http://www.invenzzia.org/license/new-bsd>
*
* Copyright (c) Invenzzia Group <http://www.invenzzia.org>
* and other contributors. See website for details.
*
*/
class Opt_Compiler_Class
{
// Opcodes
const OP_VARIABLE = 1;
const OP_LANGUAGE_VAR = 2;
const OP_STRING = 4;
const OP_NUMBER = 8;
const OP_ARRAY = 16;
const OP_OBJECT = 32;
const OP_IDENTIFIER = 64;
const OP_OPERATOR = 128;
const OP_POST_OPERATOR = 256;
const OP_PRE_OPERATOR = 512;
const OP_ASSIGN = 1024;
const OP_NULL = 2048;
const OP_SQ_BRACKET = 4096;
const OP_SQ_BRACKET_E = 8192;
const OP_FUNCTION = 16384;
const OP_METHOD = 32768;
const OP_BRACKET = 65536;
const OP_CLASS = 131072;
const OP_CALL = 262144;
const OP_FIELD = 524288;
const OP_EXPRESSION = 1048576;
const OP_OBJMAN = 2097152;
const OP_BRACKET_E = 4194304;
const OP_TU = 8388608;
const OP_CURLY_BRACKET = 16777216;
const ESCAPE_ON = true;
const ESCAPE_OFF = false;
const ESCAPE_BOTH = 2;
// Current compilation
protected $_template = NULL;
protected $_attr = array();
protected $_stack = NULL;
protected $_node = NULL;
static protected $_recursionDetector = NULL;
// Compiler info
protected $_tags = array();
protected $_attributes = array();
protected $_conversions = array();
protected $_processors = array();
protected $_dependencies = array();
// OPT parser info
protected $_tpl;
protected $_instructions;
protected $_namespaces;
protected $_functions;
protected $_classes;
protected $_blocks;
protected $_components;
protected $_tf;
protected $_entities;
protected $_formnatInfo;
protected $_formats = array();
protected $_formatObj = array();
protected $_inheritance;
// Regular expressions
private $_rCDataExpression = '/(\<\!\[CDATA\[|\]\]\>)/msi';
private $_rCommentExpression = '/(\<\!\-\-|\-\-\>)/si';
private $_rCommentSplitExpression = '/(\<\!\-\-(.*?)\-\-\>)/si';
private $_rOpeningChar = '[a-zA-Z\:\_]';
private $_rNameChar = '[a-zA-Z0-9\:\.\_\-]';
private $_rNameExpression;
private $_rXmlTagExpression;
private $_rTagExpandExpression;
private $_rQuirksTagExpression = '';
private $_rExpressionTag = '/(\{([^\}]*)\})/msi';
private $_rAttributeTokens = '/(?:[^\=\"\'\s]+|\=|\"|\'|\s)/x';
private $_rPrologTokens = '/(?:[^\=\"\'\s]+|\=|\'|\"|\s)/x';
private $_rModifiers = 'si';
private $_rXmlHeader = '/(\<\?xml.+\?\>)/msi';
private $_rProlog = '/\<\?xml(.+)\?\>|/msi';
private $_rEncodingName = '/[A-Za-z]([A-Za-z0-9.\_]|\-)*/si';
private $_rBacktickString = '`[^`\\\\]*(?:\\\\.[^`\\\\]*)*`';
private $_rSingleQuoteString = '\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'';
private $_rHexadecimalNumber = '\-?0[xX][0-9a-fA-F]+';
private $_rDecimalNumber = '[0-9]+\.?[0-9]*';
private $_rLanguageVar = '\$[a-zA-Z0-9\_]+@[a-zA-Z0-9\_]+';
private $_rVariable = '(\$|@)[a-zA-Z0-9\_\.]*';
private $_rOperators = '\-\>|!==|===|==|!=|\=\>|<>|<<|>>|<=|>=|\&\&|\|\||\(|\)|,|\!|\^|=|\&|\~|<|>|\||\%|\+\+|\-\-|\+|\-|\*|\/|\[|\]|\.|\:\:|\{|\}|\'|\"|';
private $_rIdentifier = '[a-zA-Z\_]{1}[a-zA-Z0-9\_\.]*';
private $_rLanguageVarExtract = '\$([a-zA-Z0-9\_]+)@([a-zA-Z0-9\_]+)';
// Help fields
private $_charset = null;
private $_translationConversion = null;
private $_initialMemory = null;
private $_comments = 0;
private $_standalone = false;
private $_dynamicBlocks = null;
static private $_templates = array();
/**
* Creates a new instance of the template compiler. The compiler can
* be created, using the settings from the main OPT class or another
* compiler.
*
* @param Opt_Class|Opt_Compiler_Class $tpl The initial object.
*/
public function __construct($tpl)
{
if($tpl instanceof Opt_Class)
{
$this->_tpl = $tpl;
$this->_namespaces = $tpl->_getList('_namespaces');
$this->_classes = $tpl->_getList('_classes');
$this->_functions = $tpl->_getList('_functions');
$this->_components = $tpl->_getList('_components');
$this->_blocks = $tpl->_getList('_blocks');
$this->_phpFunctions = $tpl->_getList('_phpFunctions');
$this->_formats = $tpl->_getList('_formats');
$this->_tf = $tpl->_getList('_tf');
$this->_entities = $tpl->_getList('_entities');
$this->_charset = strtoupper($tpl->charset);
// Create the processors and call their configuration method in the constructors.
$instructions = $tpl->_getList('_instructions');
foreach($instructions as $instructionClass)
{
$obj = new $instructionClass($this, $tpl);
$this->_processors[$obj->getName()] = $obj;
// Add the tags and attributes registered by this processor.
foreach($obj->getInstructions() as $item)
{
$this->_instructions[$item] = $obj;
}
foreach($obj->getAttributes() as $item)
{
$this->_attributes[$item] = $obj;
}
}
}
elseif($tpl instanceof Opt_Compiler_Class)
{
// Simply import the data structures from that compiler.
$this->_tpl = $tpl->_tpl;
$this->_namespaces = $tpl->_namespaces;
$this->_classes = $tpl->_classes;
$this->_functions = $tpl->_functions;
$this->_components = $tpl->_components;
$this->_blocks = $tpl->_blocks;
$this->_inheritance = $tpl->_inheritance;
$this->_formatInfo = $tpl->_formatInfo;
$this->_formats = $tpl->_formats;
$this->_tf = $tpl->_tf;
$this->_processor = $tpl->_processors;
$this->_instructions = $tpl->_instructions;
$this->_attributes = $tpl->_attributes;
$this->_charset = $tpl->_charset;
$this->_entities = $tpl->_entities;
$tpl = $this->_tpl;
}
if($tpl->unicodeNames)
{
// Register unicode name regular expressions
$this->_rOpeningChar = '[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Nl}\_\:]';
$this->_rNameChar = '[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Nl}\p{Mc}\p{Me}\p{Mn}\p{Nd}\_\:\.\-]';
$this->_rModifiers = 'msiu';
}
// Register the rest of the expressions
$this->_rNameExpression = '/('.$this->_rOpeningChar.'?'.$this->_rNameChar.'*)/'.$this->_rModifiers;
$this->_rXmlTagExpression = '/(\<((\/)?('.$this->_rOpeningChar.'?'.$this->_rNameChar.'*)( [^\<\>]*)?(\/)?)\>)/'.$this->_rModifiers;
$this->_rTagExpandExpression = '/^(\/)?('.$this->_rOpeningChar.'?'.$this->_rNameChar.'*)( [^\<\>]*)?(\/)?$/'.$this->_rModifiers;
$this->_rQuirksTagExpression = '/(\<((\/)?(('.implode('|', $this->_namespaces).')\:'.$this->_rNameChar.'*)( [^\<\>]+)?(\/)?)\>)/'.$this->_rModifiers;
// We've just thrown the performance away by loading the compiler, so this won't make things worse
// but the user may be happy :). However, don't show this message, if we are in the performance mode.
if(!is_writable($tpl->compileDir) && $tpl->_compileMode != Opt_Class::CM_PERFORMANCE)
{
throw new Opt_FilesystemAccess_Exception('compilation', 'writeable');
}
// If the debug console is active, preload the XML tree classes.
// Without it, the debug console would show crazy things about the memory usage.
if($this->_tpl->debugConsole && !class_exists('Opt_Xml_Root'))
{
Opl_Loader::load('Opt_Xml_Root');
Opl_Loader::load('Opt_Xml_Text');
Opl_Loader::load('Opt_Xml_Cdata');
Opl_Loader::load('Opt_Xml_Element');
Opl_Loader::load('Opt_Xml_Attribute');
Opl_Loader::load('Opt_Xml_Expression');
Opl_Loader::load('Opt_Xml_Prolog');
Opl_Loader::load('Opt_Xml_Dtd');
Opl_Loader::load('Opt_Format_Array');
}
} // end __construct();
/**
* Allows to clone the original compiler, creating new instruction
* processors for the new instance.
*/
public function __clone()
{
$this->_processors = array();
$this->_tags = array();
$this->_attributes = array();
$this->_conversions = array();
$instructions = $this->_tpl->_getList('_instructions');
$cnt = sizeof($instructions);
for($i = 0; $i < $cnt; $i++)
{
$obj = new $instructions[$i]($this, $tpl);
$this->_processors[$obj->getName()] = $obj;
}
} // end __clone();
/*
* General purpose tools and utilities
*/
/**
* Returns the currently processed template file name.
*
* @static
* @return String The currently processed template name
*/
static public function getCurrentTemplate()
{
return end(self::$_templates);
} // end getCurrentTemplate();
/**
* Cleans the compiler state after the template compilation.
* It is necessary in the exception processing - if the exception
* is thrown in the middle of the compilation, the compiler becomes
* useless, because it is locked. The compilation algorithm automatically
* filters the exceptions, cleans the compiler state and throws the captured
* exceptions again, to the script.
*
* @static
*/
static public function cleanCompiler()
{
self::$_recursionDetector = null;
self::$_templates = array();
} // end cleanCompiler();
/**
* Returns the value of the compiler state variable or
* NULL if the variable is not set.
*
* @param String $name Compiler variable name
* @return Mixed The compiler variable value.
*/
public function get($name)
{
if(!isset($this->_attr[$name]))
{
return NULL;
}
return $this->_attr[$name];
} // end get();
/**
* Creates or modifies the compiler state variable.
*
* @param String $name The name
* @param Mixed $value The value
*/
public function set($name, $value)
{
$this->_attr[$name] = $value;
} // end set();
/**
* Adds the escaping formula to the specified expression using the current escaping
* rules:
*
* 1. The $status variable.
* 2. The current template settings.
* 3. The OPT settings.
*
* @param String $expression The PHP expression to be escaped.
* @param Boolean $status The status of escaping for this expression or NULL, if not set.
* @return String The expression with the escaping formula added, if necessary.
*/
public function escape($expression, $status = null)
{
// OPT Configuration
$escape = $this->_tpl->escape;
// Template configuration
if(!is_null($this->get('escaping')))
{
$escape = ($this->get('escaping') == true ? true : false);
}
// Expression settings
if(!is_null($status))
{
$escape = ($status == true ? true : false);
}
if($escape)
{
// The user may define a custom escaping function
if($this->isFunction('escape'))
{
if(strpos($this->_functions['escape'], '#', 0) !== false)
{
throw new Opt_InvalidArgumentFormat_Exception('escape', 'escape');
}
return $this->_functions['escape'].'('.$expression.')';
}
return 'htmlspecialchars('.$expression.')';
}
return $expression;
} // end escape();
/**
* Returns the format object for the specified variable.
*
* @param String $variable The variable identifier.
* @param Boolean $restore optional Whether to load a previously created format object (false) or to create a new one.
* @return Opt_Compiler_Format The format object.
*/
public function getFormat($variable, $restore = false)
{
$hc = $this->_tpl->defaultFormat;
if(isset($this->_formatInfo[$variable]))
{
$hc = $this->_formatInfo[$variable];
}
if($restore && isset($this->_formatObj[$hc]))
{
return $this->_formatObj[$hc];
}
$top = $this->createFormat($variable, $hc);
if($restore)
{
$this->_formatObj[$hc] = $top;
}
return $top;
} // end getFormat();
/**
* Creates a format object for the specified description string.
*
* @param String $variable The variable name (for debug purposes)
* @param String $hc The description string.
* @return Opt_Compiler_Format The newly created format object.
*/
public function createFormat($variable, $hc)
{
// Decorate the objects, if necessary
$expanded = explode('/', $hc);
$obj = null;
foreach($expanded as $class)
{
if(!isset($this->_formats[$class]))
{
throw new Opt_FormatNotFound_Exception($variable, $class);
}
$hcName = $this->_formats[$class];
if(!is_null($obj))
{
$obj->decorate($obj2 = new $hcName($this->_tpl, $this));
$obj = $obj2;
}
else
{
$top = $obj = new $hcName($this->_tpl, $this, $hc);
}
}
return $top;
} // end createFormat();
/**
* Allows to export the list of variables and their data formats to
* the template compiler.
*
* @param Array $list An associative array of pairs "variable => format description"
*/
public function setFormatList(Array $list)
{
$this->_formatInfo = $list;
} // end setFormatList();
/**
* Converts the specified item into another string using one of the
* registered patterns. If the pattern is not found, the method returns
* the original item unmodified.
*
* @param String $item The item to be converted.
* @return String
*/
public function convert($item)
{
// the converter allows to convert one name into another and keep it, if there is no
// conversion pattern. Used in connection with sections + snippets.
if(isset($this->_conversions[$item]))
{
return $this->_conversions[$item];
}
return $item;
} // end convert();
/**
* Creates a new conversion pattern. The string $from will be converted
* into $to.
*
* @param String $from The original string
* @param String $to The new string
*/
public function setConversion($from, $to)
{
$this->_conversions[$from] = $to;
} // end setConversion();
/**
* Removes the conversion pattern from the compiler memory.
*
* @param String $from The original string.
* @return Boolean
*/
public function unsetConversion($from)
{
if(isset($this->_conversions[$from]))
{
unset($this->_conversions[$from]);
return true;
}
return false;
} // end unsetConversion();
/**
* Registers the dynamic inheritance rules for the templates. The
* array taken as a parameter must be an associative array of pairs
* 'extending' => 'extended' file names.
*
* @param Array $inheritance The list of inheritance rules.
*/
public function setInheritance(Array $inheritance)
{
$this->_inheritance = $inheritance;
} // end setInheritance();
/**
* Parses the entities in the specified text.
*
* @param String $text The original text
* @return String
*/
public function parseEntities($text)
{
return preg_replace_callback('/\&(([a-zA-Z\_\:]{1}[a-zA-Z0-9\_\:\-\.]*)|(\#((x[a-fA-F0-9]+)|([0-9]+))))\;/', array($this, '_decodeEntity'), $text);
// return htmlspecialchars_decode(str_replace(array_keys($this->_entities), array_values($this->_entities), $text));
} // end parseEntities();
/**
* Replaces only OPT-specific entities &lb; and &rb; to the corresponding
* characters.
*
* @param String $text Input text
* @return String output text
*/
public function parseShortEntities($text)
{
return str_replace(array('&lb;', '&rb;'), array('{', '}'), $text);
} // end parseShortEntities();
/**
* Replaces the XML special characters back to entities with smart ommiting of &
* that already creates an entity.
*
* @param String $text Input text.
* @return String Output text.
*/
public function parseSpecialChars($text)
{
return htmlspecialchars($text);
return preg_replace_callback('/(\&\#?[a-zA-Z0-9]*\;)|\<|\>|\"|\&/', array($this, '_entitize'), $text);
} // end parseSpecialChars();
/**
* Returns 'true', if the argument is a valid identifier. An identifier
* must begin with a letter or underscore, and later, the numbers are also
* allowed.
*
* @param String $id The tested string
* @return Boolean
*/
public function isIdentifier($id)
{
return preg_match($this->_rEncodingName, $id);
} // end isIdentifier();
/**
* Checks whether the specified tag name is registered as an instruction.
* Returns its processor in case of success or NULL.
*
* @param String $tag The tag name (with the namespace)
* @return Opt_Compiler_Processor|NULL The processor that registered this tag.
*/
public function isInstruction($tag)
{
if(isset($this->_instructions[$tag]))
{
return $this->_instructions[$tag];
}
return NULL;
} // end isInstruction();
/**
* Returns true, if the argument is the name of an OPT attribute.
*
* @param String $tag The attribute name
* @return Boolean
*/
public function isOptAttribute($tag)
{
if(isset($this->_attributes[$tag]))
{
return $this->_attributes[$tag];
}
return NULL;
} // end isOptAttribute();
/**
* Returns true, if the argument is the OPT function name.
*
* @param String $name The function name
* @return Boolean
*/
public function isFunction($name)
{
if(isset($this->_functions[$name]))
{
return $this->_functions[$name];
}
return NULL;
} // end isFunction();
/**
* Returns true, if the argument is the name of the class
* accepted by OPT.
*
* @param String $id The class name.
* @return Boolean
*/
public function isClass($id)
{
if(isset($this->_classes[$id]))
{
return $this->_classes[$id];
}
return NULL;
} // end isClass();
/**
* Returns true, if the argument is the name of the namespace
* processed by OPT.
*
* @param String $ns The namespace name
* @return Boolean
*/
public function isNamespace($ns)
{
return in_array($ns, $this->_namespaces);
} // end isNamespace();
/**
* Returns true, if the argument is the name of the component tag.
* @param String $component The component tag name
* @return Boolean
*/
public function isComponent($component)
{
return isset($this->_components[$component]);
} // end isComponent();
/**
* Returns true, if the argument is the name of the block tag.
* @param String $block The block tag name.
* @return Boolean
*/
public function isBlock($block)
{
return isset($this->_blocks[$block]);
} // end isComponent();
/**
* Returns true, if the argument is the processor name.
*
* @param String $name The instruction processor name
* @return Boolean
*/
public function isProcessor($name)
{
if(!isset($this->_processors[$name]))
{
return NULL;
}
return $this->_processors[$name];
} // end isProcessor();
/**
* Returns the processor object with the specified name. If
* the processor does not exist, it generates an exception.
*
* @param String $name The processor name
* @return Opt_Compiler_Processor
*/
public function processor($name)
{
if(!isset($this->_processors[$name]))
{
throw new Opt_ObjectNotExists_Exception('processor', $name);
}
return $this->_processors[$name];
} // end processor();
/**
* Returns the component class name assigned to the specified
* XML tag. If the component class is not registered, it throws
* an exception.
*
* @param String $name The component XML tag name.
* @return Opt_Component_Interface
*/
public function component($name)
{
if(!isset($this->_components[$name]))
{
throw new Opt_ObjectNotExists_Exception('component', $name);
}
return $this->_components[$name];
} // end component();
/**
* Returns the block class name assigned to the specified
* XML tag. If the block class is not registered, it throws
* an exception.
*
* @param String $name The block XML tag name.
* @return Opt_Block_Interface
*/
public function block($name)
{
if(!isset($this->_blocks[$name]))
{
throw new Opt_ObjectNotExists_Exception('block', $name);
}
return $this->_blocks[$name];
} // end block();
/**
* Returns the template name that is inherited by the template '$name'
*
* @param String $name The "current" template file name
* @return String
*/
public function inherits($name)
{
if(isset($this->_inheritance[$name]))
{
return $this->_inheritance[$name];
}
return NULL;
} // end inherits();
/**
* Adds the template file name to the dependency list of the currently
* compiled file, so that it could be checked for modifications during
* the execution.
*
* @param String $template The template file name.
*/
public function addDependantTemplate($template)
{
if(in_array($template, $this->_dependencies))
{
$exception = new Opt_InheritanceRecursion_Exception($template);
$exception->setData($this->_dependencies);
throw $exception;
}
$this->_dependencies[] = $template;
} // end addDependantTemplate();
/**
* Imports the dependencies from another compiler object and adds them
* to the actual dependency list.
*
* @param Opt_Compiler_Class $compiler Another compiler object.
*/
public function importDependencies(Opt_Compiler_Class $compiler)
{
$this->_dependencies = array_merge($this->_dependencies, $compiler->_dependencies);
} // end importDependencies();
/*
* Internal tools and utilities
*/
/**
* Compiles the attribute part of the opening tag and extracts the tag
* attributes to an array. Moreover, it performs the entity conversion
* to the corresponding characters.
*
* @internal
* @param String $attrList The attribute list string
* @param string $tagName The tag name for debug purposes
* @return Array The list of attributes with the values.
*/
protected function _compileAttributes($attrList, $tagName = '')
{
// Tokenize the list
preg_match_all($this->_rAttributeTokens, $attrList, $match, PREG_SET_ORDER);
$size = sizeof($match);
$result = array();
for($i = 0; $i < $size; $i++)
{
/**
* The algorithm scans the tokens on the list and determines, where
* the beginning and the end of the attribute is. We do not use the
* regular expressions, because they are not able to capture the
* invalid content between the expressions.
*
* The sub-loops can modify the iteration variable to skip the found
* elements, white characters etc. This means that the main loop
* does only a few iteration number, equal approximately the number
* of attributes.
*/
if(!ctype_space($match[$i][0]))
{
if(!preg_match($this->_rNameExpression, $match[$i][0]))
{
return false;
}
$vret = false;
$name = $match[$i][0];
if(substr_count($name, ':') > 1)
{
throw new Opt_InvalidNamespace_Exception($name);
}
$value = null;
for($i++; ctype_space($match[$i][0]) && $i < $size; $i++){}
if($match[$i][0] != '=')
{
if($this->_tpl->htmlAttributes)
{
$result[$name] = $name;
continue;
}
else
{
return false;
}
}
// Look for the attribute value start
for($i++; ctype_space($match[$i][0]) && $i < $size; $i++){}
if($match[$i][0] != '"' && $match[$i][0] != '\'')
{
return false;
}
// Save the delimiter, because we will use it to make the error checking
$delimiter = $match[$i][0];
$value = '';
for($i++; $i < $size; $i++)
{
if($match[$i][0] == $delimiter)
{
break;
}
$value .= $match[$i][0];
}
if(!isset($match[$i][0]))
{
return false;
}
if($match[$i][0] != $delimiter)
{
return false;
}
// We return the decoded attribute values, because they are
// stored without the entities.
if(isset($result[$name]))
{
throw new Opt_XmlDuplicatedAttribute_Exception($name, $tagName);
}
$result[$name] = htmlspecialchars_decode($value);
}
}
return $result;
} // end _compileAttributes();
/**
* Parses the XML prolog and returns its attributes as an array. The parsing
* algorith is the same, as in _compileAttributes().
*
* @internal
* @param String $prolog The prolog string.
* @return Array
*/
protected function _compileProlog($prolog)
{
// Tokenize the list
preg_match_all($this->_rPrologTokens, $prolog, $match, PREG_SET_ORDER);
$size = sizeof($match);
$result = array();
for($i = 0; $i < $size; $i++)
{
if(!ctype_space($match[$i][0]))
{
// Traverse through a single attribute
if(!preg_match($this->_rNameExpression, $match[$i][0]))
{
throw new Opt_XmlInvalidProlog_Exception('invalid attribute format');
}
$vret = false;
$name = $match[$i][0];
$value = null;
for($i++; $i < $size && ctype_space($match[$i][0]); $i++){}
if($i >= $size || $match[$i][0] != '=')
{
throw new Opt_XmlInvalidProlog_Exception('invalid attribute format');
}
for($i++; ctype_space($match[$i][0]) && $i < $size; $i++){}
if($match[$i][0] != '"' && $match[$i][0] != '\'')
{
throw new Opt_XmlInvalidProlog_Exception('invalid attribute format');
}
$opening = $match[$i][0];
$value = '';
for($i++; $i < $size; $i++)
{
if($match[$i][0] == $opening)
{
break;
}
$value .= $match[$i][0];
}
if(!isset($match[$i][0]) || $match[$i][0] != $opening)
{
throw new Opt_XmlInvalidProlog_Exception('invalid attribute format');
}
// If we are here, the attribute is correct. No shit on the way detected.
$result[$name] = $value;
}
}
$returnedResult = $result;
// Check, whether the arguments are correct.
if(isset($result['version']))
{
// There is no other version so far, so report a warning. For 99,9% this is a mistake.
if($result['version'] != '1.0')
{
$this->_tpl->debugConsole and Opt_Support::warning('OPT', 'XML prolog warning: strange XML version: '.$result['version']);
}
unset($result['version']);
}
if(isset($result['encoding']))
{
if(!preg_match($this->_rEncodingName, $result['encoding']))
{
throw new Opt_XmlInvalidProlog_Exception('invalid encoding name format');
}
// The encoding should match the value we mentioned in the OPT configuration and sent to the browser.
$result['encoding'] = strtolower($result['encoding']);
$charset = is_null($this->_tpl->charset) ? null : strtolower($this->_tpl->charset);
if($result['encoding'] != $charset && !is_null($charset))
{
$this->_tpl->debugConsole and Opt_Support::warning('OPT', 'XML prolog warning: the declared encoding: "'.$result['encoding'].'" differs from setContentType() setting: "'.$charset.'"');
}
unset($result['encoding']);
}
else
{
$this->_tpl->debugConsole and Opt_Support::warning('XML prolog warning: no encoding information. Remember your content must be pure UTF-8 or UTF-16 then.');
}
if(isset($result['standalone']))
{
if($result['standalone'] != 'yes' && $result['standalone'] != 'no')
{
throw new Opt_XmlInvalidProlog_Exception('invalid value for "standalone" attribute: "'.$result['standalone'].'"; expected: "yes", "no".');
}
unset($result['standalone']);
}
if(sizeof($result) > 0)
{
throw new Opt_XmlInvalidProlog_Exception('invalid attributes in prolog.');
}
return $returnedResult;
} // end _compileProlog();
/**
* Adds the PHP code with dependencies to the code buffers in the tree
* root node.
*
* @internal
* @param Opt_Xml_Node $tree The tree root node.
*/
protected function _addDependencies($tree)
{
// OK, there is really some info to include!
$list = '';
foreach($this->_dependencies as $a)
{
$list .= '\''.$a.'\',';
}
$tree->addBefore(Opt_Xml_Buffer::TAG_BEFORE, 'if(!$this->_massPreprocess($compileTime, array('.$list.'))){ ');
$tree->addAfter(Opt_Xml_Buffer::TAG_AFTER, ' }else{ $compileTime = $this->_compile($this->_template); require(__FILE__); } ');
} // end _addDependencies();
/**
* Compiles the current text block between two XML tags, creating a
* complete Opt_Xml_Text node. It looks for the expressions in the
* curly brackets, extracts them and packs as separate nodes.
*
* Moreover, it replaces the entities with the corresponding characters.
*
* @internal
* @param Opt_Xml_Node $current The current XML node.
* @param String $text The text block between two tags.
* @param Boolean $noExpressions=false If true, do not look for the expressions.
* @return Opt_Xml_Node The current XML node.
*/
protected function _treeTextCompile($current, $text, $noExpressions = false)
{
// Yes, we parse entities, but the text itself should not contain
// any special characters.
if(strcspn($text, '<>') != strlen($text))
{
throw new Opt_XmlInvalidCharacter_Exception(htmlspecialchars($text));
}
if($noExpressions)
{
$current = $this->_treeTextAppend($current, $this->parseEntities($text));
}
preg_match_all($this->_rExpressionTag, $text, $result, PREG_SET_ORDER);
$resultSize = sizeof($result);
$offset = 0;
for($i = 0; $i < $resultSize; $i++)
{
$id = strpos($text, $result[$i][0], $offset);
if($id > $offset)
{
$current = $this->_treeTextAppend($current, $this->parseEntities(substr($text, $offset, $id - $offset)));
}
$offset = $id + strlen($result[$i][0]);
$current = $this->_treeTextAppend($current, new Opt_Xml_Expression($this->parseEntities($result[$i][2])));
}
$i--;
// Now the remaining content of the file
if(strlen($text) > $offset)
{
$current = $this->_treeTextAppend($current, $this->parseEntities(substr($text, $offset, strlen($text) - $offset)));
}
return $current;
} // end _treeTextCompile();
/**
* An utility method that simplifies inserting the text to the XML
* tree. Depending on the last child type, it can create a new text
* node or add the text to the existing one.
*
* @internal
* @param Opt_Xml_Node $current The currently built XML node.
* @param String|Opt_Xml_Node $text The text or the expression node.
* @return Opt_Xml_Node The current XML node.
*/
protected function _treeTextAppend($current, $text)
{
$last = $current->getLastChild();
if(!is_object($last) || !($last instanceof Opt_Xml_Text))
{
if(!is_object($text))
{
$node = new Opt_Xml_Text($text);
}
else
{
$node = new Opt_Xml_Text();
$node->appendChild($text);
}
$current->appendChild($node);
}
else
{
if(!is_object($text))
{
$last->appendData($text);
}
else
{
$last->appendChild($text);
}
}
return $current;
} // end _treeTextAppend();
/**
* A helper method for building the XML tree. It appends the
* node to the current node and returns the new node that should
* become the new current node.
*
* @internal
* @param Opt_Xml_Node $current The current node.
* @param Opt_Xml_Node $node The newly created node.
* @param Boolean $goInto Whether we visit the new node.
* @return Opt_Xml_Node
*/
protected function _treeNodeAppend($current, $node, $goInto)
{
$current->appendChild($node);
if($goInto)
{
return $node;
}
return $current;
} // end _treeNodeAppend();
/**
* A helper method for building the XML tree. It jumps out of the
* current node to the parent and switches to it.
*
* @internal
* @param Opt_Xml_Node $current The current node.
* @return Opt_Xml_Node
*/
protected function _treeJumpOut($current)
{
$parent = $current->getParent();
if(!is_null($parent))
{
return $parent;
}
return $current;
} // end _treeJumpOut();
/**
* Looks for special OPT attributes in the element attribute list and
* processes them. Returns the list of nodes that need to be postprocessed.
*
* @internal
* @param Opt_Xml_Element $node The scanned element.
* @param Boolean $specialNs Do we recognize "parse" and "str" namespaces?
* @return Array
*/
protected function _processXml(Opt_Xml_Element $node, $specialNs = true)
{
if(!$node->hasAttributes())
{
return array();
}
$attributes = $node->getAttributes();
$pp = array();
// Look for special OPT attributes
foreach($attributes as $attr)
{
if($this->isNamespace($attr->getNamespace()))
{
$xml = $attr->getXmlName();
// Check the namespace we found
switch($attr->getNamespace())
{
case 'parse':
if($specialNs)
{
$result = $this->compileExpression((string)$attr, false, Opt_Compiler_Class::ESCAPE_BOTH);
$attr->addAfter(Opt_Xml_Buffer::ATTRIBUTE_VALUE, ' echo '.$result[0].'; ');
$attr->setNamespace(null);
}
break;
case 'str':
if($specialNs)
{
$attr->setNamespace(null);
}
break;
default:
if(isset($this->_attributes[$xml]))
{
$this->_attributes[$xml]->processAttribute($node, $attr);
if($attr->get('postprocess'))
{
$pp[] = array($this->_attributes[$xml], $attr);
}
}
$node->removeAttribute($xml);
}
}
}
return $pp;
} // end _processXml();
/**
* Runs the postprocessors for the specified attributes.
*
* @internal
* @param Opt_Xml_Node $node The scanned node.
* @param Array $list The list of XML attribute processors that need to be postprocessed.
*/
protected function _postprocessXml(Opt_Xml_Node $node, Array $list)
{
$cnt = sizeof($list);
for($i = 0; $i < $cnt; $i++)
{
$list[$i][0]->postprocessAttribute($node, $list[$i][1]);
}
} // end _postprocessXml();
/**
* An utility method for the stage 2 and 3 of the compilation. It is
* used to create a non-recursive depth-first search algorithm. The
* current queue is sent to a stack, and the new queue if initialized,
* if $item contains children.
*
* @internal
* @param SplStack $stack The processing stack.
* @param SplQueue $queue The processing queue.
* @param Opt_Xml_Scannable $item The item, where to import the nodes from.
* @param Boolean $pp The postprocess flag.
* @return SplQueue The new queue (or the old one, if none has been created).
*/
protected function _pushQueue($stack, $queue, $item, $pp)
{
if($item->hasChildren())
{
$stack->push(array($item, $queue, $pp));
$pp = NULL;
$queue = new SplQueue;
foreach($item as $child)
{
$queue->enqueue($child);
}
}
return $queue;
} // end _pushQueue();
/**
* Does the postprocessing in the second stage of compilation.
*
* @internal
* @param Opt_Xml_Node|Null $item The postprocessed node.
* @param Array $pp The list of postprocessed attributes.
*/
protected function _doPostprocess($item, $pp)
{
// Postprocess code for the compilation stage 2
// Packed into a method, because it is used twice.
if(is_null($item))
{
return;
}
if(sizeof($pp) > 0)
{
$this->_postprocessXml($item, $pp);
}
if($item->get('postprocess'))
{
if(!is_null($processor = $this->isInstruction($item->getXmlName())))
{
$processor->postprocessNode($item);
}
elseif($this->isComponent($item->getXmlName()))
{
$processor = $this->processor('component');
$processor->postprocessComponent($item);
}
elseif($this->isBlock($item->getXmlName()))
{
$processor = $this->processor('block');
$processor->postprocessBlock($item);
}
else
{
throw new Opt_UnknownProcessor_Exception($item->getXmlName());
}
}
} // end _doPostprocess();
/**
* Does the post-linking for the third stage of the compilation and returns
* the linked code.
*
* @internal
* @param Opt_Xml_Node $item The linked item.
* @return String
*/
protected function _doPostlinking($item)
{
// Post code
if(is_null($item))
{
return '';
}
// This prevents from displaying </> if the HTML node was hidden.
if($item->get('hidden') !== false)
{
return '';
}
if($item->get('_skip_postlinking') == true)
{
return '';
}
$output = '';
switch($item->getType())
{
case 'Opt_Xml_Text':
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_AFTER);
break;
case 'Opt_Xml_Element':
if($this->isNamespace($item->getNamespace()))
{
if($item->get('single'))
{
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_SINGLE_AFTER, Opt_Xml_Buffer::TAG_AFTER);
}
else
{
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_CONTENT_AFTER, Opt_Xml_Buffer::TAG_CLOSING_BEFORE,
Opt_Xml_Buffer::TAG_CLOSING_AFTER, Opt_Xml_Buffer::TAG_AFTER);
}
}
else
{
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_CONTENT_AFTER, Opt_Xml_Buffer::TAG_CLOSING_BEFORE).'</'.$item->get('_name').'>'.$item->buildCode(Opt_Xml_Buffer::TAG_CLOSING_AFTER, Opt_Xml_Buffer::TAG_AFTER);
$item->set('_name', NULL);
}
break;
case 'Opt_Xml_Root':
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_AFTER);
break;
}
$this->_closeComments($item, $output);
return $output;
} // end _doPostlinking();
/**
* Closes the XML comment for the commented item.
*
* @internal
* @param Opt_Xml_Node $item The commented item.
* @param String &$output The reference to the output buffer.
*/
protected function _closeComments($item, &$output)
{
if($item->get('commented'))
{
$this->_comments--;
if($this->_comments == 0)
{
// According to the XML grammar, the construct "--->" is not allowed.
if(strlen($output) > 0 && $output[strlen($output)-1] == '-')
{
throw new Opt_XmlComment_Exception('--->');
}
$output .= '-->';
}
}
} // end _closeComments();
/**
* Links the element attributes into a valid XML code and returns
* the output code.
*
* @internal
* @param Opt_Xml_Element $subitem The XML element.
* @return String
*/
protected function _linkAttributes($subitem)
{
// Links the attributes into the PHP code
if($subitem->hasAttributes() || $subitem->bufferSize(Opt_Xml_Buffer::TAG_BEGINNING_ATTRIBUTES) > 0 || $subitem->bufferSize(Opt_Xml_Buffer::TAG_ENDING_ATTRIBUTES) > 0)
{
$code = $subitem->buildCode(Opt_Xml_Buffer::TAG_ATTRIBUTES_BEFORE, Opt_Xml_Buffer::TAG_BEGINNING_ATTRIBUTES);
$attrList = $subitem->getAttributes();
// Link attributes into a string
foreach($attrList as $attribute)
{
$s = $attribute->bufferSize(Opt_Xml_Buffer::ATTRIBUTE_NAME);
switch($s)
{
case 0:
$code .= $attribute->buildCode(Opt_Xml_Buffer::ATTRIBUTE_BEGIN).' '.$attribute->getXmlName();
break;
case 1:
$code .= ($attribute->bufferSize(Opt_Xml_Buffer::ATTRIBUTE_BEGIN) == 0 ? ' ' : '').$attribute->buildCode(Opt_Xml_Buffer::ATTRIBUTE_BEGIN, ' ', Opt_Xml_Buffer::ATTRIBUTE_NAME);
break;
default:
throw new Opt_CompilerCodeBufferConflict_Exception(1, 'ATTRIBUTE_NAME', $subitem->getXmlName());
}
if($attribute->bufferSize(Opt_Xml_Buffer::ATTRIBUTE_VALUE) == 0)
{
// Static value
if(!($this->_tpl->htmlAttributes && $attribute->getValue() == $attribute->getName()))
{
$code .= '="'.htmlspecialchars($attribute->getValue()).'"';
}
}
else
{
$code .= '="'.$attribute->buildCode(Opt_Xml_Buffer::ATTRIBUTE_VALUE).'"';
}
$code .= $attribute->buildCode(Opt_Xml_Buffer::ATTRIBUTE_END);
}
return $code.$subitem->buildCode(Opt_Xml_Buffer::TAG_ENDING_ATTRIBUTES, Opt_Xml_Buffer::TAG_ATTRIBUTES_AFTER);
}
return '';
} // end _linkAttributes();
/*
* Main compilation methods
*/
/**
* The compilation launcher. It executes the proper compilation steps
* according to the inheritance rules etc.
*
* @param String $code The source code to be compiled.
* @param String $filename The source template filename.
* @param String $compiledFilename The output template filename.
* @param Int $mode The compilation mode.
*/
public function compile($code, $filename, $compiledFilename, $mode)
{
try
{
// We cannot compile two templates at the same time
if(!is_null($this->_template))
{
throw new Opt_CompilerLocked_Exception($filename, $this->_template);
}
// Detecting recursive inclusion
if(is_null(self::$_recursionDetector))
{
self::$_recursionDetector = array(0 => $filename);
$weFree = true;
}
else
{
if(in_array($filename, self::$_recursionDetector))
{
$exception = new Opt_CompilerRecursion_Exception($filename);
$exception->setData(self::$_recursionDetector);
throw $exception;
}
self::$_recursionDetector[] = $filename;
$weFree = false;
}
// Cleaning up the processors
foreach($this->_processors as $proc)
{
$proc->reset();
}
// Initializing the template launcher
$this->set('template', $this->_template = $filename);
$this->set('mode', $mode);
$this->set('currentTemplate', $this->_template);
array_push(self::$_templates, $filename);
// $this->_stack = new SplStack;
$i = 0;
$extend = $filename;
$memory = 0;
// The inheritance loop
do
{
// Stage 1 - code compilation
if($this->_tpl->debugConsole)
{
$initial = memory_get_usage();
$tree = $this->_stage1($code, $extend, $mode);
// Stage 2 - PHP tree processing
// $this->_stack = array();
$this->_stage2($tree);
$this->set('escape', NULL);
// unset($this->_stack);
$memory += (memory_get_usage() - $initial);
unset($code);
}
else
{
$tree = $this->_stage1($code, $extend, $mode);
unset($code);
// Stage 2 - PHP tree processing
// $this->_stack = array();
$this->_stage2($tree);
$this->set('escape', NULL);
// unset($this->_stack);
}
// if the template extends something, load it and also process
if(isset($extend) && $extend != $filename)
{
$this->addDependantTemplate($extend);
}
if(!is_null($snippet = $tree->get('snippet')))
{
$tree->dispose();
unset($tree);
// Change the specified snippet into a root node.
$tree = new Opt_Xml_Root;
$attribute = new Opt_Xml_Attribute('opt:use', $snippet);
$this->processor('snippet')->processAttribute($tree, $attribute);
$this->processor('snippet')->postprocessAttribute($tree, $attribute);
$this->_stage2($tree, true);
break;
}
if(!is_null($extend = $tree->get('extend')))
{
$tree->dispose();
unset($tree);
$this->set('currentTemplate', $extend);
array_pop(self::$_templates);
array_push(self::$_templates, $extend);
$code = $this->_tpl->_getSource($extend);
}
$i++;
}
while(!is_null($extend));
// There are some dependant templates. We must add a suitable PHP code
// to the output.
if(sizeof($this->_dependencies) > 0)
{
$this->_addDependencies($tree);
}
if($this->_tpl->debugConsole)
{
Opt_Support::addCompiledTemplate($this->_template, $memory);
}
// Stage 3 - linking the last tree
if(!is_null($compiledFilename))
{
$output = '';
$this->_dynamicBlocks = array();
$this->_stage3($output, $tree);
$tree->dispose();
unset($tree);
$output = str_replace('?><'.'?php', '', $output);
// Build the directories, if needed.
if(($pos = strrpos($compiledFilename, '/')) !== false)
{
$path = $this->_tpl->compileDir.substr($compiledFilename, 0, $pos);
if(!is_dir($path))
{
mkdir($path, 0750, true);
}
}
// Save the file
if(sizeof($this->_dynamicBlocks) > 0)
{
file_put_contents($this->_tpl->compileDir.$compiledFilename.'.dyn', serialize($this->_dynamicBlocks));
}
file_put_contents($this->_tpl->compileDir.$compiledFilename, $output);
}
else
{
$tree->dispose();
}
array_pop(self::$_templates);
$this->_inheritance = array();
if($weFree)
{
// Do the cleanup.
$this->_dependencies = array();
self::$_recursionDetector = NULL;
foreach($this->_processors as $processor)
{
$processor->reset();
}
}
$this->_template = NULL;
// Run the new garbage collector, if it is available.
/* if(version_compare(PHP_VERSION, '5.3.0', '>='))
{
gc_collect_cycles();
}*/
}
catch(Exception $e)
{
// Free the memory
if(isset($tree))
{
$tree->dispose();
}
// Clean the compiler state in case of exception
$this->_template = NULL;
$this->_dependencies = array();
self::$_recursionDetector = NULL;
foreach($this->_processors as $processor)
{
$processor->reset();
}
// Run the new garbage collector, if it is available.
/* if(version_compare(PHP_VERSION, '5.3.0', '>='))
{
gc_collect_cycles();
}*/
// And throw it forward.
throw $e;
}
} // end compile();
/**
* Compilation - stage 1 - parsing the input template and
* building an XML tree.
*
* @internal
* @param String &$code The code to be parsed
* @param String $filename Currently unused.
* @param Int $mode The compilation mode.
* @return Opt_Xml_Root The root node of the new tree.
*/
protected function _stage1(&$code, $filename, $mode)
{
$current = $tree = new Opt_Xml_Root;
$codeSize = strlen($code);
$encoding = $this->_tpl->charset;
// First we have to find the prolog and DTD. Then we will be able to parse tags.
if($mode != Opt_Class::QUIRKS_MODE)
{
// Find and parse XML prolog
$endProlog = 0;
$endDoctype = 0;
if(substr($code, 0, 5) == '<?xml')
{
$endProlog = strpos($code, '?>', 5);
if($endProlog === false)
{
throw new Opt_XmlInvalidProlog_Exception('prolog ending is missing');
}
$values = $this->_compileProlog(substr($code, 5, $endProlog - 5));
$endProlog += 2;
if(!$this->_tpl->prologRequired)
{
// The prolog must be displayed
$tree->setProlog(new Opt_Xml_Prolog($values));
}
}
// Skip white spaces
for($i = $endProlog; $i < $codeSize; $i++)
{
if($code[$i] != ' ' && $code[$i] != ' ' && $code[$i] != "\r" && $code[$i] != "\n")
{
break;
}
}
// Try to find doctype at the new position.
$possibleDoctype = substr($code, $i, 9);
if($possibleDoctype == '<!doctype' || $possibleDoctype == '<!DOCTYPE')
{
// OK, we've found it, now determine the doctype end.
$bracketCounter = 0;
$doctypeStart = $i;
for($i += 9; $i < $codeSize; $i++)
{
if($code[$i] == '<')
{
$bracketCounter++;
}
else if($code[$i] == '>')
{
if($bracketCounter == 0)
{
$endDoctype = $i;
break;
}
$bracketCounter--;
}
}
if($endDoctype == 0)
{
throw new Opt_XmlInvalidDoctype_Exception('doctype ending is missing');
}
if(!$this->_tpl->prologRequired)
{
$tree->setDtd(new Opt_Xml_Dtd(substr($code, $doctypeStart, $i - $doctypeStart + 1)));
}
$endDoctype++;
}
else
{
$endDoctype = $endProlog;
}
// OK, now skip that part.
$code = substr($code, $endDoctype, $codeSize);
// In the quirks mode, some results from the regular expression parser are
// moved by one position, so we must add some dynamics here.
$attributeCell = 5;
$endingSlashCell = 6;
$tagExpression = $this->_rXmlTagExpression;
}
else
{
$tagExpression = $this->_rQuirksTagExpression;
$attributeCell = 6;
$endingSlashCell = 7;
}
// Split through the general groups (cdata-content)
$groups = preg_split($this->_rCDataExpression, $code, 0, PREG_SPLIT_DELIM_CAPTURE);
$groupCnt = sizeof($groups);
$groupState = 0;
Opt_Xml_Cdata::$mode = $mode;
for($k = 0; $k < $groupCnt; $k++)
{
// Process CDATA
if($groupState == 0 && $groups[$k] == '<![CDATA[')
{
$cdata = new Opt_Xml_Cdata('');
$cdata->set('cdata', true);
$groupState = 1;
continue;
}
if($groupState == 1)
{
if($groups[$k] == ']]>')
{
$current = $this->_treeTextAppend($current, $cdata);
$groupState = 0;
}
else
{
$cdata->appendData($groups[$k]);
}
continue;
}
$subgroups = preg_split($this->_rCommentExpression, $groups[$k], 0, PREG_SPLIT_DELIM_CAPTURE);
$subgroupCnt = sizeof($subgroups);
$subgroupState = 0;
for($i = 0; $i < $subgroupCnt; $i++)
{
// Process comments
if($subgroupState == 0 && $subgroups[$i] == '<!--')
{
$commentNode = new Opt_Xml_Comment();
$subgroupState = 1;
continue;
}
if($subgroupState == 1)
{
if($subgroups[$i] == '-->')
{
$current->appendChild($commentNode);
$subgroupState = 0;
}
else
{
$commentNode->appendData($subgroups[$i]);
}
continue;
}
elseif($subgroups[$i] == '-->')
{
throw new Opt_XmlInvalidCharacter_Exception('-->');
}
// Find XML tags
preg_match_all($tagExpression, $subgroups[$i], $result, PREG_SET_ORDER);
/*
* Output field description for $result array:
* 0 - original content
* 1 - tag content (without delimiters)
* 2 - /, if enclosing tag
* 3 - name
* 4 - arguments (5 in quirks mode)
* 5 - /, if enclosing tag without subcontent (6 in quirks mode)
*/
$resultSize = sizeof($result);
$offset = 0;
for($j = 0; $j < $resultSize; $j++)
{
// Copy the remaining text to the text node
$id = strpos($subgroups[$i], $result[$j][0], $offset);
if($id > $offset)
{
$current = $this->_treeTextCompile($current, substr($subgroups[$i], $offset, $id - $offset));
}
$offset = $id + strlen($result[$j][0]);
if(!isset($result[$j][$endingSlashCell]))
{
$result[$j][$endingSlashCell] = '';
}
// Process the argument list
$attributes = array();
if(!empty($result[$j][$attributeCell]))
{
// Just for sure...
$result[$j][$attributeCell] = trim($result[$j][$attributeCell]);
$oldLength = strlen($result[$j][$attributeCell]);
$result[$j][$attributeCell] = rtrim($result[$j][$attributeCell], '/');
if(strlen($result[$j][$attributeCell]) != $oldLength)
{
$result[$j][$endingSlashCell] = '/';
}
$attributes = $this->_compileAttributes($result[$j][$attributeCell], $result[$j][4]);
if(!is_array($attributes))
{
throw new Opt_XmlInvalidAttribute_Exception($result[$j][0]);
}
}
// Recognize the tag type
if(substr_count($result[$j][4], ':') > 1)
{
throw new Opt_InvalidNamespace_Exception($result[$j][4]);
}
if($result[$j][3] != '/')
{
// Opening tag
$node = new Opt_Xml_Element($result[$j][4]);
$node->set('single', $result[$j][$endingSlashCell] == '/');
foreach($attributes as $name => $value)
{
$node->addAttribute($anode = new Opt_Xml_Attribute($name, $value));
}
$current = $this->_treeNodeAppend($current, $node, $result[$j][$endingSlashCell] != '/');
}
elseif($result[$j][3] == '/')
{
if(sizeof($attributes) > 0)
{
throw new Opt_XmlInvalidTagStructure_Exception($result[$j][0]);
}
if($current instanceof Opt_Xml_Element)
{
if($current->getXmlName() != $result[$j][4])
{
throw new Opt_XmlInvalidOrder_Exception($result[$j][4], $current->getXmlName());
}
}
else
{
throw new Opt_XmlInvalidOrder_Exception($result[$j][4], 'NULL');
}
$current = $this->_treeJumpOut($current);
}
else
{
throw new Opt_XmlInvalidTagStructure_Exception($result[$j][0]);
}
}
if(strlen($subgroups[$i]) > $offset)
{
$current = $this->_treeTextCompile($current, substr($subgroups[$i], $offset, strlen($subgroups[$i]) - $offset));
}
}
}
// Testing if everything was closed.
if($current !== $tree)
{
// Error handling - determine the name of the unclosed tag.
while(! $current instanceof Opt_Xml_Element)
{
$current = $current->getParent();
}
throw new Opt_UnclosedTag_Exception($current->getXmlName());
}
// Testing the single root node.
if($mode == Opt_Class::XML_MODE && $this->_tpl->singleRootNode)
{
// TODO: The current code does not check the contents of Opt_Text_Nodes and other root elements
// that may contain invalid and valid XML syntax at the same time.
// For now, this code is frozen, we'll think a bit about it in the future. Maybe nobody
// will notice this :)
$elementFound = false;
foreach($tree as $item)
{
if($item instanceof Opt_Xml_Element)
{
if($elementFound)
{
// Oops, there is already another root node!
throw new Opt_XmlRootElement_Exception($item->getXmlName());
}
$elementFound = true;
}
}
}
return $tree;
} // end _stage1();
/**
* Compilation - stage 2. Traversing through the tree and doing something
* with the tree and the nodes. The method is recursion-safe.
*
* @internal
* @param Opt_Xml_Node $node The initial node.
*/
protected function _stage2(Opt_Xml_Node $node)
{
$queue = new SplQueue;
$stack = new SplStack;
$queue->enqueue($node);
while(true)
{
$item = NULL;
if($queue->count() > 0)
{
$item = $queue->dequeue();
}
$pp = array();
// We set the "hidden" state unless it is set.
try
{
if(is_null($item))
{
throw new Opl_Goto_Exception;
}
$stateSet = false;
if(is_null($item->get('hidden')))
{
$item->set('hidden', true);
$stateSet = true;
}
// Proper processing
switch($item->getType())
{
case 'Opt_Xml_Cdata':
$stateSet and $item->set('hidden', false);
break;
case 'Opt_Xml_Text':
$stateSet and $item->set('hidden', false);
if($item->hasChildren())
{
$stack->push(array($item, $queue, $pp));
$pp = NULL;
$queue = new SplQueue;
foreach($item as $child)
{
$queue->enqueue($child);
}
continue 2;
}
break;
case 'Opt_Xml_Element':
if($this->isNamespace($item->getNamespace()))
{
$name = $item->getXmlName();
$pp = $this->_processXml($item, false);
// Look for the processor
if(!is_null($processor = $this->isInstruction($name)))
{
$processor->processNode($item);
}
elseif($this->isComponent($name))
{
$processor = $this->processor('component');
$processor->processComponent($item);
}
elseif($this->isBlock($name))
{
$processor = $this->processor('block');
$processor->processBlock($item);
}
if(is_object($processor))
{
$stateSet and $item->set('hidden', false);
$result = $processor->getQueue();
if(!is_null($result))
{
$stack->push(array($item, $queue, $pp));
$queue = $result;
continue 2;
}
}
elseif($item->get('processAll'))
{
$stateSet and $item->set('hidden', false);
$stack->push(array($item, $queue, $pp));
$pp = NULL;
$queue = new SplQueue;
foreach($item as $child)
{
$queue->enqueue($child);
}
continue 2;
}
unset($processor);
}
else
{
$pp = $this->_processXml($item, true);
$stateSet and $item->set('hidden', false);
if($item->hasChildren())
{
$stack->push(array($item, $queue, $pp));
$pp = NULL;
$queue = new SplQueue;
foreach($item as $child)
{
$queue->enqueue($child);
}
continue 2;
}
}
break;
case 'Opt_Xml_Expression':
$stateSet and $item->set('hidden', false);
// Empty expressions will be caught by the try... catch.
try
{
$result = $this->compileExpression((string)$item, true);
if(!$result[1])
{
$item->addAfter(Opt_Xml_Buffer::TAG_BEFORE, 'echo '.$result[0].'; ');
}
else
{
$item->addAfter(Opt_Xml_Buffer::TAG_BEFORE, $result[0].';');
}
}
catch(Opt_EmptyExpression_Exception $e){}
break;
case 'Opt_Xml_Root':
$stateSet and $item->set('hidden', false);
$queue = $this->_pushQueue($stack, $queue, $item, array());
break;
case 'Opt_Xml_Comment':
if($this->_tpl->printComments)
{
$stateSet and $item->set('hidden', false);
}
break;
}
}
catch(Opl_Goto_Exception $e){}
$this->_doPostprocess($item, $pp);
if($queue->count() == 0)
{
unset($queue);
if($stack->count() == 0)
{
break;
}
list($item, $queue, $pp) = $stack->pop();
$this->_doPostprocess($item, $pp);
}
}
} // end _stage2();
/**
* Links the tree into a valid XML file with embedded PHP commands
* from the tag buffers. The method is recursion-free.
*
* @internal
* @param String &$output Where to store the output.
* @param Opt_Xml_Node $node The initial node.
*/
protected function _stage3(&$output, Opt_Xml_Node $node)
{
// To avoid the recursion, we need a queue and a stack.
$queue = new SplQueue;
$stack = new SplStack;
$queue->enqueue($node);
// Reset the output
$realOutput = &$output;
$output = '';
$wasElement = false;
// We will be processing the tags in a "infinite" loop
// In fact, the stop condition can be found within the loop.
while(true)
{
// Obtain the new item to process from the queue.
$item = NULL;
if($queue->count() > 0)
{
$item = $queue->dequeue();
}
// Note that this code uses Opl_Goto_Exception to simulate
// "goto" instruction.
try
{
// Should we display this node?
if(is_null($item) || $item->get('hidden') !== false)
{
throw new Opl_Goto_Exception; // Goto postprocess;
}
// If the tag has the "commented" flag, we comment it
// and its content.
if($item->get('commented'))
{
$this->_comments++;
if($this->_comments == 1)
{
$output .= '<!--';
}
}
// If the block is dynamic, then replace the output with some other variable.
if($item->get('dynamic'))
{
$i = sizeof($this->_dynamicBlocks);
$this->_dynamicBlocks[$i] = '';
$output = &$this->_dynamicBlocks[$i];
}
/* Note that some of the node types must execute some code both before
* processing their children and after them. This method processes only
* the code executed BEFORE the children are parsed. The latter situation
* is implemented in the _doPostlinking() method.
*/
$doPostlinking = false;
switch($item->getType())
{
case 'Opt_Xml_Cdata':
if($item->get('cdata'))
{
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_BEFORE).'<![CDATA['.$item.']]>'.$item->buildCode(Opt_Xml_Buffer::TAG_AFTER);
break;
}
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_BEFORE);
// We strip the white spaces at the linking level.
if($this->_tpl->stripWhitespaces)
{
// The CDATA composed of white characters only is reduced to a single space.
if(ctype_space((string)$item))
{
if($wasElement)
{
$output .= ' ';
}
}
else
{
// In the opposite case reduce all the groups of the white characters
// to single spaces in the text.
if($item->get('noEntitize') === true)
{
$output .= preg_replace('/\s\s+/', ' ', (string)$item);
}
else
{
$output .= $this->parseSpecialChars(preg_replace('/(\s){1,}/', ' ', (string)$item));
}
}
}
else
{
$output .= ($item->get('noEntitize') ? (string)$item : $this->parseSpecialChars($item));
}
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_AFTER);
$this->_closeComments($item, $output);
break;
case 'Opt_Xml_Text':
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_BEFORE);
$queue = $this->_pushQueue($stack, $queue, $item, NULL);
// Next part in the post-process section
break;
case 'Opt_Xml_Element':
if($this->isNamespace($item->getNamespace()))
{
// This code handles the XML elements that represent the
// OPT instructions. They have shorter code, because
// we do not need to display their tags.
if(!$item->hasChildren() && $item->get('single'))
{
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_BEFORE, Opt_Xml_Buffer::TAG_SINGLE_BEFORE);
$doPostlinking = true;
}
elseif($item->hasChildren())
{
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_BEFORE, Opt_Xml_Buffer::TAG_OPENING_BEFORE,
Opt_Xml_Buffer::TAG_OPENING_AFTER, Opt_Xml_Buffer::TAG_CONTENT_BEFORE);
$queue = $this->_pushQueue($stack, $queue, $item, NULL);
// Next part in the post-process section
}
else
{
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_BEFORE, Opt_Xml_Buffer::TAG_OPENING_BEFORE,
Opt_Xml_Buffer::TAG_OPENING_AFTER, Opt_Xml_Buffer::TAG_CONTENT_BEFORE);
$doPostlinking = true;
}
}
else
{
$wasElement = true;
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_BEFORE, Opt_Xml_Buffer::TAG_OPENING_BEFORE);
if($item->bufferSize(Opt_Xml_Buffer::TAG_NAME) == 0)
{
$name = $item->getXmlName();
}
elseif($item->bufferSize(Opt_Xml_Buffer::TAG_NAME) == 1)
{
$name = $item->buildCode(Opt_Xml_Buffer::TAG_NAME);
}
else
{
throw new Opt_CompilerCodeBufferConflict_Exception(1, 'TAG_NAME', $item->getXmlName());
}
if(!$item->hasChildren() && $item->bufferSize(Opt_Xml_Buffer::TAG_CONTENT) == 0 && $item->get('single'))
{
$output .= '<'.$name.$this->_linkAttributes($item).' />'.$item->buildCode(Opt_Xml_Buffer::TAG_SINGLE_AFTER,Opt_Xml_Buffer::TAG_AFTER);
$item = null;
}
else
{
$output .= '<'.$name.$this->_linkAttributes($item).'>'.$item->buildCode(Opt_Xml_Buffer::TAG_OPENING_AFTER);
$item->set('_name', $name);
if($item->bufferSize(Opt_Xml_Buffer::TAG_CONTENT) > 0)
{
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_CONTENT_BEFORE, Opt_Xml_Buffer::TAG_CONTENT, Opt_Xml_Buffer::TAG_CONTENT_AFTER);
}
elseif($item->hasChildren())
{
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_CONTENT_BEFORE);
$queue = $this->_pushQueue($stack, $queue, $item, NULL);
// Next part in the post-process section
break;
}
else
{
// The postlinking is already done here, so skip this part
// in the linker
$item->set('_skip_postlinking', true);
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_CLOSING_BEFORE).'</'.$name.'>'.$item->buildCode(Opt_Xml_Buffer::TAG_CLOSING_AFTER, Opt_Xml_Buffer::TAG_AFTER);
}
}
}
break;
case 'Opt_Xml_Expression':
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_BEFORE);
$this->_closeComments($item, $output);
break;
case 'Opt_Xml_Root':
$output .= $item->buildCode(Opt_Xml_Buffer::TAG_BEFORE);
// Display the prolog and DTD, if it was set in the node.
// Such construct ensures us that they will appear in the
// valid place in the output document.
if($item->hasProlog())
{
$output .= str_replace('<?xml', '<<?php echo \'?\'; ?>xml', $item->getProlog()->getProlog())."\r\n";
}
if($item->hasDtd())
{
$output .= $item->getDtd()->getDoctype()."\r\n";
}
// And go to their children.
$queue = $this->_pushQueue($stack, $queue, $item, NULL);
break;
case 'Opt_Xml_Comment':
// The comment tags are added automatically.
$output .= (string)$item;
$this->_closeComments($item, $output);
break;
}
}
catch(Opl_Goto_Exception $goto){} // postprocess:
if($doPostlinking)
{
$output .= $this->_doPostlinking($item);
if(is_object($item) && $item->get('dynamic'))
{
$realOutput .= $output;
$output = &$realOutput;
}
}
if($queue->count() == 0)
{
if($stack->count() == 0)
{
break;
}
/**
* !!!!!!!!!!!!THIS IS WRONG!!!!!!!!!!!!!!!!
*
* Some items get this called only if they are
* the last in the queue.
*/
if(!$doPostlinking)
{
$output .= $this->_doPostlinking($item);
if(is_object($item) && $item->get('dynamic'))
{
$realOutput .= $output;
$output = &$realOutput;
}
}
list($item, $queue, $pp) = $stack->pop();
$output .= $this->_doPostlinking($item);
if($item->get('dynamic'))
{
$realOutput .= $output;
$output = &$realOutput;
}
}
}
if($this->_tpl->stripWhitespaces)
{
$output = rtrim($output);
}
} // end _stage3();
/*
* Expression compiler
*/
/**
* Compiles the template expression to the PHP code and checks the syntax
* errors. The method is recursion-free.
*
* @param String $expr The expression
* @param Boolean $allowAssignment=false True, if the assignments are allowed.
* @param Int $escape=self::ESCAPE_ON The HTML escaping policy for this expression.
* @return Array An array consisting of four elements: the compiled expression,
* the assignment status and the variable status (if the expression is in fact
* a single variable). If the escaping is controlled by the template or the
* script, the fourth element contains also an unescaped PHP expression.
*/
public function compileExpression($expr, $allowAssignment = false, $escape = self::ESCAPE_ON)
{
// The expression modifier must not be tokenized, so we
// capture it before doing anything with the expression.
$modifier = '';
if(preg_match('/^([^\'])\:[^\:]/', $expr, $found))
{
$modifier = $found[1];
if($modifier != 'e' && $modifier != 'u')
{
throw new Opt_InvalidExpressionModifier_Exception($modifier, $expr);
}
$expr = substr($expr, 2, strlen($expr) - 2);
}
// cat $expr > /dev/oracle > $result > happy programmer :)
preg_match_all('/(?:'.
$this->_rSingleQuoteString.'|'.
$this->_rBacktickString.'|'.
$this->_rHexadecimalNumber.'|'.
$this->_rDecimalNumber.'|'.
$this->_rLanguageVar.'|'.
$this->_rVariable.'|'.
$this->_rOperators.'|'.
$this->_rIdentifier.')/x', $expr, $match);
// Skip the whitespaces and create the translation units
$cnt = sizeof($match[0]);
$stack = new SplStack;
$tu = array(0 => array());
$tuid = 0;
$maxTuid = 0;
$prev = '';
$chr = chr(18);
$assignments = array();
/* The translation units allow to avoid recursive compilation of the
* expression. Each sub-expression within parentheses and that is a
* function call parameter, becomes a separate translation unit. The
* loop below scans the array of tokens, looking for translation
* unit separators and builds suitable arrays of tokens for each
* TU.
*/
for($i = 0; $i < $cnt; $i++)
{
if(ctype_space($match[0][$i]) || $match[0][$i] == '')
{
continue;
}
switch($match[0][$i])
{
case ',':
if($prev == '(' || $prev == ',')
{
throw new Opt_Expression_Exception('OP_COMMA', $match[0][$i], $expr);
}
$tuid = $stack->pop();
if(in_array($tuid, $assignments))
{
$tuid = $stack->pop();
}
case '[':
case '(':
case 'is':
case '=':
if($match[0][$i] == '=' || $match[0][$i] == 'is')
{
$assignments[] = $tuid;
}
$tu[$tuid][] = $match[0][$i];
++$maxTuid;
$tu[$tuid][] = $chr.$maxTuid; // A fake token that marks the translation unit which goes here.
$stack->push($tuid);
$tuid = $maxTuid;
$tu[$tuid] = array();
break;
case ']':
case ')':
// If we have a situation like (), we can remove the TU we've just created,
// because it's empty and will confuse the expression compiler later.
if($prev == '(')
{
unset($tu[$tuid]);
--$maxTuid;
}
if($stack->count() > 0)
{
$tuid = $stack->pop();
if(in_array($tuid, $assignments))
{
$tuid = $stack->pop();
}
}
if($prev == '(')
{
array_pop($tu[$tuid]);
}
if($prev == ',')
{
throw new Opt_Expression_Exception('OP_BRACKET', $match[0][$i], $expr);
}
$tu[$tuid][] = $match[0][$i];
break;
default:
$tu[$tuid][] = $match[0][$i];
}
$prev = $match[0][$i];
}
if(sizeof($tu[0]) == 0)
{
throw new Opt_EmptyExpression_Exception();
}
/*
* Now we have an array of translation units and their tokens and
* we can process it linearly, thus avoiding recursive calls.
*/
foreach($tu as $id => &$tuItem)
{
$tuItem = $this->_compileExpression($expr, $allowAssignment, $tuItem, $id);
}
$assign = $tu[0][1];
$variable = $tu[0][2];
/*
* Finally, we have to link all the subexpressions into an output
* expression. We use SPL stack to achieve this, because we need
* to store the current subexpression status while finding a new one.
*/
$tuid = 0;
$i = -1;
$cnt = sizeof($tu[0][0]);
$stack = new SplStack;
$prev = null;
$expression = '';
while(true)
{
$i++;
$token = &$tu[$tuid][0][$i];
// If we've found a translation unit, we must stop for a while the current one
// and link the new.
if(strlen($token) > 0 && $token[0] == $chr)
{
$wasAssignment = in_array($tuid, $assignments);
$stack->push(Array($tuid, $i, $cnt));
$tuid = (int)ltrim($token, $chr);
$i = -1;
$cnt = sizeof($tu[$tuid][0]);
if($cnt == 0 && $wasAssignment)
{
throw new Opt_Expression_Exception('OP_NULL', '', $expr);
}
continue;
}
else
{
$expression .= $token;
}
if($i >= $cnt)
{
if($stack->count() == 0)
{
break;
}
// OK, current TU is ready. Check, whether there are unfinished upper-level TUs
// on the stack
unset($tu[$tuid]);
list($tuid, $i, $cnt) = $stack->pop();
}
$prev = $token;
}
/*
* Now it's time to apply the escaping policy to this expression. We check
* the expression for the "e:" and "u:" modifiers and redirect the task to
* the escape() method.
*/
$result = $expression;
if($escape != self::ESCAPE_OFF && !$assign)
{
if($modifier != '')
{
$result = $this->escape($result, $modifier == 'e');
}
else
{
$result = $this->escape($result);
}
}
// Pack everything
if($escape != self::ESCAPE_BOTH)
{
return array(0 => $result, $assign, $variable, NULL);
}
else
{
return array(0 => $result, $assign, $variable, $expression);
}
} // end compileExpression();
/**
* Compiles a single translation unit in the expression.
*
* @internal
* @param String &$expr A reference to the compiled expressions for debug purposes.
* @param Boolean $allowAssignment True, if the assignments are allowed in this unit.
* @param Array &$tokens A reference to the array of tokens for this translation unit.
* @param String $tu The number of the current translation unit.
* @return Array An array build of three items: the compiled expression, the assignment status
* and the variable status (whether the expression is in fact a single variable).
*/
protected function _compileExpression(&$expr, $allowAssignment, Array &$tokens, $tu)
{
/* The method processes a single translation unit (TU). For example, in the expression
* $a is ($b + $c) * $d
* we have the following translation units:
* 1. $a is #TU2 * $d
* 2. $b + $c
*
* They are compiled separately and automatically, so you do not have to do this on
* your own. This has been done to remove the recursion from the source code, and moreover
* it allows, for example, to manage the argument order in the functions.
*/
// Operator mappings
$wordOperators = array(
'eq' => '==',
'eqt' => '===',
'ne' => '!=',
'net' => '!==',
'neq' => '!=',
'neqt' => '!==',
'lt' => '<',
'le' => '<=',
'lte' => '<=',
'gt' => '>',
'ge' => '>=',
'gte' => '>=',
'and' => '&&',
'or' => '||',
'xor' => 'xor',
'not' => '!',
'mod' => '%',
'div' => '/',
'add' => '+',
'sub' => '-',
'mul' => '*',
'shl' => '<<',
'shr' => '>>'
);
// Previous token information
$previous = array(
'token' => null,
'source' => null,
'result' => null
);
// Some standard "next token sets"
$valueSet = self::OP_VARIABLE | self::OP_LANGUAGE_VAR | self::OP_STRING | self::OP_NUMBER |
self::OP_IDENTIFIER | self::OP_PRE_OPERATOR | self::OP_OBJMAN | self::OP_BRACKET;
$operatorSet = self::OP_OPERATOR | self::OP_POST_OPERATOR | self::OP_NULL;
// Initial state
$state = array(
'next' => $valueSet | self::OP_NULL, // What token must occur next.
'step' => 0, // This flag helps processing brackets by saving some extra token information.
'func' => 0, // The function call type: 0 - OPT function (with "$this" as the first argument); 1 - ordinary function
'oper' => false, // The assignment flag. The value must be assigned to a variable, so on the left side there must not be any operator (false).
'clone' => 0, // We've already used "clone"
'preop' => false, // Prefix operators ++ and -- found. This flag is cancelled by any other operator.
'rev' => NULL, // Changing the argument order options
'assign_func' => false, // Informing the bracket parser that the first argument must be a language block, which must be processed separately.
'tu' => 0, // What has opened a translation unit? The field contains the token type.
'variable' => NULL, // To detect if the expression is a single variable or not.
'function' => NULL // Function name for the argument checker errors
);
$chr = chr(18); // Which ASCII code marks the translation unit
$result = array(); // Here we put the compilation result
$void = false; // This is a fake variable for a recursive call, as a last argument (reference)
$assign = false;
$to = sizeof($tokens);
// Loop through the token list.
for($i = 0; $i < $to; $i++)
{
// Some initializing stuff.
$token = &$tokens[$i];
$parsefunc = false;
$current = array(
'token' => null, // Symbolic token type. Look at the file header to find the token definitions.
'source' => $token, // Original form of the token is also remembered.
'result' => null, // Here we have to put the result PHP code generated from the token.
);
// Find out, what it is and process it.
switch($token)
{
case '[':
// This code checks, whether the token is properly used. We have to assign it to one of the token groups.
if(!($state['next'] & self::OP_SQ_BRACKET))
{
throw new Opt_Expression_Exception('OP_SQ_BRACKET', $token, $expr);
}
$result[] = '[';
$state['tu'] = self::OP_SQ_BRACKET_E;
$state['next'] = self::OP_TU;
$state['step'] = self::OP_VARIABLE;
continue;
case ']':
if(!($state['next'] & self::OP_SQ_BRACKET_E))
{
throw new Opt_Expression_Exception('OP_SQ_BRACKET_E', $token, $expr);
}
$current['token'] = $state['step'];
$current['result'] = ']';
$state['step'] = 0;
// This is the way we mark, what tokens can occur next.
$state['next'] = self::OP_OPERATOR | self::OP_NULL | self::OP_SQ_BRACKET;
if($state['clone'] == 1)
{
$state['next'] = self::OP_NULL | self::OP_SQ_BRACKET;
}
break;
// These tokens are invalid and must produce an error
case '\'':
case '"':
case '{':
case '}':
throw new Opt_Expression_Exception('OP_CURLY_BRACKET', $token, $expr);
break;
// Text operators.
case 'add':
case 'sub':
case 'mul':
case 'div':
case 'mod':
case 'shl':
case 'shr':
case 'eq':
case 'neq':
case 'eqt':
case 'neqt':
case 'ne':
case 'net':
case 'lt':
case 'le':
case 'lte':
case 'gt':
case 'gte':
case 'ge':
// These guys can be also method names, if in proper context
if($previous['token'] == self::OP_CALL)
{
$this->_compileIdentifier($token, $previous['token'], $previous['result'],
isset($tokens[$i+1]) ? $tokens[$i+1] : null, $operatorSet, $expr, $current, $state);
break;
}
case 'and':
case 'or':
case 'xor':
$this->_testPreOperators($previous['token'], $state['preop'], $token, $expr);
// And these three ones - only strings.
if($state['next'] & self::OP_STRING)
{
$current['result'] = '\''.$token.'\'';
$current['token'] = self::OP_STRING;
$state['next'] = $operatorSet | self::OP_SQ_BRACKET_E;
}
else
{
if(!($state['next'] & self::OP_OPERATOR))
{
throw new Opt_Expression_Exception('OP_OPERATOR', $token, $expr);
}
$current['result'] = $wordOperators[$token];
$current['token'] = self::OP_OPERATOR;
$state['next'] = $valueSet;
$state['preop'] = false;
}
$state['variable'] = false;
break;
case 'not':
if(!($state['next'] & self::OP_PRE_OPERATOR))
{
throw new Opt_Expression_Exception('OP_PRE_OPERATOR', $token, $expr);
}
$current['token'] = self::OP_PRE_OPERATOR;
$current['result'] = $wordOperators[$token];
$state['next'] = $valueSet;
$state['variable'] = false;
break;
case 'new':
case 'clone':
// These operators are active only if the directive advancedOOP is true.
if(!$this->_tpl->advancedOOP)
{
throw new Opt_ExpressionOptionDisabled_Exception($token, 'security reasons');
}
if(!($state['next'] & self::OP_OBJMAN))
{
throw new Opt_Expression_Exception('OP_OBJMAN', $token, $expr);
}
$current['result'] = $token.' ';
$current['token'] = self::OP_OBJMAN;
$state['next'] = ($token == 'new' ? self::OP_IDENTIFIER : self::OP_BLOCK);
$state['clone'] = 1;
$state['variable'] = false;
break;
case 'is':
if($state['next'] & self::OP_STRING)
{
$current['result'] = '\''.$token.'\'';
$state['next'] = $operatorSet | self::OP_SQ_BRACKET_E | self::OP_TU;
break;
}
case '=':
if(!$allowAssignment)
{
throw new Opt_ExpressionOptionDisabled_Exception('Assignments', 'compiler requirements');
}
// We have to assign the data to the variable or object field.
if(($previous['token'] == self::OP_VARIABLE || $previous['token'] == self::OP_FIELD) && !$state['oper'] && $previous['token'] != self::OP_LANGUAGE_VAR)
{
$current['result'] = '';
$current['token'] = self::OP_ASSIGN;
$state['variable'] = false;
$state['next'] = self::OP_TU;
$state['tu'] = self::OP_NULL;
$assign = true;
}
else
{
throw new Opt_Expression_Exception('OP_ASSIGN', $token, $expr);
}
break;
case '!==':
case '==':
case '===':
case '!=':
case '+':
case '*':
case '/':
case '%':
if(!($state['next'] & self::OP_OPERATOR))
{
throw new Opt_Expression_Exception('OP_OPERATOR', $token, $expr);
}
$this->_testPreOperators($previous['token'], $state['preop'], $token, $expr);
$current['result'] = $token;
$state['next'] = $valueSet;
$state['oper'] = true;
$state['preop'] = false;
$state['variable'] = false;
break;
case '-':
if($state['next'] & self::OP_OPERATOR)
{
$this->_testPreOperators($previous['token'], $state['preop'], $token, $expr);
$current['result'] = $token;
$state['oper'] = true;
$state['next'] = $valueSet;
$state['preop'] = false;
}
elseif($state['next'] & self::OP_NUMBER | self::OP_VARIABLE | self::OP_IDENTIFIER)
{
$current['result'] = $token;
$state['next'] = self::OP_NUMBER | self::OP_VARIABLE | self::OP_IDENTIFIER;
}
else
{
throw new Opt_Expression_Exception('OP_OPERATOR', $token, $expr);
}
$state['variable'] = false;
break;
case '~':
if(!($state['next'] & self::OP_OPERATOR))
{
throw new Opt_Expression_Exception('OP_OPERATOR', $token, $expr);
}
$current['result'] = '.';
$state['next'] = $valueSet;
$state['oper'] = true;
$state['preop'] = false;
$state['variable'] = false;
break;
case '++':
case '--':
$current['token'] = self::OP_PRE_OPERATOR;
if(!($state['next'] & self::OP_PRE_OPERATOR))
{
$current['token'] = self::OP_POST_OPERATOR;
if(!($state['next'] & self::OP_POST_OPERATOR))
{
throw new Opt_Expression_Exception('OP_POST_OPERATOR', $token, $expr);
}
else
{
$state['next'] = self::OP_OPERATOR | self::OP_NULL;
}
}
else
{
$state['next'] = self::OP_VARIABLE | self::OP_LANGUAGE_VAR | self::OP_NUMBER;
$state['preop'] = true;
}
$state['oper'] = true;
$state['variable'] = false;
$current['result'] = $token;
break;
case '!':
if(!($state['next'] & self::OP_PRE_OPERATOR))
{
throw new Opt_Expression_Exception('OP_PRE_OPERATOR', $token, $expr);
}
$current['result'] = $token;
$current['token'] = self::OP_PRE_OPERATOR;
$state['variable'] = false;
break;
case 'null':
case 'false':
case 'true':
// These special values are treated as numbers by the compiler.
if(!($state['next'] & self::OP_NUMBER))
{
throw new Opt_Expression_Exception('OP_NUMBER', $token, $expr);
}
$current['token'] = self::OP_NUMBER;
$current['result'] = $token;
$state['next'] = $operatorSet;
break;
case '.':
throw new Opt_Expression_Exception('.', $token, $expr);
break;
case '::':
if(!($state['next'] & self::OP_CALL))
{
throw new Opt_Expression_Exception('OP_CALL', $token, $expr);
}
if(!$this->_tpl->basicOOP)
{
throw new Opt_NotSupported_Exception('object-oriented programming', 'disabled');
}
// OPT decides from the context, whether "::" means a static
// or dynamic call.
if($previous['token'] == self::OP_CLASS)
{
$current['result'] = '::';
$state['call'] = 0;
}
else
{
$current['result'] = '->';
}
$current['token'] = self::OP_CALL;
$state['next'] = self::OP_IDENTIFIER;
break;
case '(':
// Check, if the parenhesis begins a function/method argument list
if($previous['token'] == self::OP_METHOD || $previous['token'] == self::OP_FUNCTION || $previous['token'] == self::OP_CLASS)
{
// Yes, this is a function call, so we need to find its arguments.
$args = array();
for($j = $i + 1; $j < $to && $tokens[$j] != ')'; $j++)
{
if($tokens[$j][0] == $chr)
{
$args[] = $tokens[$j];
}
elseif($tokens[$j] != ',')
{
throw new Opt_Expression_Exception('OP_UNKNOWN', $tokens[$j], $expr);
}
}
$argNum = sizeof($args);
// Optionally, change the argument order
if(!is_null($state['rev']))
{
$this->_reverseArgs($args, $state['rev'], $state['function']);
$state['rev'] = null;
$argNum = sizeof($args);
}
// Put the parenthesis to the compiled token list.
$result[] = '(';
// If we have a call of the assign() function, we need to store the
// number of the translation unit in the _translationConversion field.
// This will allow the language variable compiler to notice that here
// we should have a language call that must be treated in a bit different
// way.
if($argNum > 0 && $state['assign_func'])
{
$this->_translationConversion = (int)trim($args[0], $chr);
}
// Build the argument list.
for($k = 0; $k < $argNum; $k++)
{
$result[] = $args[$k];
if($k < $argNum - 1)
{
$result[] = ',';
}
}
$i = $j-1;
$state['next'] = self::OP_BRACKET_E;
$state['step'] = $previous['token'];
continue;
}
else
{
if(!($state['next'] & self::OP_BRACKET))
{
throw new Opt_Expression_Exception('OP_BRACKET', $token, $expr);
}
$result[] = '(';
$state['tu'] = self::OP_BRACKET_E;
$state['next'] = self::OP_TU;
$state['step'] = self::OP_VARIABLE;
}
break;
case ')':
if($state['step'] == 0)
{
throw new Opt_Expression_Exception('OP_BRACKET', $token, $expr);
}
else
{
if(!($state['next'] & self::OP_BRACKET_E))
{
throw new Opt_Expression_Exception('OP_BRACKET_E', $token, $expr);
}
$current['token'] = $state['step'];
$current['result'] = ')';
$state['step'] = 0;
$state['next'] = self::OP_OPERATOR | self::OP_NULL | self::OP_CALL;
if($state['clone'] == 1)
{
$state['next'] = self::OP_NULL | self::OP_CALL;
}
}
break;
default:
if($token[0] == $chr)
{
// We've found another translation unit.
if(!($state['next'] & self::OP_TU))
{
throw new Opt_Expression_Exception('OP_TU', 'Translation unit #'.ltrim($token, $chr), $expr);
}
if($previous['token'] != self::OP_ASSIGN)
{
$result[] = $token;
}
$state['next'] = $state['tu'];
}
elseif(preg_match('/^'.$this->_rVariable.'$/', $token))
{
// Variable call.
if(!($state['next'] & self::OP_VARIABLE))
{
throw new Opt_Expression_Exception('OP_VARIABLE', $token, $expr);
}
// We do the first character test manually, because
// in regular expression the parser would receive too much rubbish.
if(!ctype_alpha($token[1]) && $token[1] != '_')
{
throw new Opt_Expression_Exception('OP_VARIABLE', $token, $expr);
}
// Moreover, we need to know the future (assignments)
$assignment = null;
if(isset($tokens[$i+1]) && ($tokens[$i+1] == '=' || $tokens[$i+1] == 'is'))
{
$assignment = $tokens[$i+2];
}
$out = $this->_compileVariable($token, $assignment);
if(is_array($out))
{
foreach($out as $t)
{
$result[] = $t;
}
$current['result'] = '';
$current['token'] = self::OP_VARIABLE;
}
else
{
$current['result'] = $out;
$current['token'] = self::OP_VARIABLE;
}
if(is_null($state['variable']))
{
$state['variable'] = true;
}
// Hmmm... and what is the purpose of this IF? Seriously, I forgot.
// So better do not touch it; it must have been very important.
if($state['clone'] == 1)
{
$state['next'] = self::OP_SQ_BRACKET | self::OP_CALL | self::OP_NULL;
}
else
{
$state['next'] = $operatorSet | self::OP_SQ_BRACKET | self::OP_CALL;
}
}
elseif(preg_match('/^'.$this->_rLanguageVarExtract.'$/', $token, $found))
{
// Extracting the language var.
if(!($state['next'] & self::OP_LANGUAGE_VAR))
{
throw new Opt_Expression_Exception('OP_LANGUAGE_VAR', $token, $expr);
}
$current['result'] = $this->_compileLanguageVar($found[1], $found[2], $tu);
$current['token'] = self::OP_LANGUAGE_VAR;
$state['next'] = $operatorSet;
}
elseif(preg_match('/^'.$this->_rDecimalNumber.'$/', $token))
{
// Handling the decimal numbers.
if(!($state['next'] & self::OP_NUMBER))
{
throw new Opt_Expression_Exception('OP_NUMBER', $token, $expr);
}
$current['result'] = $token;
$state['next'] = $operatorSet | self::OP_SQ_BRACKET_E;
}
elseif(preg_match('/^'.$this->_rHexadecimalNumber.'$/', $token))
{
// Hexadecimal, too.
if(!($state['next'] & self::OP_NUMBER))
{
throw new Opt_Expression_Exception('OP_NUMBER', $token, $expr);
}
$current['result'] = $token;
$state['next'] = $operatorSet | self::OP_SQ_BRACKET_E;
}
elseif(preg_match('/^'.$this->_rSingleQuoteString.'$/', $token))
{
if(!($state['next'] & self::OP_STRING))
{
throw new Opt_Expression_Exception('OP_STRING', $token, $expr);
}
$current['result'] = $this->_compileString($token);
$state['next'] = $operatorSet | self::OP_SQ_BRACKET_E;
}
elseif(preg_match('/^'.$this->_rBacktickString.'$/', $token))
{
if(!($state['next'] & self::OP_STRING))
{
throw new Opt_Expression_Exception('OP_STRING', $token, $expr);
}
$current['result'] = $this->_compileString($token);
$state['next'] = $operatorSet | self::OP_SQ_BRACKET_E;
}
elseif(preg_match('/^'.$this->_rIdentifier.'$/', $token))
{
$this->_compileIdentifier($token, $previous['token'], $previous['result'],
isset($tokens[$i+1]) ? $tokens[$i+1] : null, $operatorSet, $expr, $current, $state);
}
}
$previous = $current;
if($current['result'] != '')
{
$result[] = $current['result'];
}
}
// Finally, test if the pre- operators have been used properly.
$this->_testPreOperators($previous['token'], $state['preop'], $token, $expr);
// And if we are allowed to finish here...
if(!($state['next'] & self::OP_NULL))
{
throw new Opt_Expression_Exception('OP_NULL', $token, $expr);
}
// TODO: For variable detection: check also class/object fields!
return array($result, $assign, $state['variable']);
} // end _compileExpression();
/**
* An utility function that allows to test the preincrementation
* operators, if they are used in the right place. In case of
* problems, it generates an exception.
*
* @internal
* @param Int $previous The previous token type.
* @param Boolean $state The state of the "preop" expression parser flag.
* @param String $token The current token provided for debug purposes.
* @param String &$expr The reference to the parsed expression for debug purposes.
*/
protected function _testPreOperators($previous, $state, &$token, &$expr)
{
if(($previous == self::OP_METHOD || $previous == self::OP_FUNCTION || $previous == self::OP_EXPRESSION) && $state)
{
// Invalid use of prefix operators!
throw new Opt_Expression_Exception('OP_PRE_OPERATOR', $token, $expr);
}
} // end _testPreOperators();
/**
* Compiles the template variable into the PHP code. It can be
* generated in two contexts: read and save. The method supports
* all the special variables, local template variables and
* chooses the correct data formats. Moreover, it provides a
* build-in support for sections.
*
* @internal
* @param String $name Variable call
* @param String $newValue Null or the new value to assign
* @return String The output PHP code.
*/
protected function _compileVariable($name, $saveContext = null)
{
$value = substr($name, 1, strlen($name) - 1);
$result = '';
if(strpos($value, '.') !== FALSE)
{
$ns = explode('.', $value);
}
else
{
$ns = array(0 => $value);
}
if($name[0] == '@')
{
// The instruction may wish to handle this variable somehow differently.
if(($to = $this->convert('##var_'.$ns[0])) == '##var_'.$ns[0])
{
$result = 'self::$_vars'; // Standard handler
}
else
{
$result = $to; // Programmer-defined handler
unset($ns[0]); // We assume that the variable name is already included into the handler.
}
// Link the rest of the array call.
foreach($ns as $item)
{
if(ctype_digit($item))
{
$result .= '['.$item.']';
}
else
{
$result .= '[\''.$item.'\']';
}
}
if($saveContext !== null)
{
return array($result.'=', $saveContext);
}
return $result;
}
else
{
/*
* This is the variable scanner that parses things like "$var.foo.bar.joe".
* Each segment of the name can be parsed in different format, depending on
* the programmer settings. Moreover, it recognizes the special calls, like "opt"/"system"
* or section element calls.
*/
$path = '';
$previous = null;
$code = '';
$count = sizeof($ns);
$state = array(
'access' => $this->_tpl->variableAccess,
'section' => null,
'first' => false
);
// Check the first element for special keywords.
switch($ns[0])
{
case 'opt':
case 'sys':
case 'system':
if($saveContext !== null)
{
throw new Opt_AssignNotSupported_Exception($name);
}
return $this->_compileSys($ns);
case 'this':
$state['access'] = Opt_Class::ACCESS_LOCAL;
unset($ns[0]);
break;
case 'global':
$state['access'] = Opt_Class::ACCESS_GLOBAL;
unset($ns[0]);
break;
}
// Scan the rest of the name
$final = sizeof($ns) - 1;
foreach($ns as $id => $item)
{
$previous = $path;
if($path == '')
{
// Parsing the first element. First, check the conversions.
if(($to = $this->convert('##simplevar_'.$item)) != '##simplevar_'.$item)
{
$item = $to;
}
$path = $item;
$state['first'] = true;
}
else
{
// Parsing one of the later elements
$path .= '.'.$item;
$state['first'] = false;
}
// Processing sections
if(!is_null($this->isProcessor('section')))
{
if(is_null($state['section']))
{
// Check if any section with the specified name exists.
$proc = $this->processor('section');
$sectionName = $this->convert($item);
if(!is_null($section = $proc->getSection($sectionName)))
{
$path = $sectionName;
$state['section'] = $section;
if($id == $count - 1)
{
// This is the last name element.
if($saveContext !== null)
{
if(!$section['format']->property('section:itemAssign'))
{
throw new Opt_AssignNotSupported_Exception($name);
}
$format->assign('value', $saveContext);
return $section['format']->get('section:itemAssign');
}
return $section['format']->get('section:item');
}
continue;
}
}
else
{
// The section has been found, we need to process the item.
$state['section']['format']->assign('item', $item);
if($saveContext !== null && $id == $final)
{
if(!$state['section']['format']->property('section:variableAssign'))
{
throw new Opt_AssignNotSupported_Exception($name);
}
$state['section']['format']->assign('value', $saveContext);
$code = $state['section']['format']->get('section:variableAssign');
}
else
{
$code = $state['section']['format']->get('section:variable');
}
$state['section'] = null;
continue;
}
}
// Now, the normal variables
if($state['first'])
{
if($state['access'] == Opt_Class::ACCESS_GLOBAL)
{
$format = $this->getFormat('global.'.$path, true);
}
else
{
$format = $this->getFormat($path, true);
}
if(!$format->supports('variable'))
{
throw new Opt_FormatNotSupported_Exception($format->getName(), 'variable');
}
$format->assign('access', $state['access']);
if($format->property('variable:captureAll'))
{
// With this property, the data format may capture
// the whole namespace and process it in a more complex
// way
$format->assign('items', $ns);
if($saveContext !== null)
{
if(!$format->property('variable:assign'))
{
throw new Opt_AssignNotSupported_Exception($name);
}
$format->assign('value', $saveContext);
$code = $format->get('variable:captureAssign');
}
else
{
$code = $format->get('variable:capture');
}
break;
}
else
{
// An ordinary call - the format captures only
// the first item, the others are processed
// by different "item" formats.
$format->assign('item', $item);
if($final == $id && $saveContext !== null)
{
if(!$format->property('variable:assign'))
{
throw new Opt_AssignNotSupported_Exception($name);
}
$format->assign('value', $saveContext);
$code = $format->get('variable:assign');
}
else
{
$code = $format->get('variable:main');
}
}
}
else
{
// The subitems are processed with the upper-item format
if($state['access'] == Opt_Class::ACCESS_GLOBAL)
{
$format = $this->getFormat('global.'.$previous, true);
}
else
{
$format = $this->getFormat($previous, true);
}
if(!$format->supports('item'))
{
throw new Opt_FormatNotSupported_Exception($format->getName(), 'item');
}
if($final == $id && $saveContext !== null)
{
if(!$format->property('item:assign'))
{
throw new Opt_AssignNotSupported_Exception($name);
}
$format->assign('item', $item);
$format->assign('value', $saveContext);
$code .= $format->get('item:assign');
}
else
{
$format->assign('item', $item);
$code .= $format->get('item:item');
}
}
}
if($saveContext !== null)
{
$out = explode($saveContext, $code);
if(sizeof($out) != 2)
{
return $code;
}
return array(0 => $out[0], $saveContext, $out[1]);
}
return $code;
}
} // end _compileVariable();
/**
* Compiles the call to the language variable into the PHP code.
*
* @param String $group Group name
* @param String $id Message identifier name within a group
* @param String $tu The ID of the current translation unit for handling the assign() function properly.
* @return String The output PHP code.
*/
protected function _compileLanguageVar($group, $id, $tu)
{
if(is_null($this->_tf))
{
throw new Opl_NoTranslationInterface_Exception('OPT template compiler');
}
if($tu === $this->_translationConversion)
{
$this->_translationConversion = null;
return '\''.$group.'\',\''.$id.'\'';
}
return '$this->_tf->_(\''.$group.'\',\''.$id.'\')';
} // end _compileLanguageVar();
/**
* Compiles the special $sys variable to PHP code.
*
* @param Array $ns The $sys call splitted into array.
* @return String The output PHP code.
*/
protected function _compileSys(Array $ns)
{
switch($ns[1])
{
case 'version':
return '\''.Opt_Class::VERSION.'\'';
case 'const':
return 'constant(\''.$ns[2].'\')';
default:
if(!is_null($this->isProcessor($ns[1])))
{
return $this->processor($ns[1])->processSystemVar($ns);
}
throw new Opt_SysVariableUnknown_Exception('$'.implode('.', $ns));
}
} // end _compileSys();
/**
* Compiles the string call in the expression to a suitable PHP source code.
*
* @internal
* @param String $str The "string" string (with the delimiting characters)
* @return String The output PHP code.
*/
protected function _compileString($str)
{
// TODO: Fix
// COMMENT: Fix what?
switch($str[0])
{
case '\'':
return $str;
case '`':
if(is_null($this->_tpl->backticks))
{
throw new Opt_NotSupported_Exception('backticks', 'not configured');
}
elseif(is_string($this->_tpl->backticks))
{
// A redirect to a function
return $this->_tpl->backticks.'(\''.str_replace('\'', '\\\'', stripslashes(substr($str, 1, strlen($str) - 2))).'\')';
}
elseif(is_array($this->_tpl->backticks) && is_object($this->_tpl->backticks[0]))
{
// A redirect to an object method
return '$this->_tpl->backticks[0]->'.$this->_tpl->backticks[1].'(\''.str_replace('\'', '\\\'', stripslashes(substr($str, 1, strlen($str) - 2))).'\')';
}
else
{
throw new Opt_InvalidCallback_Exception('backticks');
}
default:
return '\''.$str.'\'';
}
} // end _compileString();
/**
* Compiles the specified identifier encountered in the expression
* to the PHP code.
*
* @internal
* @param String $token The encountered token.
* @param Int $previous Previous token
* @param String $pt Used for OOP parsing to determine whether we have a static call.
* @param String $next The next token in the list
* @param Int $operatorSet The flag of allowed opcodes at this position.
* @param String &$expr The current expression (for debug purposes)
* @param Array &$current Reference to the current token information
* @param Array &$state Reference to the parser state flags.
*/
protected function _compileIdentifier($token, $previous, $pt, $next, $operatorSet, &$expr, &$current, &$state)
{
if($previous == self::OP_OBJMAN)
{
// Class constructor call
if(isset($this->_classes[$token]) && $this->_tpl->basicOOP)
{
$current['result'] = $this->_classes[$token];
$current['token'] = self::OP_CLASS;
$state['next'] = self::OP_BRACKET | self::OP_NULL;
if($next == '(')
{
$state['func'] = 1;
}
}
else
{
throw new Opt_ItemNotAllowed_Exception('Class', $token);
}
}
elseif($next == '(')
{
// Function/method call
if($previous == self::OP_CALL)
{
$current['result'] = $token;
$current['token'] = self::OP_METHOD;
$state['next'] = self::OP_BRACKET;
$state['func'] = 1;
}
elseif(isset($this->_functions[$token]))
{
$name = $this->_functions[$token];
if($name[0] == '#')
{
$pos = strpos($name, '#', 1);
if($pos === false)
{
throw new Opt_InvalidArgumentFormat_Exception($name, $token);
}
$state['rev'] = substr($name, 1, $pos - 1);
$name = substr($name, $pos+1, strlen($name));
}
$current['result'] = $name;
$current['token'] = self::OP_FUNCTION;
$state['next'] = self::OP_BRACKET;
$state['function'] = $token;
}
elseif($token == 'assign')
{
$current['result'] = '$this->_tf->assign';
$current['token'] = self::OP_FUNCTION;
$state['next'] = self::OP_BRACKET;
$state['assign_func'] = true;
$state['function'] = $token;
}
else
{
throw new Opt_ItemNotAllowed_Exception('Function', $token);
}
}
elseif($previous == self::OP_CALL)
{
// Class/object field call, check whether static or not.
$current['result'] = ($pt == '::' ? '$'.$token : $token);
$current['token'] = self::OP_FIELD;
$state['next'] = $operatorSet | self::OP_SQ_BRACKET | self::OP_CALL;
if($state['clone'] == 1)
{
$state['next'] = self::OP_SQ_BRACKET | self::OP_CALL | self::OP_NULL;
}
}
elseif($next == '::')
{
// Static class call
if(isset($this->_classes[$token]))
{
$current['result'] = $this->_classes[$token];
$current['token'] = self::OP_CLASS;
$state['next'] = self::OP_CALL;
}
else
{
throw new Opt_ItemNotAllowed_Exception('Class', $token);
}
}
else
{
// An ending string.
if(!($state['next'] & self::OP_STRING))
{
throw new Opt_Expression_Exception('OP_STRING', $token, $expr);
}
$state['next'] = self::OP_NULL;
$current['token'] = self::OP_STRING;
$current['result'] = '\''.$token.'\'';
}
} // end _compileIdentifier();
/**
* Processes the argument order change functionality for function
* parsing in expressions.
*
* @internal
* @param Array &$args Reference to a list of function arguments.
* @param String $format The new order format code.
* @param String $function The function name provided for debugging purposes.
*/
protected function _reverseArgs(&$args, $format, $function)
{
$codes = explode(',', $format);
$newArgs = array();
$i = 0;
foreach($codes as $code)
{
$data = explode(':', $code);
if(!isset($args[$i]))
{
if(!isset($data[1]))
{
throw new Opt_FunctionArgument_Exception($i, $function);
}
$newArgs[(int)$data[0]-1] = $data[1];
}
else
{
$newArgs[(int)$data[0]-1] = $args[$i];
}
$i++;
}
$args = $newArgs;
} // end _reverseArgs();
/**
* Smart special character replacement that leaves entities
* unmodified. Used by parseSpecialChars().
*
* @internal
* @param Array $text Matching string
* @return String Modified text
*/
protected function _entitize($text)
{
switch($text[0])
{
case '&': return '&';
case '>': return '>';
case '<': return '<';
case '"': return '"';
default: return $text[0];
}
} // end _entitize();
/**
* Smart entity replacement that makes use of
*
* @internal
* @param Array $text Matching string
* @return String Modified text
*/
protected function _decodeEntity($text)
{
switch($text[1])
{
case 'amp': return '&';
case 'quot': return '"';
case 'lt': return '<';
case 'gt': return '>';
case 'apos': return "'";
default:
if(isset($this->_entities[$text[1]]))
{
return $this->_entities[$text[1]];
}
if($text[1][0] == '#')
{
return html_entity_decode($text[0], ENT_COMPAT, $this->_tpl->charset);
}
elseif($this->_tpl->htmlEntities && $text[0] != ($result = html_entity_decode($text[0], ENT_COMPAT, $this->_tpl->charset)))
{
return $result;
}
throw new Opt_UnknownEntity_Exception(htmlspecialchars($text[0]));
}
} // end _entitize();
} // end Opt_Compiler_Class;