diff --git a/README.md b/README.md index cd3c301..3de58d4 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,13 @@ Aspect PHP Template Engine Features: -* Simple Smarty-like syntax -* Fast -* Secure -* No one regexp +* Simple Smarty-like [syntax](./docs/syntax.md) +* [Fast](./docs/benchmark.md) +* [Secure](./docs/settings.md) +* Without regexp +* [Flexible](./docs/main.md#extends) +* [Lightweight](./docs/benchmark.md#satistic) +* Easy to use Primitive template diff --git a/src/Aspect.php b/src/Aspect.php index d803069..b87ecb4 100644 --- a/src/Aspect.php +++ b/src/Aspect.php @@ -102,18 +102,18 @@ class Aspect { * @var array of modifiers [modifier_name => callable] */ protected $_modifiers = array( - "upper" => 'strtoupper', - "up" => 'strtoupper', - "lower" => 'strtolower', - "low" => 'strtolower', + "upper" => 'strtoupper', + "up" => 'strtoupper', + "lower" => 'strtolower', + "low" => 'strtolower', "date_format" => 'Aspect\Modifier::dateFormat', - "date" => 'Aspect\Modifier::date', - "truncate" => 'Aspect\Modifier::truncate', - "escape" => 'Aspect\Modifier::escape', - "e" => 'Aspect\Modifier::escape', // alias of escape - "unescape" => 'Aspect\Modifier::unescape', - "strip" => 'Aspect\Modifier::strip', - "default" => 'Aspect\Modifier::defaultValue' + "date" => 'Aspect\Modifier::date', + "truncate" => 'Aspect\Modifier::truncate', + "escape" => 'Aspect\Modifier::escape', + "e" => 'Aspect\Modifier::escape', // alias of escape + "unescape" => 'Aspect\Modifier::unescape', + "strip" => 'Aspect\Modifier::strip', + "default" => 'Aspect\Modifier::defaultValue' ); /** @@ -341,7 +341,7 @@ class Aspect { /** * @param string $function * @param callable $callback - * @param callable $parser + * @param callable|string $parser * @return Aspect */ public function addFunction($function, $callback, $parser = self::DEFAULT_FUNC_PARSER) { @@ -436,7 +436,10 @@ class Aspect { } } - + /** + * @param string $tag + * @return array + */ public function getTagOwners($tag) { $tags = array(); foreach($this->_actions as $owner => $params) { diff --git a/src/Aspect/Compiler.php b/src/Aspect/Compiler.php index 05e23e4..6464dd9 100644 --- a/src/Aspect/Compiler.php +++ b/src/Aspect/Compiler.php @@ -377,13 +377,12 @@ class Compiler { $tpl_name = $tpl->parseFirstArg($tokens, $name); $tpl->addPostCompile(__CLASS__."::extendBody"); if($name) { // static extends - //$tpl->_static = true; $tpl->_extends = $tpl->getStorage()->getRawTemplate()->load($name, false); + $tpl->_compatible = &$tpl->_extends->_compatible; $tpl->addDepend($tpl->_extends); // for valid compile-time need take template from storage return ""; } else { // dynamic extends $tpl->_extends = $tpl_name; - //$tpl->_static = false; return '$parent = $tpl->getStorage()->getTemplate('.$tpl_name.');'; } } @@ -397,7 +396,14 @@ class Compiler { if(isset($tpl->_extends)) { // is child if(is_object($tpl->_extends)) { // static extends /* @var Template $t */ - $t = $tpl->_extends; + $tpl->_extends->_extended = true; + $tpl->_extends->blocks = &$tpl->blocks; + $tpl->_extends->compile(); + if($tpl->_compatible) { + $body .= $tpl->_extends->_body; + } else { + $body = $tpl->_extends->_body; + } if(empty($tpl->_dynamic)) { do { $t->_blocks = &$tpl->_blocks; @@ -419,7 +425,7 @@ class Compiler { $body = ''.$body.''.$t->_body; } } else { // dynamic extends - $body .= 'blocks = &$tpl->blocks; $parent->display((array)$tpl); unset($tpl->blocks, $parent->blocks); ?>'; + $body = ''.$body.'b = &$tpl->b; $parent->display((array)$tpl); unset($tpl->b, $parent->b); ?>'; } } } @@ -444,29 +450,8 @@ class Compiler { */ public static function tagBlockOpen(Tokenizer $tokens, Scope $scope) { $p = $scope->tpl->parseFirstArg($tokens, $name); - if ($name) { - $scope["name"] = $name; - $scope["cname"] = $p; - } else { - $scope->tpl->_dynamic = true; - $scope["name"] = $scope["cname"] = $p; - } - /*if($scope->level) { - $scope->tpl->_static = false; - }*/ - if(isset($scope->tpl->_extends)) { // is child - return 'if(empty($tpl->blocks['.$scope["cname"].'])) { $tpl->blocks['.$scope["cname"].'] = function($tpl) {'; - } else { // is parent - if(isset($scope->tpl->_blocks)) { // has blocks from child - if(isset($scope->tpl->_blocks[ $scope["name"] ])) { // skip own block and insert child's block after - $scope["body"] = $scope->tpl->_body; - $scope->tpl->_body = ""; - } // else just put block content as is - return ''; - } else { - return 'if(isset($tpl->blocks['.$scope["cname"].'])) { echo $tpl->blocks['.$scope["cname"].']->__invoke($tpl); } else {'; - } - } + $scope["name"] = false; + $scope["cname"] = $p; } /** @@ -476,25 +461,59 @@ class Compiler { * @return string */ public static function tagBlockClose($tokens, Scope $scope) { - - if(isset($scope->tpl->_extends)) { // is child - if(!isset($scope->tpl->_blocks[ $scope["name"] ])) { - $scope->tpl->_blocks[ $scope["name"] ] = $scope->getContent(); - } // dynamic extends - return '}; }'; - } else { // is parent - if(isset($scope->tpl->_blocks)) { - if(isset($scope["body"])) { - $scope->tpl->_body = $scope["body"]. - 'blocks['.$scope["cname"].'])) { echo $tpl->blocks['.$scope["cname"].']->__invoke($tpl); } else {?>'. - $scope->tpl->_blocks[ $scope["name"] ].'}'; - return ""; + $tpl = $scope->tpl; + if(isset($tpl->_extends)) { // is child + if($scope["name"]) { // is scalar name + if(!isset($tpl->blocks[ $scope["name"] ])) { // is block still doesn't preset + if($tpl->_compatible) { // is compatible mode + $scope->replace( + 'if(empty($tpl->blocks['.$scope["cname"].'])) { '. + '$tpl->b['.$scope["cname"].'] = function($tpl) {'. + $scope->getContent(). + "};". + "}\n" + ); + } else { + $tpl->blocks[ $scope["name"] ] = $scope->getContent(); + $scope->replace( + '$tpl->b['.$scope["cname"].'] = function($tpl) {'. + $scope->getContent(). + "};\n" + ); + } + } + } else { // dynamic name + $tpl->_compatible = true; // go to compatible mode + $scope->replace( + 'if(empty($tpl->b['.$scope["cname"].'])) { '. + '$tpl->b['.$scope["cname"].'] = function($tpl) {'. + $scope->getContent(). + "};". + "}\n" + ); + } + } else { // is parent + if(isset($tpl->blocks[ $scope["name"] ])) { // has block + if($tpl->_compatible) { + $scope->tpl->replace( + 'blocks['.$scope["cname"].'])) { echo $tpl->blocks['.$scope["cname"].']->__invoke($tpl); } else {?>'. + $tpl->blocks[ $scope["body"] ]. + '}' + ); + } else { + $tpl->replace($tpl->blocks[ $scope["name"] ]); } - return ""; } else { - return '}'; + if(isset($tpl->_extended)) { + $scope->tpl->replace( + 'blocks['.$scope["cname"].'])) { echo $tpl->blocks['.$scope["cname"].']->__invoke($tpl); } else {?>'. + $scope->getContent(). + '}' + ); + } } } + return ''; } public static function tagParent($tokens, Scope $scope) { diff --git a/src/Aspect/Scope.php b/src/Aspect/Scope.php index a38fbea..f722375 100644 --- a/src/Aspect/Scope.php +++ b/src/Aspect/Scope.php @@ -14,8 +14,6 @@ class Scope extends \ArrayObject { * @var Template */ public $tpl; - public $closed = false; - public $is_next_close = false; public $is_compiler = true; private $_action; private static $count = 0; @@ -108,4 +106,19 @@ class Scope extends \ArrayObject { throw new \LogicException("Trying get content of non-block scope"); } } + + /** + * @return string + * @throws \LogicException + */ + public function cutContent() { + if($pos = strpos($this->tpl->_body, "/*#{$this->id}#*/")) { + $begin = strpos($this->tpl->_body, "?>", $pos); + $content = substr($this->tpl->_body, $begin + 2); + $this->tpl->_body = substr($this->tpl->_body, 0, $begin + 1); + return $content; + } else { + throw new \LogicException("Trying cut content of non-block scope"); + } + } } \ No newline at end of file diff --git a/src/Aspect/Template.php b/src/Aspect/Template.php index d350ef8..3ffc4ba 100644 --- a/src/Aspect/Template.php +++ b/src/Aspect/Template.php @@ -134,6 +134,7 @@ class Template extends Render { return; } $pos = 0; + $frag = ""; while(($start = strpos($this->_src, '{', $pos)) !== false) { // search open-char of tags switch($this->_src[$start + 1]) { // check next char case "\n": case "\r": case "\t": case " ": case "}": // ignore the tag @@ -141,8 +142,8 @@ class Template extends Render { continue 2; case "*": // if comment block $end = strpos($this->_src, '*}', $start); // finding end of the comment block - $frag = substr($this->_src, $this->_pos, $start - $end); // read the comment block for precessing - $this->_line += substr_count($frag, "\n"); // count skipped lines + $_frag = substr($this->_src, $this->_pos, $start - $end); // read the comment block for precessing + $this->_line += substr_count($_frag, "\n"); // count skipped lines $pos = $end + 1; // trying finding tags after the comment block continue 2; } @@ -150,37 +151,33 @@ class Template extends Render { if(!$end) { // if unexpected end of template throw new CompileException("Unclosed tag in line {$this->_line}", 0, 1, $this->_name, $this->_line); } - $frag = substr($this->_src, $this->_pos, $start - $this->_pos); // variable $frag contains chars after last '}' and next '{' + $frag .= substr($this->_src, $this->_pos, $start - $this->_pos); // variable $frag contains chars after last '}' and next '{' $tag = substr($this->_src, $start, $end - $start + 1); // variable $tag contains aspect tag '{...}' $this->_line += substr_count($this->_src, "\n", $this->_pos, $end - $start + 1); // count lines in $frag and $tag (using original text $code) $pos = $this->_pos = $end + 1; // move search-pointer to end of the tag - //if($this->_trim) { // if previous tag has trim flag - // $frag = ltrim($frag); - //} - $frag = str_replace("'."\n", $frag); - - $this->_body .= $this->_tag($tag, $frag); // dispatching tags - - // duplicate new line http://docs.php.net/manual/en/language.basic-syntax.instruction-separation.php - if(substr($this->_body, -2) === "?>" && isset($this->_src[ $end+1 ])) { - $c = $this->_src[ $end+1 ]; - if($c === "\n") { - $this->_body .= "\n"; - } elseif($c === "\r") { - if(isset($this->_src[ $end+2 ]) && $this->_src[ $end+2 ] == "\n") { - $this->_body .= "\r\n"; - } else { - $this->_body .= "\r"; - } - } + if($tag[strlen($tag) - 2] === "-") { + $_tag = substr($tag, 1, -2); + $_frag = rtrim($frag); + } else { + $_tag = substr($tag, 1, -1); + $_frag = $frag; } - //if($this->_trim) { // if current tag has trim flag - // $frag = rtrim($frag); - //} - //$this->_body .= $tag; + if($this->_ignore) { + if($_tag === '/ignore') { + $this->_ignore = false; + $this->_appendText($_frag); + } else { + $frag .= $tag; + continue; + } + } else { + $this->_appendText($_frag); + $this->_appendCode($this->_tag($_tag)); + } + $frag = ""; } - $this->_body .= str_replace("', substr($this->_src, $this->_pos)); + $this->_appendText(substr($this->_src, $this->_pos)); if($this->_stack) { $_names = array(); $_line = 0; @@ -200,7 +197,28 @@ class Template extends Render { } } - public function addPostCompile($cb) { + /** + * Append plain text to template body + * + * @param string $text + */ + private function _appendText($text) { + $this->_body .= str_replace("'.PHP_EOL, $text); + } + + /** + * Append PHP code to template body + * + * @param string $code + */ + private function _appendCode($code) { + $this->_body .= $code; + } + + /** + * @param callable[] $cb + */ + public function addPostCompile(array $cb) { $this->_post[] = $cb; } @@ -283,34 +301,15 @@ class Template extends Render { /** * Internal tags router * @param string $src - * @param string $frag * @throws UnexpectedException * @throws CompileException * @throws SecurityException - * @return string + * @return string executable PHP code */ - private function _tag($src, &$frag) { - if($src[strlen($src) - 2] === "-") { - $token = substr($src, 1, -2); - //$frag = ltrim($frag); - } else { - $token = substr($src, 1, -1); - } - - $this->_body .= $frag; - $token = trim($token); - if($this->_ignore) { - if($token === '/ignore') { - $this->_ignore = false; - return ''; - } else { - return $src; - } - } - - $tokens = new Tokenizer($token); + private function _tag($src) { + $tokens = new Tokenizer($src); try { - switch($token[0]) { + switch($src[0]) { case '"': case '\'': case '$': @@ -428,19 +427,6 @@ class Template extends Render { } } - /** - * @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. * @@ -459,7 +445,6 @@ class Template extends Render { $cond = false; while($tokens->valid()) { if(!$term && $tokens->is(Tokenizer::MACRO_SCALAR, '"', '`', T_ENCAPSED_AND_WHITESPACE)) { - $_exp .= $this->parseScalar($tokens, true); $term = 1; } elseif(!$term && $tokens->is(T_VARIABLE)) {