Passed
Push — main ( 989696...2152d5 )
by Colin
05:14 queued 02:09
created

CloseBracketParser::tryParseLink()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 13
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 4
crap 3
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\Environment\EnvironmentAwareInterface;
20
use League\CommonMark\Environment\EnvironmentInterface;
21
use League\CommonMark\Extension\CommonMark\Node\Inline\AbstractWebResource;
22
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
23
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
24
use League\CommonMark\Node\Inline\AdjacentTextMerger;
25
use League\CommonMark\Parser\Cursor;
26
use League\CommonMark\Parser\Inline\InlineParserInterface;
27
use League\CommonMark\Parser\Inline\InlineParserMatch;
28
use League\CommonMark\Parser\InlineParserContext;
29
use League\CommonMark\Reference\ReferenceInterface;
30
use League\CommonMark\Reference\ReferenceMapInterface;
31
use League\CommonMark\Util\LinkParserHelper;
32
use League\CommonMark\Util\RegexHelper;
33
34
final class CloseBracketParser implements InlineParserInterface, EnvironmentAwareInterface
35
{
36
    /**
37
     * @var EnvironmentInterface
38
     *
39
     * @psalm-readonly-allow-private-mutation
40
     */
41
    private $environment;
42
43 3060
    public function getMatchDefinition(): InlineParserMatch
44
    {
45 3060
        return InlineParserMatch::string(']');
46
    }
47
48 525
    public function parse(InlineParserContext $inlineContext): bool
49
    {
50
        // Look through stack of delimiters for a [ or !
51 525
        $opener = $inlineContext->getDelimiterStack()->searchByCharacter(['[', '!']);
52 525
        if ($opener === null) {
53 12
            return false;
54
        }
55
56 516
        if (! $opener->isActive()) {
57
            // no matched opener; remove from emphasis stack
58 18
            $inlineContext->getDelimiterStack()->removeDelimiter($opener);
59
60 18
            return false;
61
        }
62
63 516
        $cursor = $inlineContext->getCursor();
64
65 516
        $startPos      = $cursor->getPosition();
66 516
        $previousState = $cursor->saveState();
67
68 516
        $cursor->advanceBy(1);
69
70
        // Check to see if we have a link/image
71
72
        // Inline link?
73 516
        if ($result = $this->tryParseInlineLinkAndTitle($cursor)) {
74 228
            $link = $result;
75 303
        } elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener->getIndex(), $startPos)) {
76 204
            $reference = $link;
77 204
            $link      = ['url' => $link->getDestination(), 'title' => $link->getTitle()];
78
        } else {
79
            // No match
80 120
            $inlineContext->getDelimiterStack()->removeDelimiter($opener); // Remove this opener from stack
81 120
            $cursor->restoreState($previousState);
82
83 120
            return false;
84
        }
85
86 426
        $isImage = $opener->getChar() === '!';
87
88 426
        $inline = $this->createInline($link['url'], $link['title'], $isImage, $reference ?? null);
89 426
        $opener->getInlineNode()->replaceWith($inline);
90 426
        while (($label = $inline->next()) !== null) {
91 417
            $inline->appendChild($label);
92
        }
93
94
        // Process delimiters such as emphasis inside link/image
95 426
        $delimiterStack = $inlineContext->getDelimiterStack();
96 426
        $stackBottom    = $opener->getPrevious();
97 426
        $delimiterStack->processDelimiters($stackBottom, $this->environment->getDelimiterProcessors());
98 426
        $delimiterStack->removeAll($stackBottom);
99
100
        // Merge any adjacent Text nodes together
101 426
        AdjacentTextMerger::mergeChildNodes($inline);
102
103
        // processEmphasis will remove this and later delimiters.
104
        // Now, for a link, we also remove earlier link openers (no links in links)
105 426
        if (! $isImage) {
106 369
            $inlineContext->getDelimiterStack()->removeEarlierMatches('[');
107
        }
108
109 426
        return true;
110
    }
111
112 3069
    public function setEnvironment(EnvironmentInterface $environment): void
113
    {
114 3069
        $this->environment = $environment;
115 3069
    }
116
117
    /**
118
     * @return array<string, string>|false
119
     */
120 516
    private function tryParseInlineLinkAndTitle(Cursor $cursor)
121
    {
122 516
        if ($cursor->getCharacter() !== '(') {
123 264
            return false;
124
        }
125
126 264
        $previousState = $cursor->saveState();
127
128 264
        $cursor->advanceBy(1);
129 264
        $cursor->advanceToNextNonSpaceOrNewline();
130 264
        if (($dest = LinkParserHelper::parseLinkDestination($cursor)) === null) {
131 9
            $cursor->restoreState($previousState);
132
133 9
            return false;
134
        }
135
136 258
        $cursor->advanceToNextNonSpaceOrNewline();
137 258
        $previousCharacter = $cursor->peek(-1);
138
        // We know from previous lines that we've advanced at least one space so far, so this next call should never be null
139
        \assert(\is_string($previousCharacter));
140
141 258
        $title = '';
142
        // make sure there's a space before the title:
143 258
        if (\preg_match(RegexHelper::REGEX_WHITESPACE_CHAR, $previousCharacter)) {
144 60
            $title = LinkParserHelper::parseLinkTitle($cursor) ?? '';
145
        }
146
147 258
        $cursor->advanceToNextNonSpaceOrNewline();
148
149 258
        if ($cursor->getCharacter() !== ')') {
150 36
            $cursor->restoreState($previousState);
151
152 36
            return false;
153
        }
154
155 228
        $cursor->advanceBy(1);
156
157 228
        return ['url' => $dest, 'title' => $title];
158
    }
159
160 303
    private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, ?int $openerIndex, int $startPos): ?ReferenceInterface
161
    {
162 303
        if ($openerIndex === null) {
163
            return null;
164
        }
165
166 303
        $savePos     = $cursor->saveState();
167 303
        $beforeLabel = $cursor->getPosition();
168 303
        $n           = LinkParserHelper::parseLinkLabel($cursor);
169 303
        if ($n === 0 || $n === 2) {
170 255
            $start  = $openerIndex;
171 255
            $length = $startPos - $openerIndex;
172
        } else {
173 60
            $start  = $beforeLabel + 1;
174 60
            $length = $n - 2;
175
        }
176
177 303
        $referenceLabel = $cursor->getSubstring($start, $length);
178
179 303
        if ($n === 0) {
180
            // If shortcut reference link, rewind before spaces we skipped
181 231
            $cursor->restoreState($savePos);
182
        }
183
184 303
        return $referenceMap->get($referenceLabel);
185
    }
186
187 426
    private function createInline(string $url, string $title, bool $isImage, ?ReferenceInterface $reference = null): AbstractWebResource
188
    {
189 426
        if ($isImage) {
190 81
            $inline = new Image($url, null, $title);
191
        } else {
192 369
            $inline = new Link($url, null, $title);
193
        }
194
195 426
        if ($reference) {
196 204
            $inline->data->set('reference', $reference);
197
        }
198
199 426
        return $inline;
200
    }
201
}
202