Passed
Push — master ( 079f1e...99c93a )
by Observer
01:36
created

Parser::parseArguments()   B

Complexity

Conditions 11
Paths 7

Size

Total Lines 35
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
c 0
b 0
f 0
dl 0
loc 35
rs 7.3166
cc 11
nc 7
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace VLF;
4
5
/**
6
 * Парсер VLF разметки
7
 */
8
class Parser
9
{
10
    // Разделитель строк
11
    public static string $divider = "\n";
12
13
    /**
14
     * Парсер AST дерева из VLF разметки
15
     * 
16
     * @param string $vlf - VLF разметка
17
     * 
18
     * @return AST - возвращает AST дерево разметки
19
     */
20
    public static function parse (string $vlf): AST
21
    {
22
        $tree    = new AST;
23
        $objects = new Stack;
24
25
        if (file_exists ($vlf))
26
            $vlf = file_get_contents ($vlf);
27
28
        $lines   = explode (self::$divider, $vlf);
29
        $skip_at = -1;
30
31
        foreach ($lines as $line_num => $line)
32
        {
33
            // \VoidEngine\pre ($line_num .', '. ($skip_at > $line_num ? 'skip' : 'not skip') .': '. $line);
34
35
            if ($skip_at > $line_num || !self::filter ($line))
36
                continue;
37
38
            $height = self::getHeight ($line);
39
            $words  = array_filter (explode (' ', $line), 'VLF\Parser::filter');
40
            $poped  = false;
41
42
            # Очищаем стек объектов
43
            while ($objects->size () > 0)
44
                if ($objects->current ()->height >= $height)
45
                {
46
                    $objects->pop ();
47
                    
48
                    $poped = true;
49
                }
50
51
                else break;
52
53
            # Создаём новую ссылку на объект
54
            if ($poped && $objects->size () > 0)
55
            {
56
                $object = $objects->pop ();
57
58
                $objects->push (new Node (array_merge ($object->export (), ['nodes' => []])));
59
                $tree->push ($objects->current ());
60
            }
61
62
            /**
63
             * Импорт таблиц стилей
64
             */
65
            if ($words[0] == 'import')
66
            {
67
                $imports = substr ($line, strlen ($words[0]));
68
                $parsed  = self::parseSubtext ($lines, $line_num, $height);
69
70
                $imports .= $parsed[0];
71
                $skip_at  = $parsed[1];
72
73
                $imports = self::filter ($imports) ?
74
                    array_map ('trim', self::parseArguments ($imports)) : [];
75
76
                $tree->push (new Node ([
77
                    'type'   => STYLES_IMPORTING,
78
                    'line'   => $line,
79
                    'words'  => $words,
80
                    'height' => $height,
81
82
                    'args' => [
83
                        'imports' => $imports
84
                    ]
85
                ]));
86
            }
87
88
            /**
89
             * Комментарии
90
             */
91
            elseif ($words[0][0] == '#')
92
            {
93
                /**
94
                 * Обработка многострочных комментариев
95
                 */
96
                if (isset ($words[0][1]))
97
                {
98
                    if ($words[0][1] == '^')
99
                        $skip_at = self::parseSubtext ($lines, $line_num, $height)[1];
100
101
                    else throw new \Exception ('Unknown char founded after comment definition at line '. ($line_num + 1));
102
                }
103
104
                continue;
105
            }
106
107
            /**
108
             * Выполнение PHP кода
109
             */
110
            elseif ($words[0][0] == '%')
111
            {
112
                $code = substr ($line, strlen ($words[0]));
113
114
                /**
115
                 * Обработка многострочного кода
116
                 */
117
                if (isset ($words[0][1]))
118
                {
119
                    if ($words[0][1] == '^')
120
                    {
121
                        $parsed = self::parseSubtext ($lines, $line_num, $height);
122
123
                        $code   .= $parsed[0];
124
                        $skip_at = $parsed[1];
125
                    }
126
127
                    else throw new \Exception ('Unknown char founded after runtime execution definition at line '. ($line_num + 1));
128
                }
129
130
                $tree->push (new Node ([
131
                    'type'   => RUNTIME_EXECUTION,
132
                    'line'   => $line,
133
                    'words'  => $words,
134
                    'height' => $height,
135
136
                    'args' => [
137
                        'code' => $code
138
                    ]
139
                ]));
140
            }
141
142
            /**
143
             * Установка свойства
144
             */
145
            elseif (($pos = strpos ($line, ':')) !== false)
146
            {
147
                if ($objects->size () == 0)
148
                    throw new \Exception ('Trying to set property to unknown object at line '. ($line_num + 1));
149
150
                if (!isset ($words[1]))
151
                    throw new \Exception ('Trying to set void property value at line '. ($line_num + 1));
152
153
                $propertyName  = substr ($line, 0, $pos);
154
                $propertyValue = substr ($line, $pos + 1);
155
156
                /**
157
                 * Обработка многострочных свойств
158
                 */
159
                if ($line[$pos + 1] == '^')
160
                {
161
                    $parsed = self::parseSubtext ($lines, $line_num, $height);
162
163
                    $propertyValue = substr ($propertyValue, 1) . $parsed[0];
164
                    $skip_at       = $parsed[1];
165
                }
166
167
                $objects->current ()->push (new Node ([
168
                    'type'   => PROPERTY_SET,
169
                    'line'   => $line,
170
                    'words'  => $words,
171
                    'height' => $height,
172
173
                    'args' => [
174
                        'name'  => $propertyName,
175
                        'value' => $propertyValue
176
                    ]
177
                ]));
178
            }
179
180
            /**
181
             * Вызов метода
182
             */
183
            elseif (isset ($words[0][1]) && $words[0][0] == '-' && $words[0][1] == '>')
184
            {
185
                if ($objects->size () == 0)
186
                    throw new \Exception ('Trying to call method from unknown object at line '. ($line_num + 1));
187
188
                $methodArgs = [];
189
190
                if (($pos = strpos ($line, '(')) !== false)
191
                {
192
                    if (($end = strrpos ($line, ')', $pos)) === false)
193
                        throw new \Exception ('Incorrect method arguments syntax at line '. ($line_num + 1));
194
195
                    $methodArgs = substr ($line, $pos + 1, $end - $pos - 1);
196
197
                    $methodName = trim (substr ($line, 2, $pos - 2));
198
                    $methodArgs = self::filter ($methodArgs) ?
199
                        self::parseArguments ($methodArgs) : [];
200
                }
201
202
                else $methodName = trim (substr ($line, 2));
203
204
                $objects->current ()->push (new Node ([
205
                    'type'   => METHOD_CALL,
206
                    'line'   => $line,
207
                    'words'  => $words,
208
                    'height' => $height,
209
210
                    'args' => [
211
                        'name' => $methodName,
212
                        'args' => $methodArgs
213
                    ]
214
                ]));
215
            }
216
217
            /**
218
             * Объявление объекта
219
             */
220
            elseif (sizeof ($words) > 1)
221
            {
222
                $class  = $words[0];
223
                $name   = $words[1];
224
                $args   = [];
225
                $styles = [];
226
227
                if ($objects->size () > 0 && $objects->current ()->height < $height)
228
                    $args[] = $objects->current ()->args['name'];
229
230
                if (($pos = strpos ($line, '(')) !== false)
231
                {
232
                    if (($end = strrpos ($line, ')', $pos)) === false)
233
                        throw new \Exception ('Incorrect class constructor arguments syntax at line '. ($line_num + 1));
234
235
                    $args = substr ($line, $pos + 1, $end - $pos - 1);
236
237
                    $name = substr ($line, $len = strlen ($class), $pos - $len);
238
                    $args = self::filter ($args) ?
239
                        self::parseArguments ($args) : [];
240
                }
241
242
                if (($end = strpos ($line, '>')) !== false)
243
                {
244
                    $styles = trim (substr ($line, $end + 1));
245
246
                    if (strlen ($styles) == 0)
247
                        throw new \Exception ('Trying to set empty style to object');
248
249
                    $styles = array_map ('trim', explode (',', $styles));
250
                }
251
252
                $objects->push (new Node ([
253
                    'type'   => OBJECT_DEFINITION,
254
                    'line'   => $line,
255
                    'words'  => $words,
256
                    'height' => $height,
257
258
                    'args' => [
259
                        'class'  => $class,
260
                        'name'   => trim ($name),
261
                        'args'   => $args,
262
                        'styles' => $styles
263
                    ]
264
                ]));
265
266
                $tree->push ($objects->current ());
267
            }
268
269
            /**
270
             * Неопознанная структура
271
             */
272
            else throw new \Exception ('Unsupported structure founded at line '. ($line_num + 1));
273
        }
274
275
        return $tree;
276
    }
277
278
    /**
279
     * Подсчёт высоты строки (кол-во пробельных символов в её начале)
280
     * 
281
     * @param string &$line - строка
282
     * 
283
     * @return int - возвращает её высоту
284
     */
285
    protected static function getHeight (string &$line): int
286
    {
287
        $i = 0;
288
        $height = 0;
289
290
        while (isset ($line[$i]) && ctype_space ($line[$i]))
291
        {
292
            ++$height;
293
294
            if ($line[$i] == "\t")
295
                $height += 3;
296
297
            ++$i;
298
        }
299
300
        $line = substr ($line, $i);
301
302
        return $height;
303
    }
304
305
    /**
306
     * Проверка строки на пустоту
307
     * 
308
     * @param string $line - строка для проверки
309
     * 
310
     * @return bool - возвращает true если строка не пустая
311
     */
312
    protected static function filter (string $line): bool
313
    {
314
        return strlen (trim ($line)) > 0;
315
    }
316
317
    /**
318
     * Парсинг текста, лежащего на указанной высоте
319
     * 
320
     * @param array $lines     - массив строк
321
     * @param mixed $begin_id  - индекс начальной строки
322
     * @param int $down_height - минимальная высота строки
323
     * 
324
     * @return array - возвращает массив [текст, конечный индекс]
325
     */
326
    protected static function parseSubtext (array $lines, $begin_id, int $down_height): array
327
    {
328
        $parsed = "\n";
329
330
        foreach ($lines as $line_id => $line)
331
        {
332
            if ($line_id <= $begin_id)
333
                continue;
334
335
            if (!self::filter ($line))
336
            {
337
                $parsed .= "\n";
338
            
339
                continue;
340
            }
341
342
            $height = self::getHeight ($line);
343
344
            if ($height > $down_height)
345
                $parsed .= str_repeat (' ', $height - $down_height) ."$line\n";
346
347
            else return [$parsed, $line_id];
348
        }
349
350
        return [$parsed, $line_id + 1];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $line_id seems to be defined by a foreach iteration on line 330. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
351
    }
352
353
    /**
354
     * Парсинг аргументов из текста
355
     * 
356
     * @param string $arguments - текст для парсинга
357
     * 
358
     * @return array - возвращает массив аргументов
359
     */
360
    protected static function parseArguments (string $arguments): array
361
    {
362
        $args = [];
363
364
        $split1   = $split2 = false;
365
        $canSplit = -1;
366
367
        $t = '';
368
369
        for ($i = 0, $len = strlen ($arguments); $i < $len; ++$i)
370
        {
371
            $t .= $arguments[$i];
372
            
373
            if ($arguments[$i] == '\\')
374
                $canSplit = $i + 1;
375
376
            elseif ($canSplit < $i)
377
            {
378
                if ($arguments[$i] == '\'' && !$split2)
379
                    $split1 = !$split1;
0 ignored issues
show
introduced by
The condition $split1 is always false.
Loading history...
380
381
                elseif ($arguments[$i] == '"' && !$split1)
382
                    $split2 = !$split2;
383
384
                elseif (!$split1 && !$split2 && $arguments[$i] == ',')
385
                {
386
                    $args[] = substr ($t, 0, -1);
387
                    $t = '';
388
                }
389
            }
390
        }
391
392
        $args[] = $t;
393
394
        return $args;
395
    }
396
}
397