Passed
Push — master ( c776c7...570285 )
by Kirill
04:05
created

DirectiveGrammar::getIterator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 7
rs 10
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Stempler\Lexer\Grammar\Dynamic;
13
14
use Spiral\Stempler\Exception\SyntaxException;
15
use Spiral\Stempler\Lexer\Buffer;
16
use Spiral\Stempler\Lexer\Byte;
17
use Spiral\Stempler\Lexer\Grammar\DynamicGrammar;
18
use Spiral\Stempler\Lexer\Grammar\Traits\TokenTrait;
19
use Spiral\Stempler\Lexer\Token;
20
21
final class DirectiveGrammar implements \IteratorAggregate
22
{
23
    use TokenTrait;
0 ignored issues
show
introduced by
The trait Spiral\Stempler\Lexer\Grammar\Traits\TokenTrait requires some properties which are not provided by Spiral\Stempler\Lexer\Gr...ynamic\DirectiveGrammar: $char, $content, $type
Loading history...
24
25
    // start directive
26
    public const DIRECTIVE_CHAR = '@';
27
28
    // whitespace
29
    private const REGEXP_WHITESPACE = '/\s/';
30
31
    // Allowed keyword characters.
32
    private const REGEXP_KEYWORD = '/[a-z0-9_\-:\.]/ui';
33
34
    /** @var array */
35
    private $name = [];
36
37
    /** @var array */
38
    private $body = [];
39
40
    /**
41
     * @param Buffer $src
42
     * @param int    $offset
43
     * @return bool
44
     */
45
    public function parse(Buffer $src, int $offset): bool
46
    {
47
        $this->tokens = [
48
            new Token(DynamicGrammar::TYPE_DIRECTIVE, $offset, self::DIRECTIVE_CHAR)
49
        ];
50
51
        $this->body = null;
52
        $hasWhitespace = false;
53
54
        while ($n = $src->next()) {
55
            if (!$n instanceof Byte) {
56
                // no other grammars are allowed
57
                break;
58
            }
59
60
            switch ($n->char) {
61
                case '(':
62
                    $this->flushName();
63
                    $this->tokens[] = new Token(DynamicGrammar::TYPE_BODY_OPEN, $n->offset, $n->char);
64
65
                    return $this->parseBody($src);
66
                default:
67
                    if (preg_match(self::REGEXP_WHITESPACE, $n->char)) {
68
                        $hasWhitespace = true;
69
                        if ($this->name !== []) {
70
                            $this->flushName();
71
                            $this->tokens[] = new Token(DynamicGrammar::TYPE_WHITESPACE, $n->offset, $n->char);
72
                            break;
73
                        }
74
75
                        if ($this->getLastToken()->type === DynamicGrammar::TYPE_WHITESPACE) {
76
                            $this->getLastToken()->content .= $n->char;
77
                            break;
78
                        }
79
80
                        // invalid directive
81
                        return false;
82
                    } elseif ($hasWhitespace) {
83
                        return $this->finalize();
84
                    }
85
86
                    if (!preg_match(self::REGEXP_KEYWORD, $n->char)) {
87
                        $this->flushName();
88
89
                        return $this->finalize();
90
                    }
91
92
                    $this->name[] = $n;
93
            }
94
        }
95
96
        $this->flushName();
97
98
        return $this->finalize();
99
    }
100
101
    /**
102
     * Directive tokens.
103
     *
104
     * @return \Generator|\Traversable
105
     */
106
    public function getIterator()
107
    {
108
        if ($this->tokens === []) {
109
            throw new \LogicException('Directive not parsed');
110
        }
111
112
        yield from $this->tokens;
113
    }
114
115
    /**
116
     * Return offset after last directive token.
117
     *
118
     * @return int
119
     */
120
    public function getLastOffset(): int
121
    {
122
        return $this->getLastToken()->offset + strlen($this->getLastToken()->content) - 1;
123
    }
124
125
    /**
126
     * Get directive keyword.
127
     *
128
     * @return string
129
     */
130
    public function getKeyword(): string
131
    {
132
        foreach ($this->tokens as $token) {
133
            if ($token->type === DynamicGrammar::TYPE_KEYWORD) {
134
                return $token->content;
135
            }
136
        }
137
138
        throw new SyntaxException('Directive not parsed', $this->tokens[0]);
139
    }
140
141
    /**
142
     * Get directive body.
143
     *
144
     * @return string
145
     */
146
    public function getBody(): ?string
147
    {
148
        foreach ($this->tokens as $token) {
149
            if ($token->type === DynamicGrammar::TYPE_BODY) {
150
                return $token->content;
151
            }
152
        }
153
154
        return null;
155
    }
156
157
    /**
158
     * Pack keyword token.
159
     */
160
    private function flushName(): void
161
    {
162
        if ($this->name === []) {
163
            return;
164
        }
165
166
        $this->tokens[] = $this->packToken($this->name, DynamicGrammar::TYPE_KEYWORD);
167
        $this->name = [];
168
    }
169
170
    /**
171
     * @param Buffer $src
172
     * @return bool
173
     */
174
    private function parseBody(Buffer $src)
175
    {
176
        $this->body = [];
177
        $level = 1;
178
179
        while ($nn = $src->next()) {
180
            if (!$nn instanceof Byte) {
181
                $this->flushBody();
182
                return $this->finalize();
183
            }
184
185
            if (in_array($nn->char, ['"', '"'])) {
186
                $this->body[] = $nn;
187
                while ($nnn = $src->next()) {
188
                    $this->body[] = $nnn;
189
                    if ($nnn instanceof Byte && $nnn->char === $nn->char) {
190
                        break;
191
                    }
192
                }
193
                continue;
194
            }
195
196
            $this->body[] = $nn;
197
198
            if ($nn->char === '(') {
199
                $level++;
200
                continue;
201
            }
202
203
            if ($nn->char === ')') {
204
                $level--;
205
206
                if ($level === 0) {
207
                    $n = array_pop($this->body);
208
209
                    $this->flushBody();
210
                    $this->tokens[] = new Token(DynamicGrammar::TYPE_BODY_CLOSE, $n->offset, $n->char);
211
212
                    return $this->finalize();
213
                }
214
215
                continue;
216
            }
217
        }
218
219
        return $this->finalize();
220
    }
221
222
    /**
223
     * Pack name token.
224
     */
225
    private function flushBody(): void
226
    {
227
        if ($this->body === []) {
228
            return;
229
        }
230
231
        $this->tokens[] = $this->packToken($this->body, DynamicGrammar::TYPE_BODY);
232
        $this->body = [];
233
    }
234
235
    /**
236
     * @return Token
237
     */
238
    private function getLastToken(): Token
239
    {
240
        if ($this->tokens === []) {
241
            throw new \LogicException('Directive not parsed');
242
        }
243
244
        return $this->tokens[count($this->tokens) - 1];
245
    }
246
247
    /**
248
     * Flush directive and seek buffer before last non WHITESPACE token.
249
     *
250
     * @return bool
251
     */
252
    private function finalize(): bool
253
    {
254
        $tokens = $this->tokens;
255
256
        foreach (array_reverse($tokens, true) as $i => $t) {
257
            if ($t->type !== DynamicGrammar::TYPE_WHITESPACE) {
258
                break;
259
            }
260
261
            unset($tokens[$i]);
262
        }
263
264
        $body = null;
265
        foreach ($tokens as $t) {
266
            if ($t->type === DynamicGrammar::TYPE_BODY_OPEN) {
267
                $body = false;
268
                continue;
269
            }
270
271
            if ($t->type === DynamicGrammar::TYPE_BODY_CLOSE) {
272
                $body = null;
273
                continue;
274
            }
275
        }
276
277
        if ($body !== null) {
278
            return false;
279
        }
280
281
        $this->tokens = $tokens;
282
283
        return true;
284
    }
285
}
286