<?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_Processor
{
// Attribute types
const STRING = 1;
const HARD_STRING = 2;
const NUMBER = 3;
const EXPRESSION = 4;
const ASSIGN_EXPR = 5;
const ID = 6;
const BOOL = 7;
const ID_EMP = 8; // Same as "ID", but allows empty content.
const REQUIRED = 1;
const OPTIONAL = 2;
// Class fields
/**
* The compiler object.
*
* @var Opt_Compiler_Class
*/
protected $_compiler;
/**
* The main class object.
*
* @var Opt_Class
*/
protected $_tpl;
/**
* The processor name (see getName() description for details).
* @var String
*/
protected $_name;
private $_queue = NULL;
private $_instructions = array();
private $_attributes = array();
/**
* Creates a new instruction processor for the specified compiler.
*
* @param Opt_Compiler_Class $compiler The compiler object.
*/
public function __construct(Opt_Compiler_Class $compiler)
{
$this->_compiler = $compiler;
$this->_tpl = Opl_Registry::get('opt');
$this->configure();
} // end __construct();
/**
* Called during the processor initialization. It allows to define
* the list of instructions and attributes supported by this processor.
*/
public function configure()
{
/* null */
} // end configure();
/**
* Resets the processor state after compiling the template. The default
* implementation is empty.
*/
public function reset()
{
/* null */
} // end reset();
/**
* This method is called automatically for each XML element that the
* processor has registered. It can handle many instructions tags, and
* the default implementation redirects the processing to the private
* user-created methods _processTagName for "opt:tagName".
*
* @param Opt_Xml_Node $node The node to be processed.
*/
public function processNode(Opt_Xml_Node $node)
{
$name = '_process'.ucfirst($node->getName());
$this->$name($node);
} // end processNode();
/**
* This method is called automatically for each XML element that the
* processor has registered during the postprocessing, if the instruction
* requested this by setting the "postprocess" variable to "true" in the
* node. It can handle many instructions tags, and
* the default implementation redirects the processing to the private
* user-created methods _postprocessTagName for "opt:tagName".
*
* @param Opt_Xml_Node $node The node to be postprocessed.
*/
public function postprocessNode(Opt_Xml_Node $node)
{
$name = '_postprocess'.ucfirst($node->getName());
$this->$name($node);
} // end postprocessNode();
/**
* This method is called automatically for each XML attribute that the
* processor has registered. It can handle many attributes, and the
* default implementation redirects the processing to the private
* user-created methods _processAttrName for "opt:name" attribute.
*
* @param Opt_Xml_Node $node The node that contains the attribute.
* @param Opt_Xml_Attribute $attr The attribute to be processed.
*/
public function processAttribute(Opt_Xml_Node $node, Opt_Xml_Attribute $attr)
{
$name = '_processAttr'.ucfirst($attr->getName());
$this->$name($node, $attr);
} // end processAttribute();
/**
* This method is called automatically for each XML attribute that the
* processor has registered during the postprocessing, if the instruction
* requested this by setting the "postprocess" variable to "true" in the
* attribute. It can handle many attributes, and the
* default implementation redirects the processing to the private
* user-created methods _postprocessAttrName for "opt:name" attribute.
*
* @param Opt_Xml_Node $node The node that contains the attribute.
* @param Opt_Xml_Attribute $attr The attribute to be processed.
*/
public function postprocessAttribute(Opt_Xml_Node $node, Opt_Xml_Attribute $attr)
{
$name = '_postprocessAttr'.ucfirst($attr->getName());
$this->$name($node, $attr);
} // end postprocessAttribute();
/**
* Processes the $system special variable call. OPT chooses the processor via
* the second part of the call. For example, if the processor's method getName()
* returns the name "foo", the variables "$system.foo.something" are be redirected
* to that processor. The method must return a valid PHP code that replaces the
* specified call.
*
* @param Array $opt The $system special variable already splitted into array.
* @return String The output PHP code
*/
public function processSystemVar($opt)
{
/* null */
} // end processSystemVar();
/**
* Returns the processor name. The name should be a valid identifier
* (the first character must be a letter or underline, the next ones
* may also contain numbers). The name is read from the protected
* property $_name;
*
* @final
* @internal
* @return String
*/
final public function getName()
{
return $this->_name;
} // end getName();
/**
* Returns the queue of children to be processed for the recently
* processed node/attribute.
*
* @internal
* @return SplQueue
*/
final public function getQueue()
{
$q = $this->_queue;
$this->_queue = NULL;
return $q;
} // end getQueue();
/**
* Returns the names of the XML instructions registered by this processor.
*
* @final
* @return Array
*/
final public function getInstructions()
{
return $this->_instructions;
} // end getInstructions();
/**
* Returns the names of the XML attributes registered by this processor.
*
* @final
* @internal
* @return Array
*/
final public function getAttributes()
{
return $this->_attributes;
} // end getAttributes();
/**
* Adds the children of the specified node to the queue of the currently
* parsed element. It allows them to be processed.
*
* @final
* @internal
* @param Opt_Xml_Scannable $node
*/
final protected function _process(Opt_Xml_Scannable $node)
{
if($this->_queue === null)
{
$this->_queue = new SplQueue;
}
if($node->hasChildren())
{
foreach($node as $child)
{
$this->_queue->enqueue($child);
}
}
} // end _process();
/**
* Directly enqueues the specified node in the queue of the elements
* waiting for parsing.
*
* @param Opt_Xml_Node $node The node to be enqueued.
*/
final protected function _enqueue(Opt_Xml_Node $node)
{
if($this->_queue === null)
{
$this->_queue = new SplQueue;
}
$this->_queue->enqueue($node);
} // end _enqueue();
final protected function _debugPrintQueue()
{
var_dump($this->_queue);
} // end _debugPrintQueue();
/**
* Allows to define the instructions parsed by this processor.
* It is intended to be used in configure() method.
*
* @final
* @param String|Array $list The name of a single instruction or list of instructions.
*/
final protected function _addInstructions($list)
{
if(is_array($list))
{
$this->_instructions = array_merge($this->_instructions, $list);
}
else
{
$this->_instructions[] = $list;
}
} // end _addInstructions();
/**
* Allows to define the attributes parsed by this processor.
* It is intended to be used in configure() method.
*
* @final
* @param String|Array $list The name of a single attribute or list of attributes.
*/
final protected function _addAttributes($list)
{
if(is_array($list))
{
$this->_attributes = array_merge($this->_attributes, $list);
}
else
{
$this->_attributes[] = $list;
}
} // end _addAttributes();
/**
* This helper method is the default instruction attribute handler in OPT.
* It allows to parse the list of attributes using the specified rules.
* The attribute configuration is passed as a second argument by reference,
* and OPT returns the compiled attribute values in the same way.
*
* If the attribute specification contains "__UNKNOWN__" element, the node
* may contain an undefined number of attributes. The undefined attributes
* must match to the rules in "__UNKNOWN__" element and are returned by the
* method as a separate array. For details, see the OPT user manual.
*
* @final
* @param Opt_Xml_Element $subitem The scanned XML element.
* @param Array &$config The reference to the attribute configuration
* @return Array|Null The list of undefined attributes, if "__UNKNOWN__" is set.
*/
final protected function _extractAttributes(Opt_Xml_Element $subitem, Array &$config)
{
$required = array();
$optional = array();
$unknown = null;
// Decide, what is what.
foreach($config as $name => &$data)
{
if($name == '__UNKNOWN__')
{
$unknown = &$data;
}
elseif($data[0] == self::REQUIRED)
{
$required[$name] = &$data;
}
elseif($data[0] == self::OPTIONAL)
{
$optional[$name] = &$data;
}
}
$config = array();
$return = array();
// Parse required attributes
$attrList = $subitem->getAttributes(false);
foreach($required as $name => &$data)
{
if(isset($attrList[$name]))
{
$aname = $name;
}
elseif(isset($attrList['str:'.$name]) && ($data[1] == self::EXPRESSION || $data[1] == self::ASSIGN_EXPR || $data[1] == self::STRING))
{
$data[1] = self::STRING;
$aname = 'str:'.$name;
}
elseif(isset($attrList['parse:'.$name]))
{
if($data[1] == self::STRING)
{
$data[1] = self::EXPRESSION;
}
$aname = 'parse:'.$name;
}
else
{
throw new Opt_AttributeNotDefined_Exception($name, $subitem->getXmlName());
}
$config[$name] = $this->_extractAttribute($subitem, $attrList[$aname], $data[1]);
unset($attrList[$aname]);
}
// Parse optional attributes
foreach($optional as $name => &$data)
{
if(isset($attrList[$name]))
{
$aname = $name;
}
elseif(isset($attrList['str:'.$name]) && ($data[1] == self::EXPRESSION || $data[1] == self::ASSIGN_EXPR || $data[1] == self::STRING))
{
$data[1] = self::STRING;
$aname = 'str:'.$name;
}
elseif(isset($attrList['parse:'.$name]) && ($data[1] == self::EXPRESSION || $data[1] == self::ASSIGN_EXPR || $data[1] == self::STRING))
{
if($data[1] == self::STRING)
{
$data[1] = self::EXPRESSION;
}
$aname = 'parse:'.$name;
}
else
{
// We can't use isset() because the default data might be "NULL"
if(!array_key_exists(2, $data))
{
throw new Opt_APIMissingDefaultValue_Exception($name, $subitem->getXmlName());
}
$config[$name] = $data[2];
continue;
}
$config[$name] = $this->_extractAttribute($subitem, $attrList[$aname], $data[1]);
unset($attrList[$aname]);
}
// The remaining tags must be processed using $unknown rule, however it
// must be defined.
if(!is_null($unknown))
{
// TODO: Add here namespace check!
foreach($attrList as $name => $attr)
{
$type = $unknown[1];
if(strpos($name, 'str:') === 0 && ($type == self::STRING || $type == self::EXPRESSION || $type == self::ASSIGN_EXPR))
{
$type = self::STRING;
$name = substr($name, 4, strlen($name) - 4);
}
elseif(strpos($name, 'parse:') === 0 && ($type == self::EXPRESSION || $type == self::ASSIGN_EXPR || $type == self::STRING))
{
if($type == self::STRING)
{
$type = self::EXPRESSION;
}
$name = substr($name, 6, strlen($name) - 6);
}
// Omit the special OPT namespaces...
$nameItems = explode(':', $name);
if(sizeof($nameItems) > 1)
{
if(!$this->_compiler->isNamespace($nameItems[0]))
{
$return[$name] = $this->_extractAttribute($subitem, $attr, $type);
}
}
else
{
$return[$name] = $this->_extractAttribute($subitem, $attr, $type);
}
}
}
return $return;
} // end _extractAttributes();
/**
* Tries to extract a single attribute, using the specified value type.
*
* @final
* @internal
* @param Opt_Xml_Element $item The scanned XML element.
* @param Opt_Xml_Attribute $attr The parsed attribute
* @param Int $type The requested value type.
* @return Mixed The extracted attribute value
*/
final private function _extractAttribute(Opt_Xml_Element $item, Opt_Xml_Attribute $attr, $type)
{
$value = (string)$attr;
switch($type)
{
// An identifier, but with empty values allowed.
case self::ID_EMP:
if($value == '')
{
return $value;
}
// An identifier
case self::ID:
if(!preg_match('/^[a-zA-Z0-9\_\.]+$/', $value))
{
throw new Opt_InvalidAttributeType_Exception($attr->getXmlName(), $item->getXmlName(), 'identifier');
}
return $value;
// A number
case self::NUMBER:
if(!preg_match('/^\-?([0-9]+\.?[0-9]*)|(0[xX][0-9a-fA-F]+)$/', $value))
{
throw new Opt_InvalidAttributeType_Exception($attr->getXmlName(), $item->getXmlName(), 'number');
}
return $value;
// Boolean value: "yes" or "no"
case self::BOOL:
if($value != 'yes' && $value != 'no')
{
throw new Opt_InvalidAttributeType_Exception($attr->getXmlName(), $item->getXmlName(), '"yes" or "no"');
}
return ($value == 'yes');
// A string packed into PHP expression. Can be switched to EXPRESSION.
case self::STRING:
if($attr->getNamespace() == 'parse')
{
$result = $this->_compiler->compileExpression($value, false, false);
return $result[0];
}
else
{
return '\''.$value.'\'';
}
break;
// An OPT expression. Can be switched to STRING.
case self::EXPRESSION:
if($attr->getNamespace() == 'str')
{
return '\''.$value.'\'';
}
else
{
// Do not allow the empty strings to be evaluated!
if(strlen(trim($value)) == 0)
{
throw new Opt_AttributeEmpty_Exception($attr->getXmlName(), $item->getXmlName());
}
$result = $this->_compiler->compileExpression($value, false, false);
return $result[0];
}
break;
// An OPT expression with assignment operators allowed.
case self::ASSIGN_EXPR:
if($attr->getNamespace() == 'str')
{
return '\''.$value.'\'';
}
else
{
// Do not allow the empty strings to be evaluated!
if(strlen(trim($value)) == 0)
{
throw new Opt_AttributeEmpty_Exception($attr->getXmlName(), $item->getXmlName());
}
$result = $this->_compiler->compileExpression($value, true, false);
return $result[0];
}
break;
// So-called "hard" string, simply return the tag value and do not bother, what it is.
case self::HARD_STRING:
return $value;
break;
}
} // end _extractAttribute();
} // end Opt_Compiler_Processor;