DelimiterStack   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 200
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 98.94%

Importance

Changes 0
Metric Value
dl 0
loc 200
c 0
b 0
f 0
wmc 37
lcom 1
cbo 6
ccs 93
cts 94
cp 0.9894
rs 9.44

9 Methods

Rating   Name   Duplication   Size   Complexity  
A push() 0 10 2
A findEarliest() 0 9 3
A removeDelimiter() 0 13 3
A removeDelimiterAndNode() 0 5 1
A removeDelimitersBetween() 0 9 3
A removeAll() 0 6 3
A removeEarlierMatches() 0 11 3
A searchByCharacter() 0 16 4
C processDelimiters() 0 86 15
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 957
    public function push(Delimiter $newDelimiter)
32
    {
33 957
        $newDelimiter->setPrevious($this->top);
34
35 957
        if ($this->top !== null) {
36 564
            $this->top->setNext($newDelimiter);
37
        }
38
39 957
        $this->top = $newDelimiter;
40 957
    }
41
42
    /**
43
     * @param Delimiter|null $stackBottom
44
     *
45
     * @return Delimiter|null
46
     */
47 1986
    private function findEarliest(Delimiter $stackBottom = null): ?Delimiter
48
    {
49 1986
        $delimiter = $this->top;
50 1986
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
51 522
            $delimiter = $delimiter->getPrevious();
52
        }
53
54 1986
        return $delimiter;
55
    }
56
57
    /**
58
     * @param Delimiter $delimiter
59
     */
60 957
    public function removeDelimiter(Delimiter $delimiter)
61
    {
62 957
        if ($delimiter->getPrevious() !== null) {
63 348
            $delimiter->getPrevious()->setNext($delimiter->getNext());
64
        }
65
66 957
        if ($delimiter->getNext() === null) {
67
            // top of stack
68 957
            $this->top = $delimiter->getPrevious();
69
        } else {
70 390
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
71
        }
72 957
    }
73
74 393
    private function removeDelimiterAndNode(Delimiter $delimiter)
75
    {
76 393
        $delimiter->getInlineNode()->detach();
77 393
        $this->removeDelimiter($delimiter);
78 393
    }
79
80 393
    private function removeDelimitersBetween(Delimiter $opener, Delimiter $closer)
81
    {
82 393
        $delimiter = $closer->getPrevious();
83 393
        while ($delimiter !== null && $delimiter !== $opener) {
84 27
            $previous = $delimiter->getPrevious();
85 27
            $this->removeDelimiter($delimiter);
86 27
            $delimiter = $previous;
87
        }
88 393
    }
89
90
    /**
91
     * @param Delimiter|null $stackBottom
92
     */
93 1986
    public function removeAll(Delimiter $stackBottom = null)
94
    {
95 1986
        while ($this->top && $this->top !== $stackBottom) {
96 555
            $this->removeDelimiter($this->top);
97
        }
98 1986
    }
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 1986
    public function processDelimiters(?Delimiter $stackBottom, DelimiterProcessorCollection $processors)
138
    {
139 1986
        $openersBottom = [];
140
141
        // Find first closer above stackBottom
142 1986
        $closer = $this->findEarliest($stackBottom);
143
144
        // Move forward, looking for closers, and handling each
145 1986
        while ($closer !== null) {
146 885
            $delimiterChar = $closer->getChar();
147
148 885
            $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
149 885
            if (!$closer->canClose() || $delimiterProcessor === null) {
150 840
                $closer = $closer->getNext();
151 840
                continue;
152
            }
153
154 465
            $useDelims = 0;
155 465
            $openerFound = false;
156 465
            $potentialOpenerFound = false;
157 465
            $opener = $closer->getPrevious();
158 465
            while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
159 441
                if ($opener->canOpen() && $opener->getChar() === $delimiterChar) {
160 393
                    $potentialOpenerFound = true;
161 393
                    $useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
162 393
                    if ($useDelims > 0) {
163 393
                        $openerFound = true;
164 393
                        break;
165
                    }
166
                }
167 84
                $opener = $opener->getPrevious();
168
            }
169
170 465
            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 393
            $openerNode = $opener->getInlineNode();
192
            /** @var Text $closerNode */
193 393
            $closerNode = $closer->getInlineNode();
194
195
            // Remove number of used delimiters from stack and inline nodes.
196 393
            $opener->setNumDelims($opener->getNumDelims() - $useDelims);
197 393
            $closer->setNumDelims($closer->getNumDelims() - $useDelims);
198
199 393
            $openerNode->setContent(\substr($openerNode->getContent(), 0, -$useDelims));
200 393
            $closerNode->setContent(\substr($closerNode->getContent(), 0, -$useDelims));
201
202 393
            $this->removeDelimitersBetween($opener, $closer);
0 ignored issues
show
Bug introduced by Colin O'Dell
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 393
            AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
206 393
            $delimiterProcessor->process($openerNode, $closerNode, $useDelims);
207
208
            // No delimiter characters left to process, so we can remove delimiter and the now empty node.
209 393
            if ($opener->getNumDelims() === 0) {
210 372
                $this->removeDelimiterAndNode($opener);
0 ignored issues
show
Bug introduced by Colin O'Dell
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 393
            if ($closer->getNumDelims() === 0) {
214 372
                $next = $closer->getNext();
215 372
                $this->removeDelimiterAndNode($closer);
216 372
                $closer = $next;
217
            }
218
        }
219
220
        // Remove all delimiters
221 1986
        $this->removeAll($stackBottom);
222 1986
    }
223
}
224