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