BacktickParser::getMatchDefinition()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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