BacktickParser::findMatchingTicks()   B
last analyzed

Complexity

Conditions 10
Paths 12

Size

Total Lines 36
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 10.0203

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 16
c 1
b 0
f 0
nc 12
nop 2
dl 0
loc 36
ccs 16
cts 17
cp 0.9412
crap 10.0203
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the league/commonmark package.
7
 *
8
 * (c) Colin O'Dell <[email protected]>
9
 *
10
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
11
 *  - (c) John MacFarlane
12
 *
13
 * For the full copyright and license information, please view the LICENSE
14
 * file that was distributed with this source code.
15
 */
16
17
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
18
19
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
20
use League\CommonMark\Node\Inline\Text;
21
use League\CommonMark\Parser\Cursor;
22
use League\CommonMark\Parser\Inline\InlineParserInterface;
23
use League\CommonMark\Parser\Inline\InlineParserMatch;
24
use League\CommonMark\Parser\InlineParserContext;
25
26
final class BacktickParser implements InlineParserInterface
27
{
28
    /**
29
     * Max bound for backtick code span delimiters.
30
     *
31
     * @see https://github.com/commonmark/cmark/commit/8ed5c9d
32
     */
33
    private const MAX_BACKTICKS = 1000;
34
35
    /** @var \WeakReference<Cursor>|null */
36
    private ?\WeakReference $lastCursor = null;
37
    private bool $lastCursorScanned     = false;
38
39
    /** @var array<int, int> backtick count => position of known ender */
40
    private array $seenBackticks = [];
41
42 2304
    public function getMatchDefinition(): InlineParserMatch
43
    {
44 2304
        return InlineParserMatch::regex('`+');
45
    }
46
47 112
    public function parse(InlineParserContext $inlineContext): bool
48
    {
49 112
        $ticks  = $inlineContext->getFullMatch();
50 112
        $cursor = $inlineContext->getCursor();
51 112
        $cursor->advanceBy($inlineContext->getFullMatchLength());
52
53 112
        $currentPosition = $cursor->getPosition();
54 112
        $previousState   = $cursor->saveState();
55
56 112
        if ($this->findMatchingTicks(\strlen($ticks), $cursor)) {
57 96
            $code = $cursor->getSubstring($currentPosition, $cursor->getPosition() - $currentPosition - \strlen($ticks));
58
59 96
            $c = \preg_replace('/\n/m', ' ', $code) ?? '';
60
61
            if (
62 96
                $c !== '' &&
63 96
                $c[0] === ' ' &&
64 96
                \substr($c, -1, 1) === ' ' &&
65 96
                \preg_match('/[^ ]/', $c)
66
            ) {
67 18
                $c = \substr($c, 1, -1);
68
            }
69
70 96
            $inlineContext->getContainer()->appendChild(new Code($c));
71
72 96
            return true;
73
        }
74
75
        // If we got here, we didn't match a closing backtick sequence
76 30
        $cursor->restoreState($previousState);
77 30
        $inlineContext->getContainer()->appendChild(new Text($ticks));
78
79 30
        return true;
80
    }
81
82
    /**
83
     * Locates the matching closer for a backtick code span.
84
     *
85
     * Leverages some caching to avoid traversing the same cursor multiple times when
86
     * we've already seen all the potential backtick closers.
87
     *
88
     * @see https://github.com/commonmark/cmark/commit/8ed5c9d
89
     *
90
     * @param int    $openTickLength Number of backticks in the opening sequence
91
     * @param Cursor $cursor         Cursor to scan
92
     *
93
     * @return bool True if a matching closer was found, false otherwise
94
     */
95 112
    private function findMatchingTicks(int $openTickLength, Cursor $cursor): bool
96
    {
97
        // Reset the seenBackticks cache if this is a new cursor
98 112
        if ($this->lastCursor === null || $this->lastCursor->get() !== $cursor) {
99 112
            $this->seenBackticks     = [];
100 112
            $this->lastCursor        = \WeakReference::create($cursor);
101 112
            $this->lastCursorScanned = false;
102
        }
103
104 112
        if ($openTickLength > self::MAX_BACKTICKS) {
105
            return false;
106
        }
107
108
        // Return if we already know there's no closer
109 112
        if ($this->lastCursorScanned && isset($this->seenBackticks[$openTickLength]) && $this->seenBackticks[$openTickLength] <= $cursor->getPosition()) {
110 2
            return false;
111
        }
112
113 112
        while ($ticks = $cursor->match('/`{1,' . self::MAX_BACKTICKS . '}/m')) {
114 98
            $numTicks = \strlen($ticks);
115
116
            // Did we find the closer?
117 98
            if ($numTicks === $openTickLength) {
118 96
                return true;
119
            }
120
121
            // Store position of closer
122 16
            if ($numTicks <= self::MAX_BACKTICKS) {
123 16
                $this->seenBackticks[$numTicks] = $cursor->getPosition() - $numTicks;
124
            }
125
        }
126
127
        // Got through whole input without finding closer
128 30
        $this->lastCursorScanned = true;
129
130 30
        return false;
131
    }
132
}
133