Big update

This commit is contained in:
bzick 2013-02-21 22:51:24 +04:00
parent b2f2e61be4
commit ee9cd9b746
14 changed files with 409 additions and 193 deletions

View File

@ -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;
}
}

View File

@ -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();
}
}

129
src/Aspect/FSProvider.php Normal file
View File

@ -0,0 +1,129 @@
<?php
namespace Aspect;
use Aspect\ProviderInterface;
/**
* Templates provider
* @author Ivan Shalganov
*/
class FSProvider implements ProviderInterface {
private $_path;
/**
* 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);
}
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;
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace Aspect;
class Func {
public static function mailto($params) {
if(empty($params["address"])) {
trigger_error(E_USER_WARNING, "Modifier mailto: paramenter 'address' required");
return "";
}
if(empty($params["text"])) {
$params["text"] = $params["address"];
}
return '<a href="mailto:'.$params["address"].'">'.$params["text"].'</a>';
}
}

View File

@ -1,71 +0,0 @@
<?php
namespace Aspect;
class Misc {
/**
* 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
*/
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);
}
}

View File

@ -1,19 +1,43 @@
<?php
/*
* This file is part of Aspect.
*
* (c) 2013 Ivan Shalganov
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Aspect;
/**
* Collection of modifiers
*
* @package aspect
* @author Ivan Shalganov <owner@bzick.net>
*/
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);
}
}
}

View File

@ -1,71 +0,0 @@
<?php
namespace Aspect\Provider;
use Aspect\ProviderInterface;
/**
* Templates provider
* @author Ivan Shalganov
*/
class FS implements ProviderInterface {
private $_path;
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;
}
}

View File

@ -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

View File

@ -1,10 +1,20 @@
<?php
/*
* This file is part of Aspect.
*
* (c) 2013 Ivan Shalganov
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Aspect;
use Aspect;
/**
* Aspect template compiler
* Template compiler
*
* @package aspect
* @author Ivan Shalganov <owner@bzick.net>
*/
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) {

View File

@ -1,4 +1,12 @@
<?php
/*
* This file is part of Aspect.
*
* (c) 2013 Ivan Shalganov
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Aspect;
defined('T_INSTEADOF') || define('T_INSTEADOF', 341);
@ -16,6 +24,9 @@ defined('T_TRAIT_C') || define('T_TRAIT_C', 365);
* @property array $prev the previous token
* @property array $curr the current token
* @property array $next the next token
*
* @package aspect
* @author Ivan Shalganov <owner@bzick.net>
*/
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) {

View File

@ -1,6 +1,6 @@
<?php
namespace Aspect;
use Aspect;
use Aspect, Aspect\FSProvider as FS;
class TestCase extends \PHPUnit_Framework_TestCase {
/**
@ -12,7 +12,7 @@ class TestCase extends \PHPUnit_Framework_TestCase {
if(!file_exists(ASPECT_RESOURCES.'/compile')) {
mkdir(ASPECT_RESOURCES.'/compile', 0777, true);
} else {
Misc::clean(ASPECT_RESOURCES.'/compile/');
FS::clean(ASPECT_RESOURCES.'/compile/');
}
$this->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/');
}
}

View File

@ -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;
}

View File

@ -1,10 +1,10 @@
<?php
namespace Aspect\Provider;
namespace Aspect;
use Aspect;
class FSTest extends \Aspect\TestCase {
class FSProviderTest extends \Aspect\TestCase {
/**
* @var Provider
* @var FSProvider
*/
public $provider;
@ -12,7 +12,7 @@ class FSTest extends \Aspect\TestCase {
parent::setUp();
$this->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() {

View File

@ -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"),