Fix: reparse close tag, custom provider, simplify compile method, add providers and tests

This commit is contained in:
bzick
2013-05-30 19:00:00 +04:00
parent 707a13cd08
commit 32ccaa91f0
13 changed files with 482 additions and 347 deletions

View File

@ -91,6 +91,7 @@ class Cytro {
"e" => 'Cytro\Modifier::escape', // alias of escape
"unescape" => 'Cytro\Modifier::unescape',
"strip" => 'Cytro\Modifier::strip',
"length" => 'Cytro\Modifier::length',
"default" => 'Cytro\Modifier::defaultValue'
);
@ -224,6 +225,7 @@ class Cytro {
throw new InvalidArgumentException("Source must be a valid path or provider object");
}
$cytro = new static($provider);
/* @var Cytro $cytro */
$cytro->setCompileDir($compile_dir);
if($options) {
$cytro->setOptions($options);
@ -525,8 +527,8 @@ class Cytro {
*/
public function getProvider($scm = false) {
if($scm) {
if(isset($this->_provider[$scm])) {
return $this->_provider[$scm];
if(isset($this->_providers[$scm])) {
return $this->_providers[$scm];
} else {
throw new InvalidArgumentException("Provider for '$scm' not found");
}
@ -565,6 +567,23 @@ class Cytro {
return $this->getTemplate($template)->fetch($vars);
}
/**
*
*
* @param string $template name of template
* @param array $vars
* @param $callback
* @param float $chunk
* @return \Cytro\Render
* @example $cytro->pipe("products.yml.tpl", $iterators, [new SplFileObject("/tmp/products.yml"), "fwrite"], 512*1024)
*/
public function pipe($template, array $vars, $callback, $chunk = 1e6) {
ob_start($callback, $chunk, true);
$this->getTemplate($template)->display($vars);
ob_end_flush();
}
/**
* Get template by name
*

View File

@ -411,6 +411,7 @@ class Compiler {
*/
public static function extendBody(&$body, $tpl) {
$t = $tpl;
// var_dump("$tpl: ".$tpl->getBody());
while(isset($t->_extends)) {
$t = $t->_extends;
if(is_object($t)) {
@ -484,7 +485,7 @@ class Compiler {
if($scope["name"]) { // is scalar name
if($tpl->_compatible) { // is compatible mode
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(empty($tpl->b['.$scope["cname"].'])) { '.
'<?php /* 1) Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(empty($tpl->b['.$scope["cname"].'])) { '.
'$tpl->b['.$scope["cname"].'] = function($tpl) { ?>'.PHP_EOL.
$scope->getContent().
"<?php };".
@ -493,7 +494,7 @@ class Compiler {
} elseif(!isset($tpl->blocks[ $scope["name"] ])) { // is block not registered
$tpl->blocks[ $scope["name"] ] = $scope->getContent();
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' '.$tpl->_compatible.' */'.PHP_EOL.' $tpl->b['.$scope["cname"].'] = function($tpl) { ?>'.PHP_EOL.
'<?php /* 2) Block '.$tpl.': '.$scope["cname"].' '.$tpl->_compatible.' */'.PHP_EOL.' $tpl->b['.$scope["cname"].'] = function($tpl) { ?>'.PHP_EOL.
$scope->getContent().
"<?php }; ?>".PHP_EOL
);
@ -501,7 +502,7 @@ class Compiler {
} else { // dynamic name
$tpl->_compatible = true; // enable compatible mode
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(empty($tpl->b['.$scope["cname"].'])) { '.
'<?php /* 3) Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(empty($tpl->b['.$scope["cname"].'])) { '.
'$tpl->b['.$scope["cname"].'] = function($tpl) { ?>'.PHP_EOL.
$scope->getContent().
"<?php };".
@ -512,7 +513,7 @@ class Compiler {
if(isset($tpl->blocks[ $scope["name"] ])) { // has block
if($tpl->_compatible) { // compatible mode enabled
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(isset($tpl->b['.$scope["cname"].'])) { echo $tpl->b['.$scope["cname"].']->__invoke($tpl); } else {?>'.PHP_EOL.
'<?php /* 4) Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(isset($tpl->b['.$scope["cname"].'])) { echo $tpl->b['.$scope["cname"].']->__invoke($tpl); } else {?>'.PHP_EOL.
$tpl->blocks[ $scope["name"] ].
'<?php } ?>'.PHP_EOL
);
@ -520,9 +521,11 @@ class Compiler {
} else {
$scope->replaceContent($tpl->blocks[ $scope["name"] ]);
}
// } elseif(isset($tpl->_extended) || !empty($tpl->_compatible)) {
} elseif(isset($tpl->_extended) && $tpl->_compatible || empty($tpl->_extended)) {
// var_dump("$tpl: exxx");
$scope->replaceContent(
'<?php /* Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(isset($tpl->b['.$scope["cname"].'])) { echo $tpl->b['.$scope["cname"].']->__invoke($tpl); } else {?>'.PHP_EOL.
'<?php /* 5) Block '.$tpl.': '.$scope["cname"].' */'.PHP_EOL.' if(isset($tpl->b['.$scope["cname"].'])) { echo $tpl->b['.$scope["cname"].']->__invoke($tpl); } else {?>'.PHP_EOL.
$scope->getContent().
'<?php } ?>'.PHP_EOL
);
@ -629,74 +632,59 @@ class Compiler {
return 'array('.implode(",", $_code).')';
}
public static function varOpen(Tokenizer $tokens, Scope $scope) {
$scope->is_closed = true;
return self::setVar($tokens, $scope->tpl).';';
/**
* @param Tokenizer $tokens
* @param Scope $scope
* @return string
*/
public static function varOpen(Tokenizer $tokens, Scope $scope) {
$var = $scope->tpl->parseVariable($tokens, Template::DENY_MODS);
if($tokens->is('=')) { // inline tag {var ...}
$scope->is_closed = true;
$tokens->next();
if($tokens->is("[")) {
return $var.'='.$scope->tpl->parseArray($tokens);
} else {
return $var.'='.$scope->tpl->parseExp($tokens, true);
}
} else {
$scope["name"] = $var;
if($tokens->is('|')) {
$scope["value"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()");
} else {
$scope["value"] = "ob_get_clean()";
}
return 'ob_start();';
}
}
public static function varClose() {
return '';
/**
* @param Tokenizer $tokens
* @param Scope $scope
* @return string
*/
public static function varClose(Tokenizer $tokens, Scope $scope) {
return $scope["name"].'='.$scope["value"].';';
}
/**
* Tag {var ...}
*
* @static
* @param Tokenizer $tokens
* @param Template $tpl
* @return string
*/
//public static function assign(Tokenizer $tokens, Template $tpl) {
// return self::setVar($tokens, $tpl).';';
//}
/**
* Set variable expression
* @param Tokenizer $tokens
* @param Template $tpl
* @param bool $allow_array
* @return string
*/
public static function setVar(Tokenizer $tokens, Template $tpl, $allow_array = true) {
$var = $tpl->parseVariable($tokens, $tpl::DENY_MODS);
$tokens->get('=');
$tokens->next();
if($tokens->is("[") && $allow_array) {
return $var.'='.$tpl->parseArray($tokens);
} else {
return $var.'='.$tpl->parseExp($tokens, true);
}
}
public static function filterOpen(Tokenizer $tokens, Scope $scope) {
$scope["filter"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()");
return "ob_start();";
}
public static function filterClose($tokens, Scope $scope) {
return "echo ".$scope["filter"].";";
}
/**
* @param Tokenizer $tokens
* @param Scope $scope
* @return string
*/
public static function captureOpen(Tokenizer $tokens, Scope $scope) {
if($tokens->is("|")) {
$scope["value"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()");
} else {
$scope["value"] = "ob_get_clean()";
}
$scope["var"] = $scope->tpl->parseVariable($tokens, Template::DENY_MODS);
public static function filterOpen(Tokenizer $tokens, Scope $scope) {
$scope["filter"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()");
return "ob_start();";
}
public static function captureClose($tokens, Scope $scope) {
return $scope["var"]." = ".$scope["value"].";";
/**
* @param $tokens
* @param Scope $scope
* @return string
*/
public static function filterClose($tokens, Scope $scope) {
return "echo ".$scope["filter"].";";
}
/**

View File

@ -80,7 +80,7 @@ class Modifier {
}
/**
* Crop string by length (support unicode)
* Crop string to specific length (support unicode)
*
* @param string $string text witch will be truncate
* @param int $length maximum symbols of result string
@ -134,7 +134,7 @@ class Modifier {
* @return int
*/
public static function length($item) {
if(is_scalar($item)) {
if(is_string($item)) {
return strlen(preg_replace('#[\x00-\x7F]|[\x80-\xDF][\x00-\xBF]|[\xE0-\xEF][\x00-\xBF]{2}#s', ' ', $item));
} elseif (is_array($item)) {
return count($item);

View File

@ -97,12 +97,12 @@ class Template extends Render {
$this->_name = $name;
if($provider = strstr($name, ":", true)) {
$this->_scm = $provider;
$this->_base_name = substr($name, strlen($provider));
$this->_base_name = substr($name, strlen($provider) + 1);
} else {
$this->_base_name = $name;
$this->_base_name = $name;
}
$this->_provider = $this->_cytro->getProvider($provider);
$this->_src = $this->_provider->getSource($name, $this->_time);
$this->_provider = $this->_cytro->getProvider($provider);
$this->_src = $this->_provider->getSource($this->_base_name, $this->_time);
if($compile) {
$this->compile();
}
@ -134,56 +134,61 @@ class Template extends Render {
if(!isset($this->_src)) { // already compiled
return;
}
$pos = 0;
$frag = "";
$end = $pos = 0;
while(($start = strpos($this->_src, '{', $pos)) !== false) { // search open-symbol of tags
switch($this->_src[$start + 1]) { // check next char
switch($this->_src[$start + 1]) { // check next character
case "\n": case "\r": case "\t": case " ": case "}": // ignore the tag
$pos = $start + 1; // try find tags after the current char
continue 2;
case "*": // if comment block
$end = strpos($this->_src, '*}', $start); // finding end of the comment block
if($end === false) {
throw new CompileException("Unclosed comment block in line {$this->_line}", 0, 1, $this->_name, $this->_line);
}
$_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
$this->_line += substr_count($_frag, "\n"); // count skipped lines in comment block
$pos = $end + 1; // seek pointer
continue 2;
}
$end = strpos($this->_src, '}', $start); // search close-symbol of the tag
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 previous '}' and current '{'
$tag = substr($this->_src, $start, $end - $start + 1); // variable $tag contains cytro 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
$frag = substr($this->_src, $pos, $start - $pos); // variable $frag contains chars after previous '}' and current '{'
$this->_appendText($frag);
if($tag[strlen($tag) - 2] === "-") { // check right trim flag
$_tag = substr($tag, 1, -2);
$_frag = rtrim($frag);
} else {
$_tag = substr($tag, 1, -1);
$_frag = $frag;
}
if($this->_ignore) { // check ignore
if($_tag === '/ignore') {
$this->_ignore = false;
$this->_appendText($_frag);
} else { // still ignore
$frag .= $tag;
continue;
}
} else {
$this->_appendText($_frag);
$tokens = new Tokenizer($_tag);
$this->_appendCode($this->_tag($tokens), $tag);
if($tokens->key()) { // if tokenizer have tokens - throws exceptions
throw new CompileException("Unexpected token '".$tokens->current()."' in {$this} line {$this->_line}, near '{".$tokens->getSnippetAsString(0,0)."' <- there", 0, E_ERROR, $this->_name, $this->_line);
$from = $start;
reparse: { // yep, i use goto operator. For this algorithm it is good choice
$end = strpos($this->_src, '}', $from); // search close-symbol of the tag
if($end === false) { // if unexpected end of template
throw new CompileException("Unclosed tag in line {$this->_line}", 0, 1, $this->_name, $this->_line);
}
$tag = substr($this->_src, $start, $end - $start + 1); // variable $tag contains cytro tag '{...}'
}
$frag = "";
$_tag = substr($tag, 1, -1); // strip delimiters '{' and '}'
if($this->_ignore) { // check ignore
if($_tag === '/ignore') { // turn off ignore
$this->_ignore = false;
} else { // still ignore
$this->_appendText($tag);
}
$pos = $start + strlen($tag);
continue;
} else {
$tokens = new Tokenizer($_tag); // tokenize the tag
if($tokens->isIncomplete()) { // all strings finished?
$from = $end + 1;
goto reparse; // find another close-symbol
}
$this->_appendCode( $this->_tag($tokens) , $tag); // start the tag lexer
$pos = $end + 1; // move search-pointer to end of the tag
if($tokens->key()) { // if tokenizer have tokens - throws exceptions
throw new CompileException("Unexpected token '".$tokens->current()."' in {$this} line {$this->_line}, near '{".$tokens->getSnippetAsString(0,0)."' <- there", 0, E_ERROR, $this->_name, $this->_line);
}
}
}
unset($frag);
}
$this->_appendText(substr($this->_src, $this->_pos));
gc_collect_cycles();
$this->_appendText(substr($this->_src, $end ? $end + 1 : 0));
if($this->_stack) {
$_names = array();
$_line = 0;
@ -191,9 +196,9 @@ class Template extends Render {
if(!$_line) {
$_line = $scope->line;
}
$_names[] = '{'.$scope->name.'} defined on line '.$scope->line;
$_names[] = '{'.$scope->name.'} opened on line '.$scope->line;
}
throw new CompileException("Unclosed tag(s): ".implode(", ", $_names), 0, 1, $this->_name, $_line);
throw new CompileException("Unclosed tag".(count($_names) == 1 ? "" : "s").": ".implode(", ", $_names), 0, 1, $this->_name, $_line);
}
unset($this->_src);
if($this->_post) {
@ -217,6 +222,7 @@ class Template extends Render {
* @param string $text
*/
private function _appendText($text) {
$this->_line += substr_count($text, "\n");
if($this->_filter) {
if(strpos($text, "<?") === false) {
$this->_body .= $text;
@ -262,9 +268,11 @@ class Template extends Render {
* @param $source
*/
private function _appendCode($code, $source) {
if(!$code) {
return;
} else {
$this->_line += substr_count($source, "\n");
if(strpos($code, '?>') !== false) {
$code = $this->_escapeCode($code); // paste PHP_EOL
}
@ -357,14 +365,14 @@ class Template extends Render {
return parent::fetch($values);
}
/**
* Internal tags router
* @param Tokenizer $tokens
* @throws UnexpectedTokenException
* @throws CompileException
* @throws SecurityException
* @return string executable PHP code
*/
/**
* Internal tags router
* @param Tokenizer $tokens
*
* @throws SecurityException
* @throws CompileException
* @return string executable PHP code
*/
private function _tag(Tokenizer $tokens) {
try {
if($tokens->is(Tokenizer::MACRO_STRING)) {
@ -742,104 +750,68 @@ class Template extends Render {
return $_scalar;
}
/**
* Parse string with or without variable
*
* @param Tokenizer $tokens
* @throws UnexpectedTokenException
* @return string
*/
/**
* Parse string with or without variable
*
* @param Tokenizer $tokens
* @throws UnexpectedTokenException
* @return string
*/
public function parseSubstr(Tokenizer $tokens) {
ref: {
if($tokens->is('"',"`")) {
$p = $tokens->p;
$stop = $tokens->current();
$_str = '"';
$tokens->next();
while($t = $tokens->key()) {
if($t === T_ENCAPSED_AND_WHITESPACE) {
$_str .= $tokens->current();
$tokens->next();
} elseif($t === T_VARIABLE) {
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).')';
/*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) {
$tokens->next();
return $_str.'"';
if($tokens->is('"',"`")) {
$stop = $tokens->current();
$_str = '"';
$tokens->next();
while($t = $tokens->key()) {
if($t === T_ENCAPSED_AND_WHITESPACE) {
$_str .= $tokens->current();
$tokens->next();
} elseif($t === T_VARIABLE) {
if(strlen($_str) > 1) {
$_str .= '".';
} else {
break;
$_str = "";
}
}
if($more = $this->_getMoreSubstr($stop)) {
$tokens->append("}".$more, $p);
goto ref;
}
throw new UnexpectedTokenException($tokens);
} elseif($tokens->is(T_CONSTANT_ENCAPSED_STRING)) {
return $tokens->getAndNext();
} elseif($tokens->is(T_ENCAPSED_AND_WHITESPACE)) {
$p = $tokens->p;
if($more = $this->_getMoreSubstr($tokens->curr[1][0])) {
$tokens->append("}".$more, $p);
goto ref;
}
throw new UnexpectedTokenException($tokens);
} else {
return "";
}
}
}
$_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).')';
if($tokens->is($stop)) {
$tokens->next();
return $_str;
} else {
$_str .= '."';
}
} elseif($t === "}") {
$tokens->next();
} elseif($t === $stop) {
$tokens->next();
return $_str.'"';
} else {
/**
* @param string $after
* @return bool|string
*/
private function _getMoreSubstr($after) {
$end = strpos($this->_src, $after, $this->_pos);
$end = strpos($this->_src, "}", $end);
if(!$end) {
return false;
break;
}
}
throw new UnexpectedTokenException($tokens);
} elseif($tokens->is(T_CONSTANT_ENCAPSED_STRING)) {
return $tokens->getAndNext();
} elseif($tokens->is(T_ENCAPSED_AND_WHITESPACE)) {
throw new UnexpectedTokenException($tokens);
} else {
return "";
}
$fragment = substr($this->_src, $this->_pos, $end - $this->_pos);
$this->_pos = $end + 1;
return $fragment;
}
/**
@ -1091,4 +1063,5 @@ class Template extends Render {
class CompileException extends \ErrorException {}
class SecurityException extends CompileException {}
class ImproperUseException extends \LogicException {}
class ImproperUseException extends \LogicException {}
class ReparseTagException extends \Exception {}

View File

@ -79,6 +79,7 @@ class Tokenizer {
public $tokens;
public $p = 0;
public $quotes = 0;
private $_max = 0;
private $_last_no = 0;
@ -100,7 +101,7 @@ class Tokenizer {
\T_NEW => 1, \T_PRINT => 1, \T_PRIVATE => 1, \T_PUBLIC => 1, \T_PROTECTED => 1, \T_REQUIRE => 1,
\T_REQUIRE_ONCE => 1,\T_RETURN => 1, \T_RETURN => 1, \T_STRING => 1, \T_SWITCH => 1, \T_THROW => 1,
\T_TRAIT => 1, \T_TRAIT_C => 1, \T_TRY => 1, \T_UNSET => 1, \T_UNSET => 1, \T_VAR => 1,
\T_WHILE => 1
\T_WHILE => 1, \T_YIELD => 1
),
self::MACRO_INCDEC => array(
\T_INC => 1, \T_DEC => 1
@ -142,52 +143,56 @@ class Tokenizer {
'true' => 1, 'false' => 1, 'null' => 1, 'TRUE' => 1, 'FALSE' => 1, 'NULL' => 1
);
/**
* Translate expression to tokens list.
*
* @static
* @param string $query
* @return array
*/
public static function decode($query) {
$tokens = array(-1 => array(\T_WHITESPACE, '', '', 1));
$_tokens = token_get_all("<?php ".$query);
$line = 1;
array_shift($_tokens);
$i = 0;
foreach($_tokens as &$token) {
if(is_string($token)) {
$tokens[] = array(
$token,
$token,
"",
$line,
);
$i++;
} elseif ($token[0] === \T_WHITESPACE) {
$tokens[$i-1][2] = $token[1];
} else {
$tokens[] = array(
$token[0],
$token[1],
"",
$line = $token[2],
);
$i++;
}
/**
* @param $query
*/
public function __construct($query) {
$tokens = array(-1 => array(\T_WHITESPACE, '', '', 1));
$_tokens = token_get_all("<?php ".$query);
$line = 1;
array_shift($_tokens);
$i = 0;
foreach($_tokens as $token) {
if(is_string($token)) {
if($token === '"' || $token === "'" || $token === "`") {
$this->quotes++;
}
$tokens[] = array(
$token,
$token,
"",
$line,
);
$i++;
} elseif ($token[0] === \T_WHITESPACE) {
$tokens[$i-1][2] = $token[1];
} else {
$tokens[] = array(
$token[0],
$token[1],
"",
$line = $token[2],
token_name($token[0]) // debug
);
$i++;
}
}
return $tokens;
}
public function __construct($query, $decode = 0) {
$this->tokens = self::decode($query, $decode);
unset($this->tokens[-1]);
}
unset($tokens[-1]);
$this->tokens = $tokens;
$this->_max = count($this->tokens) - 1;
$this->_last_no = $this->tokens[$this->_max][3];
}
/**
* Is incomplete mean some string not closed
*
* @return int
*/
public function isIncomplete() {
return ($this->quotes % 2) || ($this->tokens[$this->_max][0] === T_ENCAPSED_AND_WHITESPACE);
}
/**
* Return the current element
*
@ -468,7 +473,7 @@ class Tokenizer {
}
/**
* Get tokens near current token
* Get tokens near current position
* @param int $before count tokens before current token
* @param int $after count tokens after current token
* @return array
@ -553,30 +558,6 @@ class Tokenizer {
public function getLine() {
return $this->curr ? $this->curr[3] : $this->_last_no;
}
/**
* Parse code and append tokens. This method move pointer to offset.
*
* @param string $code
* @param int $offset if not -1 replace tokens from position $offset
* @return Tokenizer
*/
public function append($code, $offset = -1) {
if($offset != -1) {
$code = $this->getSubstr($offset).$code;
if($this->p > $offset) {
$this->p = $offset;
}
$this->tokens = array_slice($this->tokens, 0, $offset);
}
$tokens = self::decode($code);
unset($tokens[-1], $this->prev, $this->curr, $this->next);
$this->tokens = array_merge($this->tokens, $tokens);
$this->_max = count($this->tokens) - 1;
$this->_last_no = $this->tokens[$this->_max][3];
return $this;
}
}
/**