diff --git a/src/Aspect.php b/src/Aspect.php index ed26211..c2e9d8f 100644 --- a/src/Aspect.php +++ b/src/Aspect.php @@ -234,7 +234,7 @@ class Aspect { */ public static function factory($source, $compile_dir = '/tmp', $options = 0) { if(is_string($source)) { - $provider = new \Aspect\Provider\FS($source); + $provider = new \Aspect\FSProvider($source); } elseif($source instanceof Aspect\ProviderInterface) { $provider = $source; } else { @@ -464,7 +464,7 @@ class Aspect { */ public function setOptions($options) { if(is_array($options)) { - $options = Aspect\Misc::makeMask($options, self::$_option_list); + $options = self::_makeMask($options, self::$_option_list); } $this->_storage = array(); $this->_options = $options; @@ -652,4 +652,28 @@ class Aspect { return Template::factory($this)->source($name, $code); } + + /** + * Create bit-mask from associative array use fully associative array possible keys with bit values + * @static + * @param array $values custom assoc array, ["a" => true, "b" => false] + * @param array $options possible values, ["a" => 0b001, "b" => 0b010, "c" => 0b100] + * @param int $mask the initial value of the mask + * @return int result, ( $mask | a ) & ~b + * @throws \RuntimeException if key from custom assoc doesn't exists into possible values + */ + private static function _makeMask(array $values, array $options, $mask = 0) { + foreach($values as $value) { + if(isset($options[$value])) { + if($options[$value]) { + $mask |= $options[$value]; + } else { + $mask &= ~$options[$value]; + } + } else { + throw new \RuntimeException("Undefined parameter $value"); + } + } + return $mask; + } } diff --git a/src/Aspect/Compiler.php b/src/Aspect/Compiler.php index cebece6..e387b6f 100644 --- a/src/Aspect/Compiler.php +++ b/src/Aspect/Compiler.php @@ -661,4 +661,53 @@ class Compiler { } } + /** + * Import macros from templates + * + * @param Tokenizer $tokens + * @param Template $tpl + * @return string + * @throws ImproperUseException + */ + public static function tagImport(Tokenizer $tokens, Template $tpl) { + $tpl->parseFirstArg($tokens, $name); + if(!$name) { + throw new ImproperUseException("Invalid usage tag {import}"); + } + $donor = $tpl->getStorage()->getRawTemplate()->load($name, true); + if(!empty($donor->_macros)) { + $tpl->_macros = array_merge($tpl->_macros, $donor->_macros); + $tpl->addDepend($donor); + + } + return ''; + } + + /** + * Declare or invoke macros + * + * @param Tokenizer $tokens + * @param Scope $scope + * @return string + */ + public static function macrosOpen(Tokenizer $tokens, Scope $scope) { + $tokens->get('.'); + $name = $tokens->get(Tokenizer::MACRO_STRING); + if($tokens->is('(')) { + $tokens->skip(); + + return ''; + } elseif(isset($scope->tpl->_macros[$name])) { + $p = $scope->tpl->parseParams($tokens); + $scope->closed = true; + return '$_tpl = $tpl; $tpl = '.self::_toArray($p).' + '.$scope->tpl->_macros[$name]["defaults"].'; '.$scope->tpl->_macros[$name]["body"].'; $tpl = $_tpl; unset($_tpl);'; + } else { + throw new ImproperUseException("Unknown tag or macros {{$name}}"); + } + } + + public static function macrosClose(Tokenizer $tokens, Scope $scope) { + $scope->tpl->_macros[ $scope["name"] ] = $scope->getContent(); + } + } diff --git a/src/Aspect/FSProvider.php b/src/Aspect/FSProvider.php new file mode 100644 index 0000000..0b5471e --- /dev/null +++ b/src/Aspect/FSProvider.php @@ -0,0 +1,129 @@ +isFile()) { + if(strpos($file->getBasename(), ",") !== 0) { + unlink($file->getRealPath()); + } + } elseif($file->isDir()) { + rmdir($file->getRealPath()); + } + } + } + } + + /** + * Recursive remove directory + * + * @param string $path + */ + public static function rm($path) { + self::clean($path); + if(is_dir($path)) { + rmdir($path); + } + } + + public static function put($path, $content) { + file_put_contents($path, $content); + } + + public function __construct($template_dir) { + if($_dir = realpath($template_dir)) { + $this->_path = $_dir; + } else { + throw new \LogicException("Template directory {$template_dir} doesn't exists"); + } + } + + /** + * + * @param string $tpl + * @param int $time + * @return string + */ + public function getSource($tpl, &$time) { + $tpl = $this->_getTemplatePath($tpl); + clearstatcache(null, $tpl); + $time = filemtime($tpl); + return file_get_contents($tpl); + } + + public function getLastModified($tpl) { + clearstatcache(null, $tpl = $this->_getTemplatePath($tpl)); + return filemtime($tpl); + } + + public function getList() { + + } + + /** + * Get template path + * @param $tpl + * @return string + * @throws \RuntimeException + */ + protected function _getTemplatePath($tpl) { + if(($path = realpath($this->_path."/".$tpl)) && strpos($path, $this->_path) === 0) { + return $path; + } else { + throw new \RuntimeException("Template $tpl not found"); + } + } + + /** + * @param string $tpl + * @return bool + */ + public function isTemplateExists($tpl) { + return file_exists($this->_path."/".$tpl); + } + + public function getLastModifiedBatch($tpls) { + $tpls = array_flip($tpls); + foreach($tpls as $tpl => &$time) { + $time = $this->getLastModified($tpl); + } + return $tpls; + } + + /** + * Verify templates by change time + * + * @param array $templates [template_name => modified, ...] By conversation you may trust the template's name + * @return bool + */ + public function verify(array $templates) { + foreach($templates as $template => $mtime) { + clearstatcache(null, $template = $this->_path.'/'.$template); + if(@filemtime($template) !== $mtime) { + return false; + } + + } + return true; + } +} \ No newline at end of file diff --git a/src/Aspect/Func.php b/src/Aspect/Func.php deleted file mode 100644 index e0459db..0000000 --- a/src/Aspect/Func.php +++ /dev/null @@ -1,17 +0,0 @@ -'.$params["text"].''; - } -} diff --git a/src/Aspect/Misc.php b/src/Aspect/Misc.php deleted file mode 100644 index 44c55fc..0000000 --- a/src/Aspect/Misc.php +++ /dev/null @@ -1,71 +0,0 @@ - true, "b" => false] - * @param array $options possible values, ["a" => 0b001, "b" => 0b010, "c" => 0b100] - * @param int $mask the initial value of the mask - * @return int result, ( $mask | a ) & ~b - * @throws \RuntimeException if key from custom assoc doesn't exists into possible values - */ - public static function makeMask(array $values, array $options, $mask = 0) { - foreach($values as $value) { - if(isset($options[$value])) { - if($options[$value]) { - $mask |= $options[$value]; - } else { - $mask &= ~$options[$value]; - } - } else { - throw new \RuntimeException("Undefined parameter $value"); - } - } - return $mask; - } - - /** - * Clean directory from files - * - * @param string $path - */ - public static function clean($path) { - if(is_file($path)) { - unlink($path); - } elseif(is_dir($path)) { - $iterator = iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, - \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST)); - foreach($iterator as $file) { - /* @var \splFileInfo $file*/ - if($file->isFile()) { - if(strpos($file->getBasename(), ",") !== 0) { - unlink($file->getRealPath()); - } - } elseif($file->isDir()) { - rmdir($file->getRealPath()); - } - } - } - } - - /** - * Recursive remove directory - * - * @param string $path - */ - public static function rm($path) { - self::clean($path); - if(is_dir($path)) { - rmdir($path); - } - } - - public static function put($path, $content) { - file_put_contents($path, $content); - } -} diff --git a/src/Aspect/Modifier.php b/src/Aspect/Modifier.php index 2fcab3f..e6d2ee2 100644 --- a/src/Aspect/Modifier.php +++ b/src/Aspect/Modifier.php @@ -1,19 +1,43 @@ + */ class Modifier { - public static function dateFormat($date, $format = "%b %e, %Y") { + /** + * Date format + * + * @param string|int $date + * @param string $format + * @return string + */ + public static function dateFormat($date, $format = "%b %e, %Y") { if(is_string($date) && !is_numeric($date)) { $date = strtotime($date); if(!$date) $date = time(); } - //dump($format, $date); return strftime($format, $date); } - public static function date($date, $format = "Y m d") { + /** + * @param string $date + * @param string $format + * @return string + */ + public static function date($date, $format = "Y m d") { if(is_string($date) && !is_numeric($date)) { $date = strtotime($date); if(!$date) $date = time(); @@ -21,6 +45,13 @@ class Modifier { return date($format, $date); } + /** + * Escape string + * + * @param string $text + * @param string $type + * @return string + */ public static function escape($text, $type = 'html') { switch($type) { case "url": @@ -32,7 +63,14 @@ class Modifier { } } - public static function unescape($text, $type = 'html') { + /** + * Unescape escaped string + * + * @param string $text + * @param string $type + * @return string + */ + public static function unescape($text, $type = 'html') { switch($type) { case "url": return urldecode($text); @@ -43,10 +81,14 @@ class Modifier { } } - public static function defaultValue(&$value, $default = null) { - return ($value === null) ? $default : $value; - } - + /** + * @param string $string + * @param int $length + * @param string $etc + * @param bool $break_words + * @param bool $middle + * @return string + */ public static function truncate($string, $length = 80, $etc = '...', $break_words = false, $middle = false) { $length -= min($length, strlen($etc)); if (!$break_words && !$middle) { @@ -73,4 +115,18 @@ class Modifier { return preg_replace('#[ \t]{2,}#', ' ', $str); } } + + /** + * @param mixed $item + * @return int + */ + public static function length($item) { + if(is_scalar($item)) { + return strlen($item); + } elseif (is_array($item)) { + return count($item); + } else { + return count((array)$item); + } + } } diff --git a/src/Aspect/Provider/FS.php b/src/Aspect/Provider/FS.php deleted file mode 100644 index c118195..0000000 --- a/src/Aspect/Provider/FS.php +++ /dev/null @@ -1,71 +0,0 @@ -_path = $_dir; - } else { - throw new \LogicException("Template directory {$template_dir} doesn't exists"); - } - } - - /** - * - * @param string $tpl - * @param int $time - * @return string - */ - public function getSource($tpl, &$time) { - $tpl = $this->_getTemplatePath($tpl); - clearstatcache(null, $tpl); - $time = filemtime($tpl); - return file_get_contents($tpl); - } - - public function getLastModified($tpl) { - clearstatcache(null, $tpl = $this->_getTemplatePath($tpl)); - return filemtime($tpl); - } - - public function getList() { - - } - - /** - * Get template path - * @param $tpl - * @return string - * @throws \RuntimeException - */ - protected function _getTemplatePath($tpl) { - if(($path = realpath($this->_path."/".$tpl)) && strpos($path, $this->_path) === 0) { - return $path; - } else { - throw new \RuntimeException("Template $tpl not found"); - } - } - - /** - * @param string $tpl - * @return bool - */ - public function isTemplateExists($tpl) { - return file_exists($this->_path."/".$tpl); - } - - public function getLastModifiedBatch($tpls) { - $tpls = array_flip($tpls); - foreach($tpls as $tpl => &$time) { - $time = $this->getLastModified($tpl); - } - return $tpls; - } -} \ No newline at end of file diff --git a/src/Aspect/ProviderInterface.php b/src/Aspect/ProviderInterface.php index a3f120b..de38066 100644 --- a/src/Aspect/ProviderInterface.php +++ b/src/Aspect/ProviderInterface.php @@ -20,7 +20,13 @@ interface ProviderInterface { */ public function getLastModified($tpl); - public function getLastModifiedBatch($tpls); + /** + * Verify templates by change time + * + * @param array $templates [template_name => modified, ...] By conversation you may trust the template's name + * @return bool + */ + public function verify(array $templates); /** * @return array diff --git a/src/Aspect/Template.php b/src/Aspect/Template.php index c455967..37bc388 100644 --- a/src/Aspect/Template.php +++ b/src/Aspect/Template.php @@ -1,10 +1,20 @@ */ class Template extends Render { @@ -303,7 +313,7 @@ class Template extends Render { $this->_ignore = true; $tokens->next(); $code = ''; - } else { + } else { $code = $this->_parseAct($tokens); } } @@ -326,6 +336,7 @@ class Template extends Render { /** * Close tag handler + * * @param Tokenizer $tokens * @return mixed * @throws TokenizeException @@ -358,14 +369,14 @@ class Template extends Render { if($tokens->is(Tokenizer::MACRO_STRING)) { $action = $tokens->current(); } else { - return 'echo '.$this->parseExp($tokens).';'; + return 'echo '.$this->parseExp($tokens).';'; // may be math and boolean expression } - if($tokens->isNext("(")) { + if($tokens->isNext("(", T_NAMESPACE, T_DOUBLE_COLON)) { // just invoke function or static method return "echo ".$this->parseExp($tokens).";"; } - if($act = $this->_aspect->getFunction($action)) { + if($act = $this->_aspect->getFunction($action)) { // call some function $tokens->next(); switch($act["type"]) { case Aspect::BLOCK_COMPILER: @@ -386,19 +397,32 @@ class Template extends Render { } } - for($j = $i = count($this->_stack)-1; $i>=0; $i--) { + for($j = $i = count($this->_stack)-1; $i>=0; $i--) { // call function's internal tag if($this->_stack[$i]->hasTag($action, $j - $i)) { $tokens->next(); return $this->_stack[$i]->tag($action, $tokens); } } - if($tags = $this->_aspect->getTagOwners($action)) { + if($tags = $this->_aspect->getTagOwners($action)) { // unknown template tag throw new TokenizeException("Unexpected tag '$action' (this tag can be used with '".implode("', '", $tags)."')"); } else { throw new TokenizeException("Unexpected tag $action"); } } + /** + * @param Tokenizer $tokens + */ + private function _parseMacros(Tokenizer $tokens) { + $tokens->get('.'); + $name = $tokens->get(Tokenizer::MACRO_STRING); + if($tokens->is('(')) { + $tokens->skip(); + } else { + + } + } + /** * Parse expressions. The mix of math operations, boolean operations, scalars, arrays and variables. * @@ -610,9 +634,9 @@ class Template extends Render { } $expr2 = $this->parseExp($tokens, true); if($empty) { - return '(empty('.$_var.') ? '.$expr2.' : '.$expr1; + return '(empty('.$_var.') ? '.$expr2.' : '.$expr1.')'; } else { - return '(isset('.$_var.') ? '.$expr1.' : '.$expr2; + return '(isset('.$_var.') ? '.$expr1.' : '.$expr2.')'; } } } elseif($t === "!") { @@ -676,11 +700,42 @@ class Template extends Render { $_str .= $tokens->current(); $tokens->next(); } elseif($t === T_VARIABLE) { - $_str .= '".$tpl["'.substr($tokens->current(), 1).'"]."'; + if(strlen($_str) > 1) { + $_str .= '".'; + } else { + $_str = ""; + } + $_str .= '$tpl["'.substr($tokens->current(), 1).'"]'; $tokens->next(); + if($tokens->is($stop)) { + $tokens->skip(); + return $_str; + } else { + $_str .= '."'; + } } elseif($t === T_CURLY_OPEN) { + if(strlen($_str) > 1) { + $_str .= '".'; + } else { + $_str = ""; + } $tokens->getNext(T_VARIABLE); - $_str .= '".('.$this->parseExp($tokens).')."'; + $_str .= '('.$this->parseExp($tokens).')'; + /*if(!$tokens->valid()) { + $more = $this->_getMoreSubstr($stop); + //var_dump($more); exit; + $tokens->append("}".$more, $p); + var_dump("Curly", $more, $tokens->getSnippetAsString()); + exit; + }*/ + + //$tokens->skip('}'); + if($tokens->is($stop)) { + $tokens->next(); + return $_str; + } else { + $_str .= '."'; + } } elseif($t === "}") { $tokens->next(); } elseif($t === $stop) { diff --git a/src/Aspect/Tokenizer.php b/src/Aspect/Tokenizer.php index 84c0620..b5b96ed 100644 --- a/src/Aspect/Tokenizer.php +++ b/src/Aspect/Tokenizer.php @@ -1,4 +1,12 @@ */ class Tokenizer { const TOKEN = 0; @@ -363,6 +374,10 @@ class Tokenizer { } } + public function count() { + return $this->_max; + } + /** * Return the key of the current element * @return mixed scalar on success, or null on failure. @@ -522,8 +537,9 @@ class Tokenizer { /** * Parse code and append tokens. This method move pointer to offset. + * * @param string $code - * @param int $offset + * @param int $offset if not -1 replace tokens from position $offset * @return Tokenizer */ public function append($code, $offset = -1) { diff --git a/tests/TestCase.php b/tests/TestCase.php index e496cb8..c337b3c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,6 +1,6 @@ aspect = Aspect::factory(ASPECT_RESOURCES.'/template', ASPECT_RESOURCES.'/compile'); } @@ -21,7 +21,7 @@ class TestCase extends \PHPUnit_Framework_TestCase { if(!file_exists(ASPECT_RESOURCES.'/template')) { mkdir(ASPECT_RESOURCES.'/template', 0777, true); } else { - Misc::clean(ASPECT_RESOURCES.'/template/'); + FS::clean(ASPECT_RESOURCES.'/template/'); } } diff --git a/tests/cases/Aspect/Template/ExtendsTest.php b/tests/cases/Aspect/ExtendsTemplateTest.php similarity index 95% rename from tests/cases/Aspect/Template/ExtendsTest.php rename to tests/cases/Aspect/ExtendsTemplateTest.php index 0780e9f..45b48be 100644 --- a/tests/cases/Aspect/Template/ExtendsTest.php +++ b/tests/cases/Aspect/ExtendsTemplateTest.php @@ -43,7 +43,7 @@ class ExtendsTemplateTest extends TestCase { * @param $vars * @param $result */ - public function testDynamicExtends($name, $code, $vars, $result) { + public function _testDynamicExtends($name, $code, $vars, $result) { static $i = 0; $vars["iteration"] = $i++; $this->execTpl($name, $code, $vars, $result); @@ -60,7 +60,7 @@ class ExtendsTemplateTest extends TestCase { /** * @group extends */ - public function _testChildLevel1() { + public function testChildLevel1() { //echo($this->aspect->fetch("child1.tpl", array("a" => "a char"))); exit; } diff --git a/tests/cases/Aspect/Provider/ProviderTest.php b/tests/cases/Aspect/ProviderTest.php similarity index 92% rename from tests/cases/Aspect/Provider/ProviderTest.php rename to tests/cases/Aspect/ProviderTest.php index ed51a7b..52d0ede 100644 --- a/tests/cases/Aspect/Provider/ProviderTest.php +++ b/tests/cases/Aspect/ProviderTest.php @@ -1,10 +1,10 @@ tpl("template1.tpl", 'Template 1 {$a}'); $this->tpl("template2.tpl", 'Template 2 {$a}'); - $this->provider = new FS(ASPECT_RESOURCES.'/template'); + $this->provider = new FSProvider(ASPECT_RESOURCES.'/template'); } public function testIsTemplateExists() { diff --git a/tests/cases/Aspect/TemplateTest.php b/tests/cases/Aspect/TemplateTest.php index 2d9d43e..9ffdb87 100644 --- a/tests/cases/Aspect/TemplateTest.php +++ b/tests/cases/Aspect/TemplateTest.php @@ -15,6 +15,10 @@ class TemplateTest extends TestCase { ))); } + /*public function testSandbox() { + var_dump($this->aspect->compileCode('{"$s:{$b+1}f d {$d}"}')->_body); + exit; + }*/ public static function providerVars() { $a = array("a" => "World"); @@ -69,6 +73,42 @@ class TemplateTest extends TestCase { ); } + public static function providerScalars() { + return array( + array('77', 77), + array('-33', -33), + array('0.2', 0.2), + array('-0.3', -0.3), + array('1e6', 1e6), + array('-2e6', -2e6), + array('"str"', 'str'), + array('"str\nand\nmany\nlines"', "str\nand\nmany\nlines"), + array('"str and \'substr\'"', "str and 'substr'"), + array('"str and \"substr\""', 'str and "substr"'), + array("'str'", 'str'), + array("'str\\nin\\none\\nline'", 'str\nin\none\nline'), + array("'str and \"substr\"'", 'str and "substr"'), + array("'str and \'substr\''", "str and 'substr'"), + array('"$one"', '1'), + array('"$one $two"', '1 2'), + array('"$one and $two"', '1 and 2'), + array('"a $one and $two b"', 'a 1 and 2 b'), + array('"{$one}"', '1'), + array('"a {$one} b"', 'a 1 b'), + array('"{$one + 2}"', '3'), + array('"{$one * $two + 1}"', '3'), + array('"{$one} and {$two}"', '1 and 2'), + array('"$one and {$two}"', '1 and 2'), + array('"{$one} and $two"', '1 and 2'), + array('"a {$one} and {$two} b"', 'a 1 and 2 b'), + array('"{$one+1} and {$two-1}"', '2 and 1'), + array('"a {$one+1} and {$two-1} b"', 'a 2 and 1 b'), + array('"a {$one|dots} and {$two|dots} b"', 'a 1... and 2... b'), + array('"a {$one|dots} and $two b"', 'a 1... and 2 b'), + array('"a $one and {$two|dots} b"', 'a 1 and 2... b'), + ); + } + public static function providerVarsInvalid() { return array( array('hello, {$a.}!', 'Aspect\CompileException', "Unexpected end of expression"),