Completed
Push — master ( 0b0b24...54ff37 )
by Colin
02:37
created

CloseBracketParser   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 175
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 98.7%

Importance

Changes 0
Metric Value
wmc 23
lcom 1
cbo 12
dl 0
loc 175
ccs 76
cts 77
cp 0.987
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getCharacters() 0 4 1
B parse() 0 56 6
A setEnvironment() 0 4 1
A tryParseLink() 0 14 3
A tryParseInlineLinkAndTitle() 0 36 5
A tryParseReference() 0 26 5
A createInline() 0 8 2
1
<?php
2
3
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
9
 *  - (c) John MacFarlane
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
16
17
use League\CommonMark\Delimiter\DelimiterInterface;
18
use League\CommonMark\Environment\EnvironmentAwareInterface;
19
use League\CommonMark\Environment\EnvironmentInterface;
20
use League\CommonMark\Extension\CommonMark\Node\Inline\AbstractWebResource;
21
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
22
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
23
use League\CommonMark\Node\Inline\AdjacentTextMerger;
24
use League\CommonMark\Parser\Cursor;
25
use League\CommonMark\Parser\Inline\InlineParserInterface;
26
use League\CommonMark\Parser\InlineParserContext;
27
use League\CommonMark\Reference\ReferenceInterface;
28
use League\CommonMark\Reference\ReferenceMapInterface;
29
use League\CommonMark\Util\LinkParserHelper;
30
use League\CommonMark\Util\RegexHelper;
31
32
final class CloseBracketParser implements InlineParserInterface, EnvironmentAwareInterface
33
{
34
    /**
35
     * @var EnvironmentInterface
36
     */
37
    private $environment;
38
39 2490
    public function getCharacters(): array
40
    {
41 2490
        return [']'];
42
    }
43
44 453
    public function parse(InlineParserContext $inlineContext): bool
45
    {
46
        // Look through stack of delimiters for a [ or !
47 453
        $opener = $inlineContext->getDelimiterStack()->searchByCharacter(['[', '!']);
48 453
        if ($opener === null) {
49 12
            return false;
50
        }
51
52 444
        if (!$opener->isActive()) {
53
            // no matched opener; remove from emphasis stack
54 18
            $inlineContext->getDelimiterStack()->removeDelimiter($opener);
55
56 18
            return false;
57
        }
58
59 444
        $cursor = $inlineContext->getCursor();
60
61 444
        $startPos = $cursor->getPosition();
62 444
        $previousState = $cursor->saveState();
63
64 444
        $cursor->advanceBy(1);
65
66
        // Check to see if we have a link/image
67 444
        if (!($link = $this->tryParseLink($cursor, $inlineContext->getReferenceMap(), $opener, $startPos))) {
68
            // No match
69 117
            $inlineContext->getDelimiterStack()->removeDelimiter($opener); // Remove this opener from stack
70 117
            $cursor->restoreState($previousState);
71
72 117
            return false;
73
        }
74
75 357
        $isImage = $opener->getChar() === '!';
76
77 357
        $inline = $this->createInline($link['url'], $link['title'], $isImage);
78 357
        $opener->getInlineNode()->replaceWith($inline);
79 357
        while (($label = $inline->next()) !== null) {
80 354
            $inline->appendChild($label);
81
        }
82
83
        // Process delimiters such as emphasis inside link/image
84 357
        $delimiterStack = $inlineContext->getDelimiterStack();
85 357
        $stackBottom = $opener->getPrevious();
86 357
        $delimiterStack->processDelimiters($stackBottom, $this->environment->getDelimiterProcessors());
87 357
        $delimiterStack->removeAll($stackBottom);
88
89
        // Merge any adjacent Text nodes together
90 357
        AdjacentTextMerger::mergeChildNodes($inline);
91
92
        // processEmphasis will remove this and later delimiters.
93
        // Now, for a link, we also remove earlier link openers (no links in links)
94 357
        if (!$isImage) {
95 303
            $inlineContext->getDelimiterStack()->removeEarlierMatches('[');
96
        }
97
98 357
        return true;
99
    }
100
101 2490
    public function setEnvironment(EnvironmentInterface $environment): void
102
    {
103 2490
        $this->environment = $environment;
104 2490
    }
105
106
    /**
107
     * @param Cursor                $cursor
108
     * @param ReferenceMapInterface $referenceMap
109
     * @param DelimiterInterface    $opener
110
     * @param int                   $startPos
111
     *
112
     * @return array<string, string>|false
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
113
     */
114 444
    private function tryParseLink(Cursor $cursor, ReferenceMapInterface $referenceMap, DelimiterInterface $opener, int $startPos)
115
    {
116
        // Check to see if we have a link/image
117
        // Inline link?
118 444
        if ($result = $this->tryParseInlineLinkAndTitle($cursor)) {
119 165
            return $result;
120
        }
121
122 294
        if ($link = $this->tryParseReference($cursor, $referenceMap, $opener, $startPos)) {
123 198
            return ['url' => $link->getDestination(), 'title' => $link->getTitle()];
124
        }
125
126 117
        return false;
127
    }
128
129
    /**
130
     * @param Cursor $cursor
131
     *
132
     * @return array<string, string>|false
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
133
     */
134 444
    private function tryParseInlineLinkAndTitle(Cursor $cursor)
135
    {
136 444
        if ($cursor->getCharacter() !== '(') {
137 258
            return false;
138
        }
139
140 195
        $previousState = $cursor->saveState();
141
142 195
        $cursor->advanceBy(1);
143 195
        $cursor->advanceToNextNonSpaceOrNewline();
144 195
        if (($dest = LinkParserHelper::parseLinkDestination($cursor)) === null) {
145 9
            $cursor->restoreState($previousState);
146
147 9
            return false;
148
        }
149
150 189
        $cursor->advanceToNextNonSpaceOrNewline();
151
152 189
        $title = '';
153
        // make sure there's a space before the title:
154 189
        if (\preg_match(RegexHelper::REGEX_WHITESPACE_CHAR, $cursor->peek(-1))) {
155 54
            $title = LinkParserHelper::parseLinkTitle($cursor) ?? '';
156
        }
157
158 189
        $cursor->advanceToNextNonSpaceOrNewline();
159
160 189
        if ($cursor->getCharacter() !== ')') {
161 30
            $cursor->restoreState($previousState);
162
163 30
            return false;
164
        }
165
166 165
        $cursor->advanceBy(1);
167
168 165
        return ['url' => $dest, 'title' => $title];
169
    }
170
171 294
    private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, DelimiterInterface $opener, int $startPos): ?ReferenceInterface
172
    {
173 294
        if ($opener->getIndex() === null) {
174
            return null;
175
        }
176
177 294
        $savePos = $cursor->saveState();
178 294
        $beforeLabel = $cursor->getPosition();
179 294
        $n = LinkParserHelper::parseLinkLabel($cursor);
180 294
        if ($n === 0 || $n === 2) {
181 243
            $start = $opener->getIndex();
182 243
            $length = $startPos - $opener->getIndex();
183
        } else {
184 66
            $start = $beforeLabel + 1;
185 66
            $length = $n - 2;
186
        }
187
188 294
        $referenceLabel = $cursor->getSubstring($start, $length);
189
190 294
        if ($n === 0) {
191
            // If shortcut reference link, rewind before spaces we skipped
192 219
            $cursor->restoreState($savePos);
193
        }
194
195 294
        return $referenceMap->getReference($referenceLabel);
196
    }
197
198 357
    private function createInline(string $url, string $title, bool $isImage): AbstractWebResource
199
    {
200 357
        if ($isImage) {
201 72
            return new Image($url, null, $title);
202
        }
203
204 303
        return new Link($url, null, $title);
205
    }
206
}
207