Passed
Push — main ( d150f9...b56166 )
by Colin
20:01 queued 10:01
created

LinkParserHelper::manuallyParseLinkDestination()   C

Complexity

Conditions 15
Paths 22

Size

Total Lines 38
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 15.1152

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 24
c 1
b 0
f 0
nc 22
nop 1
dl 0
loc 38
ccs 23
cts 25
cp 0.92
crap 15.1152
rs 5.9166

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\Util;
18
19
use League\CommonMark\Parser\Cursor;
20
21
/**
22
 * @psalm-immutable
23
 */
24
final class LinkParserHelper
25
{
26
    /**
27
     * Attempt to parse link destination
28
     *
29
     * @return string|null The string, or null if no match
30
     */
31 394
    public static function parseLinkDestination(Cursor $cursor): ?string
32
    {
33 394
        if ($cursor->getCurrentCharacter() === '<') {
34 34
            return self::parseDestinationBraces($cursor);
35
        }
36
37 362
        $destination = self::manuallyParseLinkDestination($cursor);
38 362
        if ($destination === null) {
39
            return null;
40
        }
41
42 362
        return UrlEncoder::unescapeAndEncode(
43 362
            RegexHelper::unescape($destination)
44 362
        );
45
    }
46
47 226
    public static function parseLinkLabel(Cursor $cursor): int
48
    {
49 226
        $match = $cursor->match('/^\[(?:[^\\\\\[\]]|\\\\.){0,1000}\]/');
50 226
        if ($match === null) {
51 172
            return 0;
52
        }
53
54 62
        $length = \mb_strlen($match, 'UTF-8');
55
56 62
        if ($length > 1001) {
57
            return 0;
58
        }
59
60 62
        return $length;
61
    }
62
63 334
    public static function parsePartialLinkLabel(Cursor $cursor): ?string
64
    {
65 334
        return $cursor->match('/^(?:[^\\\\\[\]]++|\\\\.?)*+/');
66
    }
67
68
    /**
69
     * Attempt to parse link title (sans quotes)
70
     *
71
     * @return string|null The string, or null if no match
72
     */
73 44
    public static function parseLinkTitle(Cursor $cursor): ?string
74
    {
75 44
        if ($title = $cursor->match('/' . RegexHelper::PARTIAL_LINK_TITLE . '/')) {
76
            // Chop off quotes from title and unescape
77 22
            return RegexHelper::unescape(\substr($title, 1, -1));
78
        }
79
80 22
        return null;
81
    }
82
83 82
    public static function parsePartialLinkTitle(Cursor $cursor, string $endDelimiter): ?string
84
    {
85 82
        $endDelimiter = \preg_quote($endDelimiter, '/');
86 82
        $regex        = \sprintf('/(%s|[^%s\x00])*(?:%s)?/', RegexHelper::PARTIAL_ESCAPED_CHAR, $endDelimiter, $endDelimiter);
87 82
        if (($partialTitle = $cursor->match($regex)) === null) {
88
            return null;
89
        }
90
91 82
        return RegexHelper::unescape($partialTitle);
92
    }
93
94 362
    private static function manuallyParseLinkDestination(Cursor $cursor): ?string
95
    {
96 362
        $remainder  = $cursor->getRemainder();
97 362
        $openParens = 0;
98 362
        $len        = \strlen($remainder);
99 362
        for ($i = 0; $i < $len; $i++) {
100 362
            $c = $remainder[$i];
101 362
            if ($c === '\\' && $i + 1 < $len && RegexHelper::isEscapable($remainder[$i + 1])) {
102 12
                $i++;
103 362
            } elseif ($c === '(') {
104 12
                $openParens++;
105
                // Limit to 32 nested parens for pathological cases
106 12
                if ($openParens > 32) {
107 6
                    return null;
108
                }
109 362
            } elseif ($c === ')') {
110 152
                if ($openParens < 1) {
111 148
                    break;
112
                }
113
114 12
                $openParens--;
115 358
            } elseif (\ord($c) <= 32 && RegexHelper::isWhitespace($c)) {
116 126
                break;
117
            }
118
        }
119
120 362
        if ($openParens !== 0) {
0 ignored issues
show
introduced by
The condition $openParens !== 0 is always false.
Loading history...
121
            return null;
122
        }
123
124 362
        if ($i === 0 && (! isset($c) || $c !== ')')) {
125
            return null;
126
        }
127
128 362
        $destination = \substr($remainder, 0, $i);
129 362
        $cursor->advanceBy(\mb_strlen($destination, 'UTF-8'));
130
131 362
        return $destination;
132
    }
133
134
    /** @var \WeakReference<Cursor>|null */
135
    private static ?\WeakReference $lastCursor       = null;
136
    private static bool $lastCursorLacksClosingBrace = false;
137
138 34
    private static function parseDestinationBraces(Cursor $cursor): ?string
139
    {
140
        // Optimization: If we've previously parsed this cursor and returned `null`, we know
141
        // that no closing brace exists, so we can skip the regex entirely. This helps avoid
142
        // certain pathological cases where the regex engine can take a very long time to
143
        // determine that no match exists.
144 34
        if (self::$lastCursor !== null && self::$lastCursor->get() === $cursor) {
145 2
            if (self::$lastCursorLacksClosingBrace) {
146 2
                return null;
147
            }
148
        } else {
149 34
            self::$lastCursor = \WeakReference::create($cursor);
150
        }
151
152 34
        if ($res = $cursor->match(RegexHelper::REGEX_LINK_DESTINATION_BRACES)) {
153 24
            self::$lastCursorLacksClosingBrace = false;
154
155
            // Chop off surrounding <..>:
156 24
            return UrlEncoder::unescapeAndEncode(
157 24
                RegexHelper::unescape(\substr($res, 1, -1))
158 24
            );
159
        }
160
161 10
        self::$lastCursorLacksClosingBrace = true;
162
163 10
        return null;
164
    }
165
}
166