Add macro support

This commit is contained in:
bzick 2013-02-23 02:03:05 +04:00
parent 2acb0feff9
commit 2ba3df6536
4 changed files with 152 additions and 56 deletions

View File

@ -24,9 +24,9 @@ class Compiler {
if($name && ($tpl->getStorage()->getOptions() & \Aspect::FORCE_INCLUDE)) { // if FORCE_INCLUDE enabled and template name known if($name && ($tpl->getStorage()->getOptions() & \Aspect::FORCE_INCLUDE)) { // if FORCE_INCLUDE enabled and template name known
$inc = $tpl->getStorage()->compile($name, false); $inc = $tpl->getStorage()->compile($name, false);
$tpl->addDepend($inc); $tpl->addDepend($inc);
return '$_tpl = (array)$tpl; $tpl->exchangeArray('.self::_toArray($p).'+$_tpl); ?>'.$inc->_body.'<?php $tpl->exchangeArray($_tpl); unset($_tpl);'; return '$_tpl = (array)$tpl; $tpl->exchangeArray('.self::toArray($p).'+$_tpl); ?>'.$inc->_body.'<?php $tpl->exchangeArray($_tpl); unset($_tpl);';
} else { } else {
return '$tpl->getStorage()->getTemplate('.$cname.')->display('.self::_toArray($p).'+(array)$tpl);'; return '$tpl->getStorage()->getTemplate('.$cname.')->display('.self::toArray($p).'+(array)$tpl);';
} }
} else { } else {
if($name && ($tpl->getStorage()->getOptions() & \Aspect::FORCE_INCLUDE)) { // if FORCE_INCLUDE enabled and template name known if($name && ($tpl->getStorage()->getOptions() & \Aspect::FORCE_INCLUDE)) { // if FORCE_INCLUDE enabled and template name known
@ -523,7 +523,7 @@ class Compiler {
* @return string * @return string
*/ */
public static function stdFuncParser($function, Tokenizer $tokens, Template $tpl) { public static function stdFuncParser($function, Tokenizer $tokens, Template $tpl) {
return "echo $function(".self::_toArray($tpl->parseParams($tokens)).', $tpl);'; return "echo $function(".self::toArray($tpl->parseParams($tokens)).', $tpl);';
} }
/** /**
@ -564,7 +564,7 @@ class Compiler {
* @return string * @return string
*/ */
public static function stdFuncOpen(Tokenizer $tokens, Scope $scope) { public static function stdFuncOpen(Tokenizer $tokens, Scope $scope) {
$scope["params"] = self::_toArray($scope->tpl->parseParams($tokens)); $scope["params"] = self::toArray($scope->tpl->parseParams($tokens));
return 'ob_start();'; return 'ob_start();';
} }
@ -580,7 +580,12 @@ class Compiler {
return "echo ".$scope["function"].'('.$scope["params"].', ob_get_clean(), $tpl);'; return "echo ".$scope["function"].'('.$scope["params"].', ob_get_clean(), $tpl);';
} }
private static function _toArray($params) { /**
* Convert array of code to string array
* @param $params
* @return string
*/
public static function toArray($params) {
$_code = array(); $_code = array();
foreach($params as $k => $v) { foreach($params as $k => $v) {
$_code[] = '"'.$k.'" => '.$v; $_code[] = '"'.$k.'" => '.$v;
@ -689,26 +694,45 @@ class Compiler {
* @param Tokenizer $tokens * @param Tokenizer $tokens
* @param Scope $scope * @param Scope $scope
* @throws ImproperUseException * @throws ImproperUseException
* @return string
*/ */
public static function macroOpen(Tokenizer $tokens, Scope $scope) { public static function macroOpen(Tokenizer $tokens, Scope $scope) {
$tokens->get('.'); $scope["name"] = $tokens->get(Tokenizer::MACRO_STRING);
$name = $tokens->get(Tokenizer::MACRO_STRING); $scope["args"] = array();
if($tokens->is('(')) { $scope["defaults"] = array();
$tokens->skip(); if(!$tokens->valid()) {
return;
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}}");
} }
$tokens->next()->need('(')->next();
if($tokens->is(')')) {
return;
}
while($tokens->is(Tokenizer::MACRO_STRING)) {
$scope["args"][] = $param = $tokens->getAndNext();
if($tokens->is('=')) {
if($tokens->is(T_CONSTANT_ENCAPSED_STRING, T_LNUMBER, T_DNUMBER) || $tokens->isSpecialVal()) {
$scope["defaults"][ $param ] = $tokens->current();
} else {
throw new ImproperUseException("Macro parameters may have only scalar defaults");
}
}
$tokens->skipIf(',');
}
$tokens->skipIf(')');
return;
} }
/**
* @param Tokenizer $tokens
* @param Scope $scope
*/
public static function macroClose(Tokenizer $tokens, Scope $scope) { public static function macroClose(Tokenizer $tokens, Scope $scope) {
$scope->tpl->_macros[ $scope["name"] ] = $scope->getContent(); $scope->tpl->macros[ $scope["name"] ] = array(
"body" => $content = $scope->getContent(),
"args" => $scope["args"],
"defaults" => $scope["defaults"]
);
$scope->tpl->_body = substr($scope->tpl->_body, 0, strlen($scope->tpl->_body) - strlen($content));
} }
} }

View File

@ -30,6 +30,11 @@ class Template extends Render {
* @var string * @var string
*/ */
public $_body; public $_body;
/**
* @var array of macros
*/
public $macros = array();
/** /**
* Call stack * Call stack
* @var Scope[] * @var Scope[]
@ -58,11 +63,12 @@ class Template extends Render {
*/ */
private $_options = 0; private $_options = 0;
/** System variables {$smarty.<...>} or {$aspect.<...>} /**
* @var array * Just factory
*
* @param \Aspect $aspect
* @return Template
*/ */
public static $sysvar = array('$aspect' => 1, '$smarty' => 1);
public static function factory(Aspect $aspect) { public static function factory(Aspect $aspect) {
return new static($aspect); return new static($aspect);
} }
@ -305,6 +311,9 @@ class Template extends Render {
case '$': case '$':
$code = "echo ".$this->parseExp($tokens).";"; $code = "echo ".$this->parseExp($tokens).";";
break; break;
case '#':
$code = "echo ".$this->parseConst($tokens);
break;
case '/': case '/':
$code = $this->_end($tokens); $code = $this->_end($tokens);
break; break;
@ -365,19 +374,24 @@ class Template extends Render {
* @return string * @return string
*/ */
private function _parseAct(Tokenizer $tokens) { private function _parseAct(Tokenizer $tokens) {
if($tokens->is(Tokenizer::MACRO_STRING)) { if($tokens->is(Tokenizer::MACRO_STRING)) {
$action = $tokens->current(); $action = $tokens->getAndNext();
} else { } else {
return 'echo '.$this->parseExp($tokens).';'; // may be math and boolean expression return 'echo '.$this->parseExp($tokens).';'; // may be math and boolean expression
} }
if($tokens->isNext("(", T_NAMESPACE, T_DOUBLE_COLON)) { // just invoke function or static method if($tokens->is("(", T_NAMESPACE, T_DOUBLE_COLON)) { // just invoke function or static method
$tokens->back();
return "echo ".$this->parseExp($tokens).";"; return "echo ".$this->parseExp($tokens).";";
} elseif($tokens->is('.')) {
$name = $tokens->skip()->get(Tokenizer::MACRO_STRING);
if($action !== "macro") {
$name = $action.".".$name;
}
return $this->parseMacro($tokens, $name);
} }
if($act = $this->_aspect->getFunction($action)) { // call some function if($act = $this->_aspect->getFunction($action)) { // call some function
$tokens->next();
switch($act["type"]) { switch($act["type"]) {
case Aspect::BLOCK_COMPILER: case Aspect::BLOCK_COMPILER:
$scope = new Scope($action, $this, $this->_line, $act, count($this->_stack)); $scope = new Scope($action, $this, $this->_line, $act, count($this->_stack));
@ -399,7 +413,6 @@ class Template extends Render {
for($j = $i = count($this->_stack)-1; $i>=0; $i--) { // call function's internal tag for($j = $i = count($this->_stack)-1; $i>=0; $i--) { // call function's internal tag
if($this->_stack[$i]->hasTag($action, $j - $i)) { if($this->_stack[$i]->hasTag($action, $j - $i)) {
$tokens->next();
return $this->_stack[$i]->tag($action, $tokens); return $this->_stack[$i]->tag($action, $tokens);
} }
} }
@ -452,6 +465,9 @@ class Template extends Render {
} else { } else {
$term = 1; $term = 1;
} }
} elseif(!$term && $tokens->is('#')) {
$term = 1;
$_exp .= $this->parseConst($tokens);
} elseif(!$term && $tokens->is("(")) { } elseif(!$term && $tokens->is("(")) {
$_exp .= $tokens->getAndNext(); $_exp .= $tokens->getAndNext();
$brackets++; $brackets++;
@ -553,11 +569,7 @@ class Template extends Render {
public function parseVar(Tokenizer $tokens, $deny = 0, &$pure_var = true) { public function parseVar(Tokenizer $tokens, $deny = 0, &$pure_var = true) {
$var = $tokens->get(T_VARIABLE); $var = $tokens->get(T_VARIABLE);
$pure_var = true; $pure_var = true;
if(isset(self::$sysvar[ $var ])) { $_var = '$tpl["'.ltrim($var,'$').'"]';
$_var = $this->_parseSystemVar($tokens);
} else {
$_var = '$tpl["'.ltrim($var,'$').'"]';
}
$tokens->next(); $tokens->next();
while($t = $tokens->key()) { while($t = $tokens->key()) {
if($t === "." && !($deny & self::DENY_ARRAY)) { if($t === "." && !($deny & self::DENY_ARRAY)) {
@ -846,7 +858,7 @@ class Template extends Render {
$key = true; $key = true;
$val = false; $val = false;
$_arr .= $tokens->getAndNext().' '; $_arr .= $tokens->getAndNext().' ';
} elseif($tokens->is(Tokenizer::MACRO_SCALAR, T_VARIABLE, T_STRING, T_EMPTY, T_ISSET, "(") && !$val) { } elseif($tokens->is(Tokenizer::MACRO_SCALAR, T_VARIABLE, T_STRING, T_EMPTY, T_ISSET, "(", "#") && !$val) {
$_arr .= $this->parseExp($tokens, true); $_arr .= $this->parseExp($tokens, true);
$key = false; $key = false;
$val = true; $val = true;
@ -874,29 +886,59 @@ class Template extends Render {
} }
/** /**
* Parse system variable, like $aspect, $smarty * Parse constant
* #Ns\MyClass::CONST1, #CONST1, #MyClass::CONST1
* *
* @param Tokenizer $tokens * @param Tokenizer $tokens
* @throws \LogicException * @return string
* @return mixed|string * @throws ImproperUseException
*/ */
private function _parseSystemVar(Tokenizer $tokens) { public function parseConst(Tokenizer $tokens) {
$tokens->getNext("."); $tokens->get('#');
$key = $tokens->getNext(T_STRING, T_CONST); $name = $tokens->getNext(T_STRING);
switch($key) { $tokens->next();
case 'get': return '$_GET'; if($tokens->is(T_NAMESPACE)) {
case 'post': return '$_POST'; $name .= '\\';
case 'cookies': return '$_COOKIES'; $name .= $tokens->getNext(T_STRING);
case 'session': return '$_SESSION'; $tokens->next();
case 'request': return '$_REQUEST'; }
case 'now': return 'time()'; if($tokens->is(T_DOUBLE_COLON)) {
case 'line': return $this->_line; $name .= '::';
case 'tpl_name': return '$tpl->getName()'; $name .= $tokens->getNext(T_STRING);
case 'const': $tokens->next();
$tokens->getNext("."); }
return $tokens->getNext(T_STRING); if(defined($name)) {
default: return $name;
throw new \LogicException("Unexpected key '".$tokens->current()."' in system variable"); } else {
throw new ImproperUseException("Use undefined constant $name");
}
}
/**
* @param Tokenizer $tokens
* @param $name
* @return string
* @throws ImproperUseException
*/
public function parseMacro(Tokenizer $tokens, $name) {
if(isset($this->macros[ $name ])) {
$macro = $this->macros[ $name ];
$p = $this->parseParams($tokens);
$args = array();
foreach($macro["args"] as $arg) {
if(isset($p[ $arg ])) {
$args[ $arg ] = $p[ $arg ];
} elseif(isset($macro["defaults"][ $arg ])) {
$args[ $arg ] = $macro["defaults"][ $arg ];
} else {
throw new ImproperUseException("Macro '$name' require '$arg' argument");
}
}
$args = $args ? '$tpl = '.Compiler::toArray($args).';' : '';
return '$_tpl = $tpl; '.$args.' ?>'.$macro["body"].'<?php $tpl = $_tpl; unset($_tpl);';
} else {
var_dump($this->macros);
throw new ImproperUseException("Undefined macro '$name'");
} }
} }
@ -983,8 +1025,7 @@ class Template extends Render {
$tokens->next(); $tokens->next();
$params[ $key ] = $this->parseExp($tokens); $params[ $key ] = $this->parseExp($tokens);
} else { } else {
$params[ $key ] = true; $params[ $key ] = 'true';
$params[] = '"'.$key.'"';
} }
} elseif($tokens->is(Tokenizer::MACRO_SCALAR, '"', '`', T_VARIABLE, "[", '(')) { } elseif($tokens->is(Tokenizer::MACRO_SCALAR, '"', '`', T_VARIABLE, "[", '(')) {
$params[] = $this->parseExp($tokens); $params[] = $this->parseExp($tokens);

View File

@ -433,6 +433,19 @@ class Tokenizer {
} }
} }
/**
* Skip specific token or do nothing
*
* @param int|string $token1
* @return Tokenizer
*/
public function skipIf($token1/*, $token2, ...*/) {
if($this->_valid(func_get_args(), $this->curr[0])) {
$this->next();
}
return $this;
}
/** /**
* Check current token's type * Check current token's type
* *

View File

@ -0,0 +1,18 @@
<?php
namespace Aspect;
class MacrosTest extends TestCase {
public function testMacros() {
$tpl = $this->aspect->compileCode('
{macro plus(x, y)}
x + y = {$x + $y}
{/macro}
Math: {macro.plus x=2 y=3}
');
$this->assertStringStartsWith('x + y = ', trim($tpl->macros["plus"]["body"]));
$this->assertSame('Math: x + y = 5', Modifier::strip($tpl->fetch(array()), true));
}
}