CloseBracketParser::parse()   B
last analyzed

Complexity

Conditions 11
Paths 23

Size

Total Lines 74
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 40
c 0
b 0
f 0
nc 23
nop 1
dl 0
loc 74
ccs 39
cts 39
cp 1
crap 11
rs 7.3166

How to fix   Long Method    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\Delimiter\Bracket;
20
use League\CommonMark\Environment\EnvironmentAwareInterface;
21
use League\CommonMark\Environment\EnvironmentInterface;
22
use League\CommonMark\Extension\CommonMark\Node\Inline\AbstractWebResource;
23
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
24
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
25
use League\CommonMark\Extension\Mention\Mention;
26
use League\CommonMark\Node\Inline\AdjacentTextMerger;
27
use League\CommonMark\Node\Inline\Text;
28
use League\CommonMark\Parser\Cursor;
29
use League\CommonMark\Parser\Inline\InlineParserInterface;
30
use League\CommonMark\Parser\Inline\InlineParserMatch;
31
use League\CommonMark\Parser\InlineParserContext;
32
use League\CommonMark\Reference\ReferenceInterface;
33
use League\CommonMark\Reference\ReferenceMapInterface;
34
use League\CommonMark\Util\LinkParserHelper;
35
use League\CommonMark\Util\RegexHelper;
36
37
final class CloseBracketParser implements InlineParserInterface, EnvironmentAwareInterface
38
{
39
    /** @psalm-readonly-allow-private-mutation */
40
    private EnvironmentInterface $environment;
41
42 2306
    public function getMatchDefinition(): InlineParserMatch
43
    {
44 2306
        return InlineParserMatch::string(']');
45
    }
46
47 402
    public function parse(InlineParserContext $inlineContext): bool
48
    {
49
        // Look through stack of delimiters for a [ or !
50 402
        $opener = $inlineContext->getDelimiterStack()->getLastBracket();
51 402
        if ($opener === null) {
52 8
            return false;
53
        }
54
55 396
        if (! $opener->isImage() && ! $opener->isActive()) {
56
            // no matched opener; remove from stack
57 12
            $inlineContext->getDelimiterStack()->removeBracket();
58
59 12
            return false;
60
        }
61
62 396
        $cursor = $inlineContext->getCursor();
63
64 396
        $startPos      = $cursor->getPosition();
65 396
        $previousState = $cursor->saveState();
66
67 396
        $cursor->advanceBy(1);
68
69
        // Check to see if we have a link/image
70
71
        // Inline link?
72 396
        if ($result = $this->tryParseInlineLinkAndTitle($cursor)) {
73 184
            $link = $result;
74 222
        } elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener, $startPos)) {
75 144
            $reference = $link;
76 144
            $link      = ['url' => $link->getDestination(), 'title' => $link->getTitle()];
77
        } else {
78
            // No match; remove this opener from stack
79 92
            $inlineContext->getDelimiterStack()->removeBracket();
80 92
            $cursor->restoreState($previousState);
81
82 92
            return false;
83
        }
84
85 324
        $inline = $this->createInline($link['url'], $link['title'], $opener->isImage(), $reference ?? null);
86 324
        $opener->getNode()->replaceWith($inline);
87 324
        while (($label = $inline->next()) !== null) {
88
            // Is there a Mention or Link contained within this link?
89
            // CommonMark does not allow nested links, so we'll restore the original text.
90 314
            if ($label instanceof Mention) {
91 2
                $label->replaceWith($replacement = new Text($label->getPrefix() . $label->getIdentifier()));
92 2
                $inline->appendChild($replacement);
93 314
            } elseif ($label instanceof Link) {
94 10
                foreach ($label->children() as $child) {
95 10
                    $label->insertBefore($child);
96
                }
97
98 10
                $label->detach();
99
            } else {
100 314
                $inline->appendChild($label);
101
            }
102
        }
103
104
        // Process delimiters such as emphasis inside link/image
105 324
        $delimiterStack = $inlineContext->getDelimiterStack();
106 324
        $stackBottom    = $opener->getPosition();
107 324
        $delimiterStack->processDelimiters($stackBottom, $this->environment->getDelimiterProcessors());
108 324
        $delimiterStack->removeBracket();
109 324
        $delimiterStack->removeAll($stackBottom);
110
111
        // Merge any adjacent Text nodes together
112 324
        AdjacentTextMerger::mergeChildNodes($inline);
113
114
        // processEmphasis will remove this and later delimiters.
115
        // Now, for a link, we also remove earlier link openers (no links in links)
116 324
        if (! $opener->isImage()) {
117 278
            $inlineContext->getDelimiterStack()->deactivateLinkOpeners();
118
        }
119
120 324
        return true;
121
    }
122
123 2320
    public function setEnvironment(EnvironmentInterface $environment): void
124
    {
125 2320
        $this->environment = $environment;
126
    }
127
128
    /**
129
     * @return array<string, string>|null
130
     */
131 396
    private function tryParseInlineLinkAndTitle(Cursor $cursor): ?array
132
    {
133 396
        if ($cursor->getCurrentCharacter() !== '(') {
134 188
            return null;
135
        }
136
137 216
        $previousState = $cursor->saveState();
138
139 216
        $cursor->advanceBy(1);
140 216
        $cursor->advanceToNextNonSpaceOrNewline();
141 216
        if (($dest = LinkParserHelper::parseLinkDestination($cursor)) === null) {
142 8
            $cursor->restoreState($previousState);
143
144 8
            return null;
145
        }
146
147 210
        $cursor->advanceToNextNonSpaceOrNewline();
148 210
        $previousCharacter = $cursor->peek(-1);
149
        // We know from previous lines that we've advanced at least one space so far, so this next call should never be null
150
        \assert(\is_string($previousCharacter));
151
152 210
        $title = '';
153
        // make sure there's a space before the title:
154 210
        if (\preg_match(RegexHelper::REGEX_WHITESPACE_CHAR, $previousCharacter)) {
155 44
            $title = LinkParserHelper::parseLinkTitle($cursor) ?? '';
156
        }
157
158 210
        $cursor->advanceToNextNonSpaceOrNewline();
159
160 210
        if ($cursor->getCurrentCharacter() !== ')') {
161 30
            $cursor->restoreState($previousState);
162
163 30
            return null;
164
        }
165
166 184
        $cursor->advanceBy(1);
167
168 184
        return ['url' => $dest, 'title' => $title];
169
    }
170
171 222
    private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, Bracket $opener, int $startPos): ?ReferenceInterface
172
    {
173 222
        $savePos     = $cursor->saveState();
174 222
        $beforeLabel = $cursor->getPosition();
175 222
        $n           = LinkParserHelper::parseLinkLabel($cursor);
176 222
        if ($n > 2) {
177 44
            $start  = $beforeLabel + 1;
178 44
            $length = $n - 2;
179 186
        } elseif (! $opener->hasNext()) {
180
            // Empty or missing second label means to use the first label as the reference.
181
            // The reference must not contain a bracket. If we know there's a bracket, we don't even bother checking it.
182 186
            $start  = $opener->getPosition();
183 186
            $length = $startPos - $start;
184
        } else {
185
            $cursor->restoreState($savePos);
186
187
            return null;
188
        }
189
190 222
        $referenceLabel = $cursor->getSubstring($start, $length);
191
192 222
        if ($n === 0) {
193
            // If shortcut reference link, rewind before spaces we skipped
194 170
            $cursor->restoreState($savePos);
195
        }
196
197 222
        return $referenceMap->get($referenceLabel);
198
    }
199
200 324
    private function createInline(string $url, string $title, bool $isImage, ?ReferenceInterface $reference = null): AbstractWebResource
201
    {
202 324
        if ($isImage) {
203 62
            $inline = new Image($url, null, $title);
204
        } else {
205 278
            $inline = new Link($url, null, $title);
206
        }
207
208 324
        if ($reference) {
209 144
            $inline->data->set('reference', $reference);
210
        }
211
212 324
        return $inline;
213
    }
214
}
215