diff options
author | Kevin Chabowski <kevin@kch42.de> | 2014-05-24 01:39:33 +0200 |
---|---|---|
committer | Kevin Chabowski <kevin@kch42.de> | 2014-05-24 01:39:33 +0200 |
commit | 84d815f4e006e02521759070bb89025dab80b219 (patch) | |
tree | 418a03f0b8be181cb29230ad5e808d3213b4d3cb | |
parent | fdbe2e9521aa54ec6e0b70c1cb0f532248e531e4 (diff) | |
download | ste-84d815f4e006e02521759070bb89025dab80b219.tar.gz ste-84d815f4e006e02521759070bb89025dab80b219.tar.bz2 ste-84d815f4e006e02521759070bb89025dab80b219.zip |
Added scoping.
ste:mktag generated tags now have an own scope. They even resemble
closures, since they inherit their parent scope.
A lot of work was done to keep this compatible with older programs.
However:
* Templates that relied on the non-scoping behavior of tags will probably
fail.
* Since $ste->vars is no longer an actual array, things like
$ste->vars["foo"]["bar"] = "baz"
are no longer possible! A single field access will still work:
$ste->vars["foo"] = "bar"
-rw-r--r-- | STECore.php | 114 | ||||
-rw-r--r-- | Scope.php | 187 | ||||
-rw-r--r-- | Transcompiler.php | 6 | ||||
-rw-r--r-- | VarNotInScope.php | 5 | ||||
-rw-r--r-- | ste.php | 3 | ||||
-rw-r--r-- | tests/test_closure/.gitignore | 3 | ||||
-rw-r--r-- | tests/test_closure/code.php | 5 | ||||
-rw-r--r-- | tests/test_closure/test.tpl | 8 | ||||
-rw-r--r-- | tests/test_closure/want | 1 | ||||
-rw-r--r-- | tests/test_mktag/test.tpl | 3 | ||||
-rw-r--r-- | tests/test_recursive/.gitignore | 3 | ||||
-rw-r--r-- | tests/test_recursive/code.php | 5 | ||||
-rw-r--r-- | tests/test_recursive/test.tpl | 11 | ||||
-rw-r--r-- | tests/test_recursive/want | 1 | ||||
-rw-r--r-- | tests/test_scoping/.gitignore | 3 | ||||
-rw-r--r-- | tests/test_scoping/code.php | 5 | ||||
-rw-r--r-- | tests/test_scoping/test.tpl | 6 | ||||
-rw-r--r-- | tests/test_scoping/want | 0 |
18 files changed, 321 insertions, 48 deletions
diff --git a/STECore.php b/STECore.php index 6bde63f..add09ad 100644 --- a/STECore.php +++ b/STECore.php @@ -10,19 +10,18 @@ class STECore { private $tags; private $storage_access; private $cur_tpl_dir; + private $scope; /* * Variables: Public variables * * $blocks - Associative array of blocks (see the language definition). * $blockorder - The order of the blocks (an array) - * $vars - Associative array of all template variables. Use this to pass data to your templates. * $mute_runtime_errors - If true (default) a <RuntimeError> 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 <FatalRuntimeError> if a tag was called that was not registered, otherwise (default) a regular <RuntimeError> will be thrown and automatically handled by STE (see <$mute_runtime_errors>). */ public $blocks; public $blockorder; - public $vars; public $mute_runtime_errors = true; public $fatal_error_on_missing_tag = false; @@ -36,9 +35,10 @@ class STECore { $this->storage_access = $storage_access; $this->cur_tpl_dir = "/"; STEStandardLibrary::_register_lib($this); - $this->vars = array(); $this->blockorder = array(); $this->blocks = array(); + + $this->set_scope(new Scope(array())); } /* @@ -140,46 +140,11 @@ class STECore { * Returns: * A Reference to the variable. */ - public function &get_var_reference($name, $create_if_not_exist) { - $ref = &$this->_get_var_reference($this->vars, $name, $create_if_not_exist); + $ref = &$this->scope->get_var_reference($name, $create_if_not_exist); return $ref; } - private function &_get_var_reference(&$from, $name, $create_if_not_exist) { - $nullref = NULL; - - $bracket_open = strpos($name, "["); - if($bracket_open === false) { - if(isset($from[$name]) or $create_if_not_exist) { - $ref = &$from[$name]; - return $ref; - } else { - return $nullref; - } - } else { - $bracket_close = strpos($name, "]", $bracket_open); - if($bracket_close === false) { - throw new RuntimeError("Invalid varname \"$name\". Missing closing \"]\"."); - } - $varname = substr($name, 0, $bracket_open); - $name = substr($name, $bracket_open + 1, $bracket_close - $bracket_open - 1) . substr($name, $bracket_close + 1); - if(!is_array($from[$varname])) { - if($create_if_not_exist) { - $from[$varname] = array(); - } else { - return $nullref; - } - } - try { - $ref = &$this->_get_var_reference($from[$varname], $name, $create_if_not_exist); - return $ref; - } catch(Exception $e) { - throw new RuntimeError("Invalid varname \"$name\". Missing closing \"]\"."); - } - } - } - /* * Function: set_var_by_name * Set a template variable by its name. @@ -193,8 +158,11 @@ class STECore { * <RuntimeError> if the variable name can not be parsed (e.g. unbalanced brackets). */ public function set_var_by_name($name, $val) { - $ref = &$this->_get_var_reference($this->vars, $name, true); - $ref = $val; + $this->scope->set_var_by_name($name, $val); + } + + public function set_local_var($name, $val) { + $this->scope->set_local_var($name, $val); } /* @@ -212,8 +180,7 @@ class STECore { * The variables value. */ public function get_var_by_name($name) { - $ref = $this->_get_var_reference($this->vars, $name, false); - return $ref === NULL ? "" : $ref; + return $this->scope->get_var_by_name($name); } /* @@ -297,4 +264,65 @@ class STECore { 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/Scope.php b/Scope.php new file mode 100644 index 0000000..53566ed --- /dev/null +++ b/Scope.php @@ -0,0 +1,187 @@ +<?php + +namespace kch42\ste; + +class Scope implements \ArrayAccess { + private $parent = NULL; + private $vars = array(); + + private static function parse_name($name) { + $remain = $name; + $fields = array(); + + while($remain !== "") { + $br_open = strpos($remain, '['); + if($br_open === false) { + $fields[] = $remain; + break; + } + + $br_close = strpos($remain, ']', $br_open); + if($br_close === false) { + throw new RuntimeError("Invalid varname \"$name\". Missing closing \"]\"."); + } + + $fields[] = substr($remain, 0, $br_open); + + $field = substr($remain, $br_open+1, $br_close-$br_open-1); + $more = substr($remain, $br_close+1); + + if(strpos($field, '[') !== false) { + throw new RuntimeError("A variable field must not contain a '[' character."); + } + + if((strlen($more) > 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(); + } + + /* + * 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: + * <RuntimeError> 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, $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; + } + + /* + * 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: + * <RuntimeError> if the variable name can not be parsed (e.g. unbalanced brackets). + */ + 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; + } + + /* + * 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: + * <RuntimeError> if the variable name can not be parsed (e.g. unbalanced brackets). + * + * Returns: + * The variables value. + */ + 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/Transcompiler.php b/Transcompiler.php index 8b52fa2..b92663c 100644 --- a/Transcompiler.php +++ b/Transcompiler.php @@ -295,7 +295,7 @@ class Transcompiler { throw new ParseCompileError("self::Transcompile Error: name missing in <ste:mktag>.", $ast->tpl, $ast->offset); } - $fxbody = "\$outputstack = array(''); \$outputstack_i = 0;\$ste->vars['_tag_parameters'] = \$params;\n"; + $fxbody = "\$outputstack = array(''); \$outputstack_i = 0;\$ste->set_local_var('_tag_parameters', \$params);\n"; list($tagname, $tagname_pre) = self::_transcompile($ast->params["name"], true); @@ -312,7 +312,7 @@ class Transcompiler { $fxbody .= self::_transcompile($ast->sub); $fxbody .= "return array_pop(\$outputstack);"; - $code .= "\$tag_fx = function(\$ste, \$params, \$sub)" . $usemandatory . "\n{\n" . self::indent_code($fxbody) . "\n};\n"; + $code .= "\$tag_fx = \$ste->make_closure(function(\$ste, \$params, \$sub)" . $usemandatory . "\n{\n" . self::indent_code($fxbody) . "\n});\n"; $code .= $tagname_pre; $code .= "\$ste->register_tag($tagname, \$tag_fx);\n"; @@ -394,7 +394,7 @@ class Transcompiler { } $code .= "\$outputstack[\$outputstack_i] .= \$ste->call_tag('" . Misc::escape_text($node->name) . "', \$${paramarray}, "; - $code .= empty($node->sub) ? "function(\$ste) { return ''; }" : self::transcompile($node->sub); + $code .= empty($node->sub) ? "function(\$ste) { return ''; }" : ("\$ste->make_closure(" . self::transcompile($node->sub) . ")"); $code .= ");\n"; } } diff --git a/VarNotInScope.php b/VarNotInScope.php new file mode 100644 index 0000000..179d07a --- /dev/null +++ b/VarNotInScope.php @@ -0,0 +1,5 @@ +<?php + +namespace kch42\ste; + +class VarNotInScope extends \Exception {} @@ -30,6 +30,9 @@ require_once(__DIR__ . "/Calc.php"); require_once(__DIR__ . "/Parser.php"); require_once(__DIR__ . "/Transcompiler.php"); +require_once(__DIR__ . "/VarNotInScope.php"); +require_once(__DIR__ . "/Scope.php"); + require_once(__DIR__ . "/STEStandardLibrary.php"); require_once(__DIR__ . "/STECore.php"); diff --git a/tests/test_closure/.gitignore b/tests/test_closure/.gitignore new file mode 100644 index 0000000..de2a41b --- /dev/null +++ b/tests/test_closure/.gitignore @@ -0,0 +1,3 @@ +have +*.ast +*.transc.php diff --git a/tests/test_closure/code.php b/tests/test_closure/code.php new file mode 100644 index 0000000..de8c553 --- /dev/null +++ b/tests/test_closure/code.php @@ -0,0 +1,5 @@ +<?php + +function test_func($ste) { + +} diff --git a/tests/test_closure/test.tpl b/tests/test_closure/test.tpl new file mode 100644 index 0000000..c13bd5a --- /dev/null +++ b/tests/test_closure/test.tpl @@ -0,0 +1,8 @@ +<ste:mktag name="initfoo"> + <ste:set var="x"><ste:tagcontent /></ste:set> + <ste:mktag name="foo"> + <ste:calc>$x + <ste:tagcontent /></ste:calc> + </ste:mktag> +</ste:mktag> +<ste:initfoo>10</ste:initfoo> +<ste:foo>20</ste:foo>
\ No newline at end of file diff --git a/tests/test_closure/want b/tests/test_closure/want new file mode 100644 index 0000000..64bb6b7 --- /dev/null +++ b/tests/test_closure/want @@ -0,0 +1 @@ +30 diff --git a/tests/test_mktag/test.tpl b/tests/test_mktag/test.tpl index c381ef4..c446206 100644 --- a/tests/test_mktag/test.tpl +++ b/tests/test_mktag/test.tpl @@ -1,11 +1,10 @@ <ste:mktag name="double"><ste:calc>2 * <ste:tagcontent /></ste:calc></ste:mktag> <ste:mktag name="foo" mandatory="a|b"> - <ste:set var="b">$_tag_parameters[b]</ste:set><ste:comment>Ugly hack, since STE does no scoping...</ste:comment> <ste:for counter="i" start="0" stop="$_tag_parameters[a]"> <ste:if> <ste:even>$i</ste:even> <ste:then><ste:double>$i</ste:double></ste:then> - <ste:else>$b</ste:else> + <ste:else>$_tag_parameters[b]</ste:else> </ste:if> </ste:for> </ste:mktag> diff --git a/tests/test_recursive/.gitignore b/tests/test_recursive/.gitignore new file mode 100644 index 0000000..de2a41b --- /dev/null +++ b/tests/test_recursive/.gitignore @@ -0,0 +1,3 @@ +have +*.ast +*.transc.php diff --git a/tests/test_recursive/code.php b/tests/test_recursive/code.php new file mode 100644 index 0000000..4ca3852 --- /dev/null +++ b/tests/test_recursive/code.php @@ -0,0 +1,5 @@ +<?php + +function test_func($ste) { + $ste->mute_runtime_errors = false; +} diff --git a/tests/test_recursive/test.tpl b/tests/test_recursive/test.tpl new file mode 100644 index 0000000..bea3934 --- /dev/null +++ b/tests/test_recursive/test.tpl @@ -0,0 +1,11 @@ +<ste:mktag name="fac" mandatory="n"> + <ste:if> + ~{$_tag_parameters[n]|eq|0} + <ste:then>1</ste:then> + <ste:else> + <ste:set var="nextn"><ste:calc>$_tag_parameters[n] - 1</ste:calc></ste:set> + <ste:calc><ste:fac n="$nextn" /> * $_tag_parameters[n]</ste:calc> + </ste:else> + </ste:if> +</ste:mktag> +<ste:fac n="10" />
\ No newline at end of file diff --git a/tests/test_recursive/want b/tests/test_recursive/want new file mode 100644 index 0000000..3fbd4a8 --- /dev/null +++ b/tests/test_recursive/want @@ -0,0 +1 @@ +3628800 diff --git a/tests/test_scoping/.gitignore b/tests/test_scoping/.gitignore new file mode 100644 index 0000000..de2a41b --- /dev/null +++ b/tests/test_scoping/.gitignore @@ -0,0 +1,3 @@ +have +*.ast +*.transc.php diff --git a/tests/test_scoping/code.php b/tests/test_scoping/code.php new file mode 100644 index 0000000..de8c553 --- /dev/null +++ b/tests/test_scoping/code.php @@ -0,0 +1,5 @@ +<?php + +function test_func($ste) { + +} diff --git a/tests/test_scoping/test.tpl b/tests/test_scoping/test.tpl new file mode 100644 index 0000000..25a46e7 --- /dev/null +++ b/tests/test_scoping/test.tpl @@ -0,0 +1,6 @@ +<ste:mktag name="foo"> + <ste:set var="bla">!</ste:set> +</ste:mktag> + +<ste:foo /> +$bla
\ No newline at end of file diff --git a/tests/test_scoping/want b/tests/test_scoping/want new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_scoping/want |