DirectiveGrammar::getLastOffset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Stempler\Lexer\Grammar\Dynamic;
6
7
use Spiral\Stempler\Exception\SyntaxException;
8
use Spiral\Stempler\Lexer\Buffer;
9
use Spiral\Stempler\Lexer\Byte;
10
use Spiral\Stempler\Lexer\Grammar\DynamicGrammar;
11
use Spiral\Stempler\Lexer\Grammar\Traits\TokenTrait;
12
use Spiral\Stempler\Lexer\Token;
13
14
/**
15
 * @implements \IteratorAggregate<int, Token>
16
 */
17
final class DirectiveGrammar implements \IteratorAggregate
18
{
19
    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...
20
21
    // start directive
22
    public const DIRECTIVE_CHAR = '@';
23
24
    // whitespace
25
    private const REGEXP_WHITESPACE = '/\\s/';
26
27
    // Allowed keyword characters.
28
    private const REGEXP_KEYWORD = '/[a-z0-9_\\-:\\.]/ui';
29
30
    private array $name = [];
31
    private ?array $body = [];
32
33 59
    public function parse(Buffer $src, int $offset): bool
34
    {
35 59
        $this->tokens = [
36 59
            new Token(DynamicGrammar::TYPE_DIRECTIVE, $offset, self::DIRECTIVE_CHAR),
37 59
        ];
38
39 59
        $this->body = null;
40 59
        $hasWhitespace = false;
41
42 59
        while ($n = $src->next()) {
43 59
            if (!$n instanceof Byte) {
44
                // no other grammars are allowed
45 2
                break;
46
            }
47
48 59
            switch ($n->char) {
49 59
                case '(':
50 48
                    $this->flushName();
51 48
                    $this->tokens[] = new Token(DynamicGrammar::TYPE_BODY_OPEN, $n->offset, $n->char);
52
53 48
                    return $this->parseBody($src);
54
                default:
55 59
                    if (\preg_match(self::REGEXP_WHITESPACE, $n->char)) {
56 15
                        $hasWhitespace = true;
57 15
                        if ($this->name !== []) {
58 15
                            $this->flushName();
59 15
                            $this->tokens[] = new Token(DynamicGrammar::TYPE_WHITESPACE, $n->offset, $n->char);
60 15
                            break;
61
                        }
62
63 1
                        if ($this->getLastToken()->type === DynamicGrammar::TYPE_WHITESPACE) {
64 1
                            $this->getLastToken()->content .= $n->char;
65 1
                            break;
66
                        }
67
68
                        // invalid directive
69
                        return false;
70 59
                    } elseif ($hasWhitespace) {
71 12
                        return $this->finalize();
72
                    }
73
74 59
                    if (!\preg_match(self::REGEXP_KEYWORD, $n->char)) {
75 3
                        $this->flushName();
76
77 3
                        return $this->finalize();
78
                    }
79
80 58
                    $this->name[] = $n;
81
            }
82
        }
83
84 26
        $this->flushName();
85
86 26
        return $this->finalize();
87
    }
88
89
    /**
90
     * Directive tokens.
91
     *
92
     * @return \Generator<int, Token>
93
     */
94 49
    public function getIterator(): \Generator
95
    {
96 49
        if ($this->tokens === []) {
97
            throw new \LogicException('Directive not parsed');
98
        }
99
100 49
        yield from $this->tokens;
101
    }
102
103
    /**
104
     * Return offset after last directive token.
105
     */
106 57
    public function getLastOffset(): int
107
    {
108 57
        return $this->getLastToken()->offset + \strlen($this->getLastToken()->content) - 1;
109
    }
110
111
    /**
112
     * Get directive keyword.
113
     */
114 57
    public function getKeyword(): string
115
    {
116 57
        foreach ($this->tokens as $token) {
117 57
            if ($token->type === DynamicGrammar::TYPE_KEYWORD) {
118 57
                return $token->content;
119
            }
120
        }
121
122
        throw new SyntaxException('Directive not parsed', $this->tokens[0]);
123
    }
124
125
    /**
126
     * Get directive body.
127
     */
128 8
    public function getBody(): ?string
129
    {
130 8
        foreach ($this->tokens as $token) {
131 8
            if ($token->type === DynamicGrammar::TYPE_BODY) {
132 6
                return $token->content;
133
            }
134
        }
135
136 2
        return null;
137
    }
138
139
    /**
140
     * Pack keyword token.
141
     */
142 59
    private function flushName(): void
143
    {
144 59
        if ($this->name === []) {
145 4
            return;
146
        }
147
148 58
        $this->tokens[] = $this->packToken($this->name, DynamicGrammar::TYPE_KEYWORD);
149 58
        $this->name = [];
150
    }
151
152
    /**
153
     * TODO issue #767
154
     * @link https://github.com/spiral/framework/issues/767
155
     * @psalm-suppress UndefinedPropertyFetch
156
     */
157 48
    private function parseBody(Buffer $src): bool
158
    {
159 48
        $this->body = [];
160 48
        $level = 1;
161
162 48
        while ($nn = $src->next()) {
163 48
            if (!$nn instanceof Byte) {
164
                $this->flushBody();
165
                return $this->finalize();
166
            }
167
168 48
            if (\in_array($nn->char, ['"', '"'])) {
169 11
                $this->body[] = $nn;
170 11
                while ($nnn = $src->next()) {
171 11
                    $this->body[] = $nnn;
172 11
                    if ($nnn instanceof Byte && $nnn->char === $nn->char) {
173 11
                        break;
174
                    }
175
                }
176 11
                continue;
177
            }
178
179 48
            $this->body[] = $nn;
180
181 48
            if ($nn->char === '(') {
182 6
                $level++;
183 6
                continue;
184
            }
185
186 48
            if ($nn->char === ')') {
187 47
                $level--;
188
189 47
                if ($level === 0) {
190 47
                    $n = \array_pop($this->body);
0 ignored issues
show
Bug introduced by
It seems like $this->body can also be of type null; however, parameter $array of array_pop() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

190
                    $n = \array_pop(/** @scrutinizer ignore-type */ $this->body);
Loading history...
191
192 47
                    $this->flushBody();
193 47
                    $this->tokens[] = new Token(DynamicGrammar::TYPE_BODY_CLOSE, $n->offset, $n->char);
194
195 47
                    return $this->finalize();
196
                }
197
            }
198
        }
199
200 1
        return $this->finalize();
201
    }
202
203
    /**
204
     * Pack name token.
205
     */
206 47
    private function flushBody(): void
207
    {
208 47
        if ($this->body === []) {
209
            return;
210
        }
211
212 47
        $this->tokens[] = $this->packToken($this->body, DynamicGrammar::TYPE_BODY);
0 ignored issues
show
Bug introduced by
It seems like $this->body can also be of type null; however, parameter $inner of Spiral\Stempler\Lexer\Gr...iveGrammar::packToken() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

212
        $this->tokens[] = $this->packToken(/** @scrutinizer ignore-type */ $this->body, DynamicGrammar::TYPE_BODY);
Loading history...
213 47
        $this->body = [];
214
    }
215
216 57
    private function getLastToken(): Token
217
    {
218 57
        if ($this->tokens === []) {
219
            throw new \LogicException('Directive not parsed');
220
        }
221
222 57
        return $this->tokens[\count($this->tokens) - 1];
223
    }
224
225
    /**
226
     * Flush directive and seek buffer before last non WHITESPACE token.
227
     */
228 59
    private function finalize(): bool
229
    {
230 59
        $tokens = $this->tokens;
231
232
        // A directive must have at least one keyword
233
        // Without it, it's just a char
234 59
        if (\count($tokens) === 1 && $tokens[0]->content === self::DIRECTIVE_CHAR) {
235 1
            return false;
236
        }
237
238 58
        foreach (\array_reverse($tokens, true) as $i => $t) {
239 58
            if ($t->type !== DynamicGrammar::TYPE_WHITESPACE) {
240 58
                break;
241
            }
242
243 13
            unset($tokens[$i]);
244
        }
245
246 58
        $body = null;
247 58
        foreach ($tokens as $t) {
248 58
            if ($t->type === DynamicGrammar::TYPE_BODY_OPEN) {
249 48
                $body = false;
250 48
                continue;
251
            }
252
253 58
            if ($t->type === DynamicGrammar::TYPE_BODY_CLOSE) {
254 47
                $body = null;
255
            }
256
        }
257
258 58
        if ($body !== null) {
259 1
            return false;
260
        }
261
262 57
        $this->tokens = $tokens;
263
264 57
        return true;
265
    }
266
}
267