<?php
class TemplateParser
{
// the prefix on a template tag, <prefix:foo>
protected $tag_prefix;
// an associative array to handle tag names and
// their compiling class names
protected $tag_handlers = array();
// a timer
protected $start_time = 0;
protected $end_time = 0;
// construct
public function __construct()
{
// add the supported tags in
$this->addTag('for');
$this->addTag('foreach');
$this->addTag('print');
$this->addTag('import');
// set the tag prefix
$this->setPrefix('fa:');
// begin a timer
$this->start_time = microtime();
}
// set tag prefix
final public function setPrefix($prefix = 'fa:')
{
$this->tag_prefix = preg_quote($prefix);
}
// add tag handlers given a tag name
final public function addTag($tag_name)
{
// figure out the class name
$class_name = ucfirst($tag_name) .'Node';
// make sure the class exists
if(!class_exists($class_name))
throw new Exception("Tag handler class for [{$class_name}] does not exist or cannot be included.");
// add the class to the tag handlers under the
// tag name
$this->tag_handlers[$tag_name] = $class_name;
}
// get a tag handler class name given the lower
// case tag name
final public function getTagHandler($tag_name)
{
// default to the unknown node tag handler
$ret = 'UnknownNode';
// check if this tag is set
if(isset($this->tag_handlers[$tag_name]))
$ret = $this->tag_handlers[$tag_name];
return $ret;
}
// parse some text
final public function parse($buffer)
{
$buffer = $this->parseTags($buffer);
$buffer = $this->parseVariables($buffer);
$this->end_time = microtime();
return $buffer;
}
// get how long it took to parse
final public function getParseTime()
{
list($start_sec, $start_micro) = explode(" ", $this->start_time);
list($end_sec, $end_micro) = explode(" ", $this->end_time);
$start = (float)$start_sec .".". $start_micro;
$end = (float)$end_sec .".". $end_micro;
return (float)round(($end - $start), 4);
}
// parse the tags
final protected function parseTags($buffer = '')
{
// split up the buffer into tags and text
$parts = preg_split("~<(/?)". $this->tag_prefix ."([^>]*)>~i", $buffer, -1, PREG_SPLIT_DELIM_CAPTURE);
// create a node stack, this will keep track of
// non-text nodes, and push our root node onto
// it as the first node.
$stack = new NodeStack;
$stack->push(new RootNode(''));
// note: when the buffer is split up, it will
// end up in this format: (do a print_r($parts))
//
//[0] text node
//[1] / or nothing
//[2] tag and arguments
// .. repeat this process ..
//
// observations we can make:
// every 0th key will be a text node.
// the 1st key will tell us if it's a closing tag
// the 2nd key will have the extra info
// store the parsed buffer
$parsed_buffer = '';
// keep track of whether we are looping over a closing
// tag or not
$closing = FALSE;
//print_r($parts); exit;
// loop through the split up parts of the buffer and
// also increment a tracker
for($i = 0; $i < count($parts); $i++)
{
// get the last node added to the stack, the first
// iteration will always have this as the root node
$parent = $stack->last();
// the results of this will tell us what type of
// node we are on
$key = $i % 3;
// text nodes
if($key == 0)
{
// add this text to the parent nodes buffer
$parent->addTextToBuffer($parts[$i]);
// reset closing to false so that future tags
// don't get popped out for no reason.
$closing = FALSE;
}
// normal tags
else
{
// split the tag up into its arguments
$tag_parts = preg_split("~([^\W]*)(=(\"|')?([a-z0-9\s.-_]*)(\"|')?)~i", $parts[$i+1], -1, PREG_SPLIT_DELIM_CAPTURE);
// try to get the tag name
$tag_name = trim(strtolower($tag_parts[0]));
// parse out the arguments from the split up tag parts
$arguments = $this->parseTagArguments($tag_parts);
// a closing tag has been found, pop it out of
// the stack
if($parts[$i] == '/')
{
// tell the next iteration that this tag was
// a closing tag
$closing = TRUE;
// need to pop the current node off the stack
// otherwise we will end up adding text to itself
$node = $stack->pop();
// the new parent is the new last node in the stack
$parent = $stack->last();
// whoops, we have a finishing tag without a starting
// one or a malformed one!
if($node->getName() != $tag_name)
{
// reset the stack to where it was
$stack->push($node);
$parent = $stack->last();
// add in a HTML error
$parent->addTextToBuffer('<!-- BAD CLOSING TAG FOR ['. $this->tag_prefix . $tag_name .'] -->');
}
else {
$parent->addTextToBuffer($node->parseBuffer());
}
}
// and opening tag, push it onto the stack
if(!$closing)
{
// get the tag handler class name given the tag name
$class_name = $this->getTagHandler($tag_name);
// instanciate the appropriate node class
$node = new $class_name($tag_name);
$node->addArguments($arguments);
// is this a non-closing tag? Parse it without ever
// putting it on the stack
if(trim($tag_parts[count($tag_parts)-1]) == '/')
$parent->addTextToBuffer($node->parseBuffer());
// normal opening tag
else
$stack->push($node);
}
$i++;
}
}
$temp_buffer = '';
while($node = $stack->pop())
// is this the root node?
if($node instanceof RootNode)
{
// add any text back into it from malformed
// tags then parse it.
$node->addTextToBuffer($temp_buffer);
$parsed_buffer .= $node->parseBuffer();
}
// extract the buffers from the malformed tags
// and put them back into the root node later.
else
$temp_buffer .= $node->extractBuffer();
return $parsed_buffer;
}
// parse variables
final protected function parseVariables($buffer)
{
$buffer = preg_replace('~{\$([a-zA-Z_-][a-zA-Z0-9_-]*)}~', '<?php echo $scope->pullVar("$1"); ?>', $buffer);
$buffer = preg_replace('~{\@([a-zA-Z_-][a-zA-Z0-9_-]*)}~', '$scope->pullVar("$1")', $buffer);
return $buffer;
}
// parse the tag arguments from a split up array
final public function parseTagArguments($parts = array())
{
unset($parts[0]);
$parts = array_values($parts);
$arguments = array();
for($i = 0; $i < count($parts); $i++)
{
if(isset($parts[$i+3]))
$arguments[$parts[$i]] = $parts[$i+3];
$i += 5;
}
return $arguments;
}
}
// a stack to keep track of non-text nodes
class NodeStack
{
protected $stack = array();
protected $key = -1;
public function push(ClosingNode $node)
{
$this->stack[] = $node;
$this->key++;
}
public function pop()
{
$ret = FALSE;
$this->key--;
return array_pop($this->stack);
}
public function last()
{
return $this->seek($this->key);
}
public function seek($index = 0)
{
$ret = FALSE;
if(isset($this->stack[$index]))
{
$ret = $this->stack[$index];
$this->key = $index;
}
return $ret;
}
}
// our base node class, all node compiling classes
// must extend this class.
abstract class ClosingNode
{
protected $buffer = '';
protected $name = '';
protected $arguments = array();
public function __construct($name = '')
{
$this->name = $name;
}
final public function addArguments($array)
{
$this->arguments = array_merge($this->arguments, $array);
}
final public function addTextToBuffer($buffer = '')
{
$this->buffer .= $buffer;
}
final public function parseBuffer()
{
return $this->parseOpen($this->arguments) . $this->buffer . $this->parseClose();
}
final public function extractBuffer()
{
return $this->buffer;
}
final public function getName()
{
return $this->name;
}
abstract public function parseOpen($args = array());
abstract public function parseClose();
}
// non-closing tags only require parseOpen functions
abstract class NonClosingNode extends ClosingNode
{
final public function parseClose()
{
return '';
}
}
// the root node
class RootNode extends ClosingNode
{
public function parseOpen($args = array())
{
return '';
}
public function parseClose()
{
return '';
}
}
// an unknown node
class UnknownNode extends ClosingNode
{
public function parseOpen($args = array())
{
return '<!-- UNKNOWN NODE -->';
}
public function parseClose()
{
return '<!-- / UNKNOWN NODE -->';
}
}
?>