544 lines
14 KiB
PHP
Executable file
544 lines
14 KiB
PHP
Executable file
<?php
|
|
|
|
namespace b8\View;
|
|
use b8\View;
|
|
|
|
class Template extends View
|
|
{
|
|
public static $templateFunctions = array();
|
|
protected static $extension = 'html';
|
|
|
|
public function __construct($viewCode)
|
|
{
|
|
$this->viewCode = $viewCode;
|
|
|
|
if (!count(self::$templateFunctions)) {
|
|
self::$templateFunctions = array('include' => array($this, 'includeTemplate'), 'call' => array($this, 'callHelperFunction'));
|
|
}
|
|
}
|
|
|
|
public static function createFromFile($file, $path = null)
|
|
{
|
|
if (!static::exists($file, $path)) {
|
|
throw new \Exception('View file does not exist: ' . $file);
|
|
}
|
|
|
|
$viewFile = static::getViewFile($file, $path);
|
|
return new static(file_get_contents($viewFile));
|
|
}
|
|
|
|
public static function createFromString($string)
|
|
{
|
|
return new static($string);
|
|
}
|
|
|
|
public function addFunction($name, $handler)
|
|
{
|
|
self::$templateFunctions[$name] = $handler;
|
|
}
|
|
|
|
public function removeFunction($name)
|
|
{
|
|
unset(self::$templateFunctions[$name]);
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return $this->parse($this->viewCode);
|
|
}
|
|
|
|
protected function parse($string)
|
|
{
|
|
$lastCond = null;
|
|
$keywords = array('ifnot', 'if', 'else', 'for', 'loop', '@', '/ifnot', '/if', '/for', '/loop');
|
|
|
|
foreach (self::$templateFunctions as $function => $handler) {
|
|
$keywords[] = $function;
|
|
}
|
|
|
|
$stack = array('children' => array(array('type' => 'string', 'body' => '')));
|
|
$stack['children'][0]['parent'] =& $stack;
|
|
$current =& $stack['children'][0];
|
|
|
|
while (!empty($string)) {
|
|
$current['body'] .= $this->readUntil('{', $string);
|
|
|
|
if (!empty($string)) {
|
|
$gotKeyword = false;
|
|
|
|
foreach($keywords as $keyword) {
|
|
$kwLen = strlen($keyword) + 1;
|
|
|
|
if (substr($string, 0, $kwLen) == '{' . $keyword) {
|
|
$gotKeyword = true;
|
|
$item = array('type' => $keyword, 'cond' => '', 'children' => '');
|
|
$string = substr($string, $kwLen);
|
|
|
|
$cond = trim($this->readUntil('}', $string));
|
|
$item['cond'] = $cond;
|
|
$lastCond = $cond;
|
|
$string = substr($string, 1);
|
|
|
|
if (array_key_exists($keyword, self::$templateFunctions)) {
|
|
$item['function_name'] = $keyword;
|
|
$item['type'] = 'function';
|
|
}
|
|
|
|
$str = array('type' => 'string', 'body' => '');
|
|
$parent =& $current['parent'];
|
|
|
|
if (substr($current['body'], (0 - strlen(PHP_EOL))) === PHP_EOL) {
|
|
$current['body'] = substr($current['body'], 0, strlen($current['body']) - strlen(PHP_EOL));
|
|
}
|
|
|
|
$item['parent'] =& $parent;
|
|
|
|
$parent['children'][] = $item;
|
|
|
|
if ($keyword == '@' || $item['type'] == 'function') {
|
|
// If we're processing a variable, add a string to the parent and move up to that as current.
|
|
$parent['children'][] = $str;
|
|
$current =& $parent['children'][count($parent['children']) - 1];
|
|
$current['parent'] =& $parent;
|
|
} elseif (substr($keyword, 0, 1) == '/') {
|
|
// If we're processing the end of a block (if/loop), add a string to the parent's parent and move up to that.
|
|
$parent =& $parent['parent'];
|
|
$parent['children'][] = $str;
|
|
$current =& $parent['children'][count($parent['children']) - 1];
|
|
$current['parent'] =& $parent;
|
|
} else {
|
|
$parent['children'][count($parent['children']) - 1]['children'][] = $str;
|
|
$current =& $parent['children'][count($parent['children']) - 1]['children'][0];
|
|
$current['parent'] =& $parent['children'][count($parent['children']) - 1];
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$gotKeyword) {
|
|
$current['body'] .= substr($string, 0, 1);
|
|
$string = substr($string, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->processStack($stack);
|
|
}
|
|
|
|
protected function processStack($stack)
|
|
{
|
|
$res = '';
|
|
|
|
while (count($stack['children'])) {
|
|
$current = array_shift($stack['children']);
|
|
|
|
switch ($current['type']) {
|
|
case 'string':
|
|
$res .= $current['body'];
|
|
break;
|
|
|
|
case '@':
|
|
$res .= $this->doParseVar($current['cond']);
|
|
break;
|
|
|
|
case 'if':
|
|
$res .= $this->doParseIf($current['cond'], $current);
|
|
break;
|
|
|
|
case 'ifnot':
|
|
$res .= $this->doParseIfNot($current['cond'], $current);
|
|
break;
|
|
|
|
case 'loop':
|
|
$res .= $this->doParseLoop($current['cond'], $current);
|
|
break;
|
|
|
|
case 'for':
|
|
$res .= $this->doParseFor($current['cond'], $current);
|
|
break;
|
|
|
|
case 'function':
|
|
$res .= $this->doParseFunction($current);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
protected function readUntil($until, &$string)
|
|
{
|
|
$read = '';
|
|
|
|
while (!empty($string)) {
|
|
$char = substr($string, 0, 1);
|
|
|
|
if ($char == $until) {
|
|
break;
|
|
}
|
|
|
|
$read .= $char;
|
|
$string = substr($string, 1);
|
|
}
|
|
|
|
return $read;
|
|
}
|
|
|
|
protected function doParseVar($var)
|
|
{
|
|
if($var == 'year')
|
|
{
|
|
return date('Y');
|
|
}
|
|
|
|
$val = $this->processVariableName($var);
|
|
return $val;
|
|
}
|
|
|
|
protected function doParseIf($condition, $stack)
|
|
{
|
|
if ($this->ifConditionIsTrue($condition)) {
|
|
return $this->processStack($stack);
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
protected function doParseIfNot($condition, $stack)
|
|
{
|
|
if (!$this->ifConditionIsTrue($condition)) {
|
|
return $this->processStack($stack);
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
protected function ifConditionIsTrue($condition)
|
|
{
|
|
$matches = array();
|
|
|
|
if (preg_match('/([a-zA-Z0-9_\-\(\):\s.\"]+)\s+?([\!\=\<\>]+)?\s+?([a-zA-Z0-9\(\)_\-:\s.\"]+)?/', $condition, $matches)) {
|
|
$left = is_numeric($matches[1]) ? intval($matches[1]) : $this->processVariableName($matches[1]);
|
|
$right = is_numeric($matches[3]) ? intval($matches[3]) : $this->processVariableName($matches[3]);
|
|
$operator = $matches[2];
|
|
|
|
switch ($operator) {
|
|
case '==':
|
|
case '=':
|
|
return ($left == $right);
|
|
|
|
case '!=':
|
|
return ($left != $right);
|
|
|
|
case '>=':
|
|
return ($left >= $right);
|
|
|
|
case '<=':
|
|
return ($left <= $right);
|
|
|
|
case '>':
|
|
return ($left > $right);
|
|
|
|
case '<':
|
|
return ($left < $right);
|
|
}
|
|
} elseif (preg_match('/([a-zA-Z0-9_\-\(\):\s.]+)/', $condition, $matches)) {
|
|
return $this->processVariableName($condition) ? true : false;
|
|
}
|
|
}
|
|
|
|
protected function doParseLoop($var, $stack)
|
|
{
|
|
$working = $this->processVariableName($var);
|
|
|
|
if(is_null($working))
|
|
{
|
|
return '';
|
|
}
|
|
|
|
if(!is_array($working))
|
|
{
|
|
$working = array($working);
|
|
}
|
|
|
|
$rtn = '';
|
|
foreach ($working as $key => $val) {
|
|
// Make sure we support nesting loops:
|
|
$keyWas = isset($this->key) ? $this->key : null;
|
|
$valWas = isset($this->value) ? $this->value : null;
|
|
$itemWas = isset($this->item) ? $this->item : null;
|
|
|
|
// Set up the necessary variables within the stack:
|
|
$this->parent = $this;
|
|
$this->item = $val;
|
|
$this->key = $key;
|
|
$this->value = $val;
|
|
$rtn .= $this->processStack($stack);
|
|
|
|
// Restore state for any parent nested loops:
|
|
$this->item = $itemWas;
|
|
$this->key = $keyWas;
|
|
$this->value = $valWas;
|
|
}
|
|
|
|
return $rtn;
|
|
}
|
|
|
|
/**
|
|
* Processes loops in templates, of the following styles:
|
|
*
|
|
* <code>
|
|
* {for myarray.items}
|
|
* {@item.title}
|
|
* {/for}
|
|
* </code>
|
|
*
|
|
* Or:
|
|
*
|
|
* <code>
|
|
* {for 0:pages.count; i++}
|
|
* <a href="/item/{@i}">{@i}</a>
|
|
* {/for}
|
|
* </code>
|
|
*
|
|
* @param $cond string The condition string for the loop, to be parsed (e.g. "myarray.items" or "0:pages.count; i++")
|
|
* @param $stack string The child stack for this loop, to be processed for each item.
|
|
* @return string
|
|
* @throws \Exception
|
|
*/
|
|
protected function doParseFor($cond, $stack)
|
|
{
|
|
// If this is a simple foreach loop, jump over to parse loop:
|
|
if (strpos($cond, ';') === false) {
|
|
return $this->doParseLoop($cond, $stack);
|
|
}
|
|
|
|
// Otherwise, process as a for loop:
|
|
$parts = explode(';', $cond);
|
|
$range = explode(':', trim($parts[0]));
|
|
|
|
// Process range:
|
|
$rangeLeft = $this->getForRangePart($range[0]);
|
|
$rangeRight = $this->getForRangePart($range[1]);
|
|
|
|
// Process variable & incrementor / decrementor:
|
|
$parts[1] = trim($parts[1]);
|
|
|
|
$matches = array();
|
|
if (preg_match('/([a-zA-Z0-9_]+)(\+\+|\-\-)/', $parts[1], $matches)) {
|
|
$varName = $matches[1];
|
|
$direction = $matches[2] == '++' ? 'increment' : 'decrement';
|
|
} else {
|
|
throw new \Exception('Syntax error in for loop: ' . $cond);
|
|
}
|
|
|
|
$rtn = '';
|
|
|
|
if ($direction == 'increment') {
|
|
for ($i = $rangeLeft; $i < $rangeRight; $i++) {
|
|
$this->parent = $this;
|
|
$this->{$varName} = $i;
|
|
$rtn .= $this->processStack($stack);
|
|
}
|
|
} else {
|
|
for ($i = $rangeLeft; $i > $rangeRight; $i--) {
|
|
$this->parent = $this;
|
|
$this->{$varName} = $i;
|
|
$rtn .= $this->processStack($stack);
|
|
}
|
|
}
|
|
|
|
return $rtn;
|
|
}
|
|
|
|
protected function getForRangePart($part)
|
|
{
|
|
if (is_numeric($part)) {
|
|
return intval($part);
|
|
}
|
|
|
|
$varPart = $this->processVariableName($part);
|
|
|
|
if (is_numeric($varPart)) {
|
|
return intval($varPart);
|
|
}
|
|
|
|
throw new \Exception('Invalid range in for loop: ' . $part);
|
|
}
|
|
|
|
public function processVariableName($varName)
|
|
{
|
|
// Case one - Test for function calls:
|
|
if (substr($varName, 0, 1) == '(' && substr($varName, -1) == ')') {
|
|
|
|
$functionCall = substr($varName, 1, -1);
|
|
$parts = explode(' ', $functionCall, 2);
|
|
$functionName = $parts[0];
|
|
$arguments = isset($parts[1]) ? $parts[1] : null;
|
|
|
|
return $this->executeTemplateFunction($functionName, $arguments);
|
|
}
|
|
|
|
// Case two - Test if it is just a string:
|
|
if (substr($varName, 0, 1) == '"' && substr($varName, -1) == '"') {
|
|
return substr($varName, 1, -1);
|
|
}
|
|
|
|
// Case three - Test if it is just a number:
|
|
if (is_numeric($varName)) {
|
|
return $varName;
|
|
}
|
|
|
|
// Case four - Test for helper calls:
|
|
if (strpos($varName, ':') !== false) {
|
|
list($helper, $property) = explode(':', $varName);
|
|
|
|
$helper = $this->{$helper}();
|
|
|
|
if (property_exists($helper, $property) || method_exists($helper, '__get')) {
|
|
return $helper->{$property};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Case five - Process as a variable:
|
|
$varPart = explode('.', $varName);
|
|
$thisPart = array_shift($varPart);
|
|
|
|
|
|
if(!array_key_exists($thisPart, $this->_vars))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
$working = $this->{$thisPart};
|
|
|
|
while(count($varPart))
|
|
{
|
|
$thisPart = array_shift($varPart);
|
|
|
|
if(is_object($working)) {
|
|
// Check if we're working with an actual property:
|
|
if (property_exists($working, $thisPart)) {
|
|
$working = $working->{$thisPart};
|
|
continue;
|
|
}
|
|
|
|
// Check if the object has a magic __get method:
|
|
if (method_exists($working, '__get')) {
|
|
$working = $working->{$thisPart};
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
if(is_array($working) && array_key_exists($thisPart, $working))
|
|
{
|
|
$working = $working[$thisPart];
|
|
continue;
|
|
}
|
|
|
|
if($thisPart == 'toLowerCase')
|
|
{
|
|
$working = strtolower($working);
|
|
continue;
|
|
}
|
|
|
|
if($thisPart == 'toUpperCase')
|
|
{
|
|
$working = strtoupper($working);
|
|
continue;
|
|
}
|
|
|
|
if ($thisPart == 'isNumeric')
|
|
{
|
|
return is_numeric($working);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return $working;
|
|
}
|
|
|
|
protected function doParseFunction($stack)
|
|
{
|
|
return $this->executeTemplateFunction($stack['function_name'], $stack['cond']);
|
|
}
|
|
|
|
protected function executeTemplateFunction($function, $args)
|
|
{
|
|
if (array_key_exists($function, self::$templateFunctions)) {
|
|
$handler = self::$templateFunctions[$function];
|
|
$args = $this->processFunctionArguments($args);
|
|
return $handler($args, $this);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function processFunctionArguments($args)
|
|
{
|
|
$rtn = array();
|
|
|
|
$args = explode(';', $args);
|
|
|
|
foreach ($args as $arg) {
|
|
$arg = explode(':', $arg);
|
|
|
|
if (count($arg) == 2) {
|
|
|
|
$key = trim($arg[0]);
|
|
$val = trim($arg[1]);
|
|
|
|
if (strpos($val, ',') !== false) {
|
|
$val = explode(',', $val);
|
|
}
|
|
|
|
$rtn[$key] = $val;
|
|
}
|
|
}
|
|
|
|
return $rtn;
|
|
}
|
|
|
|
public function getVariable($variable)
|
|
{
|
|
return $this->processVariableName($variable);
|
|
}
|
|
|
|
protected function includeTemplate($args, $view)
|
|
{
|
|
$template = static::createFromFile($view->getVariable($args['template']));
|
|
|
|
if (isset($args['variables'])) {
|
|
if (!is_array($args['variables'])) {
|
|
$args['variables'] = array($args['variables']);
|
|
}
|
|
|
|
foreach ($args['variables'] as $variable) {
|
|
|
|
$variable = explode('=>', $variable);
|
|
$variable = array_map('trim', $variable);
|
|
|
|
if (count($variable) == 1) {
|
|
$template->{$variable[0]} = $view->getVariable($variable[0]);
|
|
} else {
|
|
$template->{$variable[1]} = $view->getVariable($variable[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $template->render();
|
|
}
|
|
|
|
protected function callHelperFunction($args)
|
|
{
|
|
$helper = $args['helper'];
|
|
$function = $args['method'];
|
|
|
|
return $this->{$helper}()->{$function}();
|
|
}
|
|
} |