All pastes #468375 Raw Edit

k4st

public text v1 · immutable
·published 2007-05-03 02:06 UTC
rendered paste body
<?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 -->';
	}
}

?>