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
$inc = $tpl->getStorage()->compile($name, false);
$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 {
return '$tpl->getStorage()->getTemplate('.$cname.')->display('.self::_toArray($p).'+(array)$tpl);';
return '$tpl->getStorage()->getTemplate('.$cname.')->display('.self::toArray($p).'+(array)$tpl);';
}
} else {
if($name && ($tpl->getStorage()->getOptions() & \Aspect::FORCE_INCLUDE)) { // if FORCE_INCLUDE enabled and template name known
@ -523,7 +523,7 @@ class Compiler {
* @return string
*/
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
*/
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();';
}
@ -580,7 +580,12 @@ class Compiler {
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();
foreach($params as $k => $v) {
$_code[] = '"'.$k.'" => '.$v;
@ -689,26 +694,45 @@ class Compiler {
* @param Tokenizer $tokens
* @param Scope $scope
* @throws ImproperUseException
* @return string
*/
public static function macroOpen(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}}");
$scope["name"] = $tokens->get(Tokenizer::MACRO_STRING);
$scope["args"] = array();
$scope["defaults"] = array();
if(!$tokens->valid()) {
return;
}
$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) {
$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
*/
public $_body;
/**
* @var array of macros
*/
public $macros = array();
/**
* Call stack
* @var Scope[]
@ -58,11 +63,12 @@ class Template extends Render {
*/
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) {
return new static($aspect);
}
@ -305,6 +311,9 @@ class Template extends Render {
case '$':
$code = "echo ".$this->parseExp($tokens).";";
break;
case '#':
$code = "echo ".$this->parseConst($tokens);
break;
case '/':
$code = $this->_end($tokens);
break;
@ -365,19 +374,24 @@ class Template extends Render {
* @return string
*/
private function _parseAct(Tokenizer $tokens) {
if($tokens->is(Tokenizer::MACRO_STRING)) {
$action = $tokens->current();
$action = $tokens->getAndNext();
} else {
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).";";
} 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
$tokens->next();
switch($act["type"]) {
case Aspect::BLOCK_COMPILER:
$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
if($this->_stack[$i]->hasTag($action, $j - $i)) {
$tokens->next();
return $this->_stack[$i]->tag($action, $tokens);
}
}
@ -452,6 +465,9 @@ class Template extends Render {
} else {
$term = 1;
}
} elseif(!$term && $tokens->is('#')) {
$term = 1;
$_exp .= $this->parseConst($tokens);
} elseif(!$term && $tokens->is("(")) {
$_exp .= $tokens->getAndNext();
$brackets++;
@ -553,11 +569,7 @@ class Template extends Render {
public function parseVar(Tokenizer $tokens, $deny = 0, &$pure_var = true) {
$var = $tokens->get(T_VARIABLE);
$pure_var = true;
if(isset(self::$sysvar[ $var ])) {
$_var = $this->_parseSystemVar($tokens);
} else {
$_var = '$tpl["'.ltrim($var,'$').'"]';
}
$_var = '$tpl["'.ltrim($var,'$').'"]';
$tokens->next();
while($t = $tokens->key()) {
if($t === "." && !($deny & self::DENY_ARRAY)) {
@ -846,7 +858,7 @@ class Template extends Render {
$key = true;
$val = false;
$_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);
$key = false;
$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
* @throws \LogicException
* @return mixed|string
* @return string
* @throws ImproperUseException
*/
private function _parseSystemVar(Tokenizer $tokens) {
$tokens->getNext(".");
$key = $tokens->getNext(T_STRING, T_CONST);
switch($key) {
case 'get': return '$_GET';
case 'post': return '$_POST';
case 'cookies': return '$_COOKIES';
case 'session': return '$_SESSION';
case 'request': return '$_REQUEST';
case 'now': return 'time()';
case 'line': return $this->_line;
case 'tpl_name': return '$tpl->getName()';
case 'const':
$tokens->getNext(".");
return $tokens->getNext(T_STRING);
default:
throw new \LogicException("Unexpected key '".$tokens->current()."' in system variable");
public function parseConst(Tokenizer $tokens) {
$tokens->get('#');
$name = $tokens->getNext(T_STRING);
$tokens->next();
if($tokens->is(T_NAMESPACE)) {
$name .= '\\';
$name .= $tokens->getNext(T_STRING);
$tokens->next();
}
if($tokens->is(T_DOUBLE_COLON)) {
$name .= '::';
$name .= $tokens->getNext(T_STRING);
$tokens->next();
}
if(defined($name)) {
return $name;
} 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();
$params[ $key ] = $this->parseExp($tokens);
} else {
$params[ $key ] = true;
$params[] = '"'.$key.'"';
$params[ $key ] = 'true';
}
} elseif($tokens->is(Tokenizer::MACRO_SCALAR, '"', '`', T_VARIABLE, "[", '(')) {
$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
*

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