<?php
/**
* $header
* @copyright Copyright 2007, Peter Goodman
* @package valkyrie
* @subpackage Template.parser
* @author Peter Goodman
*/
define ('VK_TPL_TAGS_DIR', dirname(__FILE__) .'/tags/');
/**
* Parse a document for specific XML tags.
* @see Response
*/
class TemplateParser {
/**
* Get a tag handler class name. If the tag handler doesn't exist, then
* use the UnknownNode tag handler.
* @param string $tag_name The tag name of the tag handler to look for.
* @return string The class name of the tag handler to use.
* @see UnknownNode, ClosingNode, NonClosingNode
* @internal
*/
private function getTagHandler($prefix = '', $tag_name)
{
// default to the unknown node tag handler
$ret = 'UnknownNode';
$file = VK_TPL_TAGS_DIR .'/'. $prefix .'/'. $tag_name .'.php';
if(file_exists($file)) {
require_once $file;
$class = ucfirst($prefix) . camelize($tag_name) .'Node';
if(class_exists($class, FALSE)) {
$ret = $class;
}
}
return new $ret($prefix, $tag_name);
}
/**
* Parse the contents of a file and return the parsed text.
* @param string $file_name The absolute path to the file to parse.
* @return string The parsed file contents.
* @see TemplateParser::parseTags(), TemplateParser::parseVariables()
*/
public function parse($file_name = '')
{
$buffer = '';
if(file_exists($file_name)) {
$buffer = file_get_contents($file_name);
$buffer = $this->parseTags($buffer);
$buffer = $this->parseVariables($buffer);
}
return $buffer;
}
/**
* Parse out the template tags.
* @param string $buffer The text to parse.
* @return string The parsed text.
* @internal
*/
private function parseTags($buffer = '')
{
// split up the buffer into tags and text
$parts = preg_split("~<(/?)([a-z0-9_]+)\:([a-z0-9_]+)((?: [^>]*)?)>~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 = array();
$stack[] = new RootNode('');
// loop through the split up parts of the buffer and
// also increment a tracker
$i = -1;
while(isset($parts[++$i])) {
// get the last node added to the stack
$parent = end($stack);
// 0 for text, the 1-4 for tag info
$key = $i % 5;
// stuff before
if($key == 0) {
$parent->buffer($parts[$i]);
}
// do we have a tag?
if(isset($parts[$i+4])) {
// get the tag info
$closing = trim($parts[$i+1]) == '/';
$prefix = trim($parts[$i+2]);
$tag_name = trim($parts[$i+3]);
$attribs = trim($parts[$i+4]);
$non_closing = FALSE;
// this tag is non-closing
if($attribs != '' && $attribs{strlen($attribs)-1} == '/') {
$non_closing = TRUE;
$attribs = trim(substr($attribs, 0, -1));
}
// closing tag
if($closing) {
$tag = array_pop($stack);
$parent = end($stack);
// right closing tag
if($tag_name == $tag->getName()) {
$parent->buffer($tag->parseTag());
// wrong closing tag
} else {
$parent->buffer('<!-- BAD CLOSING TAG FOR ['. $prefix .':'. $tag_name .'] -->');
}
// opening tag
} else {
// get the tag handler
$tag = $this->getTagHandler($prefix, $tag_name);
// parse out the attributes
if($attribs != '') {
preg_match_all('~(?P<attrib>[a-z]+)="(?P<value>[^"]*)"~i', $attribs, $attributes);
$attributes = $this->parseTagAttributes($attributes);
if(!empty($attributes)) {
$tag->addAttributes($attributes);
}
}
// non-closing node, parse it right away and add it to the parent
// nodes buffer
if($non_closing) {
$parent->buffer($tag->parseTag());
// opening tag of a closing node, add it to the stack
} else {
$stack[] = $tag;
}
}
}
$i+= 4;
}
// parse out any tags that somehow weren't properly parsed. If there were
// any tags that weren't properly closed, this will catch them.
$parsed_buffer = '';
$temp_buffer = '';
while($node = array_pop($stack)) {
// is this the root node?
if($node instanceof RootNode) {
// add any text back into it from malformed
// tags then parse it.
$node->buffer($temp_buffer);
// parse the root node
$parsed_buffer = $node->parseTag();
}
// extract the buffers from the malformed tags
// to put them back into the root node later.
else {
$temp_buffer .= $node->buffer();
}
}
return $parsed_buffer;
}
/**
* Parse the tag attributes.
* @param array $parts The tag attributes.
* @return array An associative array of attributes => value
* @internal
*/
private function parseTagAttributes(array $parts = array())
{
$attributes = array();
foreach($parts['attrib'] as $i => $attrib) {
$attributes[strtolower($attrib)] = $parts['value'][$i];
}
return $attributes;
}
/**
* Handle matched template variables.
* @param array $matches An array returned from a preg_replace_callback
* @return string A compiled version of the template variable.
* @see TemplateParser::parseVariables, preg_replace_callback
* @internal
*/
private function handleVariable($matches) {
$matches[2] = trim($matches[2]);
$buffer = '';
// template variable
if($matches[1] == '$') {
if($matches[2] != '') {
$parts = explode('|', $matches[2]);
$varname = array_shift($parts);
$buffer = '$scope["'. $varname .'"]';
foreach($parts as $callback) {
// call a callback if it exists
if($callback !== NULL) {
$callback_args = explode(":", $callback);
$callback = array_shift($callback_args);
if(function_exists($callback)) {
$callback_args = !empty($callback_args) ? ','. implode(",", $callback_args) : '';
$buffer = $callback .'('. $buffer . $callback_args .')';
}
}
}
if($matches[1] == '$') {
$buffer = '<?='. $buffer .';?>';
}
}
}
// url / path variable
else if($matches[1] == '/') {
if($matches[0] == '{/}') {
$matches[2] = '/';
}
$buffer = vk_url(vk_path($matches[2]));
// need to put $ in subpattern so that handleVariable accepts it properly :P
$buffer = preg_replace_callback("~(\\$)([^/]+)~", array($this, 'handleVariable'), $buffer);
}
// get text
else {
$buffer = '';
}
return $buffer;
}
/**
* Find and parse variables in the text.
* @param string $buffer The text to parse.
* @return string The text with all matched variables compiled.
* @see TemplateParser::parseVariables, preg_replace_callback
* @internal
*/
private function parseVariables($buffer)
{
$buffer = preg_replace_callback('~{(\$|\@|\/)([^}]*)}~', array($this, 'handleVariable'), $buffer);
return $buffer;
}
}
/**
* Exception for template nodes, gets called for missing attributes.
*/
class TemplateNodeException extends Exception {
}
/**
* A template node, this is any xml tag that uses a namespace,
* for example: <tpl:something>
*/
abstract class TemplateNode implements ArrayAccess {
/**
* The text in-between the opening and closing nodes od this tag.
* @internal
*/
private $buffer = '';
/**
* This tags name.
* @internal
*/
protected $name = '',
$prefix = '';
/**
* An associative array of this tags attributes.
* @internal
*/
private $attributes = array();
/**
* Constructor, pass in this tags name.
* @param string $name The tag name.
*/
public function __construct($prefix = '', $name = '') {
$this->prefix = $prefix;
$this->name = $name;
}
/**
* Add the tags attributes to the attributes array.
* @param array $array An associative array of attribute=>value.
*/
public function addAttributes(array $array = array()) {
$this->attributes = array_merge($this->attributes, $array);
}
/**
* Add text to this tags buffer.
* @param string $buffer The text to append to the buffer.
* @see ClosingNode::$buffer
* @internal
*/
public function buffer($buffer = FALSE) {
if($buffer !== FALSE) {
$this->buffer .= $buffer;
} else {
return $this->buffer;
}
}
/**
* Get the name of this tag.
* @return string The tag name.
*/
public function getName() {
return $this->name;
}
/**
* Require that this tag be passed certain arguments.
* <code>
* $this->requireAttributes('href', 'title', ...);
* </code>
*/
public function requireAttributes() {
foreach(func_get_args() as $attrib) {
if(!array_key_exists($attrib, $this->attributes)) {
throw new TemplateNodeException("Missing attribute [$attrib] for tag [{$this->name}].");
}
}
}
/**
* Require at least one out of a number of attributes to exist.
* <code>
* $this->requireOne('href', 'src');
* </code>
* @return boolean The first present attribute from the set found.
*/
public function requireOne() {
$ret = FALSE;
$attribs = func_get_args();
foreach($attribs as $attrib) {
if(array_key_exists($attrib, $this->attributes)) {
$ret = $attrib;
break;
}
}
if($ret === FALSE) {
throw new TemplateNodeException("Missing at least one required attribute of set [". implode(',', $attribs) ."] for tag [{$this->name}].");
}
return $ret;
}
/**
* Get an attribute.
* @param string $key The attribute name.
* @return mixed The value of the attribute.
* @see ArrayAccess
*/
public function offsetGet($key) {
return $this->offsetExists($key) ? $this->attributes[$key] : NULL;
}
/**
* Set an attribute.
* @param string $key The attribute name.
* @param mixed $val The value of the attribute to set.
* @see ArrayAccess
*/
public function offsetSet($key, $val) {
$this->attributes[$key] = $val;
}
/**
* Check if an attribute exists.
* @param string $key The attribute name.
* @return boolean Whether or not the attribute exists.
* @see ArrayAccess
*/
public function offsetExists($key) {
return isset($this->attributes[$key]);
}
/**
* Unset an attribute.
* @param string $key The attribute name.
* @see ArrayAccess
*/
public function offsetUnset($key) {
unset($this->attributes[$key]);
}
/**
* Parse the tag.
*/
abstract public function parseTag();
}
/**
* Class to handle the root node in the template parser stack.
*/
class RootNode extends TemplateNode {
public function parseTag() {
return $this->buffer();
}
}
/**
* Class to handle nodes that the parser doesn't recognize.
*/
class UnknownNode extends TemplateNode {
public function parseTag() {
$buffer = "<". $this->prefix .":". $this->name .">\n";
$buffer .= $this->buffer();
$buffer .= "\n</". $this->prefix .':'. $this->name .'>';
return $buffer;
}
}