Completed
Push — master ( bfe104...7d2154 )
by Colin
7s
created

Cursor::advanceBySpaceOrTab()   A

Complexity

Conditions 3
Paths 2

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 12
Code Lines 6

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 9.4285
cc 3
eloc 6
nc 2
nop 0
crap 3
1
<?php
2
3
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace League\CommonMark;
13
14
class Cursor
15
{
16
    const INDENT_LEVEL = 4;
17
18
    /**
19
     * @var string
20
     */
21
    private $line;
22
23
    /**
24
     * @var int
25
     */
26
    private $length;
27
28
    /**
29
     * @var int
30
     *
31
     * It's possible for this to be 1 char past the end, meaning we've parsed all chars and have
32
     * reached the end.  In this state, any character-returning method MUST return null.
33
     */
34
    private $currentPosition = 0;
35
36
    /**
37
     * @var int
38
     */
39
    private $column = 0;
40
41
    /**
42
     * @var int
43
     */
44
    private $indent = 0;
45
46
    /**
47
     * @var int
48
     */
49
    private $previousPosition = 0;
50
51
    /**
52
     * @var int|null
53
     */
54
    private $firstNonSpaceCache;
55
56
    /**
57
     * @var bool
58
     */
59
    private $partiallyConsumedTab = false;
60
61
    /**
62
     * @param string $line
63
     */
64 2301
    public function __construct($line)
65
    {
66 2301
        $this->line = $line;
67 2301
        $this->length = mb_strlen($line, 'utf-8');
68 2301
    }
69
70
    /**
71
     * Returns the position of the next non-space character
72
     *
73
     * @return int
74
     */
75 1989
    public function getFirstNonSpacePosition()
76
    {
77 1989
        if ($this->firstNonSpaceCache !== null) {
78 1863
            return $this->firstNonSpaceCache;
79
        }
80
81 1989
        $i = $this->currentPosition;
82 1989
        $cols = $this->column;
83
84 1989
        while (($c = $this->getCharacter($i)) !== null) {
85 1974
            if ($c === ' ') {
86 468
                $i++;
87 468
                $cols++;
88 1974
            } elseif ($c === "\t") {
89 30
                $i++;
90 30
                $cols += (4 - ($cols % 4));
91 30
            } else {
92 1941
                break;
93
            }
94 483
        }
95
96 1989
        $nextNonSpace = ($c === null) ? $this->length : $i;
97 1989
        $this->indent = $cols - $this->column;
98
99 1989
        return $this->firstNonSpaceCache = $nextNonSpace;
100
    }
101
102
    /**
103
     * Returns the next character which isn't a space
104
     *
105
     * @return string
106
     */
107 1836
    public function getFirstNonSpaceCharacter()
108
    {
109 1836
        return $this->getCharacter($this->getFirstNonSpacePosition());
110
    }
111
112
    /**
113
     * Calculates the current indent (number of spaces after current position)
114
     *
115
     * @return int
116
     */
117 1923
    public function getIndent()
118
    {
119 1923
        $this->getFirstNonSpacePosition();
120
121 1923
        return $this->indent;
122
    }
123
124
    /**
125
     * Whether the cursor is indented to INDENT_LEVEL
126
     *
127
     * @return bool
128
     */
129 1863
    public function isIndented()
130
    {
131 1863
        return $this->getIndent() >= self::INDENT_LEVEL;
132
    }
133
134
    /**
135
     * @param int|null $index
136
     *
137
     * @return string|null
138
     */
139 2082
    public function getCharacter($index = null)
140
    {
141 2082
        if ($index === null) {
142 1617
            $index = $this->currentPosition;
143 1617
        }
144
145
        // Index out-of-bounds, or we're at the end
146 2082
        if ($index < 0 || $index >= $this->length) {
147 1803
            return;
148
        }
149
150 2049
        return mb_substr($this->line, $index, 1, 'utf-8');
151
    }
152
153
    /**
154
     * Returns the next character (or null, if none) without advancing forwards
155
     *
156
     * @param int $offset
157
     *
158
     * @return string|null
159
     */
160 987
    public function peek($offset = 1)
161
    {
162 987
        return $this->getCharacter($this->currentPosition + $offset);
163
    }
164
165
    /**
166
     * Whether the remainder is blank
167
     *
168
     * @return bool
169
     */
170 1881
    public function isBlank()
171
    {
172 1881
        return $this->getFirstNonSpacePosition() === $this->length;
173
    }
174
175
    /**
176
     * Move the cursor forwards
177
     */
178 756
    public function advance()
179
    {
180 756
        $this->advanceBy(1);
181 756
    }
182
183
    /**
184
     * Move the cursor forwards
185
     *
186
     * @param int $characters Number of characters to advance by
187
     */
188 2124
    public function advanceBy($characters, $advanceByColumns = false)
189
    {
190 2124
        $this->previousPosition = $this->currentPosition;
191 2124
        $this->firstNonSpaceCache = null;
192
193 2124
        $nextFewChars = mb_substr($this->line, $this->currentPosition, $characters, 'utf-8');
194 2124
        if ($characters === 1 && !empty($nextFewChars)) {
195 1389
            $asArray = [$nextFewChars];
196 1389
        } else {
197 2010
            $asArray = preg_split('//u', $nextFewChars, null, PREG_SPLIT_NO_EMPTY);
198
        }
199
200 2124
        foreach ($asArray as $relPos => $c) {
201 2040
            if ($c === "\t") {
202 36
                $charsToTab = 4 - ($this->column % 4);
203 36
                $this->partiallyConsumedTab = $advanceByColumns && $charsToTab > $characters;
204 36
                $charsToAdvance = $charsToTab > $characters ? $characters : $charsToTab;
205 36
                $this->column += $charsToAdvance;
206 36
                $this->currentPosition += $this->partiallyConsumedTab ? 0 : 1;
207 36
                $characters -= ($advanceByColumns ? $charsToAdvance : 1);
208 36
            } else {
209 2037
                $this->partiallyConsumedTab = false;
210 2037
                $this->currentPosition++;
211 2037
                $this->column++;
212 2037
                $characters--;
213
            }
214
215 2040
            if ($characters <= 0) {
216 2034
                break;
217
            }
218 2124
        }
219 2124
    }
220
221
    /**
222
     * Advances the cursor by a single space or tab, if present
223
     *
224
     * @return bool
225
     */
226 336
    public function advanceBySpaceOrTab()
227
    {
228 336
        $character = $this->getCharacter();
229
230 336
        if ($character === ' ' || $character === "\t") {
231 321
            $this->advanceBy(1, true);
232
233 321
            return true;
234
        }
235
236 252
        return false;
237
    }
238
239
    /**
240
     * Advances the cursor while the given character is matched
241
     *
242
     * @param string   $character                  Character to match
243
     * @param int|null $maximumCharactersToAdvance Maximum number of characters to advance before giving up
244
     *
245
     * @return int Number of positions moved (0 if unsuccessful)
246
     */
247 144
    public function advanceWhileMatches($character, $maximumCharactersToAdvance = null)
248
    {
249
        // Calculate how far to advance
250 144
        $start = $this->currentPosition;
251 144
        $newIndex = $start;
252 144
        if ($maximumCharactersToAdvance === null) {
253 18
            $maximumCharactersToAdvance = $this->length;
254 18
        }
255
256 144
        $max = min($start + $maximumCharactersToAdvance, $this->length);
257
258 144
        while ($newIndex < $max && $this->getCharacter($newIndex) === $character) {
259 45
            ++$newIndex;
260 45
        }
261
262 144
        if ($newIndex <= $start) {
263 108
            return 0;
264
        }
265
266 45
        $this->advanceBy($newIndex - $start);
267
268 45
        return $this->currentPosition - $this->previousPosition;
269
    }
270
271
    /**
272
     * Parse zero or more space characters, including at most one newline
273
     *
274
     * @return int Number of positions moved
275
     */
276 1872
    public function advanceToFirstNonSpace()
277
    {
278 1872
        $matches = [];
279 1872
        preg_match('/^ *(?:\n *)?/', $this->getRemainder(), $matches, PREG_OFFSET_CAPTURE);
280
281
        // [0][0] contains the matched text
282
        // [0][1] contains the index of that match
283 1872
        $increment = $matches[0][1] + strlen($matches[0][0]);
284
285 1872
        if ($increment === 0) {
286 1809
            return 0;
287
        }
288
289 468
        $this->advanceBy($increment);
290
291 468
        return $this->currentPosition - $this->previousPosition;
292
    }
293
294
    /**
295
     * @return string
296
     */
297 1965
    public function getRemainder()
298
    {
299 1965
        if ($this->isAtEnd()) {
300 660
            return '';
301
        }
302
303 1953
        $prefix = '';
304 1953
        $position = $this->currentPosition;
305 1953
        if ($this->partiallyConsumedTab) {
306 12
            $position++;
307 12
            $charsToTab = 4 - ($this->column % 4);
308 12
            $prefix = str_repeat(' ', $charsToTab);
309 12
        }
310
311 1953
        return $prefix . mb_substr($this->line, $position, null, 'utf-8');
312
    }
313
314
    /**
315
     * @return string
316
     */
317 1818
    public function getLine()
318
    {
319 1818
        return $this->line;
320
    }
321
322
    /**
323
     * @return bool
324
     */
325 1986
    public function isAtEnd()
326
    {
327 1986
        return $this->currentPosition >= $this->length;
328
    }
329
330
    /**
331
     * Try to match a regular expression
332
     *
333
     * Returns the matching text and advances to the end of that match
334
     *
335
     * @param string $regex
336
     *
337
     * @return string|null
338
     */
339 1833
    public function match($regex)
340
    {
341 1833
        $subject = $this->getRemainder();
342
343 1833
        $matches = [];
344 1833
        if (!preg_match($regex, $subject, $matches, PREG_OFFSET_CAPTURE)) {
345 1695
            return;
346
        }
347
348
        // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying
349 1716
        $offset = mb_strlen(mb_strcut($subject, 0, $matches[0][1], 'utf-8'), 'utf-8');
350
351
        // [0][0] contains the matched text
352
        // [0][1] contains the index of that match
353 1716
        $this->advanceBy($offset + mb_strlen($matches[0][0], 'utf-8'));
354
355 1716
        return $matches[0][0];
356
    }
357
358
    /**
359
     * @return CursorState
360
     */
361 1776
    public function saveState()
362
    {
363 1776
        return new CursorState(
364 1776
            $this->line,
365 1776
            $this->length,
366 1776
            $this->currentPosition,
367 1776
            $this->previousPosition,
368 1776
            $this->firstNonSpaceCache,
369 1776
            $this->indent,
370 1776
            $this->column
371 1776
        );
372
    }
373
374
    /**
375
     * @param CursorState $state
376
     */
377 1695
    public function restoreState(CursorState $state)
378
    {
379 1695
        $this->line = $state->getLine();
380 1695
        $this->length = $state->getLength();
381 1695
        $this->currentPosition = $state->getCurrentPosition();
382 1695
        $this->previousPosition = $state->getPreviousPosition();
383 1695
        $this->firstNonSpaceCache = $state->getFirstNonSpaceCache();
384 1695
        $this->column = $state->getColumn();
385 1695
        $this->indent = $state->getIndent();
386 1695
    }
387
388
    /**
389
     * @return int
390
     */
391 588
    public function getPosition()
392
    {
393 588
        return $this->currentPosition;
394
    }
395
396
    /**
397
     * @return string
398
     */
399 810
    public function getPreviousText()
400
    {
401 810
        return mb_substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition, 'utf-8');
402
    }
403
404
    /**
405
     * @return int
406
     */
407 243
    public function getColumn()
408
    {
409 243
        return $this->column;
410
    }
411
}
412