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

DynamicGrammar::declare()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 36
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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