<?php

/*************************************************************************
 * HTML Tag / Template Tag Parser                                        *
 * parser.php                                                            *
 * Copyright (c) 2007, Peter Goodman                                     *
 * http://ioreader.com/code/Template/                                    *
 *                                                                       *
 * Permission is hereby granted, free of charge, to any person obtaining *
 * a copy of this software and associated documentation files (the       *
 * "Software"), to deal in the Software without restriction, including   *
 * without limitation the rights to use, copy, modify, merge, publish,   *
 * distribute, sublicense, and/or sell copies of the Software, and to    *
 * permit persons to whom the Software is furnished to do so, subject to *
 * the following conditions:                                             *
 *                                                                       *
 * The above copyright notice and this permission notice shall be        *
 * included in all copies or substantial portions of the Software.       *
 *                                                                       *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       *
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    *
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                 *
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS   *
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN    *
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN     *
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE      *
 * SOFTWARE.                                                             *
 *************************************************************************/

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 ."([^>]*)>~"$buffer, -1PREG_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
        
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], -1PREG_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 {
                        
// parse the tag and add it to the parent node's buffer
                        
$parent->addTextToBuffer($node->parseBuffer());
                    }
                }
                
                
// an 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);
                    
                    
// add the arguments array to the node
                    
$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);
                }
                
                
// increment again to skip the next iteration
                
$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();
                
                break;
            }
            
            
// extract the buffers from the malformed tags
            // and put them back into the root node later.
            
else
                
$temp_buffer .= $node->extractBuffer();
        
        
// we're done! return the final parsed and put back together buffer.
        
return $parsed_buffer;
    }
    
    
// parse variables
    
final protected function parseVariables($buffer)
    {
        
$buffer preg_replace('~{\$([^}]*)}~''<?php echo $scope->pullVar("$1"); ?>'$buffer);
        
$buffer preg_replace('~{\@([^}]*)}~''<?=$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
{
    
// the array that is the stack
    
protected $stack = array();
    
    
// keep track of what the current key of
    // the last element on the node stack is.
    // this starts at -1 because when we push
    // the first element on, it will become 0.
    
protected $key = -1;
    
    
// push an element on to the node stack
    
final public function push(ClosingNode $node)
    {
        
$this->stack[] = $node;
        
$this->key++;
    }
    
    
// pop the last element off of the node stack
    
final public function pop()
    {        
        
$ret FALSE;
        
$this->key--;
        return 
array_pop($this->stack);
    }
    
    
// return the last element of the node stack
    
final public function last()
    {
        return 
$this->stack[$this->key];
    }
}

// our base node class, all node compiling classes
// must extend this class.
abstract class ClosingNode
{
    
protected $buffer '';
    
protected $name '';
    
protected $_arguments = array(); // before
    
protected $arguments// after, default to NULL
    
protected $accepted_arguments = array();
    
    
    
public function __construct($name '')
    {
        
$this->name $name;
    }
    
    
final public function acceptArgument($name ''$type '')
    {
        
// allowed types to typecast
        
$types = array(
                    
'int','integer',
                    
'bool','boolean',
                    
'float','double','real',
                    
'string',
                    );
        
        
// fix the typecast if necessary
        
$type = !in_array($type$types) ? 'string' $type;
        
        
// accept this argument
        
$this->accepted_arguments[$name] = $type;
    }
    
    
final public function acceptArguments($args = array())
    {
        foreach(
$args as $key => $type)
            
$this->acceptArgument($key$type);
    }
    
    
// add an argument 
    
final public function addArguments($array)
    {
        
$this->_arguments array_merge($this->_arguments$array);
    }
    
    
final public function getArguments()
    {
        if(
$this->arguments === NULL)
        {
            foreach(
$this->accepted_arguments as $key => $type)
                if(isset(
$this->_arguments[$key]))
                {
                    
// fix the value if it's a bool and the passed was a string
                    // representation of a bool
                    
if($type == 'bool' || $type == 'boolean')
                    {
                        
$val strtolower($this->_arguments[$key]);
                        
$this->_arguments[$key] = ($val == 'false' || $val == 'null') ? FALSE $this->_arguments[$key];
                        
$this->_arguments[$key] = 'true' TRUE $this->_arguments[$key];
                    }
                    
                    
// typecast the arguments
                    
$this->arguments = eval('('$type .')$this->_arguments['$key .']');
                }
        }
        
        return 
$this->arguments;
    }
    
    
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();
    
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()
    {
        return 
'';
    }
    
public function parseClose()
    {    
        return 
'';
    }
}

// an unknown node, execute as-if these are normal
// functions
class UnknownNode extends ClosingNode
{
    
public function parseOpen()
    {
        return 
'';
    }
    
public function parseClose()
    {    
        return 
'';
    }
}

?>