1<?php
2
3namespace Gettext\Utils;
4
5class JsFunctionsScanner extends FunctionsScanner
6{
7    protected $code;
8    protected $status = [];
9
10    /**
11     * Constructor.
12     *
13     * @param string $code The php code to scan
14     */
15    public function __construct($code)
16    {
17        // Normalize newline characters
18        $this->code = str_replace(["\r\n", "\n\r", "\r"], "\n", $code);
19    }
20
21    /**
22     * {@inheritdoc}
23     */
24    public function getFunctions(array $constants = [])
25    {
26        $length = strlen($this->code);
27        $line = 1;
28        $buffer = '';
29        $functions = [];
30        $bufferFunctions = [];
31        $char = null;
32
33        for ($pos = 0; $pos < $length; ++$pos) {
34            $prev = $char;
35            $char = $this->code[$pos];
36            $next = isset($this->code[$pos + 1]) ? $this->code[$pos + 1] : null;
37
38            switch ($char) {
39                case '\\':
40                    switch ($this->status()) {
41                        case 'simple-quote':
42                            if ($next !== "'") {
43                                break 2;
44                            }
45                            break;
46
47                        case 'double-quote':
48                            if ($next !== '"') {
49                                break 2;
50                            }
51                            break;
52
53                        case 'back-tick':
54                            if ($next !== '`') {
55                                break 2;
56                            }
57                            break;
58                    }
59
60                    $prev = $char;
61                    $char = $next;
62                    $pos++;
63                    $next = isset($this->code[$pos]) ? $this->code[$pos] : null;
64                    break;
65
66                case "\n":
67                    ++$line;
68
69                    if ($this->status('line-comment')) {
70                        $this->upStatus();
71                    }
72                    break;
73
74                case '/':
75                    switch ($this->status()) {
76                        case 'simple-quote':
77                        case 'double-quote':
78                        case 'back-tick':
79                        case 'line-comment':
80                            break;
81
82                        case 'block-comment':
83                            if ($prev === '*') {
84                                $this->upStatus();
85                            }
86                            break;
87
88                        default:
89                            if ($next === '/') {
90                                $this->downStatus('line-comment');
91                            } elseif ($next === '*') {
92                                $this->downStatus('block-comment');
93                            }
94                            break;
95                    }
96                    break;
97
98                case "'":
99                    switch ($this->status()) {
100                        case 'simple-quote':
101                            $this->upStatus();
102                            break;
103
104                        case 'line-comment':
105                        case 'block-comment':
106                        case 'double-quote':
107                        case 'back-tick':
108                            break;
109
110                        default:
111                            $this->downStatus('simple-quote');
112                            break;
113                    }
114                    break;
115
116                case '"':
117                    switch ($this->status()) {
118                        case 'double-quote':
119                            $this->upStatus();
120                            break;
121
122                        case 'line-comment':
123                        case 'block-comment':
124                        case 'simple-quote':
125                        case 'back-tick':
126                            break;
127
128                        default:
129                            $this->downStatus('double-quote');
130                            break;
131                    }
132                    break;
133
134                case '`':
135                    switch ($this->status()) {
136                        case 'back-tick':
137                            $this->upStatus();
138                            break;
139
140                        case 'line-comment':
141                        case 'block-comment':
142                        case 'simple-quote':
143                        case 'double-quote':
144                            break;
145
146                        default:
147                            $this->downStatus('back-tick');
148                            break;
149                    }
150                    break;
151
152                case '(':
153                    switch ($this->status()) {
154                        case 'simple-quote':
155                        case 'double-quote':
156                        case 'back-tick':
157                        case 'line-comment':
158                        case 'block-comment':
159                            break;
160
161                        default:
162                            if ($buffer && preg_match('/(\w+)$/', $buffer, $matches)) {
163                                $this->downStatus('function');
164                                array_unshift($bufferFunctions, [$matches[1], $line, []]);
165                                $buffer = '';
166                                continue 3;
167                            }
168                            break;
169                    }
170                    break;
171
172                case ')':
173                    switch ($this->status()) {
174                        case 'function':
175                            if (($argument = static::prepareArgument($buffer))) {
176                                $bufferFunctions[0][2][] = $argument;
177                            }
178
179                            if (!empty($bufferFunctions)) {
180                                $functions[] = array_shift($bufferFunctions);
181                            }
182
183                            $this->upStatus();
184                            $buffer = '';
185                            continue 3;
186                    }
187                    break;
188
189                case ',':
190                    switch ($this->status()) {
191                        case 'function':
192                            if (($argument = static::prepareArgument($buffer))) {
193                                $bufferFunctions[0][2][] = $argument;
194                            }
195
196                            $buffer = '';
197                            continue 3;
198                    }
199                    break;
200
201                case ' ':
202                case '\t':
203                    switch ($this->status()) {
204                        case 'double-quote':
205                        case 'simple-quote':
206                        case 'back-tick':
207                            break;
208
209                        default:
210                            $buffer = '';
211                            continue 3;
212                    }
213                    break;
214            }
215
216            switch ($this->status()) {
217                case 'line-comment':
218                case 'block-comment':
219                    break;
220
221                default:
222                    $buffer .= $char;
223                    break;
224            }
225        }
226
227        return $functions;
228    }
229
230    /**
231     * Get the current context of the scan.
232     *
233     * @param null|string $match To check whether the current status is this value
234     *
235     * @return string|bool
236     */
237    protected function status($match = null)
238    {
239        $status = isset($this->status[0]) ? $this->status[0] : null;
240
241        if ($match !== null) {
242            return $status === $match;
243        }
244
245        return $status;
246    }
247
248    /**
249     * Add a new status to the stack.
250     *
251     * @param string $status
252     */
253    protected function downStatus($status)
254    {
255        array_unshift($this->status, $status);
256    }
257
258    /**
259     * Removes and return the current status.
260     *
261     * @return string|null
262     */
263    protected function upStatus()
264    {
265        return array_shift($this->status);
266    }
267
268    /**
269     * Prepares the arguments found in functions.
270     *
271     * @param string $argument
272     *
273     * @return string
274     */
275    protected static function prepareArgument($argument)
276    {
277        if ($argument && in_array($argument[0], ['"', "'", '`'], true)) {
278            return static::convertString(substr($argument, 1, -1));
279        }
280    }
281
282    /**
283     * Decodes a string with an argument.
284     *
285     * @param string $value
286     *
287     * @return string
288     */
289    protected static function convertString($value)
290    {
291        if (strpos($value, '\\') === false) {
292            return $value;
293        }
294
295        return preg_replace_callback(
296            '/\\\(n|r|t|v|e|f|"|\\\)/',
297            function ($match) {
298                switch ($match[1][0]) {
299                    case 'n':
300                        return "\n";
301                    case 'r':
302                        return "\r";
303                    case 't':
304                        return "\t";
305                    case 'v':
306                        return "\v";
307                    case 'e':
308                        return "\e";
309                    case 'f':
310                        return "\f";
311                    case '"':
312                        return '"';
313                    case '\\':
314                        return '\\';
315                }
316            },
317            $value
318        );
319    }
320}
321