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

InlineGrammar::flushDefault()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 8
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;
13
14
use Spiral\Stempler\Lexer\Buffer;
15
use Spiral\Stempler\Lexer\Byte;
16
use Spiral\Stempler\Lexer\Grammar\Traits\TokenTrait;
17
use Spiral\Stempler\Lexer\GrammarInterface;
18
use Spiral\Stempler\Lexer\Token;
19
20
/**
21
 * Handle block inline injects ${name|default}, to be used in combination with HTML grammar.
22
 */
23
final class InlineGrammar implements GrammarInterface
24
{
25
    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\Grammar\InlineGrammar: $char, $content, $type
Loading history...
26
27
    // inject grammar tokens
28
    public const TYPE_OPEN_TAG  = 1;
29
    public const TYPE_CLOSE_TAG = 2;
30
    public const TYPE_NAME      = 3;
31
    public const TYPE_SEPARATOR = 4;
32
    public const TYPE_DEFAULT   = 5;
33
34
    // whitespace
35
    private const REGEXP_WHITESPACE = '/\s/';
36
37
    // Allowed keyword characters.
38
    private const REGEXP_KEYWORD = '/[a-z0-9_\-:\.]/ui';
39
40
    /** @var Byte[] */
41
    private $name = [];
42
43
    /** @var Byte[]|null */
44
    private $default = null;
45
46
    /**
47
     * @inheritDoc
48
     */
49
    public function parse(Buffer $src): \Generator
50
    {
51
        while ($n = $src->next()) {
52
            if (!$n instanceof Byte || $n->char !== '$' || $src->lookaheadByte() !== '{') {
53
                yield $n;
54
                continue;
55
            }
56
57
            $binding = (clone $this)->parseGrammar($src, $n->offset);
58
            if ($binding === null) {
59
                yield $n;
60
                $src->replay($n->offset);
61
                continue;
62
            }
63
64
            yield from $binding;
65
        }
66
    }
67
68
    /**
69
     * @codeCoverageIgnore
70
     * @inheritDoc
71
     */
72
    public static function tokenName(int $token): string
73
    {
74
        switch ($token) {
75
            case self::TYPE_OPEN_TAG:
76
                return 'INLINE:OPEN_TAG';
77
            case self::TYPE_CLOSE_TAG:
78
                return 'INLINE:CLOSE_TAG';
79
            case self::TYPE_NAME:
80
                return 'INLINE:NAME';
81
            case self::TYPE_SEPARATOR:
82
                return 'INLINE:SEPARATOR';
83
            case self::TYPE_DEFAULT:
84
                return 'INLINE:DEFAULT';
85
            default:
86
                return 'INLINE:UNDEFINED';
87
        }
88
    }
89
90
    /**
91
     * @param Buffer $src
92
     * @param int    $offset
93
     * @return Token[]|null
94
     */
95
    private function parseGrammar(Buffer $src, int $offset): ?array
96
    {
97
        $this->tokens = [
98
            new Token(self::TYPE_OPEN_TAG, $offset, '$' . $src->next()->char)
0 ignored issues
show
Bug introduced by
The property char does not seem to exist on Spiral\Stempler\Lexer\Token.
Loading history...
99
        ];
100
101
        while ($n = $src->next()) {
102
            if (!$n instanceof Byte) {
103
                // no other grammars are allowed
104
                return null;
105
            }
106
107
            switch ($n->char) {
108
                case '"':
109
                case "'":
110
                    if ($this->default === null) {
111
                        // " and ' not allowed in names
112
                        return null;
113
                    }
114
115
                    $this->default[] = $n;
116
                    while ($nn = $src->next()) {
117
                        $this->default[] = $nn;
118
                        if ($nn instanceof Byte && $nn->char === $n->char) {
119
                            break;
120
                        }
121
                    }
122
                    break;
123
124
                case '}':
125
                    $this->flushName();
126
                    $this->flushDefault();
127
128
                    $this->tokens[] = new Token(
129
                        self::TYPE_CLOSE_TAG,
130
                        $n->offset,
131
                        $n->char
132
                    );
133
134
                    break 2;
135
136
                case '|':
137
                    $this->flushName();
138
                    $this->flushDefault();
139
140
                    $this->tokens[] = new Token(
141
                        self::TYPE_SEPARATOR,
142
                        $n->offset,
143
                        $n->char
144
                    );
145
146
                    $this->default = [];
147
148
                    break;
149
150
                default:
151
                    if ($this->default !== null) {
152
                        // default allows spaces
153
                        $this->default[] = $n;
154
                        break;
155
                    }
156
157
                    if (preg_match(self::REGEXP_WHITESPACE, $n->char)) {
158
                        break;
159
                    }
160
161
                    if (preg_match(self::REGEXP_KEYWORD, $n->char)) {
162
                        $this->name[] = $n;
163
                        break;
164
                    }
165
166
                    return null;
167
            }
168
        }
169
170
        if (!$this->isValid()) {
171
            return null;
172
        }
173
174
        return $this->tokens;
175
    }
176
177
    /**
178
     * @return bool
179
     */
180
    private function isValid(): bool
181
    {
182
        if (count($this->tokens) < 3) {
183
            return false;
184
        }
185
186
        $hasName = false;
187
        $hasDefault = null;
188
        foreach ($this->tokens as $token) {
189
            if ($token->type === self::TYPE_NAME) {
190
                $hasName = true;
191
                continue;
192
            }
193
194
            if ($token->type === self::TYPE_SEPARATOR && $hasDefault === null) {
195
                $hasDefault = false;
196
                continue;
197
            }
198
199
            if ($token->type === self::TYPE_DEFAULT) {
200
                if ($hasDefault === true) {
201
                    // multiple default value
202
                    return false;
203
                }
204
205
                $hasDefault = true;
206
            }
207
        }
208
209
        return $hasName && ($hasDefault === null || $hasDefault === true);
210
    }
211
212
    /**
213
     * Pack name token.
214
     */
215
    private function flushName(): void
216
    {
217
        if ($this->name === []) {
218
            return;
219
        }
220
221
        $this->tokens[] = $this->packToken($this->name, self::TYPE_NAME);
222
        $this->name = [];
223
    }
224
225
    /**
226
     * Pack default token.
227
     */
228
    private function flushDefault(): void
229
    {
230
        if ($this->default === [] || $this->default === null) {
231
            return;
232
        }
233
234
        $this->tokens[] = $this->packToken($this->default, self::TYPE_DEFAULT);
235
        $this->default = [];
236
    }
237
}
238