      1: <?php
      2: /*
      3:
      4: MIT License
      5: Copyright 2013-2018 Zordius Chen. All Rights Reserved.
      6: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
      7: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
      8: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      9:
     10: Origin: https://github.com/zordius/lightncandy
     11: */
     12:
     13: /**
     14:  * file to keep LightnCandy Parser
     15:  *
     16:  * @package    LightnCandy
     17:  * @author     Zordius <zordius@gmail.com>
     18:  */
     19:
     20: namespace LightnCandy;
     21:
     22: use \LightnCandy\Token;
     23: use \LightnCandy\SafeString;
     24:
     25: /**
     26:  * LightnCandy Parser
     27:  */
     28: class Parser extends Token
     29: {
     30:     // Compile time error handling flags
     31:     const BLOCKPARAM = 9999;
     32:     const PARTIALBLOCK = 9998;
     33:     const LITERAL = -1;
     34:     const SUBEXP = -2;
     35:
     36:     /**
     37:      * Get partial block id and fix the variable list
     38:      *
     39:      * @param array<boolean|integer|string|array> $vars parsed token
     40:      *
     41:      * @return integer Return partial block id
     42:      *
     43:      */
     44:     public static function getPartialBlock(&$vars)
     45:     {
     46:         if (isset($vars[static::PARTIALBLOCK])) {
     47:             $id = $vars[static::PARTIALBLOCK];
     48:             unset($vars[static::PARTIALBLOCK]);
     49:             return $id;
     50:         }
     51:         return 0;
     52:     }
     53:
     54:     /**
     55:      * Get block params and fix the variable list
     56:      *
     57:      * @param array<boolean|integer|string|array> $vars parsed token
     58:      *
     59:      * @return array<string>|null Return list of block params or null
     60:      *
     61:      */
     62:     public static function getBlockParams(&$vars)
     63:     {
     64:         if (isset($vars[static::BLOCKPARAM])) {
     65:             $list = $vars[static::BLOCKPARAM];
     66:             unset($vars[static::BLOCKPARAM]);
     67:             return $list;
     68:         }
     69:     }
     70:
     71:     /**
     72:      * Return array presentation for a literal
     73:      *
     74:      * @param string $name variable name.
     75:      * @param boolean $asis keep the name as is or not
     76:      * @param boolean $quote add single quote or not
     77:      *
     78:      * @return array<integer|string> Return variable name array
     79:      *
     80:      */
     81:     protected static function getLiteral($name, $asis, $quote = false)
     82:     {
     83:         return $asis ? array($name) : array(static::LITERAL, $quote ? "'$name'" : $name);
     84:     }
     85:
     86:     /**
     87:      * Return array presentation for an expression
     88:      *
     89:      * @param string $v analyzed expression names.
     90:      * @param array<string,array|string|integer> $context Current compile content.
     91:      * @param integer $pos expression position
     92:      *
     93:      * @return array<integer,string> Return variable name array
     94:      *
     95:      * @expect array('this') when input 'this', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 0)), 0
     96:      * @expect array() when input 'this', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1)), 0
     97:      * @expect array(1) when input '..', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
     98:      * @expect array(1) when input '../', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
     99:      * @expect array(1) when input '../.', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
    100:      * @expect array(1) when input '../this', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
    101:      * @expect array(1, 'a') when input '../a', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
    102:      * @expect array(2, 'a', 'b') when input '../../a.b', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
    103:      * @expect array(2, '[a]', 'b') when input '../../[a].b', array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
    104:      * @expect array(2, 'a', 'b') when input '../../[a].b', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
    105:      * @expect array(0, 'id') when input 'this.id', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
    106:      * @expect array('this', 'id') when input 'this.id', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
    107:      * @expect array(0, 'id') when input './id', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 0
    108:      * @expect array(\LightnCandy\Parser::LITERAL, '\'a.b\'') when input '"a.b"', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 1
    109:      * @expect array(\LightnCandy\Parser::LITERAL, '123') when input '123', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 1
    110:      * @expect array(\LightnCandy\Parser::LITERAL, 'null') when input 'null', array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)), 1
    111:      */
    112:     protected static function getExpression($v, &$context, $pos)
    113:     {
    114:         $asis = ($pos === 0);
    115:
    116:         // handle number
    117:         if (is_numeric($v)) {
    118:             return static::getLiteral(strval(1 * $v), $asis);
    119:         }
    120:
    121:         // handle double quoted string
    122:         if (preg_match('/^"(.*)"$/', $v, $matched)) {
    123:             return static::getLiteral(preg_replace('/([^\\\\])\\\\\\\\"/', '$1"', preg_replace('/^\\\\\\\\"/', '"', $matched[1])), $asis, true);
    124:         }
    125:
    126:         // handle single quoted string
    127:         if (preg_match('/^\\\\\'(.*)\\\\\'$/', $v, $matched)) {
    128:             return static::getLiteral($matched[1], $asis, true);
    129:         }
    130:
    131:         // handle boolean, null and undefined
    132:         if (preg_match('/^(true|false|null|undefined)$/', $v)) {
    133:             return static::getLiteral($v, $asis);
    134:         }
    135:
    136:         $ret = array();
    137:         $levels = 0;
    138:
    139:         // handle ..
    140:         if ($v === '..') {
    141:             $v = '../';
    142:         }
    143:
    144:         // Trace to parent for ../ N times
    145:         $v = preg_replace_callback('/\\.\\.\\//', function () use (&$levels) {
    146:             $levels++;
    147:             return '';
    148:         }, trim($v));
    149:
    150:         // remove ./ in path
    151:         $v = preg_replace('/\\.\\//', '', $v, -1, $scoped);
    152:
    153:         $strp = (($pos !== 0) && $context['flags']['strpar']);
    154:         if ($levels && !$strp) {
    155:             $ret[] = $levels;
    156:             if (!$context['flags']['parent']) {
    157:                 $context['error'][] = 'Do not support {{../var}}, you should do compile with LightnCandy::FLAG_PARENT flag';
    158:             }
    159:             $context['usedFeature']['parent'] ++;
    160:         }
    161:
    162:         if ($context['flags']['advar'] && preg_match('/\\]/', $v)) {
    163:             preg_match_all(static::VARNAME_SEARCH, $v, $matchedall);
    164:         } else {
    165:             preg_match_all('/([^\\.\\/]+)/', $v, $matchedall);
    166:         }
    167:
    168:         if ($v !== '.') {
    169:             $vv = implode('.', $matchedall[1]);
    170:             if (strlen($v) !== strlen($vv)) {
    171:                 $context['error'][] = "Unexpected charactor in '$v' ! (should it be '$vv' ?)";
    172:             }
    173:         }
    174:
    175:         foreach ($matchedall[1] as $m) {
    176:             if ($context['flags']['advar'] && substr($m, 0, 1) === '[') {
    177:                 $ret[] = substr($m, 1, -1);
    178:             } elseif ((!$context['flags']['this'] || ($m !== 'this')) && ($m !== '.')) {
    179:                 $ret[] = $m;
    180:             } else {
    181:                 $scoped++;
    182:             }
    183:         }
    184:
    185:         if ($strp) {
    186:             return array(static::LITERAL, "'" . implode('.', $ret) . "'");
    187:         }
    188:
    189:         if (($scoped > 0) && ($levels === 0) && (count($ret) > 0)) {
    190:             array_unshift($ret, 0);
    191:         }
    192:
    193:         return $ret;
    194:     }
    195:
    196:     /**
    197:      * Parse the token and return parsed result.
    198:      *
    199:      * @param array<string> $token preg_match results
    200:      * @param array<string,array|string|integer> $context current compile context
    201:      *
    202:      * @return array<boolean|integer|array> Return parsed result
    203:      *
    204:      * @expect array(false, array(array())) when input array(0,0,0,0,0,0,0,''), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
    205:      * @expect array(true, array(array())) when input array(0,0,0,'{{',0,'{',0,''), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
    206:      * @expect array(true, array(array())) when input array(0,0,0,0,0,0,0,''), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 1), 'rawblock' => false)
    207:      * @expect array(false, array(array('a'))) when input array(0,0,0,0,0,0,0,'a'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
    208:      * @expect array(false, array(array('a'), array('b'))) when input array(0,0,0,0,0,0,0,'a  b'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
    209:      * @expect array(false, array(array('a'), array('"b'), array('c"'))) when input array(0,0,0,0,0,0,0,'a "b c"'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
    210:      * @expect array(false, array(array('a'), array(-1, '\'b c\''))) when input array(0,0,0,0,0,0,0,'a "b c"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
    211:      * @expect array(false, array(array('a'), array('[b'), array('c]'))) when input array(0,0,0,0,0,0,0,'a [b c]'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
    212:      * @expect array(false, array(array('a'), array('[b'), array('c]'))) when input array(0,0,0,0,0,0,0,'a [b c]'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    213:      * @expect array(false, array(array('a'), array('b c'))) when input array(0,0,0,0,0,0,0,'a [b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
    214:      * @expect array(false, array(array('a'), array('b c'))) when input array(0,0,0,0,0,0,0,'a [b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    215:      * @expect array(false, array(array('a'), 'q' => array('b c'))) when input array(0,0,0,0,0,0,0,'a q=[b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    216:      * @expect array(false, array(array('a'), array('q=[b c'))) when input array(0,0,0,0,0,0,0,'a [q=[b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    217:      * @expect array(false, array(array('a'), 'q' => array('[b'), array('c]'))) when input array(0,0,0,0,0,0,0,'a q=[b c]'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    218:      * @expect array(false, array(array('a'), 'q' => array('b'), array('c'))) when input array(0,0,0,0,0,0,0,'a [q]=b c'), array('flags' => array('strpar' => 0, 'advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    219:      * @expect array(false, array(array('a'), 'q' => array(-1, '\'b c\''))) when input array(0,0,0,0,0,0,0,'a q="b c"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    220:      * @expect array(false, array(array(-2, array(array('foo'), array('bar')), '(foo bar)'))) when input array(0,0,0,0,0,0,0,'(foo bar)'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 1, 'lambda' => 0), 'ops' => array('seperator' => ''), 'usedFeature' => array('subexp' => 0), 'rawblock' => false)
    221:      * @expect array(false, array(array('foo'), array("'=='"), array('bar'))) when input array(0,0,0,0,0,0,0,"foo '==' bar"), array('flags' => array('strpar' => 0, 'advar' => 1, 'namev' => 1, 'noesc' => 0, 'this' => 0), 'rawblock' => false)
    222:      * @expect array(false, array(array(-2, array(array('foo'), array('bar')), '( foo bar)'))) when input array(0,0,0,0,0,0,0,'( foo bar)'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 1, 'lambda' => 0), 'ops' => array('seperator' => ''), 'usedFeature' => array('subexp' => 0), 'rawblock' => false)
    223:      * @expect array(false, array(array('a'), array(-1, '\' b c\''))) when input array(0,0,0,0,0,0,0,'a " b c"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'rawblock' => false)
    224:      * @expect array(false, array(array('a'), 'q' => array(-1, '\' b c\''))) when input array(0,0,0,0,0,0,0,'a q=" b c"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    225:      * @expect array(false, array(array('foo'), array(-1, "' =='"), array('bar'))) when input array(0,0,0,0,0,0,0,"foo \' ==\' bar"), array('flags' => array('strpar' => 0, 'advar' => 1, 'namev' => 1, 'noesc' => 0, 'this' => 0), 'rawblock' => false)
    226:      * @expect array(false, array(array('a'), array(' b c'))) when input array(0,0,0,0,0,0,0,'a [ b c]'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    227:      * @expect array(false, array(array('a'), 'q' => array(-1, "' d e'"))) when input array(0,0,0,0,0,0,0,"a q=\' d e\'"), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'rawblock' => false)
    228:      * @expect array(false, array('q' => array(-2, array(array('foo'), array('bar')), '( foo bar)'))) when input array(0,0,0,0,0,0,0,'q=( foo bar)'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false, 'helperresolver' => 0)
    229:      * @expect array(false, array(array('foo'))) when input array(0,0,0,0,0,0,'>','foo'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false)
    230:      * @expect array(false, array(array('foo'))) when input array(0,0,0,0,0,0,'>','"foo"'), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false)
    231:      * @expect array(false, array(array('foo'))) when input array(0,0,0,0,0,0,'>','[foo] '), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false)
    232:      * @expect array(false, array(array('foo'))) when input array(0,0,0,0,0,0,'>','\\\'foo\\\''), array('flags' => array('strpar' => 0, 'advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0, 'lambda' => 0), 'usedFeature' => array('subexp' => 0), 'ops' => array('seperator' => 0), 'rawblock' => false)
    233:      */
    234:     public static function parse(&$token, &$context)
    235:     {
    236:         $vars = static::analyze($token[static::POS_INNERTAG], $context);
    237:         if ($token[static::POS_OP] === '>') {
    238:             $fn = static::getPartialName($vars);
    239:         } elseif ($token[static::POS_OP] === '#*') {
    240:             $fn = static::getPartialName($vars, 1);
    241:         }
    242:
    243:         $avars = static::advancedVariable($vars, $context, static::toString($token));
    244:
    245:         if (isset($fn) && ($fn !== null)) {
    246:             if ($token[static::POS_OP] === '>') {
    247:                 $avars[0] = $fn;
    248:             } elseif ($token[static::POS_OP] === '#*') {
    249:                 $avars[1] = $fn;
    250:             }
    251:         }
    252:
    253:         return array(($token[static::POS_BEGINRAW] === '{') || ($token[static::POS_OP] === '&') || $context['flags']['noesc'] || $context['rawblock'], $avars);
    254:     }
    255:
    256:     /**
    257:      * Get partial name from "foo" or [foo] or \'foo\'
    258:      *
    259:      * @param array<boolean|integer|array> $vars parsed token
    260:      * @param integer $pos position of partial name
    261:      *
    262:      * @return array<string>|null Return one element partial name array
    263:      *
    264:      * @expect null when input array()
    265:      * @expect array('foo') when input array('foo')
    266:      * @expect array('foo') when input array('"foo"')
    267:      * @expect array('foo') when input array('[foo]')
    268:      * @expect array('foo') when input array("\\'foo\\'")
    269:      * @expect array('foo') when input array(0, 'foo'), 1
    270:      */
    271:     public static function getPartialName(&$vars, $pos = 0)
    272:     {
    273:         if (!isset($vars[$pos])) {
    274:             return;
    275:         }
    276:         return preg_match(SafeString::IS_SUBEXP_SEARCH, $vars[$pos]) ? null : array(preg_replace('/^("(.+)")|(\\[(.+)\\])|(\\\\\'(.+)\\\\\')$/', '$2$4$6', $vars[$pos]));
    277:     }
    278:
    279:     /**
    280:      * Parse a subexpression then return parsed result.
    281:      *
    282:      * @param string $expression the full string of a sub expression
    283:      * @param array<string,array|string|integer> $context current compile context
    284:      *
    285:      * @return array<boolean|integer|array> Return parsed result
    286:      *
    287:      * @expect array(\LightnCandy\Parser::SUBEXP, array(array('a'), array('b')), '(a b)') when input '(a b)', array('usedFeature' => array('subexp' => 0), 'flags' => array('advar' => 0, 'namev' => 0, 'this' => 0, 'exhlp' => 1, 'strpar' => 0))
    288:      */
    289:     public static function subexpression($expression, &$context)
    290:     {
    291:         $context['usedFeature']['subexp']++;
    292:         $vars = static::analyze(substr($expression, 1, -1), $context);
    293:         $avars = static::advancedVariable($vars, $context, $expression);
    294:         if (isset($avars[0][0]) && !$context['flags']['exhlp']) {
    295:             if (!Validator::helper($context, $avars, true)) {
    296:                 $context['error'][] = "Can not find custom helper function defination {$avars[0][0]}() !";
    297:             }
    298:         }
    299:         return array(static::SUBEXP, $avars, $expression);
    300:     }
    301:
    302:     /**
    303:      * Check a parsed result is a subexpression or not
    304:      *
    305:      * @param array<string|integer|array> $var
    306:      *
    307:      * @return boolean return true when input is a subexpression
    308:      *
    309:      * @expect false when input 0
    310:      * @expect false when input array()
    311:      * @expect false when input array(\LightnCandy\Parser::SUBEXP, 0)
    312:      * @expect false when input array(\LightnCandy\Parser::SUBEXP, 0, 0)
    313:      * @expect false when input array(\LightnCandy\Parser::SUBEXP, 0, '', 0)
    314:      * @expect true when input array(\LightnCandy\Parser::SUBEXP, 0, '')
    315:      */
    316:     public static function isSubExp($var)
    317:     {
    318:         return is_array($var) && (count($var) === 3) && ($var[0] === static::SUBEXP) && is_string($var[2]);
    319:     }
    320:
    321:     /**
    322:      * Analyze parsed token for advanved variables.
    323:      *
    324:      * @param array<boolean|integer|array> $vars parsed token
    325:      * @param array<string,array|string|integer> $context current compile context
    326:      * @param string $token original token
    327:      *
    328:      * @return array<boolean|integer|array> Return parsed result
    329:      *
    330:      * @expect array(array('this')) when input array('this'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0,)), 0
    331:      * @expect array(array()) when input array('this'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 1)), 0
    332:      * @expect array(array('a')) when input array('a'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0, 'strpar' => 0)), 0
    333:      * @expect array(array('a'), array('b')) when input array('a', 'b'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0, 'strpar' => 0)), 0
    334:      * @expect array('a' => array('b')) when input array('a=b'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0, 'strpar' => 0)), 0
    335:      * @expect array('fo o' => array(\LightnCandy\Parser::LITERAL, '123')) when input array('[fo o]=123'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0)), 0
    336:      * @expect array('fo o' => array(\LightnCandy\Parser::LITERAL, '\'bar\'')) when input array('[fo o]="bar"'), array('flags' => array('advar' => 1, 'namev' => 1, 'this' => 0)), 0
    337:      */
    338:     protected static function advancedVariable($vars, &$context, $token)
    339:     {
    340:         $ret = array();
    341:         $i = 0;
    342:         foreach ($vars as $idx => $var) {
    343:             // handle (...)
    344:             if (preg_match(SafeString::IS_SUBEXP_SEARCH, $var)) {
    345:                 $ret[$i] = static::subexpression($var, $context);
    346:                 $i++;
    347:                 continue;
    348:             }
    349:
    350:             // handle |...|
    351:             if (preg_match(SafeString::IS_BLOCKPARAM_SEARCH, $var, $matched)) {
    352:                 $ret[static::BLOCKPARAM] = explode(' ', $matched[1]);
    353:                 continue;
    354:             }
    355:
    356:             if ($context['flags']['namev']) {
    357:                 if (preg_match('/^((\\[([^\\]]+)\\])|([^=^["\']+))=(.+)$/', $var, $m)) {
    358:                     if (!$context['flags']['advar'] && $m[3]) {
    359:                         $context['error'][] = "Wrong argument name as '[$m[3]]' in $token ! You should fix your template or compile with LightnCandy::FLAG_ADVARNAME flag.";
    360:                     }
    361:                     $idx = $m[3] ? $m[3] : $m[4];
    362:                     $var = $m[5];
    363:                     // handle foo=(...)
    364:                     if (preg_match(SafeString::IS_SUBEXP_SEARCH, $var)) {
    365:                         $ret[$idx] = static::subexpression($var, $context);
    366:                         continue;
    367:                     }
    368:                 }
    369:             }
    370:
    371:             if ($context['flags']['advar'] && !preg_match("/^(\"|\\\\')(.*)(\"|\\\\')$/", $var)) {
    372:                 // foo]  Rule 1: no starting [ or [ not start from head
    373:                 if (preg_match('/^[^\\[\\.]+[\\]\\[]/', $var)
    374:                     // [bar  Rule 2: no ending ] or ] not in the end
    375:                     || preg_match('/[\\[\\]][^\\]\\.]+$/', $var)
    376:                     // ]bar. Rule 3: middle ] not before .
    377:                     || preg_match('/\\][^\\]\\[\\.]+\\./', $var)
    378:                     // .foo[ Rule 4: middle [ not after .
    379:                     || preg_match('/\\.[^\\]\\[\\.]+\\[/', preg_replace('/^(..\\/)+/', '', preg_replace('/\\[[^\\]]+\\]/', '[XXX]', $var)))
    380:                 ) {
    381:                     $context['error'][] = "Wrong variable naming as '$var' in $token !";
    382:                 } else {
    383:                     $name = preg_replace('/(\\[.+?\\])/', '', $var);
    384:                     // Scan for invalid charactors which not be protected by [ ]
    385:                     // now make ( and ) pass, later fix
    386:                     if (preg_match('/[!"#%\'*+,;<=>{|}~]/', $name)) {
    387:                         if (!$context['flags']['namev'] && preg_match('/.+=.+/', $name)) {
    388:                             $context['error'][] = "Wrong variable naming as '$var' in $token ! If you try to use foo=bar param, you should enable LightnCandy::FLAG_NAMEDARG !";
    389:                         } else {
    390:                             $context['error'][] = "Wrong variable naming as '$var' in $token ! You should wrap ! \" # % & ' * + , ; < = > { | } ~ into [ ]";
    391:                         }
    392:                     }
    393:                 }
    394:             }
    395:
    396:             $var = static::getExpression($var, $context, $idx);
    397:
    398:             if (is_string($idx)) {
    399:                 $ret[$idx] = $var;
    400:             } else {
    401:                 $ret[$i] = $var;
    402:                 $i++;
    403:             }
    404:         }
    405:         return $ret;
    406:     }
    407:
    408:     /**
    409:      * Detect quote charactors
    410:      *
    411:      * @param string $string the string to be detect the quote charactors
    412:      *
    413:      * @return array<string,integer>|null Expected ending string when quote charactor be detected
    414:      */
    415:     protected static function detectQuote($string)
    416:     {
    417:         // begin with '(' without ending ')'
    418:         if (preg_match('/^\([^\)]*$/', $string)) {
    419:             return array(')', 1);
    420:         }
    421:
    422:         // begin with '"' without ending '"'
    423:         if (preg_match('/^"[^"]*$/', $string)) {
    424:             return array('"', 0);
    425:         }
    426:
    427:         // begin with \' without ending '
    428:         if (preg_match('/^\\\\\'[^\']*$/', $string)) {
    429:             return array('\'', 0);
    430:         }
    431:
    432:         // '="' exists without ending '"'
    433:         if (preg_match('/^[^"]*="[^"]*$/', $string)) {
    434:             return array('"', 0);
    435:         }
    436:
    437:         // '[' exists without ending ']'
    438:         if (preg_match('/^([^"\'].+)?\\[[^\\]]*$/', $string)) {
    439:             return array(']', 0);
    440:         }
    441:
    442:         // =\' exists without ending '
    443:         if (preg_match('/^[^\']*=\\\\\'[^\']*$/', $string)) {
    444:             return array('\'', 0);
    445:         }
    446:
    447:         // continue to next match when =( exists without ending )
    448:         if (preg_match('/.+(\(+)[^\)]*$/', $string, $m)) {
    449:             return array(')', strlen($m[1]));
    450:         }
    451:     }
    452:
    453:     /**
    454:      * Analyze a token string and return parsed result.
    455:      *
    456:      * @param string $token preg_match results
    457:      * @param array<string,array|string|integer> $context current compile context
    458:      *
    459:      * @return array<boolean|integer|array> Return parsed result
    460:      *
    461:      * @expect array('foo', 'bar') when input 'foo bar', array('flags' => array('advar' => 1))
    462:      * @expect array('foo', "'bar'") when input "foo 'bar'", array('flags' => array('advar' => 1))
    463:      * @expect array('[fo o]', '"bar"') when input '[fo o] "bar"', array('flags' => array('advar' => 1))
    464:      * @expect array('fo=123', 'bar="45', '6"') when input 'fo=123 bar="45 6"', array('flags' => array('advar' => 0))
    465:      * @expect array('fo=123', 'bar="45 6"') when input 'fo=123 bar="45 6"', array('flags' => array('advar' => 1))
    466:      * @expect array('[fo', 'o]=123') when input '[fo o]=123', array('flags' => array('advar' => 0))
    467:      * @expect array('[fo o]=123') when input '[fo o]=123', array('flags' => array('advar' => 1))
    468:      * @expect array('[fo o]=123', 'bar="456"') when input '[fo o]=123 bar="456"', array('flags' => array('advar' => 1))
    469:      * @expect array('[fo o]="1 2 3"') when input '[fo o]="1 2 3"', array('flags' => array('advar' => 1))
    470:      * @expect array('foo', 'a=(foo a=(foo a="ok"))') when input 'foo a=(foo a=(foo a="ok"))', array('flags' => array('advar' => 1))
    471:      */
    472:     protected static function analyze($token, &$context)
    473:     {
    474:         $count = preg_match_all('/(\s*)([^\s]+)/', $token, $matchedall);
    475:         // Parse arguments and deal with "..." or [...] or (...) or \'...\' or |...|
    476:         if (($count > 0) && $context['flags']['advar']) {
    477:             $vars = array();
    478:             $prev = '';
    479:             $expect = 0;
    480:             $quote = 0;
    481:             $stack = 0;
    482:
    483:             foreach ($matchedall[2] as $index => $t) {
    484:                 $detected = static::detectQuote($t);
    485:
    486:                 if ($expect === ')') {
    487:                     if ($detected && ($detected[0] !== ')')) {
    488:                         $quote = $detected[0];
    489:                     }
    490:                     if (substr($t, -1, 1) === $quote) {
    491:                         $quote = 0;
    492:                     }
    493:                 }
    494:
    495:                 // continue from previous match when expect something
    496:                 if ($expect) {
    497:                     $prev .= "{$matchedall[1][$index]}$t";
    498:                     if (($quote === 0) && ($stack > 0) && preg_match('/(.+=)*(\\(+)/', $t, $m)) {
    499:                         $stack += strlen($m[2]);
    500:                     }
    501:                     // end an argument when end with expected charactor
    502:                     if (substr($t, -1, 1) === $expect) {
    503:                         if ($stack > 0) {
    504:                             preg_match('/(\\)+)$/', $t, $matchedq);
    505:                             $stack -= isset($matchedq[0]) ? strlen($matchedq[0]) : 1;
    506:                             if ($stack > 0) {
    507:                                 continue;
    508:                             }
    509:                             if ($stack < 0) {
    510:                                 $context['error'][] = "Unexcepted ')' in expression '$token' !!";
    511:                                 $expect = 0;
    512:                                 break;
    513:                             }
    514:                         }
    515:                         $vars[] = $prev;
    516:                         $prev = '';
    517:                         $expect = 0;
    518:                         continue;
    519:                     } elseif (($expect == ']') && (strpos($t, $expect) !== false)) {
    520:                         $t = $prev;
    521:                         $detected = static::detectQuote($t);
    522:                         $expect = 0;
    523:                     } else {
    524:                         continue;
    525:                     }
    526:                 }
    527:
    528:
    529:                 if ($detected) {
    530:                     $prev = $t;
    531:                     $expect = $detected[0];
    532:                     $stack = $detected[1];
    533:                     continue;
    534:                 }
    535:
    536:                 // continue to next match when 'as' without ending '|'
    537:                 if (($t === 'as') && (count($vars) > 0)) {
    538:                     $prev = '';
    539:                     $expect = '|';
    540:                     $stack=1;
    541:                     continue;
    542:                 }
    543:
    544:                 $vars[] = $t;
    545:             }
    546:
    547:             if ($expect) {
    548:                 $context['error'][] = "Error in '$token': expect '$expect' but the token ended!!";
    549:             }
    550:
    551:             return $vars;
    552:         }
    553:         return ($count > 0) ? $matchedall[2] : explode(' ', $token);
    554:     }
    555: }
    556: