Passed
Push — master ( 9a8532...e68fd2 )
by Johnny
02:06
created

Node::isInterrupted()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
/*
3
 * This file is part of Rivescript-php
4
 *
5
 * (c) Shea Lewis <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Axiom\Rivescript\Cortex;
12
13
/**
14
 * Node class
15
 *
16
 * The Node class stores information about a parsed
17
 * line from the script.
18
 *
19
 * PHP version 7.4 and higher.
20
 *
21
 * @category Core
22
 * @package  Cortext
23
 * @author   Shea Lewis <[email protected]>
24
 * @license  https://opensource.org/licenses/MIT MIT
25
 * @link     https://github.com/axiom-labs/rivescript-php
26
 * @since    0.3.0
27
 */
28
class Node
29
{
30
    /**
31
     * The source string for the node.
32
     *
33
     * @var string
34
     */
35
    protected string $source = '';
36
37
    /**
38
     * The line number of the node source.
39
     *
40
     * @var int
41
     */
42
    protected int $number = 0;
43
44
    /**
45
     * The command symbol.
46
     *
47
     * @var string
48
     */
49
    protected string $command = '';
50
51
    /**
52
     * The source without the command symbol.
53
     *
54
     * @var string
55
     */
56
    protected string $value = '';
57
58
    /**
59
     * Is this line part of a docblock.
60
     *
61
     * @var bool
62
     */
63
    protected bool $isInterrupted = false;
64
65
    /**
66
     * Is this node a comment.
67
     *
68
     * @var bool
69
     */
70
    protected bool $isComment = false;
71
72
    /**
73
     * Is this an empty line.
74
     *
75
     * @var bool
76
     */
77
    public bool $isEmpty = false;
78
79
    /**
80
     * Is UTF8 modes enabled.
81
     *
82
     * @var bool
83
     */
84
    protected bool $allowUtf8 = false;
85
86
    /**
87
     * Create a new Source instance.
88
     *
89
     * @param string $source The source line.
90
     * @param int    $number The line number in the script.
91
     */
92
    public function __construct(string $source, int $number)
93
    {
94
        $this->source = remove_whitespace($source);
95
        $this->number = $number;
96
97
        $this->determineEmpty();
98
        $this->determineComment();
99
        $this->determineCommand();
100
        $this->determineValue();
101
    }
102
103
    /**
104
     * Returns the node's command trigger.
105
     *
106
     * @return string
107
     */
108
    public function command(): string
109
    {
110
        return $this->command;
111
    }
112
113
    /**
114
     * Returns the node's value.
115
     *
116
     * @return string
117
     */
118
    public function value(): string
119
    {
120
        return $this->value;
121
    }
122
123
    /**
124
     * Returns the node's source.
125
     *
126
     * @return string
127
     */
128
    public function source(): string
129
    {
130
        return $this->source;
131
    }
132
133
    /**
134
     * Returns the node's line number.
135
     *
136
     * @return int
137
     */
138
    public function number(): int
139
    {
140
        return $this->number;
141
    }
142
143
    public function isEmpty(): bool
144
    {
145
        return $this->isEmpty;
146
    }
147
148
    /**
149
     * Returns true if node is a comment.
150
     *
151
     * @return bool
152
     */
153
    public function isComment(): bool
154
    {
155
        return $this->isComment;
156
    }
157
158
    /**
159
     * Returns true is node has been interrupted.
160
     *
161
     * @return bool
162
     */
163
    public function isInterrupted(): bool
164
    {
165
        return $this->isInterrupted;
166
    }
167
168
    /**
169
     * Determine the command type of the node.
170
     *
171
     * @return void
172
     */
173
    protected function determineCommand(): void
174
    {
175
        if ($this->source === '') {
176
            $this->isInterrupted = true;
177
178
            return;
179
        }
180
181
        $this->command = mb_substr($this->source, 0, 1);
182
    }
183
184
    protected function determineEmpty(): void
185
    {
186
        $this->isEmpty = empty(trim($this->source()));
187
    }
188
189
    /**
190
     * Determine if the current node source is a comment.
191
     *
192
     * @return void
193
     */
194
    protected function determineComment(): void
195
    {
196
        if (starts_with($this->source, '//')) {
197
            $this->isInterrupted = true;
198
        } elseif (starts_with($this->source, '#')) {
199
            log_warning('Using the # symbol for comments is deprecated');
200
            $this->isInterrupted = true;
201
        } elseif (starts_with($this->source, '/*')) {
202
            if (ends_with($this->source, '*/')) {
203
                return;
204
            }
205
            $this->isComment = true;
206
        } elseif (ends_with($this->source, '*/')) {
207
            $this->isComment = false;
208
        }
209
    }
210
211
    /**
212
     * Determine the value of the node.
213
     *
214
     * @return void
215
     */
216
    protected function determineValue(): void
217
    {
218
        $this->value = trim(mb_substr($this->source, 1));
219
    }
220
221
    /**
222
     * Enable the UTF8 mode.
223
     *
224
     * @param bool $allowUtf8 True of false.
225
     */
226
    public function setAllowUtf8(bool $allowUtf8): void
227
    {
228
        $this->allowUtf8 = $allowUtf8;
229
    }
230
231
    /**
232
     * Check the syntax
233
     *
234
     * @return string
235
     */
236
    public function checkSyntax(): string
237
    {
238
        if (starts_with($this->source, '!')) {
239
            # ! Definition
240
            #   - Must be formatted like this:
241
            #     ! type name = value
242
            #     OR
243
            #     ! type = value
244
            #   - Type options are NOT enforceable, for future compatibility; if RiveScript
245
            #     encounters a new type that it can't handle, it can safely warn and skip it.
246
            if ($this->matchesPattern("/^.+(?:\s+.+|)\s*=\s*.+?$/", $this->source) === false) {
247
                return "Invalid format for !Definition line: must be '! type name = value' OR '! type = value'";
248
            }
249
        } elseif (starts_with($this->source, '>')) {
250
            # > Label
251
            #   - The "begin" label must have only one argument ("begin")
252
            #   - "topic" labels must be lowercase but can inherit other topics ([A-Za-z0-9_\s])
253
            #   - "object" labels follow the same rules as "topic" labels, but don't need be lowercase
254
            if ($this->matchesPattern("/^begin/", $this->value) === true
255
                && $this->matchesPattern("/^begin$/", $this->value) === false) {
256
                return "The 'begin' label takes no additional arguments, should be verbatim '> begin'";
257
            } elseif ($this->matchesPattern("/^topic/", $this->value) === true
258
                && $this->matchesPattern("/[^a-z0-9_\-\s]/", $this->value) === true) {
259
                return "Topics should be lowercased and contain only numbers and letters!";
260
            } elseif ($this->matchesPattern("/^object/", $this->value) === true
261
                && $this->matchesPattern("/[^a-z0-9_\-\s]/", $this->value) === true) {
262
                return "Objects can only contain numbers and lowercase letters!";
263
            }
264
        } elseif (starts_with($this->source, '+') || starts_with($this->source, '%')
265
            || starts_with($this->source, '@')) {
266
            # + Trigger, % Previous, @ Redirect
267
            #   This one is strict. The triggers are to be run through Perl's regular expression
268
            #   engine. Therefore, it should be acceptable by the regexp engine.
269
            #   - Entirely lowercase
270
            #   - No symbols except: ( | ) [ ] * _ # @ { } < > =
271
            #   - All brackets should be matched
272
273
            if ($this->allowUtf8 === true) {
274
                if ($this->matchesPattern("/[A-Z\\.]/", $this->value) === true) {
275
                    return "Triggers can't contain uppercase letters, backslashes or dots in UTF-8 mode.";
276
                }
277
            } elseif ($this->matchesPattern("/[^a-z0-9(\|)\[\]*_#\@{}<>=\s]/", $this->value) === true) {
278
                return "Triggers may only contain lowercase letters, numbers, and these symbols: ( | ) [ ] * _ # @ { } < > =";
279
            }
280
281
            $parens = 0; # Open parenthesis
282
            $square = 0; # Open square brackets
283
            $curly = 0; # Open curly brackets
284
            $chevron = 0; # Open angled brackets
285
            $len = strlen($this->value);
286
287
            for ($i = 0; $i < $len; $i++) {
288
                $chr = $this->value[$i];
289
290
                # Count brackets.
291
                if ($chr === '(') {
292
                    $parens++;
293
                }
294
                if ($chr === ')') {
295
                    $parens--;
296
                }
297
                if ($chr === '[') {
298
                    $square++;
299
                }
300
                if ($chr === ']') {
301
                    $square--;
302
                }
303
                if ($chr === '{') {
304
                    $curly++;
305
                }
306
                if ($chr === '}') {
307
                    $curly--;
308
                }
309
                if ($chr === '<') {
310
                    $chevron++;
311
                }
312
                if ($chr === '>') {
313
                    $chevron--;
314
                }
315
            }
316
317
            if ($parens) {
318
                return "Unmatched " . ($parens > 0 ? "left" : "right") . " parenthesis bracket ()";
319
            }
320
            if ($square) {
321
                return "Unmatched " . ($square > 0 ? "left" : "right") . " square bracket []";
322
            }
323
            if ($curly) {
324
                return "Unmatched " . ($curly > 0 ? "left" : "right") . " curly bracket {}";
325
            }
326
            if ($chevron) {
327
                return "Unmatched " . ($chevron > 0 ? "left" : "right") . " angled bracket <>";
328
            }
329
        } elseif (starts_with($this->source, '-') || starts_with($this->source, '^')
330
            || starts_with($this->source, '/')) {
331
            # - Trigger, ^ Continue, / Comment
332
            # These commands take verbatim arguments, so their syntax is loose.
333
        } elseif (starts_with($this->source, '*') === true && $this->isComment() === false) {
334
            # * Condition
335
            #   Syntax for a conditional is as follows:
336
            #   * value symbol value => response
337
            if ($this->matchesPattern("/.+?\s(==|eq|!=|ne|<>|<|<=|>|>=)\s.+?=>.+?$/", $this->value) === false) {
338
                return "Invalid format for !Condition: should be like `* value symbol value => response`";
339
            }
340
        }
341
342
        return "";
343
    }
344
345
    /**
346
     * Check for patterns in a given string.
347
     *
348
     * @param string $regex  The pattern to detect.
349
     * @param string $string The string that could contain the pattern.
350
     *
351
     * @return bool
352
     */
353
    private function matchesPattern(string $regex = '', string $string = ''): bool
354
    {
355
        preg_match_all($regex, $string, $matches);
356
357
        return isset($matches[0][0]);
358
    }
359
}
360