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