LinkParserHelper::manuallyParseLinkDestination()   C
last analyzed

Complexity

Conditions 15
Paths 22

Size

Total Lines 38
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 15.13

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 22
cts 24
cp 0.9167
crap 15.13
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 384
    public static function parseLinkDestination(Cursor $cursor): ?string
32
    {
33 384
        if ($cursor->getCurrentCharacter() === '<') {
34
            return self::parseDestinationBraces($cursor);
35 26
        }
36 26
37 26
        $destination = self::manuallyParseLinkDestination($cursor);
38
        if ($destination === null) {
39
            return null;
40 360
        }
41 10
42
        return UrlEncoder::unescapeAndEncode(
43
            RegexHelper::unescape($destination)
44 352
        );
45 352
    }
46
47
    public static function parseLinkLabel(Cursor $cursor): int
48
    {
49 352
        $match = $cursor->match('/^\[(?:[^\\\\\[\]]|\\\\.){0,1000}\]/');
50 352
        if ($match === null) {
51 352
            return 0;
52
        }
53
54 222
        $length = \mb_strlen($match, 'UTF-8');
55
56 222
        if ($length > 1001) {
57 222
            return 0;
58 170
        }
59
60
        return $length;
61 60
    }
62
63 60
    public static function parsePartialLinkLabel(Cursor $cursor): ?string
64
    {
65
        return $cursor->match('/^(?:[^\\\\\[\]]++|\\\\.?)*+/');
66
    }
67 60
68
    /**
69
     * Attempt to parse link title (sans quotes)
70 328
     *
71
     * @return string|null The string, or null if no match
72 328
     */
73
    public static function parseLinkTitle(Cursor $cursor): ?string
74
    {
75
        if ($title = $cursor->match('/' . RegexHelper::PARTIAL_LINK_TITLE . '/')) {
76
            // Chop off quotes from title and unescape
77
            return RegexHelper::unescape(\substr($title, 1, -1));
78
        }
79
80 46
        return null;
81
    }
82 46
83
    public static function parsePartialLinkTitle(Cursor $cursor, string $endDelimiter): ?string
84 22
    {
85
        $endDelimiter = \preg_quote($endDelimiter, '/');
86
        $regex        = \sprintf('/(%s|[^%s\x00])*(?:%s)?/', RegexHelper::PARTIAL_ESCAPED_CHAR, $endDelimiter, $endDelimiter);
87 24
        if (($partialTitle = $cursor->match($regex)) === null) {
88
            return null;
89
        }
90 78
91
        return RegexHelper::unescape($partialTitle);
92 78
    }
93 78
94 78
    private static function manuallyParseLinkDestination(Cursor $cursor): ?string
95
    {
96
        $remainder  = $cursor->getRemainder();
97
        $openParens = 0;
98 78
        $len        = \strlen($remainder);
99
        for ($i = 0; $i < $len; $i++) {
100
            $c = $remainder[$i];
101 352
            if ($c === '\\' && $i + 1 < $len && RegexHelper::isEscapable($remainder[$i + 1])) {
102
                $i++;
103 352
            } elseif ($c === '(') {
104 352
                $openParens++;
105
                // Limit to 32 nested parens for pathological cases
106 352
                if ($openParens > 32) {
107 352
                    return null;
108 352
                }
109 12
            } elseif ($c === ')') {
110 352
                if ($openParens < 1) {
111 12
                    break;
112 12
                }
113 352
114 148
                $openParens--;
115 144
            } elseif (\ord($c) <= 32 && RegexHelper::isWhitespace($c)) {
116
                break;
117
            }
118 12
        }
119 12
120 348
        if ($openParens !== 0) {
0 ignored issues
show
introduced by
The condition $openParens !== 0 is always false.
Loading history...
121 124
            return null;
122
        }
123 348
124
        if ($i === 0 && (! isset($c) || $c !== ')')) {
125
            return null;
126
        }
127 352
128
        $destination = \substr($remainder, 0, $i);
129
        $cursor->advanceBy(\mb_strlen($destination, 'UTF-8'));
130
131 352
        return $destination;
132
    }
133
134
    /** @var \WeakReference<Cursor>|null */
135 352
    private static ?\WeakReference $lastCursor       = null;
136 352
    private static bool $lastCursorLacksClosingBrace = false;
137
138 352
    private static function parseDestinationBraces(Cursor $cursor): ?string
139
    {
140 352
        // 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
        if (self::$lastCursor !== null && self::$lastCursor->get() === $cursor) {
145
            if (self::$lastCursorLacksClosingBrace) {
146
                return null;
147
            }
148
        } else {
149
            self::$lastCursor = \WeakReference::create($cursor);
150
        }
151
152
        if ($res = $cursor->match(RegexHelper::REGEX_LINK_DESTINATION_BRACES)) {
153
            self::$lastCursorLacksClosingBrace = false;
154
155
            // Chop off surrounding <..>:
156
            return UrlEncoder::unescapeAndEncode(
157
                RegexHelper::unescape(\substr($res, 1, -1))
158
            );
159
        }
160
161
        self::$lastCursorLacksClosingBrace = true;
162
163
        return null;
164
    }
165
}
166