<?php
/*======================================================================*\
|| #################################################################### ||
|| # vBulletin 4.0.5
|| # ---------------------------------------------------------------- # ||
|| # Copyright ©2000-2010 vBulletin Solutions Inc. All Rights Reserved. ||
|| # This file may not be redistributed in whole or significant part. # ||
|| # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html # ||
|| #################################################################### ||
\*======================================================================*/
if (!isset($GLOBALS['vbulletin']->db))
{
exit;
}
require_once(DIR . '/includes/class_xml_dom.php');
require_once(DIR . '/includes/class_generator.php');
require_once(DIR . '/includes/template_tag_defs.php');
require_once(DIR . '/includes/template_curly_defs.php');
require_once(DIR . '/vb/exception.php');
require_once(DIR . '/vb/exception/parser.php');
/**
* A derivative of an HTML/XML tokenizing parser, used to parse special tags.
*
* Parses special tags embedded in an HTML document. This differs from a standard
* HTML parser in that the special tags can be embedded within valid HTML tags.
* Eg: <a <if condition="$something">href=""</if>>
* Only the named tags are treated as tags; all other data is treated as text.
*
* Uses state based parsing to move through the string, maintaining a pointer
* to the current position in the string.
*/
class vB_SpecialtyTagParser
{
// namespace and names of tags to limit the search to
private $namespace = '';
private $tag_list = array();
// The compiled tags to search for and the positions in the string they're at
private $tag_search = array();
private $locations = array();
// data to search and strlen of data
private $data = '';
private $data_len = 0;
// current position in the string
private $position = 0;
// output tokens
private $tokens = array();
//errors
private $errors = array();
/**
* List of states. Key is state name, value is the name of the method to call.
*
* @var array
*/
private $states = array(
'start' => 'state_start_all',
'literal' => 'state_literal',
'tag' => 'state_tag',
'end' => 'state_end',
);
private $state_tag = 'state_tag';
/**
* Constructor. Sets up the class and creates the tag search array as an optimization.
*
* @param string An optional namespace to search in. If no tag list is specified, all tags in the namespace will be found.
* @param array An optional list of valid tags.
*/
public function __construct($namespace = 'vb', $tag_list = array())
{
$this->namespace = $namespace;
$this->tag_list = $tag_list;
if ($namespace OR $tag_list)
{
if ($tag_list)
{
foreach ($tag_list AS $tag)
{
$tag = strtolower(($namespace ? "$namespace:" : '') . $tag);
$this->tag_search[] = $tag;
$this->tag_search[] = "/$tag";
}
}
else
{
$this->tag_search[] = strtolower($namespace) . ':';
$this->tag_search[] = '/' . strtolower($namespace) . ':';
}
//$this->states['start'] = 'state_start_special';
}
}
/**
* Returns any parse errors.
*/
public function get_errors()
{
return $this->errors;
}
/**
* Returns then tokens associated with the parser
*
* @return array
*/
public function get_tokens()
{
return $this->tokens;
}
/**
* Returns true if the parser is at the end of the string to parse.
*
* @return boolean
*/
private function is_end()
{
return ($this->position >= $this->data_len);
}
/**
* Reads the current character from the string.
*
* @return string
*/
private function read_current()
{
return $this->data[$this->position];
}
/**
* Returns the next character from the string. Moves the pointer forward.
*
* @return string
*/
private function read_next()
{
++$this->position;
return $this->data[$this->position];
}
/**
* Returns the previous character from the string. Leaves pointer intact.
*
* @return string
*/
private function read_previous()
{
if ($this->position)
{
return $this->data[$this->position-1];
}
return null;
}
private function read_rest()
{
return substr($this->data, $this->position);
}
/**
* Peeks at the next character in the string. Does not move the pointer.
*
* @return string
*/
private function peek()
{
return $this->data[$this->position + 1];
}
/**
* Moves the pointer forward a character.
*
* @return void
*/
private function step_forward()
{
++$this->position;
}
/**
* Moves the pointer back a character.
*
* @return void
*/
private function step_backwards()
{
--$this->position;
}
/**
* Reads until a character from the list is found.
*
* @param string A list of characters to stop when found. Each byte is treated as a character.
*
* @return string
*/
private function read_until_character($character_list)
{
$read_until = $this->data_len;
$strlen = strlen($character_list);
for ($i = 0; $i < $strlen; ++$i)
{
// step through each character in the list and find the first occurance
// after the current position
$char_pos = strpos($this->data, $character_list[$i], $this->position);
// if that occurred earlier than the previous first occurance, only read until there
if ($char_pos !== false AND $char_pos < $read_until)
{
$read_until = $char_pos;
}
}
$text = strval(substr($this->data, $this->position, $read_until - $this->position));
$this->position = $read_until;
return $text;
}
/**
* Reads until the exact string is found.
*
* @param string When this string is encountered, reading is stopped.
*
* @return string
*/
private function read_until_string($string, &$not_found = false)
{
$string_pos = strpos($this->data, $string, $this->position);
if ($string_pos === false)
{
$not_found = true;
$string_pos = $this->data_len;
}
$text = substr($this->data, $this->position, $string_pos - $this->position);
$this->position = $string_pos;
return $text;
}
/**
* Reads until the current character is *not* found in the list.
*
* @param string A list of characters to read while matched.
*
* @return string
*/
private function read_while_character($character_list)
{
$length = strspn(substr($this->data, $this->position), $character_list);
$text = substr($this->data, $this->position, $this->position + $length);
$this->position += $length;
return $text;
}
/**
* Reads until the end of the string.
*
* @return string
*/
private function read_until_end()
{
$text = substr($this->data, $this->position);
$this->position = $this->data_len;
return $text;
}
/**
* Skips past any whitespace (spaces, carriage returns, new lines, tabs).
*/
private function skip_whitespace()
{
$this->read_while_character(" \r\n\t");
}
/**
* Adds to the list of output tokens. Folds contiguous cdata tokens automatically.
*
* @param string Type of token. Usually 'cdata' or 'tag', though not explicitly limited.
* @param mixed The data of the token. String for 'cdata', an array for 'tag'
*/
private function add_token($type, $data)
{
if ($type == 'cdata' AND is_array($this->tokens) AND $last = end($this->tokens) AND $last['type'] == 'cdata')
{
$key = key($this->tokens);
$this->tokens["$key"]['data'] .= $data;
}
else
{
$this->tokens[] = array('type' => $type, 'data' => $data);
}
}
/**
* Parses the data into tokens.
*
* @param string The data to parse
*
* @return array The tokenized data
*/
public function parse($data)
{
$this->data = $data;
$this->data_len = strlen($data);
$this->position = 0;
$this->tokens = array();
$lower_data = strtolower($data);
// optimization -- if we know what tags to search for, let's find them using strpos.
// note that this may find false positives.
$this->locations = array();
if ($this->tag_search)
{
foreach ($this->tag_search AS $tag_search)
{
// entries in this array are "tag" and "/tag", so we just need the <
$curpos = 0;
do
{
$pos = strpos($lower_data, '<' . $tag_search, $curpos);
if ($pos !== false)
{
$this->locations[] = $pos;
}
// +1 accounts for the <
$curpos = $pos + strlen($tag_search) + 1;
}
while ($pos !== false);
}
}
sort($this->locations, SORT_NUMERIC);
reset($this->locations);
// Work through the states. The functions themselves move the pointer around.
// When false is returned, we're done.
$state = $this->states['start'];
do
{
$state = $this->{$state}();
}
while ($state);
return $this->tokens;
}
/**
* Special case of the start state. Used when we have done a tag search optimization.
*
* @return string Next state to move to
*/
private function state_start_special()
{
// find the first tag location
if ($this->position == 0)
{
$location = current($this->locations);
}
else
{
$location = next($this->locations);
}
if ($location === false)
{
// no more locations, so the rest is text
if (($text = $this->read_until_end()) !== '')
{
$this->add_token('cdata', $text);
}
return $this->states['end'];
}
else if ($location >= $this->position)
{
// next location is later in the string, so find the text up until that point
$text = substr($this->data, $this->position, ($location - $this->position));
if ($text !== '')
{
$this->add_token('cdata', $text);
}
// ...move to the tag and start to parse it
$this->position = $location + 1;
return $this->states['tag'];
}
else if ($location < $this->position)
{
// this location is before we were are, so just ignore it
// and move to the next one
return $this->states['start'];
}
}
/**
* Default start state function. Used if we haven't done tag search processing.
* This assumes no knowledge of the string.
*
* @return string Next state to move to
*/
private function state_start_all()
{
// find what would be the start of a tag (a '<' or '{')
// if we are in a literal tag, make sure we dont use '{' as a delimiter
$read_until_delim = ($this->states['tag'] == $this->states['literal']?'<':'<{');
$text = $this->read_until_character($read_until_delim);
if ($text !== '')
{
// ... anything we found is just text
$this->add_token('cdata', $text);
}
// it's possible that we hit the end of the string instead of a < or {, so check for that
if ($this->is_end())
{
return $this->states['end'];
}
// else, the next char is < or {
if ($this->read_current() === '<')
{
$this->step_forward();
}
while ($this->read_current() == '<')
{
// we're seeing <<, so the first should be cdata
$this->add_token('cdata', '<');
$this->step_forward();
}
return $this->states['tag'];
}
private function state_literal()
{
$tag_start = $this->position - 1;
// There was a < but it was eaten already in state_start_all()
$this->step_backwards();
$search = '</' . (!empty($this->namespace) ? $this->namespace . ':' : '') . 'literal>';
$text = $this->read_until_string($search);
$this->add_token('cdata', $text);
$this->parse_close_tag('literal', (!empty($this->namespace) ? $this->namespace : ''), $tag_start);
$this->states['tag'] = $this->state_tag;
return $this->states['start'];
}
/**
* End state. Does nothing
*
* @return string Empty string (stop processing)
*/
private function state_end()
{
return '';
}
/**
* State to process a tag from the start.
*
* @return string State to move to
*/
private function state_tag()
{
$tag_start = $this->position - 1;
$previous_character = $this->read_current();
if ($previous_character === '{')
{ // we can't walk forward in the start state since for curlys we need to know what we're dealing with
$this->parse_curlys($tag_start);
}
else
{
$name = $this->read_until_character(">< \r\n\t\${");
if ($name === '')
{
// the tag either began with whitespace, was <>, or the < was the last character in the string
$this->add_token('cdata', '<');
return $this->states['start'];
}
// match: is closing, namespace, tagname
if (preg_match('#^(/?)(([a-z0-9_-]+):)?([^/:]+)$#siU', $name, $match))
{
$skip_parse = false;
$namespace = strtolower($match[3]);
$tag_name = strtolower($match[4]);
if ($this->namespace)
{
// if namespace, only parse if the namespace is correct
$do_parse = ($namespace == strtolower($this->namespace));
}
else
{
// no namespace, so include the namespace in the tag name
$tag_name = ($namespace ? ($namespace . ':') : '') . $tag_name;
$namespace = '';
$do_parse = true;
}
if ($do_parse)
{
// always do this check if we need to parse: make sure the tag name is in the tag list
$do_parse = (empty($this->tag_list) OR in_array($tag_name, $this->tag_list));
}
if (!$skip_parse AND !$do_parse)
{
// not a tag we're parsing, just text
$this->add_token('cdata', '<' . $name);
return $this->states['start'];
}
if ($match[1]) // if we start with a /
{
$this->parse_close_tag($tag_name, $namespace, $tag_start);
}
else
{
$this->parse_tag($tag_name, $namespace, $tag_start);
}
if ($tag_name == 'literal')
{
$last = end($this->tokens);
if ($last['type'] == 'tag' AND $last['data']['tag_name'] == 'literal' AND $last['data']['type'] == 'open')
{
$this->state_tag = $this->states['tag'];
$this->states['tag'] = $this->states['literal'];
}
}
return $this->states['start'];
}
else
{
// didn't match, so this tag name isn't valid anyway
$this->add_token('cdata', '<' . $name);
}
}
return $this->states['start'];
}
/**
* Parse the attributes of a tag name.
*
* @param string Name of the tag
* @param string Namespace the tag is in (if there is one)
* @param integer Position of the tag's start
*/
private function parse_tag($tag_name, $tag_namespace, $tag_start)
{
// ignore any whitespace after the tag start
$this->skip_whitespace();
$attributes = array();
$in_tag = true;
$self_close = false;
do
{
// attribute name is anything but a delimiter, end of the tag, or whitespace
$attr_name = $this->read_until_character("=/> \r\n\t");
$attr_value = false;
switch ($this->read_current())
{
// if we're at a >, we're at the end of a tag
case '>':
$this->step_forward();
$in_tag = false;
if (empty($attributes) AND substr($tag_name, -1) == '/')
{
// no attributes and the last character of the tag name is a /
// -- the tag is in <tag/> form, which is self-closing
$tag_name = substr($tag_name, 0, -1);
$self_close = true;
}
break;
// if we're at a /, then we're probably in a self-closing tag
// (if the next character is a >)
case '/':
$this->skip_whitespace();
if ($this->read_next() == '>')
{
$this->step_forward();
$in_tag = false;
$self_close = true;
}
break;
// found whitespace, let's move through the rest and hope we hit =.
// if we do, treat it as "x=y". Otherwise, there's not value to the attribute
case ' ':
case "\r":
case "\n":
case "\t":
$this->skip_whitespace();
if ($this->read_current() != '=')
{
// no attribute value
break;
}
// else fall through
// oh, found an equals -- this is the standard attribute form
case '=':
$delimiter = $this->read_next();
if ($delimiter == '"' OR $delimiter == "'")
{
// delimited by " or ', read until that character comes again
$this->step_forward();
$attr_value = $this->read_until_character($delimiter);
$this->step_forward();
}
else
{
// no delimiter -- read until the end of the tag or whitespace
$attr_value = $this->read_until_character("> \r\n\t");
}
$this->skip_whitespace();
}
if (!empty($attr_name))
{
$attributes["$attr_name"] = $attr_value;
}
}
while ($in_tag == true AND !$this->is_end());
if ($in_tag == true)
{
// the tag was never closed, so consider it to be text only
$this->position = $tag_start;
$text = $this->read_until_end();
$this->add_token('cdata', $text);
}
else
{
// finished the tag, so add it to the tokens
$message = array(
'tag_name' => $tag_name,
'namespace' => $tag_namespace,
'type' => $self_close ? 'open_close' : 'open',
'attributes' => $attributes
);
$this->add_token('tag', $message);
}
}
/**
* Parses the close of a tag.
*
* @param string Tag name
* @param string Tag namespace
* @param integer Position of the tag start
*/
private function parse_close_tag($tag_name, $tag_namespace, $tag_start)
{
$this->read_until_character('>');
if ($this->read_current() != '>')
{
// this means we're at the end of the string
$this->position = $tag_start;
$text = $this->read_until_end();
$this->add_token('cdata', $text);
}
else
{
$this->step_forward();
$message = array(
'tag_name' => $tag_name,
'namespace' => $tag_namespace,
'type' => 'close'
);
$this->add_token('tag', $message);
}
}
/**
* Parse the attributes of a tag name.
*
* @param integer Position of the tag's start
*/
private function parse_curlys($tag_start)
{
$position = $this->position;
$data = $this->read_until_end();
$this->position = $position;
$scanner = new vbTemplateLexer($data);
$parser = new vB_Template_Parser($scanner);
try
{
$result = $parser->run();
if (!empty($result[0]))
{
if ($result[0]['namespace'] == $this->namespace)
{
$this->position += $scanner->getCurrentPosition();
// if we aren't at the end then getCurrentPosition is the start of the next token, so step back.
if (!$this->is_end() OR $this->read_previous() !== '}') {
$this->position -= strlen($scanner->value);
}
$this->add_token('curly', $result[0]);
return;
}
}
}
catch (vB_Exception_Parser $e)
{
$this->errors[] = $e->getMessage();
}
catch (Exception $e)
{
// ksours 2009-06-25 -- parser error on a curly. If a parse error occurs the curly text will be passed
// uniterpreted to the output. However, a number of legitimate constructs -- for example css definitions --
// will be interpreted as curlys and potentially fail to parse in ways that end up here. Flagging those
// as errors is bad.
// potentially print an error here once a different exception is thrown for syntax errors
}
$this->step_forward();
$this->add_token('cdata', '{');
}
/**
* Fetchs a DOM-class compatible version of the tokens.
* Note that this only works if the token list is valid XML!
* It will error otherwise!
*
* @return array DOM-compatible output
*/
public function fetch_dom_compatible()
{
$node_list = array(
0 => array('type' => 'start', 'children' => array())
);
$cur_nodeid = 0;
foreach ($this->tokens AS $token)
{
$data = $token['data'];
if ($token['type'] == 'tag')
{
switch ($data['type'])
{
case 'open':
// opening a tag -- add it, and make the current parent become the tag
$cur_nodeid = $this->_add_dom_node($node_list, 'tag', $cur_nodeid, strtolower($data['tag_name']), $data['attributes'], $data['namespace']);
break;
case 'close':
// closing a tag -- move up a level
$cur_nodeid = $node_list[$cur_nodeid]['parent'];
break;
case 'open_close':
// same as an open and a close -- add the tag, but don't move the current node
$this->_add_dom_node($node_list, 'tag', $cur_nodeid, strtolower($data['tag_name']), $data['attributes'], $data['namespace']);
break;
}
}
else if ($token['type'] == 'curly')
{
$this->_add_dom_node($node_list, 'curly', $cur_nodeid, strtolower($data['tag_name']), $data['attributes'], '');
}
else if ($token['type'] == 'cdata')
{
if (isset($node_list[$cur_nodeid]['children']))
{
$last_child = end($node_list[$cur_nodeid]['children']);
}
else
{
$last_child = false;
}
if ($last_child !== false AND $node_list["$last_child"]['type'] == 'text')
{
// the previous thing we ran into on this tag was text, so fold into that
$node_list["$last_child"]['value'] .= $data;
}
else
{
$this->_add_dom_node($node_list, 'text', $cur_nodeid, $data);
}
}
}
return $node_list;
}
/**
* Add an node to the DOM node list
*
* @param array DOM node list
* @param string Type of node to add
* @param integer Unique ID for the parent
* @param string Value of the node (tag name, text, etc)
* @param array Array of attributes (keyed on name)
* @param string Namespace of the tag
*
* @return integer ID of the added node
*/
private function _add_dom_node(&$node_list, $type, $parent_node, $value, $attributes = array(), $namespace = '')
{
if (!isset($node_list["$parent_node"]))
{
echo "adding child to non-existent node!";
exit;
}
$node_list[] = array(
'type' => $type,
'value' => $value,
'parent' => $parent_node
);
end($node_list);
$nodeid = key($node_list);
if (!empty($namespace))
{
$node_list["$nodeid"]['namespace'] = $namespace;
}
if (!empty($attributes))
{
$node_list["$nodeid"]['attributes'] = $attributes;
}
if (!isset($node_list["$parent_node"]['children']))
{
$node_list["$parent_node"]['children'] = array();
}
$node_list["$parent_node"]['children'][] = $nodeid;
return $nodeid;
}
}
// ##########################################################################
/**
* Extendable class to parse templates to be evaled. Supports proprietary tags
* through a modular class system. Uses DOM-parsing.
*/
class vB_TemplateParser
{
private $tag_parser = null;
private $output = null;
public $dom_doc = null;
private $dom_nodes = null;
public $text = '';
public $outputVar = '$final_rendered';
/**
* Constructor.
*
* @param string Text to parse
*/
public function __construct($text)
{
//An egregious hack. The parser doesn't work right in some cases where
//the last character is a "}" (There is a problem with the curly tags
//where the character after gets trimmed. We can account for every case
//but the one where the last character is also a }). Since this problem
//doesn't occur if the last character is a space and the resulting compiled
//template will end in an "extra" space we can add it here and remove it
//when we are done.
$this->text = $text . " ";
$this->output = new vB_PHP_Generator();
$this->tag_parser = new vB_SpecialtyTagParser('vb');
$this->tag_parser->parse($this->text);
}
/**
* Validate the name of a variable to ensure its valid PHP
*
* @param string Name of the variable
*
* @return boolean True if valid, else false
*/
public static function validPHPVariable($name)
{
if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*/', $name))
{
return true;
}
return false;
}
/**
* Validate the text for invalid tag nesting and usage (eg, an elseif after an else).
*
* @param array An array of errors. An extra return value!
*
* @return boolean True on success, false on failure
*/
public function validate(&$errors)
{
$parse_errors = $this->tag_parser->get_errors();
if (count($parse_errors))
{
$errors = $parse_errors;
return false;
}
$errors = array();
$stack = array();
foreach ($this->tag_parser->get_tokens() AS $token)
{
if ($token['type'] == 'tag')
{
$data = $token['data'];
if ($data['type'] == 'open')
{
// push this take onto the top of the stack
array_push($stack, $data['tag_name']);
}
else if ($data['type'] == 'close')
{
$last_item = end($stack);
if ($last_item != $data['tag_name'])
{
// top of the stack isn't this tag, so we didn't close the proper tag
$errors[] = 'invalid_tag_nesting';
}
else
{
// closed the proper tag on the stack
array_pop($stack);
}
}
// open_close doesn't need to be handled
}
}
if (count($stack) > 0)
{
// forgot to close a tag
$errors[] = 'unclosed_tag';
}
// no errors, so let's do DOM-level validation
if (!$errors)
{
$this->dom_nodes = $this->fetch_dom_compatible();
$this->dom_doc = new vB_DomDocument($this->dom_nodes);
foreach (array_keys($this->dom_nodes) AS $key)
{
switch ($this->dom_nodes["$key"]['type'])
{
case 'tag':
{
$node = new vB_DomNode($this->dom_nodes["$key"], $key, $this->dom_doc);
$node_class = 'vB_TemplateParser_Tag' . preg_replace('#[^a-z0-9_]#i', '', $node->value);
if (class_exists($node_class, false))
{
// validate the tag's usage (eg, the attributes, how it relates to siblings, etc)
$dom_errors = call_user_func_array(
array($node_class, 'validate'),
array($node, $this)
);
if ($dom_errors)
{
$errors = array_merge($errors, $dom_errors);
}
}
else
{
echo "No such class: $node_class<br />";
}
}
break;
case 'curly':
$node = new vB_CurlyNode($this->dom_nodes["$key"], $this->dom_doc);
$this->validate_curly($node, $errors);
/*
{
$node = new vB_CurlyNode($this->dom_nodes["$key"], $this->dom_doc);
$node_class = 'vB_TemplateParser_Curly' . preg_replace('#[^a-z0-9_]#i', '', $node->value);
if (class_exists($node_class, false))
{
// validate the curly usage (eg, the attributes, how it relates to siblings, etc)
$dom_errors = call_user_func_array(
array($node_class, 'validate'),
array($node, $this)
);
if ($dom_errors)
{
$errors = array_merge($errors, $dom_errors);
}
}
else
{
throw new Exception('Unable to find a class to validate: ' . $node_class);
}
}
*/
break;
}
}
}
return (count($errors) == 0);
}
/*
Seperate this out so we can make it recursive.
*/
protected function validate_curly($node, &$errors)
{
$node_class = 'vB_TemplateParser_Curly' . preg_replace('#[^a-z0-9_]#i', '', $node->value);
if (class_exists($node_class, false))
{
// validate the curly usage (eg, the attributes, how it relates to siblings, etc)
$dom_errors = call_user_func_array(
array($node_class, 'validate'),
array($node, $this)
);
if ($dom_errors)
{
$errors = array_merge($errors, $dom_errors);
}
//handle validation of nested curlies.
if (is_array($node->attributes))
{
foreach($node->attributes as $attribute)
{
if($attribute instanceof vB_CurlyNode)
{
$this->validate_curly($attribute, $errors);
}
}
}
}
else
{
throw new Exception('Unable to find a class to validate: ' . $node_class);
}
}
/**
* Compile the template into a directly eval()'able format.
*
* @return string Evalable version of the template
*/
public function compile()
{
if (!$this->dom_doc)
{
$this->dom_nodes = $this->fetch_dom_compatible();
$this->dom_doc = new vB_DomDocument($this->dom_nodes);
}
//the other half of the hack started in the constructor. Strip the bogus
//space that we added above.
$output = $this->_parse_nodes($this->dom_doc->childNodes());
if ($output[-1] == ' ')
{
$output = substr($output, 0, -1);
}
return $this->outputVar . ' = \'' . $output . "';";
}
/**
* Parse an array of nodes. Append the output to a variable in order of occurrance.
*
* @param array An array of DOMNodes
*
* @return string Outputtable string
*/
public function _parse_nodes($nodes)
{
$output = '';
$keys = array_keys($nodes);
foreach ($keys AS $key)
{
$node =& $nodes["$key"];
$output .= $this->_default_node_handler($node);
}
return $output;
}
/**
* Default way to handle a node. If it's text, use that class. If it's a tag,
* look for a specific tag handler and use it if possible;
* if not, ignore it and parse the children.
*
* @param object DOMNode object
*
* @return string Outputtable string
*/
public function _default_node_handler(&$node)
{
if ($node->type == 'text')
{
return vB_TemplateParser_Text::compile($node, $this);
}
else if ($node->type == 'tag')
{
$node_class = 'vB_TemplateParser_Tag' . preg_replace('#[^a-z0-9_]#i', '', $node->value);
if (class_exists($node_class, false))
{
return "' . " . call_user_func_array(
array($node_class, 'compile'),
array($node, $this)
) . " . '";
}
else
{
return $this->_parse_nodes($node->childNodes());
}
}
else if ($node->type == 'curly')
{
$node_class = 'vB_TemplateParser_Curly' . preg_replace('#[^a-z0-9_]#i', '', $node->value);
if (class_exists($node_class, false))
{
return "' . " . call_user_func_array(
array($node_class, 'compile'),
array($node, $this)
) . " . '";
}
else
{
return $this->_parse_nodes($node->childNodes());
}
}
}
/**
* Default way to escape a string. Suitable for evaling.
*
* @param string String to escape
*
* @return string Escaped string
*/
public function _escape_string($string)
{
return str_replace('\"', '"', addslashes($string));
}
/**
* Fetch the DOM-compatible version of the tokenized version of this template.
* Note that the tokens must be well-formed (proper nesting, all closed).
* Make sure it passes validate() first!
*
* @return array Array of nodes in a DOM-compatible format
*/
public function fetch_dom_compatible()
{
// note: this function assumes XML-valid nesting! Make sure it passes validate() first.
return $this->tag_parser->fetch_dom_compatible();
}
}
// ##########################################################################
/**
* Text handler for the vB_TemplateParser. Should be called statically.
*/
class vB_TemplateParser_Text
{
/**
* Compiles a text node in the vB_TemplateParser format (evalable).
*
* @param object DOM Node of type text
* @param object vB_TemplateParser object
*
* @return string Evalable text
*/
public static function compile(vB_DomNode $main_node, vB_TemplateParser $parser)
{
return $parser->_escape_string($main_node->value);
}
}
require_once(DIR . '/includes/vb_template.php');
class vB_Template_Parser
{
private $scanner;
public function __construct(vbTemplateLexer $scanner)
{
$this->scanner = $scanner;
}
public function dumpTokens()
{
while ($this->scanner->yylex() !== false)
{
var_dump($this->scanner->token);
}
}
public function run($parse_all = false)
{
$statements = array();
// left hand side is required for when we recurse
while ($this->scanner->token == vbTemplateParser::CURLY_START OR $this->scanner->yylex() !== false)
{
if ($statement = $this->statement())
{
$statements[] = $statement;
if (!$parse_all) {
break;
}
}
}
return $statements;
}
private function statement()
{
if ($this->scanner->token == vbTemplateParser::CURLY_START AND $this->scanner->yylex())
{
$result = $this->expression();
if ($this->scanner->token == vbTemplateParser::CURLY_END)
{
$this->scanner->yylex();
return $result;
}
}
return false;
}
private function expression()
{
if ($this->scanner->token === vbTemplateParser::SIMPLE_VAR)
{ // hack because we allow someone not to specify {var }
$token = array(
'tag_name' => 'var',
'type' => 'curly',
);
}
else
{
$token = array(
'tag_name' => $this->scanner->currentValue,
'type' => 'curly',
);
$this->scanner->yylex();
if ($this->scanner->token === vbTemplateParser::TOKEN AND $this->scanner->currentValue == ':')
{
$this->scanner->yylex();
$token['namespace'] = $token['tag_name'];
$token['tag_name'] = $this->scanner->currentValue;
$this->scanner->yylex();
}
}
if ($this->scanner->token != vbTemplateParser::CURLY_END)
{
$token['attributes'] = $this->variables();
}
return $token;
}
private function variables()
{
$variables = array();
while ($this->scanner->token != vbTemplateParser::CURLY_END)
{
$variables[] = $this->variable();
// TODO: Fix this, but requires changing simple_var() to not read as far as it should
if ($this->scanner->token == vbTemplateParser::TOKEN AND $this->scanner->currentValue == ',') {
$this->scanner->yylex();
}
}
return $variables;
}
private function variable()
{
if ($this->scanner->token == vbTemplateParser::CURLY_START)
{
return $this->statement();
}
else if ($this->scanner->token == vbTemplateParser::STRING_VALUE)
{
$value = $this->scanner->currentValue;
// throw exception if we reach end of input before closing curly brace
if ($this->scanner->yylex() === false AND $this->scanner->token != vbTemplateParser::CURLY_END)
{
throw new vB_Exception_Parser('unclosed_curlybrace');
}
return $value;
}
else
{
return $this->simple_var();
}
}
private function simple_var()
{
$token = '';
while (
($this->scanner->token == vbTemplateParser::TOKEN AND $this->scanner->currentValue != ',')
OR
($this->scanner->token != vbTemplateParser::CURLY_END AND $this->scanner->token != vbTemplateParser::CURLY_START AND $this->scanner->token != vbTemplateParser::TOKEN)
)
{
$token .= $this->scanner->currentValue;
if ($this->scanner->yylex() === false)
{
throw new vB_Exception_Parser('unclosed_curlybrace');
}
}
if (stripos($token, 'null') === 0)
{
$token = NULL;
}
return $token;
}
}