BacktickParser::findMatchingTicks()   B
last analyzed

Complexity

Conditions 10
Paths 12

Size

Total Lines 36
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 16
nc 12
nop 2
dl 0
loc 36
rs 7.6666
c 1
b 0
f 0

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
    public function getMatchDefinition(): InlineParserMatch
43
    {
44
        return InlineParserMatch::regex('`+');
45
    }
46
47
    public function parse(InlineParserContext $inlineContext): bool
48
    {
49
        $ticks  = $inlineContext->getFullMatch();
50
        $cursor = $inlineContext->getCursor();
51
        $cursor->advanceBy($inlineContext->getFullMatchLength());
52
53
        $currentPosition = $cursor->getPosition();
54
        $previousState   = $cursor->saveState();
55
56
        if ($this->findMatchingTicks(\strlen($ticks), $cursor)) {
57
            $code = $cursor->getSubstring($currentPosition, $cursor->getPosition() - $currentPosition - \strlen($ticks));
58
59
            $c = \preg_replace('/\n/m', ' ', $code) ?? '';
60
61
            if (
62
                $c !== '' &&
63
                $c[0] === ' ' &&
64
                \substr($c, -1, 1) === ' ' &&
65
                \preg_match('/[^ ]/', $c)
66
            ) {
67
                $c = \substr($c, 1, -1);
68
            }
69
70
            $inlineContext->getContainer()->appendChild(new Code($c));
71
72
            return true;
73
        }
74
75
        // If we got here, we didn't match a closing backtick sequence
76
        $cursor->restoreState($previousState);
77
        $inlineContext->getContainer()->appendChild(new Text($ticks));
78
79
        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
    private function findMatchingTicks(int $openTickLength, Cursor $cursor): bool
96
    {
97
        // Reset the seenBackticks cache if this is a new cursor
98
        if ($this->lastCursor === null || $this->lastCursor->get() !== $cursor) {
99
            $this->seenBackticks     = [];
100
            $this->lastCursor        = \WeakReference::create($cursor);
101
            $this->lastCursorScanned = false;
102
        }
103
104
        if ($openTickLength > self::MAX_BACKTICKS) {
105
            return false;
106
        }
107
108
        // Return if we already know there's no closer
109
        if ($this->lastCursorScanned && isset($this->seenBackticks[$openTickLength]) && $this->seenBackticks[$openTickLength] <= $cursor->getPosition()) {
110
            return false;
111
        }
112
113
        while ($ticks = $cursor->match('/`{1,' . self::MAX_BACKTICKS . '}/m')) {
114
            $numTicks = \strlen($ticks);
115
116
            // Did we find the closer?
117
            if ($numTicks === $openTickLength) {
118
                return true;
119
            }
120
121
            // Store position of closer
122
            if ($numTicks <= self::MAX_BACKTICKS) {
123
                $this->seenBackticks[$numTicks] = $cursor->getPosition() - $numTicks;
124
            }
125
        }
126
127
        // Got through whole input without finding closer
128
        $this->lastCursorScanned = true;
129
130
        return false;
131
    }
132
}
133