Passed
Pull Request — master (#1197)
by butschster
11:02
created

DynamicGrammar   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 215
Duplicated Lines 0 %

Test Coverage

Coverage 95.19%

Importance

Changes 0
Metric Value
wmc 34
eloc 129
dl 0
loc 215
ccs 99
cts 104
cp 0.9519
rs 9.68
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 1
A tokenName() 0 12 1
B declare() 0 36 9
B fetchOptions() 0 39 8
C parse() 0 73 15
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Stempler\Lexer\Grammar;
6
7
use Spiral\Stempler\Directive\DirectiveRendererInterface;
8
use Spiral\Stempler\Lexer\Buffer;
9
use Spiral\Stempler\Lexer\Byte;
10
use Spiral\Stempler\Lexer\Grammar\Dynamic\BracesGrammar;
11
use Spiral\Stempler\Lexer\Grammar\Dynamic\DeclareGrammar;
12
use Spiral\Stempler\Lexer\Grammar\Dynamic\DirectiveGrammar;
13
use Spiral\Stempler\Lexer\Grammar\Traits\TokenTrait;
14
use Spiral\Stempler\Lexer\GrammarInterface;
15
use Spiral\Stempler\Lexer\Lexer;
16
use Spiral\Stempler\Lexer\StringStream;
17
use Spiral\Stempler\Lexer\Token;
18
19
/**
20
 * Similar to Laravel blade, this grammar defines the ability to echo PHP variables using {{ $var }} statements
21
 * Grammar also support various component support using [@directive(options)] pattern.
22
 *
23
 * Attention, the syntaxt will treat all [@sequnce()] as directive unless DirectiveRendererInterface is provided.
24
 */
25
final class DynamicGrammar implements GrammarInterface
26
{
27
    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\DynamicGrammar: $char, $content, $type
Loading history...
28
29
    // inject grammar tokens
30
    public const TYPE_OPEN_TAG      = 1;
31
    public const TYPE_CLOSE_TAG     = 2;
32
    public const TYPE_OPEN_RAW_TAG  = 3;
33
    public const TYPE_CLOSE_RAW_TAG = 4;
34
    public const TYPE_BODY_OPEN     = 5;
35
    public const TYPE_BODY_CLOSE    = 6;
36
    public const TYPE_BODY          = 7;
37
    public const TYPE_DIRECTIVE     = 8;
38
    public const TYPE_KEYWORD       = 9;
39
    public const TYPE_WHITESPACE    = 10;
40
41
    // grammar control directive
42
    public const DECLARE_DIRECTIVE = 'declare';
43
44
    private readonly BracesGrammar $echo;
45
    private readonly BracesGrammar $raw;
46
47 181
    public function __construct(
48
        private readonly ?DirectiveRendererInterface $directiveRenderer = null,
49
    ) {
50 181
        $this->echo = new BracesGrammar(
0 ignored issues
show
Bug introduced by
The property echo is declared read-only in Spiral\Stempler\Lexer\Grammar\DynamicGrammar.
Loading history...
51 181
            '{{',
52 181
            '}}',
53 181
            self::TYPE_OPEN_TAG,
54 181
            self::TYPE_CLOSE_TAG,
55 181
        );
56
57 181
        $this->raw = new BracesGrammar(
0 ignored issues
show
Bug introduced by
The property raw is declared read-only in Spiral\Stempler\Lexer\Grammar\DynamicGrammar.
Loading history...
58 181
            '{!!',
59 181
            '!!}',
60 181
            self::TYPE_OPEN_RAW_TAG,
61 181
            self::TYPE_CLOSE_RAW_TAG,
62 181
        );
63
    }
64
65
    /**
66
     * @return \Generator<int, Byte|Token|null>
67
     */
68 169
    public function parse(Buffer $src): \Generator
69
    {
70 169
        while ($n = $src->next()) {
71 169
            if (!$n instanceof Byte) {
72 46
                yield $n;
73 46
                continue;
74
            }
75
76 169
            if ($n->char === DirectiveGrammar::DIRECTIVE_CHAR) {
77
                if (
78 61
                    $this->echo->nextToken($src) ||
79 60
                    $this->raw->nextToken($src) ||
80 61
                    $src->lookaheadByte() === DirectiveGrammar::DIRECTIVE_CHAR
81
                ) {
82
                    // escaped echo sequence, hide directive byte
83 2
                    yield $src->next();
84 2
                    continue;
85
                }
86
87 59
                $directive = new DirectiveGrammar();
88 59
                if ($directive->parse($src, $n->offset)) {
89 57
                    if (\strtolower($directive->getKeyword()) === self::DECLARE_DIRECTIVE) {
90
                        // configure braces syntax
91 8
                        $this->declare($directive->getBody());
92
                    } else {
93
                        if (
94 49
                            $this->directiveRenderer !== null
95 49
                            && !$this->directiveRenderer->hasDirective($directive->getKeyword())
96
                        ) {
97
                            // directive opening char
98 1
                            yield $n;
99
100
                            // unknown directive, treat as plain test
101 1
                            $src->replay($n->offset);
102 1
                            continue;
103
                        }
104
105 49
                        yield from $directive;
106
                    }
107
108 57
                    $src->replay($directive->getLastOffset());
109 57
                    continue;
110
                } else {
111
                    // When we found directive char but it's not a directive, we need to flush the replay buffer
112
                    // because it may contain extra tokens that we don't need to return back to the stream
113 2
                    $src->flushReplay();
114
                }
115
116 2
                $src->replay($n->offset);
117
            }
118
119
            /** @var BracesGrammar|null $braces */
120 150
            $braces = null;
121 150
            if ($this->echo->starts($src, $n)) {
122 38
                $braces = clone $this->echo;
123 141
            } elseif ($this->raw->starts($src, $n)) {
124 6
                $braces = clone $this->raw;
125
            }
126
127 150
            if ($braces !== null) {
128 44
                $echo = $braces->parse($src, $n);
129 44
                if ($echo !== null) {
130 43
                    yield from $echo;
131 43
                    continue;
132
                }
133
134 1
                $src->replay($n->offset);
135
            }
136
137 138
            yield $n;
138
        }
139
140 169
        yield from $src;
141
    }
142
143
    /**
144
     * @codeCoverageIgnore
145
     */
146
    public static function tokenName(int $token): string
147
    {
148
        return match ($token) {
149
            self::TYPE_OPEN_TAG => 'DYNAMIC:OPEN_TAG',
150
            self::TYPE_CLOSE_TAG => 'DYNAMIC:CLOSE_TAG',
151
            self::TYPE_OPEN_RAW_TAG => 'DYNAMIC:OPEN_RAW_TAG',
152
            self::TYPE_CLOSE_RAW_TAG => 'DYNAMIC:CLOSE_RAW_TAG',
153
            self::TYPE_BODY => 'DYNAMIC:BODY',
154
            self::TYPE_DIRECTIVE => 'DYNAMIC:DIRECTIVE',
155
            self::TYPE_KEYWORD => 'DYNAMIC:KEYWORD',
156
            self::TYPE_WHITESPACE => 'DYNAMIC:WHITESPACE',
157
            default => 'DYNAMIC:UNDEFINED',
158
        };
159
    }
160
161 8
    private function declare(?string $body): void
162
    {
163 8
        if ($body === null) {
164 2
            return;
165
        }
166
167 6
        foreach ($this->fetchOptions($body) as $option => $value) {
168 5
            $value = \trim((string) $value, '\'" ');
169
            switch ($option) {
170 5
                case 'syntax':
171 3
                    $this->echo->setActive($value !== 'off');
172 3
                    $this->raw->setActive($value !== 'off');
173
174 3
                    if ($value === 'default') {
175 1
                        $this->echo->setStartSequence('{{');
176 1
                        $this->echo->setEndSequence('}}');
177 1
                        $this->raw->setStartSequence('{!!');
178 1
                        $this->raw->setStartSequence('!!}');
179
                    }
180 3
                    break;
181
182 3
                case 'open':
183 2
                    $this->echo->setStartSequence($value);
184 2
                    break;
185
186 3
                case 'close':
187 2
                    $this->echo->setEndSequence($value);
188 2
                    break;
189
190 1
                case 'openRaw':
191 1
                    $this->raw->setStartSequence($value);
192 1
                    break;
193
194 1
                case 'closeRaw':
195 1
                    $this->raw->setEndSequence($value);
196 1
                    break;
197
            }
198
        }
199
    }
200
201 6
    private function fetchOptions(string $body): array
202
    {
203 6
        $lexer = new Lexer();
204 6
        $lexer->addGrammar(new DeclareGrammar());
205
206
        // generated options
207 6
        $options = [];
208 6
        $keyword = null;
209
210
        /** @var Token $token */
211 6
        foreach ($lexer->parse(new StringStream($body)) as $token) {
212 6
            switch ($token->type) {
213 6
                case DeclareGrammar::TYPE_KEYWORD:
214 6
                    if ($keyword !== null) {
215 1
                        $options[$keyword] = $token->content;
216 1
                        $keyword = null;
217 1
                        break;
218
                    }
219 6
                    $keyword = $token->content;
220 6
                    break;
221 5
                case DeclareGrammar::TYPE_QUOTED:
222 4
                    if ($keyword !== null) {
223 4
                        $options[$keyword] = \trim($token->content, $token->content[0]);
224 4
                        $keyword = null;
225 4
                        break;
226
                    }
227
228
                    $keyword = \trim($token->content, $token->content[0]);
229
                    break;
230 5
                case DeclareGrammar::TYPE_COMMA:
231 3
                    if ($keyword !== null) {
232
                        $options[$keyword] = null;
233
                        $keyword = null;
234
                        break;
235
                    }
236
            }
237
        }
238
239 6
        return $options;
240
    }
241
}
242