Completed
Push — master ( da54d7...33d164 )
by Colin
25s queued 10s
created

DelimiterStack::removeDelimiterAndNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
c 0
b 0
f 0
ccs 4
cts 4
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
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
 * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
12
 *  - (c) Atlassian Pty Ltd
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace League\CommonMark\Delimiter;
19
20
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
21
use League\CommonMark\Inline\AdjacentTextMerger;
22
use League\CommonMark\Inline\Element\Text;
23
24
class DelimiterStack
25
{
26
    /**
27
     * @var Delimiter|null
28
     */
29
    private $top;
30
31 912
    public function push(Delimiter $newDelimiter)
32
    {
33 912
        $newDelimiter->setPrevious($this->top);
34
35 912
        if ($this->top !== null) {
36 522
            $this->top->setNext($newDelimiter);
37
        }
38
39 912
        $this->top = $newDelimiter;
40 912
    }
41
42
    /**
43
     * @param Delimiter|null $stackBottom
44
     *
45
     * @return Delimiter|null
46
     */
47 1743
    private function findEarliest(Delimiter $stackBottom = null): ?Delimiter
48
    {
49 1743
        $delimiter = $this->top;
50 1743
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
51 480
            $delimiter = $delimiter->getPrevious();
52
        }
53
54 1743
        return $delimiter;
55
    }
56
57
    /**
58
     * @param Delimiter $delimiter
59
     */
60 912
    public function removeDelimiter(Delimiter $delimiter)
61
    {
62 912
        if ($delimiter->getPrevious() !== null) {
63 345
            $delimiter->getPrevious()->setNext($delimiter->getNext());
64
        }
65
66 912
        if ($delimiter->getNext() === null) {
67
            // top of stack
68 912
            $this->top = $delimiter->getPrevious();
69
        } else {
70 348
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
71
        }
72 912
    }
73
74 351
    private function removeDelimiterAndNode(Delimiter $delimiter)
75
    {
76 351
        $delimiter->getInlineNode()->detach();
77 351
        $this->removeDelimiter($delimiter);
78 351
    }
79
80 351
    private function removeDelimitersBetween(Delimiter $opener, Delimiter $closer)
81
    {
82 351
        $delimiter = $closer->getPrevious();
83 351
        while ($delimiter !== null && $delimiter !== $opener) {
84 27
            $previous = $delimiter->getPrevious();
85 27
            $this->removeDelimiter($delimiter);
86 27
            $delimiter = $previous;
87
        }
88 351
    }
89
90
    /**
91
     * @param Delimiter|null $stackBottom
92
     */
93 1743
    public function removeAll(Delimiter $stackBottom = null)
94
    {
95 1743
        while ($this->top && $this->top !== $stackBottom) {
96 549
            $this->removeDelimiter($this->top);
97
        }
98 1743
    }
99
100
    /**
101
     * @param string $character
102
     */
103 297
    public function removeEarlierMatches(string $character)
104
    {
105 297
        $opener = $this->top;
106 297
        while ($opener !== null) {
107 54
            if ($opener->getChar() === $character) {
108 24
                $opener->setActive(false);
109
            }
110
111 54
            $opener = $opener->getPrevious();
112
        }
113 297
    }
114
115
    /**
116
     * @param string|string[] $characters
117
     *
118
     * @return Delimiter|null
119
     */
120 438
    public function searchByCharacter($characters): ?Delimiter
121
    {
122 438
        if (!\is_array($characters)) {
123
            $characters = [$characters];
124
        }
125
126 438
        $opener = $this->top;
127 438
        while ($opener !== null) {
128 432
            if (\in_array($opener->getChar(), $characters)) {
129 429
                break;
130
            }
131 72
            $opener = $opener->getPrevious();
132
        }
133
134 438
        return $opener;
135
    }
136
137 1743
    public function processDelimiters(?Delimiter $stackBottom, DelimiterProcessorCollection $processors)
138
    {
139 1743
        $openersBottom = [];
140
141
        // Find first closer above stackBottom
142 1743
        $closer = $this->findEarliest($stackBottom);
143
144
        // Move forward, looking for closers, and handling each
145 1743
        while ($closer !== null) {
146 840
            $delimiterChar = $closer->getChar();
147
148 840
            $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
149 840
            if (!$closer->canClose() || $delimiterProcessor === null) {
150 795
                $closer = $closer->getNext();
151 795
                continue;
152
            }
153
154 423
            $useDelims = 0;
155 423
            $openerFound = false;
156 423
            $potentialOpenerFound = false;
157 423
            $opener = $closer->getPrevious();
158 423
            while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
159 399
                if ($opener->canOpen() && $opener->getChar() === $delimiterChar) {
160 351
                    $potentialOpenerFound = true;
161 351
                    $useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
162 351
                    if ($useDelims > 0) {
163 351
                        $openerFound = true;
164 351
                        break;
165
                    }
166
                }
167 84
                $opener = $opener->getPrevious();
168
            }
169
170 423
            if (!$openerFound) {
171 141
                if (!$potentialOpenerFound) {
172
                    // Only do this when we didn't even have a potential
173
                    // opener (one that matches the character and can open).
174
                    // If an opener was rejected because of the number of
175
                    // delimiters (e.g. because of the "multiple of 3"
176
                    // Set lower bound for future searches for openersrule),
177
                    // we want to consider it next time because the number
178
                    // of delimiters can change as we continue processing.
179 129
                    $openersBottom[$delimiterChar] = $closer->getPrevious();
180 129
                    if (!$closer->canOpen()) {
181
                        // We can remove a closer that can't be an opener,
182
                        // once we've seen there's no matching opener.
183 87
                        $this->removeDelimiter($closer);
184
                    }
185
                }
186 141
                $closer = $closer->getNext();
187 141
                continue;
188
            }
189
190
            /** @var Text $openerNode */
191 351
            $openerNode = $opener->getInlineNode();
192
            /** @var Text $closerNode */
193 351
            $closerNode = $closer->getInlineNode();
194
195
            // Remove number of used delimiters from stack and inline nodes.
196 351
            $opener->setNumDelims($opener->getNumDelims() - $useDelims);
197 351
            $closer->setNumDelims($closer->getNumDelims() - $useDelims);
198
199 351
            $openerNode->setContent(\substr($openerNode->getContent(), 0, -$useDelims));
200 351
            $closerNode->setContent(\substr($closerNode->getContent(), 0, -$useDelims));
201
202 351
            $this->removeDelimitersBetween($opener, $closer);
0 ignored issues
show
Bug introduced by
It seems like $opener can be null; however, removeDelimitersBetween() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
203
            // The delimiter processor can re-parent the nodes between opener and closer,
204
            // so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
205 351
            AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
206 351
            $delimiterProcessor->process($openerNode, $closerNode, $useDelims);
207
208
            // No delimiter characters left to process, so we can remove delimiter and the now empty node.
209 351
            if ($opener->getNumDelims() === 0) {
210 330
                $this->removeDelimiterAndNode($opener);
0 ignored issues
show
Bug introduced by
It seems like $opener can be null; however, removeDelimiterAndNode() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
211
            }
212
213 351
            if ($closer->getNumDelims() === 0) {
214 330
                $next = $closer->getNext();
215 330
                $this->removeDelimiterAndNode($closer);
216 330
                $closer = $next;
217
            }
218
        }
219
220
        // Remove all delimiters
221 1743
        $this->removeAll($stackBottom);
222 1743
    }
223
}
224