Completed
Push — feature/resolve-parent-class ( 1a8a55...248da8 )
by Colin
35:17 queued 33:54
created

DelimiterStack::removeDelimitersBetween()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 9
c 0
b 0
f 0
ccs 7
cts 7
cp 1
rs 9.9666
cc 3
nc 2
nop 2
crap 3
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
23
final class DelimiterStack
24
{
25
    /**
26
     * @var DelimiterInterface|null
27
     */
28
    private $top;
29
30 1062
    public function push(DelimiterInterface $newDelimiter)
31
    {
32 1062
        $newDelimiter->setPrevious($this->top);
33
34 1062
        if ($this->top !== null) {
35 609
            $this->top->setNext($newDelimiter);
36
        }
37
38 1062
        $this->top = $newDelimiter;
39 1062
    }
40
41
    /**
42
     * @param DelimiterInterface|null $stackBottom
43
     *
44
     * @return DelimiterInterface|null
45
     */
46 2415
    private function findEarliest(DelimiterInterface $stackBottom = null): ?DelimiterInterface
47
    {
48 2415
        $delimiter = $this->top;
49 2415
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
50 567
            $delimiter = $delimiter->getPrevious();
51
        }
52
53 2415
        return $delimiter;
54
    }
55
56
    /**
57
     * @param DelimiterInterface $delimiter
58
     */
59 1062
    public function removeDelimiter(DelimiterInterface $delimiter)
60
    {
61 1062
        if ($delimiter->getPrevious() !== null) {
62 309
            $delimiter->getPrevious()->setNext($delimiter->getNext());
63
        }
64
65 1062
        if ($delimiter->getNext() === null) {
66
            // top of stack
67 1062
            $this->top = $delimiter->getPrevious();
68
        } else {
69 495
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
70
        }
71 1062
    }
72
73 504
    private function removeDelimiterAndNode(DelimiterInterface $delimiter)
74
    {
75 504
        $delimiter->getInlineNode()->detach();
76 504
        $this->removeDelimiter($delimiter);
77 504
    }
78
79 504
    private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer)
80
    {
81 504
        $delimiter = $closer->getPrevious();
82 504
        while ($delimiter !== null && $delimiter !== $opener) {
83 24
            $previous = $delimiter->getPrevious();
84 24
            $this->removeDelimiter($delimiter);
85 24
            $delimiter = $previous;
86
        }
87 504
    }
88
89
    /**
90
     * @param DelimiterInterface|null $stackBottom
91
     */
92 2415
    public function removeAll(DelimiterInterface $stackBottom = null)
93
    {
94 2415
        while ($this->top && $this->top !== $stackBottom) {
95 516
            $this->removeDelimiter($this->top);
96
        }
97 2415
    }
98
99
    /**
100
     * @param string $character
101
     */
102 303
    public function removeEarlierMatches(string $character)
103
    {
104 303
        $opener = $this->top;
105 303
        while ($opener !== null) {
106 57
            if ($opener->getChar() === $character) {
107 24
                $opener->setActive(false);
108
            }
109
110 57
            $opener = $opener->getPrevious();
111
        }
112 303
    }
113
114
    /**
115
     * @param string|string[] $characters
116
     *
117
     * @return DelimiterInterface|null
118
     */
119 453
    public function searchByCharacter($characters): ?DelimiterInterface
120
    {
121 453
        if (!\is_array($characters)) {
122
            $characters = [$characters];
123
        }
124
125 453
        $opener = $this->top;
126 453
        while ($opener !== null) {
127 447
            if (\in_array($opener->getChar(), $characters)) {
128 444
                break;
129
            }
130 72
            $opener = $opener->getPrevious();
131
        }
132
133 453
        return $opener;
134
    }
135
136 2415
    public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterProcessorCollection $processors)
137
    {
138 2415
        $openersBottom = [];
139
140
        // Find first closer above stackBottom
141 2415
        $closer = $this->findEarliest($stackBottom);
142
143
        // Move forward, looking for closers, and handling each
144 2415
        while ($closer !== null) {
145 984
            $delimiterChar = $closer->getChar();
146
147 984
            $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
148 984
            if (!$closer->canClose() || $delimiterProcessor === null) {
149 894
                $closer = $closer->getNext();
150 894
                continue;
151
            }
152
153 603
            $openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
154
155 603
            $useDelims = 0;
156 603
            $openerFound = false;
157 603
            $potentialOpenerFound = false;
158 603
            $opener = $closer->getPrevious();
159 603
            while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
160 528
                if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
161 507
                    $potentialOpenerFound = true;
162 507
                    $useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
163 507
                    if ($useDelims > 0) {
164 504
                        $openerFound = true;
165 504
                        break;
166
                    }
167
                }
168 57
                $opener = $opener->getPrevious();
169
            }
170
171 603
            if (!$openerFound) {
172 180
                if (!$potentialOpenerFound) {
173
                    // Only do this when we didn't even have a potential
174
                    // opener (one that matches the character and can open).
175
                    // If an opener was rejected because of the number of
176
                    // delimiters (e.g. because of the "multiple of 3"
177
                    // Set lower bound for future searches for openersrule),
178
                    // we want to consider it next time because the number
179
                    // of delimiters can change as we continue processing.
180 165
                    $openersBottom[$delimiterChar] = $closer->getPrevious();
181 165
                    if (!$closer->canOpen()) {
182
                        // We can remove a closer that can't be an opener,
183
                        // once we've seen there's no matching opener.
184 123
                        $this->removeDelimiter($closer);
185
                    }
186
                }
187 180
                $closer = $closer->getNext();
188 180
                continue;
189
            }
190
191 504
            $openerNode = $opener->getInlineNode();
192 504
            $closerNode = $closer->getInlineNode();
193
194
            // Remove number of used delimiters from stack and inline nodes.
195 504
            $opener->setLength($opener->getLength() - $useDelims);
196 504
            $closer->setLength($closer->getLength() - $useDelims);
197
198 504
            $openerNode->setContent(\substr($openerNode->getContent(), 0, -$useDelims));
199 504
            $closerNode->setContent(\substr($closerNode->getContent(), 0, -$useDelims));
200
201 504
            $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...
202
            // The delimiter processor can re-parent the nodes between opener and closer,
203
            // so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
204 504
            AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
205 504
            $delimiterProcessor->process($openerNode, $closerNode, $useDelims);
206
207
            // No delimiter characters left to process, so we can remove delimiter and the now empty node.
208 504
            if ($opener->getLength() === 0) {
209 477
                $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...
210
            }
211
212 504
            if ($closer->getLength() === 0) {
213 477
                $next = $closer->getNext();
214 477
                $this->removeDelimiterAndNode($closer);
215 477
                $closer = $next;
216
            }
217
        }
218
219
        // Remove all delimiters
220 2415
        $this->removeAll($stackBottom);
221 2415
    }
222
}
223