Node::isInterrupted()   A
last analyzed

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
    /**
74
     * Is this an empty line.
75
     *
76
     * @var bool
77
     */
78
    public bool $isEmpty = false;
79
80
    /**
81
     * Is UTF8 modes enabled.
82
     *
83
     * @var bool
84
     */
85
    protected bool $allowUtf8 = false;
86
87
    /**
88
     * Create a new Source instance.
89
     *
90
     * @param string $source The source line.
91
     * @param int    $number The line number in the script.
92
     */
93
    public function __construct(string $source, int $number)
94
    {
95
        $this->source = remove_whitespace($source);
96
        $this->number = $number;
97
98
        $this->determineEmpty();
99
        $this->determineComment();
100
        $this->determineCommand();
101
        $this->determineValue();
102
    }
103
104
    /**
105
     * Returns the node's command trigger.
106
     *
107
     * @return string
108
     */
109
    public function command(): string
110
    {
111
        return $this->command;
112
    }
113
114
    /**
115
     * Returns the node's value.
116
     *
117
     * @return string
118
     */
119
    public function value(): string
120
    {
121
        return $this->value;
122
    }
123
124
    /**
125
     * Returns the node's source.
126
     *
127
     * @return string
128
     */
129
    public function source(): string
130
    {
131
        return $this->source;
132
    }
133
134
    /**
135
     * Returns the node's line number.
136
     *
137
     * @return int
138
     */
139
    public function number(): int
140
    {
141
        return $this->number;
142
    }
143
144
    public function isEmpty(): bool
145
    {
146
        return $this->isEmpty;
147
    }
148
149
    /**
150
     * Returns true if node is a comment.
151
     *
152
     * @return bool
153
     */
154
    public function isComment(): bool
155
    {
156
        return $this->isComment;
157
    }
158
159
    /**
160
     * Returns true is node has been interrupted.
161
     *
162
     * @return bool
163
     */
164
    public function isInterrupted(): bool
165
    {
166
        return $this->isInterrupted;
167
    }
168
169
    /**
170
     * Determine the command type of the node.
171
     *
172
     * @return void
173
     */
174
    protected function determineCommand(): void
175
    {
176
        if ($this->source === '') {
177
            $this->isInterrupted = true;
178
179
            return;
180
        }
181
182
        $this->command = mb_substr($this->source, 0, 1);
183
    }
184
185
    protected function determineEmpty(): void
186
    {
187
        $this->isEmpty = empty(trim($this->source()));
188
    }
189
190
    /**
191
     * Determine if the current node source is a comment.
192
     *
193
     * @return void
194
     */
195
    protected function determineComment(): void
196
    {
197
        if (starts_with($this->source, '//')) {
198
            $this->isInterrupted = true;
199
        } elseif (starts_with($this->source, '#')) {
200
            log_warning('Using the # symbol for comments is deprecated');
201
            $this->isInterrupted = true;
202
        } elseif (starts_with($this->source, '/*')) {
203
            if (ends_with($this->source, '*/')) {
204
                return;
205
            }
206
            $this->isComment = true;
207
        } elseif (ends_with($this->source, '*/')) {
208
            $this->isComment = false;
209
        }
210
    }
211
212
    /**
213
     * Determine the value of the node.
214
     *
215
     * @return void
216
     */
217
    protected function determineValue(): void
218
    {
219
        $this->value = trim(mb_substr($this->source, 1));
220
    }
221
222
    /**
223
     * Enable the UTF8 mode.
224
     *
225
     * @param bool $allowUtf8 True of false.
226
     */
227
    public function setAllowUtf8(bool $allowUtf8): void
228
    {
229
        $this->allowUtf8 = $allowUtf8;
230
    }
231
232
    /**
233
     * Check the syntax
234
     *
235
     * @return string
236
     */
237
    public function checkSyntax(): string
238
    {
239
        if (starts_with($this->source, '!')) {
240
            # ! Definition
241
            #   - Must be formatted like this:
242
            #     ! type name = value
243
            #     OR
244
            #     ! type = value
245
            #   - Type options are NOT enforceable, for future compatibility; if RiveScript
246
            #     encounters a new type that it can't handle, it can safely warn and skip it.
247
            if ($this->matchesPattern("/^.+(?:\s+.+|)\s*=\s*.+?$/", $this->source) === false) {
248
                return "Invalid format for !Definition line: must be '! type name = value' OR '! type = value'";
249
            }
250
        } elseif (starts_with($this->source, '>')) {
251
            # > Label
252
            #   - The "begin" label must have only one argument ("begin")
253
            #   - "topic" labels must be lowercase but can inherit other topics ([A-Za-z0-9_\s])
254
            #   - "object" labels follow the same rules as "topic" labels, but don't need be lowercase
255
            if ($this->matchesPattern("/^begin/", $this->value) === true
256
                && $this->matchesPattern("/^begin$/", $this->value) === false) {
257
                return "The 'begin' label takes no additional arguments, should be verbatim '> begin'";
258
            } elseif ($this->matchesPattern("/^topic/", $this->value) === true
259
                && $this->matchesPattern("/[^a-z0-9_\-\s]/", $this->value) === true) {
260
                return "Topics should be lowercased and contain only numbers and letters!";
261
            } elseif ($this->matchesPattern("/^object/", $this->value) === true
262
                && $this->matchesPattern("/[^a-z0-9_\-\s]/", $this->value) === true) {
263
                return "Objects can only contain numbers and lowercase letters!";
264
            }
265
        } elseif (starts_with($this->source, '+') || starts_with($this->source, '%')
266
            || starts_with($this->source, '@')) {
267
            # + Trigger, % Previous, @ Redirect
268
            #   This one is strict. The triggers are to be run through Perl's regular expression
269
            #   engine. Therefore, it should be acceptable by the regexp engine.
270
            #   - Entirely lowercase
271
            #   - No symbols except: ( | ) [ ] * _ # @ { } < > =
272
            #   - All brackets should be matched
273
274
            if ($this->allowUtf8 === true) {
275
                if ($this->matchesPattern("/[A-Z\\.]/", $this->value) === true) {
276
                    return "Triggers can't contain uppercase letters, backslashes or dots in UTF-8 mode.";
277
                }
278
            } elseif ($this->matchesPattern("/[^a-z0-9(\|)\[\]*_#\@{}<>=\s]/", $this->value) === true) {
279
                return "Triggers may only contain lowercase letters, numbers, and these symbols: ( | ) [ ] * _ # @ { } < > =";
280
            }
281
282
            $parens = 0; # Open parenthesis
283
            $square = 0; # Open square brackets
284
            $curly = 0; # Open curly brackets
285
            $chevron = 0; # Open angled brackets
286
            $len = strlen($this->value);
287
288
            for ($i = 0; $i < $len; $i++) {
289
                $chr = $this->value[$i];
290
291
                # Count brackets.
292
                if ($chr === '(') {
293
                    $parens++;
294
                }
295
                if ($chr === ')') {
296
                    $parens--;
297
                }
298
                if ($chr === '[') {
299
                    $square++;
300
                }
301
                if ($chr === ']') {
302
                    $square--;
303
                }
304
                if ($chr === '{') {
305
                    $curly++;
306
                }
307
                if ($chr === '}') {
308
                    $curly--;
309
                }
310
                if ($chr === '<') {
311
                    $chevron++;
312
                }
313
                if ($chr === '>') {
314
                    $chevron--;
315
                }
316
            }
317
318
            if ($parens) {
319
                return "Unmatched " . ($parens > 0 ? "left" : "right") . " parenthesis bracket ()";
320
            }
321
            if ($square) {
322
                return "Unmatched " . ($square > 0 ? "left" : "right") . " square bracket []";
323
            }
324
            if ($curly) {
325
                return "Unmatched " . ($curly > 0 ? "left" : "right") . " curly bracket {}";
326
            }
327
            if ($chevron) {
328
                return "Unmatched " . ($chevron > 0 ? "left" : "right") . " angled bracket <>";
329
            }
330
        } elseif (starts_with($this->source, '-') || starts_with($this->source, '^')
331
            || starts_with($this->source, '/')) {
332
            # - Trigger, ^ Continue, / Comment
333
            # These commands take verbatim arguments, so their syntax is loose.
334
        } elseif (starts_with($this->source, '*') === true && $this->isComment() === false) {
335
            # * Condition
336
            #   Syntax for a conditional is as follows:
337
            #   * value symbol value => response
338
            if ($this->matchesPattern("/.+?\s(==|eq|!=|ne|<>|<|<=|>|>=)\s.+?=>.+?$/", $this->value) === false) {
339
                return "Invalid format for !Condition: should be like `* value symbol value => response`";
340
            }
341
        }
342
343
        return "";
344
    }
345
346
    /**
347
     * Check for patterns in a given string.
348
     *
349
     * @param string $regex  The pattern to detect.
350
     * @param string $string The string that could contain the pattern.
351
     *
352
     * @return bool
353
     */
354
    private function matchesPattern(string $regex = '', string $string = ''): bool
355
    {
356
        preg_match_all($regex, $string, $matches);
357
358
        return isset($matches[0][0]);
359
    }
360
}
361