From 3c51baa4167d5a68ff010dc81525b9340c5f1329 Mon Sep 17 00:00:00 2001 From: Kevin Chabowski Date: Sat, 24 May 2014 14:45:13 +0200 Subject: Moved code into src subfolder and fixed docu --- src/ASTNode.php | 12 ++ src/BreakException.php | 5 + src/Calc.php | 138 ++++++++++++ src/CantLoadTemplate.php | 12 ++ src/CantSaveTemplate.php | 12 ++ src/ContinueException.php | 5 + src/FatalRuntimeError.php | 13 ++ src/FilesystemStorageAccess.php | 71 +++++++ src/Misc.php | 9 + src/ParseCompileError.php | 22 ++ src/Parser.php | 459 ++++++++++++++++++++++++++++++++++++++++ src/RuntimeError.php | 13 ++ src/STECore.php | 342 ++++++++++++++++++++++++++++++ src/STEStandardLibrary.php | 149 +++++++++++++ src/Scope.php | 146 +++++++++++++ src/StorageAccess.php | 55 +++++ src/StorageAccessFailure.php | 5 + src/TagNode.php | 13 ++ src/TextNode.php | 11 + src/Transcompiler.php | 456 +++++++++++++++++++++++++++++++++++++++ src/VarNotInScope.php | 5 + src/VariableNode.php | 25 +++ 22 files changed, 1978 insertions(+) create mode 100644 src/ASTNode.php create mode 100644 src/BreakException.php create mode 100644 src/Calc.php create mode 100644 src/CantLoadTemplate.php create mode 100644 src/CantSaveTemplate.php create mode 100644 src/ContinueException.php create mode 100644 src/FatalRuntimeError.php create mode 100644 src/FilesystemStorageAccess.php create mode 100644 src/Misc.php create mode 100644 src/ParseCompileError.php create mode 100644 src/Parser.php create mode 100644 src/RuntimeError.php create mode 100644 src/STECore.php create mode 100644 src/STEStandardLibrary.php create mode 100644 src/Scope.php create mode 100644 src/StorageAccess.php create mode 100644 src/StorageAccessFailure.php create mode 100644 src/TagNode.php create mode 100644 src/TextNode.php create mode 100644 src/Transcompiler.php create mode 100644 src/VarNotInScope.php create mode 100644 src/VariableNode.php (limited to 'src') diff --git a/src/ASTNode.php b/src/ASTNode.php new file mode 100644 index 0000000..fc5d9cc --- /dev/null +++ b/src/ASTNode.php @@ -0,0 +1,12 @@ +tpl = $tpl; + $this->offset = $off; + } +} diff --git a/src/BreakException.php b/src/BreakException.php new file mode 100644 index 0000000..563bf53 --- /dev/null +++ b/src/BreakException.php @@ -0,0 +1,5 @@ + */ +class Calc { + private function __construct() {} + + /* We could also just eval() the $infix_math code, but this is much cooler :-D (Parser inception) */ + public static function shunting_yard($infix_math) { + $operators = array( + "+" => array("l", 2), + "-" => array("l", 2), + "*" => array("l", 3), + "/" => array("l", 3), + "^" => array("r", 4), + "_" => array("r", 5), + "(" => array("", 0), + ")" => array("", 0) + ); + + preg_match_all("/\s*(?:(?:[+\\-\\*\\/\\^\\(\\)])|(\\d*[\\.]?\\d*))\\s*/s", $infix_math, $tokens, PREG_PATTERN_ORDER); + $tokens_raw = array_filter(array_map('trim', $tokens[0]), function($x) { return ($x === "0") || (!empty($x)); }); + $output_queue = array(); + $op_stack = array(); + + $lastpriority = NULL; + /* Make - unary, if neccessary */ + $tokens = array(); + foreach($tokens_raw as $token) { + $priority = isset($operators[$token]) ? $operators[$token][1] : -1; + if(($token == "-") && (($lastpriority === NULL) || ($lastpriority >= 0))) { + $priority = $operators["_"][1]; + $tokens[] = "_"; + } else { + $tokens[] = $token; + } + $lastpriority = $priority; + } + + while(!empty($tokens)) { + $token = array_shift($tokens); + if(is_numeric($token)) { + $output_queue[] = $token; + } else if($token == "(") { + $op_stack[] = $token; + } else if($token == ")") { + $lbr_found = false; + while(!empty($op_stack)) { + $op = array_pop($op_stack); + if($op == "(") { + $lbr_found = true; + break; + } + $output_queue[] = $op; + } + if(!$lbr_found) { + throw new RuntimeError("Bracket mismatch."); + } + } else if(!isset($operators[$token])) { + throw new RuntimeError("Invalid token ($token): Not a number, bracket or operator. Stop."); + } else { + $priority = $operators[$token][1]; + if($operators[$token][0] == "l") { + while((!empty($op_stack)) and ($priority <= $operators[$op_stack[count($op_stack)-1]][1])) { + $output_queue[] = array_pop($op_stack); + } + } else { + while((!empty($op_stack)) and ($priority < $operators[$op_stack[count($op_stack)-1]][1])) { + $output_queue[] = array_pop($op_stack); + } + } + $op_stack[] = $token; + } + } + + while(!empty($op_stack)) { + $op = array_pop($op_stack); + if($op == "(") { + throw new RuntimeError("Bracket mismatch..."); + } + $output_queue[] = $op; + } + + return $output_queue; + } + + public static function pop2(&$array) { + $rv = array(array_pop($array), array_pop($array)); + if(array_search(NULL, $rv, true) !== false) { + throw new RuntimeError("Not enough numbers on stack. Invalid formula."); + } + return $rv; + } + + public static function calc_rpn($rpn) { + $stack = array(); + foreach($rpn as $token) { + switch($token) { + case "+": + list($b, $a) = self::pop2($stack); + $stack[] = $a + $b; + break; + case "-": + list($b, $a) = self::pop2($stack); + $stack[] = $a - $b; + break; + case "*": + list($b, $a) = self::pop2($stack); + $stack[] = $a * $b; + break; + case "/": + list($b, $a) = self::pop2($stack); + $stack[] = $a / $b; + break; + case "^": + list($b, $a) = self::pop2($stack); + $stack[] = pow($a, $b); + break; + case "_": + $a = array_pop($stack); + if($a === NULL) { + throw new RuntimeError("Not enough numbers on stack. Invalid formula."); + } + $stack[] = -$a; + break; + default: + $stack[] = $token; + break; + } + } + return array_pop($stack); + } + + public static function calc($expr) { + return self::calc_rpn(self::shunting_yard($expr)); + } +} \ No newline at end of file diff --git a/src/CantLoadTemplate.php b/src/CantLoadTemplate.php new file mode 100644 index 0000000..82c7a9f --- /dev/null +++ b/src/CantLoadTemplate.php @@ -0,0 +1,12 @@ + implementation can throw, if it is unable to load a template. + */ +class CantLoadTemplate extends StorageAccessFailure { } diff --git a/src/CantSaveTemplate.php b/src/CantSaveTemplate.php new file mode 100644 index 0000000..1072ad1 --- /dev/null +++ b/src/CantSaveTemplate.php @@ -0,0 +1,12 @@ + implementation can throw, if it is unable to save a template. + */ +class CantSaveTemplate extends StorageAccessFailure { } diff --git a/src/ContinueException.php b/src/ContinueException.php new file mode 100644 index 0000000..af79b1b --- /dev/null +++ b/src/ContinueException.php @@ -0,0 +1,5 @@ + implementation for loading / saving templates into a directory structure. + */ +class FilesystemStorageAccess implements StorageAccess { + protected $sourcedir; + protected $transcompileddir; + + /* + * Constructor: __construct + * + * Parameters: + * $src - The directory with the sources (Writing permissions are not mandatory, because STE does not save template sources). + * $transc - The directory with the transcompiled templates (the PHP instance / the HTTP Server needs writing permissions to this directory). + */ + public function __construct($src, $transc) { + $this->sourcedir = $src; + $this->transcompileddir = $transc; + } + + public function load($tpl, &$mode) { + $src_fn = $this->sourcedir . "/" . $tpl; + $transc_fn = $this->transcompileddir . "/" . $tpl . ".php"; + + if($mode == StorageAccess::MODE_SOURCE) { + $content = @file_get_contents($src_fn); + if($content === false) { + throw new CantLoadTemplate("Template not found."); + } + return $content; + } + + $src_stat = @stat($src_fn); + $transc_stat = @stat($transc_fn); + + if(($src_stat === false) and ($transc_stat === false)) { + throw new CantLoadTemplate("Template not found."); + } else if($transc_stat === false) { + $mode = StorageAccess::MODE_SOURCE; + return file_get_contents($src_fn); + } else if($src_stat === false) { + include($transc_fn); + return $transcompile_fx; + } else { + if($src_stat["mtime"] > $transc_stat["mtime"]) { + $mode = StorageAccess::MODE_SOURCE; + return file_get_contents($src_fn); + } else { + include($transc_fn); + return $transcompile_fx; + } + } + } + + public function save($tpl, $data, $mode) { + $fn = (($mode == StorageAccess::MODE_SOURCE) ? $this->sourcedir : $this->transcompileddir) . "/" . $tpl . (($mode == StorageAccess::MODE_TRANSCOMPILED) ? ".php" : ""); + @mkdir(dirname($fn), 0777, true); + if(file_put_contents($fn, "") === false) { + throw new CantSaveTemplate("Unable to save template."); + } + } +} diff --git a/src/Misc.php b/src/Misc.php new file mode 100644 index 0000000..56ae759 --- /dev/null +++ b/src/Misc.php @@ -0,0 +1,9 @@ +msg = $msg; + $this->tpl = $tpl; + $this->off = $offset; + $this->message = "$msg (Template $tpl, Offset $offset)"; + } + + public function rewrite($code) { + $line = substr_count(str_replace("\r\n", "\n", substr($code, 0, $this->off)), "\n") + 1; + $this->message = "{$this->msg} (Template {$this->tpl}, Line $line)"; + $this->is_rewritten = true; + } +} diff --git a/src/Parser.php b/src/Parser.php new file mode 100644 index 0000000..7e93e47 --- /dev/null +++ b/src/Parser.php @@ -0,0 +1,459 @@ +text = $text; + $this->name = $name; + $this->off = 0; + $this->len = mb_strlen($text); + } + + private function next($n = 1) { + if($n <= 0) { + throw new \InvalidArgumentException("\$n must be > 0"); + } + $c = mb_substr($this->text, $this->off, $n); + $this->off = min($this->off + $n, $this->len); + return $c; + } + + private function eof() { + return ($this->off == $this->len); + } + + private function back($n = 1) { + if($n <= 0) { + throw new \InvalidArgumentException("\$n must be > 0"); + } + $this->off = max($this->off - $n, 0); + } + + private function search_off($needle) { + return mb_strpos($this->text, $needle, $this->off); + } + + private function search_multi($needles) { + $oldoff = $this->off; + + $minoff = $this->len; + $which = NULL; + + foreach($needles as $key => $needle) { + if(($off = $this->search_off($needle)) === false) { + continue; + } + + if($off < $minoff) { + $minoff = $off; + $which = $key; + } + } + + $this->off = $minoff + (($which === NULL) ? 0 : mb_strlen((string) $needles[$which])); + + return array($which, $minoff, mb_substr($this->text, $oldoff, $minoff - $oldoff), $oldoff); + } + + private function search($needle) { + $oldoff = $this->off; + + $off = $this->search_off($needle); + if($off === false) { + $this->off = $this->len; + return array(false, mb_substr($this->text, $oldoff), $oldoff); + } + + $this->off = $off + mb_strlen($needle); + return array($off, mb_substr($this->text, $oldoff, $off - $oldoff), $oldoff); + } + + private function take_while($cb) { + $s = ""; + while($c = $this->next()) { + if(!call_user_func($cb, $c)) { + $this->back(); + return $s; + } + $s .= $c; + } + return $s; + } + + private function skip_ws() { + $this->take_while("ctype_space"); + } + + private function get_name() { + $off = $this->off; + $name = $this->take_while(function($c) { return ctype_alnum($c) || ($c == "_"); }); + if(mb_strlen($name) == 0) { + throw new ParseCompileError("Expected a name (alphanumeric chars + '_', at least one char)", $this->name, $off); + } + return $name; + } + + /* + * Function: parse + * Parses the input into an AST. + * + * You only need this function, if you want to manually trnascompile a template. + * + * Parameters: + * $text - The input code. + * $name - The name of the template. + * + * Returns: + * An array of objects. + * + * Throws: + * + */ + public static function parse($text, $name) { + $obj = new self($text, $name); + $res = $obj->parse_text( + self::ESCAPES_DEFAULT, /* Escapes */ + self::PARSE_SHORT | self::PARSE_TAG /* Flags */ + ); + return self::tidyup_ast($res[0]); + } + + private static function tidyup_ast($ast) { + $out = array(); + + $prevtext = NULL; + $first = true; + + foreach($ast as $node) { + if($node instanceof TextNode) { + if($prevtext === NULL) { + $prevtext = $node; + } else { + $prevtext->text .= $node->text; + } + } else { + if($prevtext !== NULL) { + if($first) { + $prevtext->text = ltrim($prevtext->text); + } + if($prevtext->text != "") { + $out[] = $prevtext; + } + } + $prevtext = NULL; + $first = false; + + if($node instanceof TagNode) { + $node->sub = self::tidyup_ast($node->sub); + foreach($node->params as $k => &$v) { + $v = self::tidyup_ast($v); + } + unset($v); + } else { /* VariableNode */ + foreach($node->arrayfields as &$v) { + $v = self::tidyup_ast($v); + } + unset($v); + } + + $out[] = $node; + } + } + + if($prevtext !== NULL) { + if($first) { + $prevtext->text = ltrim($prevtext->text); + } + if($prevtext->text != "") { + $out[] = $prevtext; + } + } + + return $out; + } + + private function parse_text($escapes, $flags, $breakon = NULL, $separator = NULL, $nullaction = NULL, $opentag = NULL, $openedat = -1) { + $elems = array(); + $astlist = array(); + + $needles = array( + "commentopen" => "", + "rawopen" => "", + "escape" => '\\', + "varcurlyopen" => '${', + "var" => '$', + ); + + if($flags & self::PARSE_TAG) { + $needles["tagopen"] = 'search_multi($needles); + + $astlist[] = new TextNode($this->name, $offbefore, $before); + + switch($which) { + case NULL: + if($nullaction === NULL) { + $elems[] = $astlist; + return $elems; + } else { + call_user_func($nullaction); + } + break; + case "commentopen": + list($off, $before, $offbefore) = $this->search(""); + if($off === false) { + throw new ParseCompileError("ste:comment was not closed", $this->name, $offbefore); + } + break; + case "rawopen": + $off_start = $off; + list($off, $before, $offbefore) = $this->search(""); + if($off === false) { + throw new ParseCompileError("ste:rawtext was not closed", $this->name, $off_start); + } + $astlist[] = new TextNode($this->name, $off_start, $before); + break; + case "tagopen": + $astlist[] = $this->parse_tag($off); + break; + case "closetagopen": + $off_start = $off; + $name = $this->get_name(); + $this->skip_ws(); + $off = $this->off; + if($this->next() != ">") { + throw new ParseCompileError("Expected '>' in closing ste-Tag", $this->name, $off); + } + + if($opentag === NULL) { + throw new ParseCompileError("Found closing ste:$name tag, but no tag was opened", $this->name, $off_start); + } + if($opentag != $name) { + throw new ParseCompileError("Open ste:$opentag was not closed", $this->name, $openedat); + } + + $elems[] = $astlist; + return $elems; + case "escape": + $c = $this->next(); + if(mb_strpos($escapes, $c) !== false) { + $astlist[] = new TextNode($this->name, $off, $c); + } else { + $astlist[] = new TextNode($this->name, $off, '\\'); + $this->back(); + } + break; + case "shortifopen": + $shortelems = $this->parse_short("?{", $off); + if(count($shortelems) != 3) { + throw new ParseCompileError("A short if tag must have the form ?{..|..|..}", $this->name, $off); + } + + list($cond, $then, $else) = $shortelems; + $thentag = new TagNode($this->name, $off); + $thentag->name = "then"; + $thentag->sub = $then; + + $elsetag = new TagNode($this->name, $off); + $elsetag->name = "else"; + $elsetag->sub = $else; + + $iftag = new TagNode($this->name, $off); + $iftag->name = "if"; + $iftag->sub = $cond; + $iftag->sub[] = $thentag; + $iftag->sub[] = $elsetag; + + $astlist[] = $iftag; + break; + case "shortcompopen": + $shortelems = $this->parse_short("~{", $off); + if(count($shortelems) != 3) { + throw new ParseCompileError("A short comparasion tag must have the form ~{..|..|..}", $this->name, $off); + } + + // TODO: What will happen, if a tag was in one of the elements? + list($a, $op, $b) = $shortelems; + $cmptag = new TagNode($this->name, $off); + $cmptag->name = "cmp"; + $cmptag->params["text_a"] = $a; + $cmptag->params["op"] = $op; + $cmptag->params["text_b"] = $b; + + $astlist[] = $cmptag; + break; + case "sep": + $elems[] = $astlist; + $astlist = array(); + break; + case "varcurlyopen": + $astlist[] = $this->parse_var($off, true); + break; + case "var": + $astlist[] = $this->parse_var($off, false); + break; + case "break": + $elems[] = $astlist; + return $elems; + } + } + + $elems[] = $astlist; + return $elems; + } + + private function parse_short($shortname, $openedat) { + $tplname = $this->name; + + return $this->parse_text( + self::ESCAPES_DEFAULT, /* Escapes */ + self::PARSE_SHORT | self::PARSE_TAG, /* Flags */ + '}', /* Break on */ + '|', /* Separator */ + function() use ($shortname, $tplname, $openedat) { /* NULL action */ + throw new ParseCompileError("Unclosed $shortname", $tplname, $openedat); + }, + NULL, /* Open tag */ + $openedat /* Opened at */ + ); + } + + private function parse_var($openedat, $curly) { + $varnode = new VariableNode($this->name, $openedat); + $varnode->name = $this->get_name(); + if(!$this->eof()) { + $varnode->arrayfields = $this->parse_array(); + } + + if(($curly) && ($this->next() != "}")) { + throw new ParseCompileError("Unclosed '\${'", $this->name, $openedat); + } + return $varnode; + } + + private function parse_array() { + $tplname = $this->name; + + $arrayfields = array(); + + while($this->next() == "[") { + $openedat = $this->off - 1; + $res = $this->parse_text( + self::ESCAPES_DEFAULT, /* Escapes */ + 0, /* Flags */ + ']', /* Break on */ + NULL, /* Separator */ + function() use ($tplname, $openedat) { /* NULL action */ + throw new ParseCompileError("Unclosed array access '[...]'", $tplname, $openedat); + }, + NULL, /* Open tag */ + $openedat /* Opened at */ + ); + $arrayfields[] = $res[0]; + } + + $this->back(); + return $arrayfields; + } + + private function parse_tag($openedat) { + $tplname = $this->name; + + $this->skip_ws(); + $tag = new TagNode($this->name, $openedat); + $name = $tag->name = $this->get_name(); + $tag->params = array(); + $tag->sub = array(); + + for(;;) { + $this->skip_ws(); + + switch($this->next()) { + case '/': /* Self-closing tag */ + $this->skip_ws(); + if($this->next() != '>') { + throw new ParseCompileError("Unclosed opening )", $this->name, $openedat); + } + + return $tag; + case '>': + $sub = $this->parse_text( + self::ESCAPES_DEFAULT, /* Escapes */ + self::PARSE_SHORT | self::PARSE_TAG, /* Flags */ + NULL, /* Break on */ + NULL, /* Separator */ + function() use ($name, $tplname, $openedat) { /* NULL action */ + throw new ParseCompileError("Open ste:$name tag was not closed", $tplname, $openedat); + }, + $tag->name, /* Open tag */ + $openedat /* Opened at */ + ); + $tag->sub = $sub[0]; + return $tag; + default: + $this->back(); + + $param = $this->get_name(); + + $this->skip_ws(); + if($this->next() != '=') { + throw new ParseCompileError("Expected '=' after tag parameter name", $this->name, $this->off - 1); + } + $this->skip_ws(); + + $quot = $this->next(); + if(($quot != '"') && ($quot != "'")) { + throw new ParseCompileError("Expected ' or \" after '=' of tag parameter", $this->name, $this->off - 1); + } + + $off = $this->off - 1; + $paramval = $this->parse_text( + self::ESCAPES_DEFAULT . $quot, /* Escapes */ + 0, /* Flags */ + $quot, /* Break on */ + NULL, /* Separator */ + function() use ($quot, $tplname, $off) { /* NULL action */ + throw new ParseCompileError("Open tag parameter value ($quot) was not closed", $tplname, $off); + }, + NULL, /* Open tag */ + $off /* Opened at */ + ); + $tag->params[$param] = $paramval[0]; + } + } + } +} diff --git a/src/RuntimeError.php b/src/RuntimeError.php new file mode 100644 index 0000000..ff3b26d --- /dev/null +++ b/src/RuntimeError.php @@ -0,0 +1,13 @@ + is false, this will generate a error message instead of the tag's output. + */ +class RuntimeError extends \Exception {} diff --git a/src/STECore.php b/src/STECore.php new file mode 100644 index 0000000..c00bf67 --- /dev/null +++ b/src/STECore.php @@ -0,0 +1,342 @@ + exception will result in no output from the tag, if false a error message will be written to output. + * $fatal_error_on_missing_tag - If true, STE will throw a if a tag was called that was not registered, otherwise (default) a regular will be thrown and automatically handled by STE (see <$mute_runtime_errors>). + */ + public $blocks; + public $blockorder; + public $mute_runtime_errors = true; + public $fatal_error_on_missing_tag = false; + + /* + * Constructor: __construct + * + * Parameters: + * $storage_access - An Instance of a implementation. + */ + public function __construct($storage_access) { + $this->storage_access = $storage_access; + $this->cur_tpl_dir = "/"; + STEStandardLibrary::_register_lib($this); + $this->blockorder = array(); + $this->blocks = array(); + + $this->set_scope(new Scope(array())); + } + + /* + * Function: register_tag + * Register a custom tag. + * + * Parameters: + * $name - The name of the tag. + * $callback - A callable function (This must take three parameters: The instance, an associative array of parameters, and a function representing the tags content(This expects the instance as its only parameter and returns its text result, i.e to get the text, you neeed to call this function with the instance as a parameter)). + * + * Throws: + * An Exception if the tag could not be registered (if $callback is not callable or if $name is empty) + */ + public function register_tag($name, $callback) { + if(!is_callable($callback)) { + throw new \Exception("Can not register tag \"$name\", not callable."); + } + if(empty($name)) { + throw new \Exception("Can not register tag, empty name."); + } + $this->tags[$name] = $callback; + } + + /* + * Function: call_tag + * Calling a custom tag (builtin ones can not be called) + * + * Parameters: + * $name - The Tag's name + * $params - Associative array of parameters + * $sub - A callable function (expecting an instance as it's parameter) that represents the tag's content. + * + * Throws: + * Might throw a (see <$fatal_error_on_missing_tag>. + * + * Returns: + * The output of the tag or, if a was thrown, the appropiate result (see <$mute_runtime_errors>). + */ + public function call_tag($name, $params, $sub) { + try { + if(!isset($this->tags[$name])) { + if($this->fatal_error_on_missing_tag) { + throw new FatalRuntimeError("Can not call tag \"$name\": Does not exist."); + } else { + throw new RuntimeError("Can not call tag \"$name\": Does not exist."); + } + } + return call_user_func($this->tags[$name], $this, $params, $sub); + } catch(RuntimeError $e) { + if(!$this->mute_runtime_errors) { + return "RuntimeError occurred on tag '$name': " . $e->getMessage(); + } + } + } + + public function calc($expression) { + return Calc::calc($expression); + } + + /* + * Function: exectemplate + * Executes a template and returns the result. The huge difference to is that this function will also output all blocks. + * + * Parameters: + * $tpl - The name of the template to execute. + * + * Throws: + * * A exception if the template could not be loaded. + * * A if the template could not be parsed or transcompiled. + * * A if a tag threw it or if a tag was not found and <$fatal_error_on_missing_tag> is true. + * * Might also throw different exceptions, if a external tag threw it (but they should use or to make it possible for STE to handle them correctly). + * + * Returns: + * The output of the template. + */ + public function exectemplate($tpl) { + $output = ""; + $lastblock = $this->load($tpl); + + foreach($this->blockorder as $blockname) { + $output .= $this->blocks[$blockname]; + } + + return $output . $lastblock; + } + + /* + * Function: get_var_reference + * Get a reference to a template variable using a variable name. + * This can be used,if your custom tag takes a variable name as a parameter. + * + * Parameters: + * $name - The variables name. + * $create_if_not_exist - Should the variable be created, if it does not exist? Otherwise NULL will be returned, if the variable does not exist. + * + * Throws: + * if the variable name can not be parsed (e.g. unbalanced brackets). + * + * Returns: + * A Reference to the variable. + */ + public function &get_var_reference($name, $create_if_not_exist) { + $ref = &$this->scope->get_var_reference($name, $create_if_not_exist); + return $ref; + } + + /* + * Function: set_var_by_name + * Set a template variable by its name. + * This can be used,if your custom tag takes a variable name as a parameter. + * + * Parameters: + * $name - The variables name. + * $val - The new value. + * + * Throws: + * if the variable name can not be parsed (e.g. unbalanced brackets). + */ + public function set_var_by_name($name, $val) { + $this->scope->set_var_by_name($name, $val); + } + + /* + * Function: set_local_var + * Like , but only sets the variable in the global scope ( will overwrite the variable in the parent scope, if it's defined there) . + * + * Parameters: + * $name - The variables name. + * $val - The new value. + * + * Throws: + * if the variable name can not be parsed (e.g. unbalanced brackets). + */ + public function set_local_var($name, $val) { + $this->scope->set_local_var($name, $val); + } + + /* + * Function: get_var_by_name + * Get a template variable by its name. + * This can be used,if your custom tag takes a variable name as a parameter. + * + * Parameters: + * $name - The variables name. + * + * Throws: + * if the variable name can not be parsed (e.g. unbalanced brackets). + * + * Returns: + * The variables value. + */ + public function get_var_by_name($name) { + return $this->scope->get_var_by_name($name); + } + + /* + * Function: load + * Load a template and return its result (blocks not included, use for this). + * + * Parameters: + * $tpl - The name of the template to be loaded. + * $quiet - If true, do not output anything and do not modify the blocks. This can be useful to load custom tags that are programmed in the STE Template Language. Default: false. + * + * Throws: + * * A exception if the template could not be loaded. + * * A if the template could not be parsed or transcompiled. + * * A if a tag threw it or if a tag was not found and <$fatal_error_on_missing_tag> is true. + * * Might also throw different exceptions, if a external tag threw it (but they should use or to make it possible for STE to handle them correctly). + * + * Returns: + * The result of the template (if $quiet == false). + */ + public function load($tpl, $quiet = false) { + $tpldir_b4 = $this->cur_tpl_dir; + + /* Resolve ".", ".." and protect from possible LFI */ + $tpl = str_replace("\\", "/", $tpl); + if($tpl[0] != "/") { + $tpl = $this->cur_tpl_dir . "/" . $tpl; + } + $pathex = array_filter(explode("/", $tpl), function($s) { return ($s != ".") and (!empty($s)); }); + $pathex = array_merge($pathex); + while(($i = array_search("..", $pathex)) !== false) { + if($i == 0) { + $pathex = array_slice($pathex, 1); + } else { + $pathex = array_merge(array_slice($pathex, 0, $i), array_slice($pathex, $i + 2)); + } + } + $tpl = implode("/", $pathex); + $this->cur_tpl_dir = dirname($tpl); + + if($quiet) { + $blocks_back = clone $this->blocks; + $blockorder_back = clone $this->blockorder; + } + + $mode = StorageAccess::MODE_TRANSCOMPILED; + $content = $this->storage_access->load($tpl, $mode); + if($mode == StorageAccess::MODE_SOURCE) { + try { + $ast = Parser::parse($content, $tpl); + $transc = Transcompiler::transcompile($ast); + } catch(ParseCompileError $e) { + $e->rewrite($content); + throw $e; + } + $this->storage_access->save($tpl, $transc, StorageAccess::MODE_TRANSCOMPILED); + eval("\$content = $transc;"); + } + + $output = $content($this); + + $this->cur_tpl_dir = $tpldir_b4; + + if($quiet) { + $this->blocks = $blocks_back; + $this->blockorder = $blockorder_back; + } else { + return $output; + } + } + + /* + * Function: evalbool + * Test, if a text represents false (an empty / only whitespace text) or true (everything else). + * + * Parameters: + * $txt - The text to test. + * + * Returns: + * true/false. + */ + public function evalbool($txt) { + return trim($txt . "") != ""; + } + + public function get_scope() { + return $this->scope; + } + + public function set_scope($scope) { + $this->scope = $scope; + } + + public function make_closure($fx) { + $bound_scope = $this->scope; + return function() use($bound_scope, $fx) { + $args = func_get_args(); + $ste = $args[0]; + + $prev = $ste->get_scope(); + $scope = $bound_scope->new_subscope(); + $ste->set_scope($scope); + + try { + $result = call_user_func_array($fx, $args); + $ste->set_scope($prev); + return $result; + } catch(\Exception $e) { + $ste->set_scope($prev); + throw $e; + } + }; + } + + public function __get($name) { + if($name === "vars") { + return $this->scope; + } + + $trace = debug_backtrace(); + trigger_error( + 'Undefined property via __get(): ' . $name . + ' in ' . $trace[0]['file'] . + ' on line ' . $trace[0]['line'], + E_USER_NOTICE); + return NULL; + } + + public function __set($name, $val) { + if($name !== "vars") { + $trace = debug_backtrace(); + trigger_error( + 'Undefined property via __set(): ' . $name . + ' in ' . $trace[0]['file'] . + ' on line ' . $trace[0]['line'], + E_USER_NOTICE); + return; + } + + if(is_array($val)) { + foreach($val as $k => $v) { + $this->scope[$k] = $v; + } + } + } +} diff --git a/src/STEStandardLibrary.php b/src/STEStandardLibrary.php new file mode 100644 index 0000000..915b699 --- /dev/null +++ b/src/STEStandardLibrary.php @@ -0,0 +1,149 @@ +register_tag($method, array(__CLASS__, $method)); + } + } + } + + static public function escape($ste, $params, $sub) { + if($ste->evalbool(@$params["lines"])) { + return nl2br(htmlspecialchars(str_replace("\r\n", "\n", $sub($ste)))); + } else { + return htmlspecialchars($sub($ste)); + } + } + + static public function strlen($ste, $params, $sub) { + return strlen($sub($ste)); + } + + static public function arraylen($ste, $params, $sub) { + if(empty($params["array"])) { + throw new RuntimeError("Missing array parameter in ."); + } + $a = $ste->get_var_by_name($params["array"], false); + return (is_array($a)) ? count($a) : ""; + } + + static public function inc($ste, $params, $sub) { + if(empty($params["var"])) { + throw new RuntimeError("Missing var parameter in ."); + } + $ref = &$ste->get_var_reference($params["var"], true); + $ref++; + } + + static public function dec($ste, $params, $sub) { + if(empty($params["var"])) { + throw new RuntimeError("Missing var parameter in ."); + } + $ref = &$ste->get_var_reference($params["var"], true); + $ref--; + } + + static public function date($ste, $params, $sub) { + return @strftime($sub($ste), empty($params["timestamp"]) ? @time() : (int) $params["timestamp"]); + } + + static public function in_array($ste, $params, $sub) { + if(empty($params["array"])) { + throw new RuntimeError("Missing array parameter in ."); + } + $ar = &$ste->get_var_reference($params["array"], false); + if(!is_array($ar)) { + return ""; + } + return in_array($sub($ste), $ar) ? "y" : ""; + } + + static public function join($ste, $params, $sub) { + if(empty($params["array"])) { + throw new RuntimeError("Missing array parameter in ."); + } + return implode($sub($ste), $ste->get_var_by_name($params["array"])); + } + + static public function split($ste, $params, $sub) { + if(empty($params["array"])) { + throw new RuntimeError("Missing array parameter in ."); + } + if(empty($params["delim"])) { + throw new RuntimeError("Missing delim parameter in ."); + } + $ste->set_var_by_name($params["array"], explode($params["delim"], $sub($ste))); + } + + static public function array_add($ste, $params, $sub) { + if(empty($params["array"])) { + throw new RuntimeError("Missing array parameter in ."); + } + + $ar = &$ste->get_var_reference($params["array"], true); + if(empty($params["key"])) { + $ar[] = $sub($ste); + } else { + $ar[$params["key"]] = $sub($ste); + } + } + + static public function array_filter($ste, $params, $sub) + { + if(empty($params["array"])) { + throw new RuntimeError("Missing array parameter in ."); + } + + $ar = $ste->get_var_by_name($params["array"]); + if(!is_array($ar)) { + throw new RuntimeError("Variable at 'array' is not an array."); + } + + $keys = array_keys($ar); + + if(!empty($params["keep_by_keys"])) { + $keep_by_keys = &$ste->get_var_reference($params["keep_by_keys"], false); + if(!is_array($keep_by_keys)) { + throw new RuntimeError("Variable at 'keep_by_keys' is not an array."); + } + $delkeys = array_filter($keys, function($k) use ($keep_by_keys) { return !in_array($k, $keep_by_keys); }); + foreach($delkeys as $dk) { + unset($ar[$dk]); + } + $keys = array_keys($ar); + } + if(!empty($params["keep_by_values"])) { + $keep_by_values = &$ste->get_var_reference($params["keep_by_values"], false); + if(!is_array($keep_by_values)) { + throw new RuntimeError("Variable at 'keep_by_values' is not an array."); + } + $ar = array_filter($ar, function($v) use ($keep_by_values) { return in_array($v, $keep_by_values); }); + $keys = array_keys($ar); + } + if(!empty($params["delete_by_keys"])) { + $delete_by_keys = &$ste->get_var_reference($params["delete_by_keys"], false); + if(!is_array($delete_by_keys)) { + throw new RuntimeError("Variable at 'delete_by_keys' is not an array."); + } + $delkeys = array_filter($keys, function($k) use ($delete_by_keys) { return in_array($k, $delete_by_keys); }); + foreach($delkeys as $dk) { + unset($ar[$dk]); + } + $keys = array_keys($ar); + } + if(!empty($params["delete_by_values"])) { + $delete_by_values = &$ste->get_var_reference($params["delete_by_values"], false); + if(!is_array($delete_by_values)) { + throw new RuntimeError("Variable at 'delete_by_values' is not an array."); + } + $ar = array_filter($ar, function($v) use ($delete_by_values) { return !in_array($v, $delete_by_values); }); + $keys = array_keys($ar); + } + + $ste->set_var_by_name($params["array"], $ar); + } +} diff --git a/src/Scope.php b/src/Scope.php new file mode 100644 index 0000000..0da2724 --- /dev/null +++ b/src/Scope.php @@ -0,0 +1,146 @@ + 0) && ($more[0] !== '[')) { + // TODO: better error message, not very non-programmer friendly... + throw new RuntimeError("A variable name must be of format name('[' name ']')*."); + } + + $remain = $field . $more; + } + + return $fields; + } + + private function &get_topvar_reference($name, $localonly) { + if(array_key_exists($name, $this->vars)) { + $ref = &$this->vars[$name]; + return $ref; + } + + if((!$localonly) && ($this->parent !== NULL)) { + $ref = &$this->parent->get_topvar_reference($name, $localonly); + return $ref; + } + + throw new VarNotInScope(); + } + + public function &get_var_reference($name, $create_if_not_exist, $localonly=false) { + $nullref = NULL; + + $fields = self::parse_name($name); + if(count($fields) == 0) { + return $nullref; // TODO: or should we throw an exception here? + } + + $first = $fields[0]; + + $ref = NULL; + try { + $ref = &$this->get_topvar_reference($first, $localonly); + } catch(VarNotInScope $e) { + if($create_if_not_exist) { + $this->vars[$first] = (count($fields) > 0) ? array() : ""; + $ref = &$this->vars[$first]; + } else { + return $nullref; + } + } + + for($i = 1; $i < count($fields); $i++) { + $field = $fields[$i]; + + if(!is_array($ref)) { + return $nullref; + } + + if(!array_key_exists($field, $ref)) { + if(!$create_if_not_exist) { + return $nullref; + } + + if($i < count($fields) - 1) { + $ref[$field] = array(); + } else { + $ref[$field] = ""; + } + } + + $ref = &$ref[$field]; + } + + return $ref; + } + + public function set_var_by_name($name, $val) { + $ref = &$this->get_var_reference($name, true); + $ref = $val; + } + + public function set_local_var($name, $val) { + $ref = &$this->get_var_reference($name, true, true); + $ref = $val; + } + + public function get_var_by_name($name) { + $ref = $this->get_var_reference($name, false); + return $ref === NULL ? "" : $ref; + } + + public function new_subscope() { + $o = new self(); + $o->parent = $this; + return $o; + } + + /* implementing ArrayAccess */ + + public function offsetSet($offset, $value) { + $this->set_var_by_name($offset, $value); + } + public function offsetGet($offset) { + return $this->get_var_by_name($offset); + } + public function offsetExists($offset) { + try { + $this->get_topvar_reference($offset); + return true; + } catch(VarNotInScope $e) { + return false; + } + } + public function offsetUnset($offset) { + unset($this->vars[$offset]); + } +} diff --git a/src/StorageAccess.php b/src/StorageAccess.php new file mode 100644 index 0000000..81f7439 --- /dev/null +++ b/src/StorageAccess.php @@ -0,0 +1,55 @@ +. + * If , the raw sourcecode is expected, if the transcompiled template *as a callable function* (expecting an instance as first parameter) is expected. + * If the transcompiled version is not available or older than the source, you can set this parameter to and return the source. + * + * Throws: + * A exception if the template could not be loaded. + * + * Returns: + * Either the sourcecode or a callable function (first, and only parameter: an instance). + */ + public function load($tpl, &$mode); + + /* + * Function: save + * Saves a template. + * + * Throws: + * A exception if the template could not be saved. + * + * Parameters: + * $tpl -The name of the template. + * $data - The data to be saved. + * $mode - A