DelimiterStack   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 200
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.95%

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 5
dl 0
loc 200
ccs 94
cts 95
cp 0.9895
rs 9.44
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A findEarliest() 0 9 3
A searchByCharacter() 0 16 4
A push() 0 10 2
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
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
23
final class DelimiterStack
24
{
25
    /**
26
     * @var DelimiterInterface|null
27
     */
28
    private $top;
29
30 990
    public function push(DelimiterInterface $newDelimiter)
31
    {
32 990
        $newDelimiter->setPrevious($this->top);
33
34 990
        if ($this->top !== null) {
35 597
            $this->top->setNext($newDelimiter);
36
        }
37
38 990
        $this->top = $newDelimiter;
39 990
    }
40
41
    /**
42
     * @param DelimiterInterface|null $stackBottom
43
     *
44
     * @return DelimiterInterface|null
45
     */
46 2022
    private function findEarliest(DelimiterInterface $stackBottom = null): ?DelimiterInterface
47
    {
48 2022
        $delimiter = $this->top;
49 2022
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
50 555
            $delimiter = $delimiter->getPrevious();
51
        }
52
53 2022
        return $delimiter;
54
    }
55
56
    /**
57
     * @param DelimiterInterface $delimiter
58
     */
59 990
    public function removeDelimiter(DelimiterInterface $delimiter)
60
    {
61 990
        if ($delimiter->getPrevious() !== null) {
62 366
            $delimiter->getPrevious()->setNext($delimiter->getNext());
63
        }
64
65 990
        if ($delimiter->getNext() === null) {
66
            // top of stack
67 990
            $this->top = $delimiter->getPrevious();
68
        } else {
69 408
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
70
        }
71 990
    }
72
73 414
    private function removeDelimiterAndNode(DelimiterInterface $delimiter)
74
    {
75 414
        $delimiter->getInlineNode()->detach();
76 414
        $this->removeDelimiter($delimiter);
77 414
    }
78
79 414
    private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer)
80
    {
81 414
        $delimiter = $closer->getPrevious();
82 414
        while ($delimiter !== null && $delimiter !== $opener) {
83 27
            $previous = $delimiter->getPrevious();
84 27
            $this->removeDelimiter($delimiter);
85 27
            $delimiter = $previous;
86
        }
87 414
    }
88
89
    /**
90
     * @param DelimiterInterface|null $stackBottom
91
     */
92 2022
    public function removeAll(DelimiterInterface $stackBottom = null)
93
    {
94 2022
        while ($this->top && $this->top !== $stackBottom) {
95 567
            $this->removeDelimiter($this->top);
96
        }
97 2022
    }
98
99
    /**
100
     * @param string $character
101
     */
102 291
    public function removeEarlierMatches(string $character)
103
    {
104 291
        $opener = $this->top;
105 291
        while ($opener !== null) {
106 54
            if ($opener->getChar() === $character) {
107 24
                $opener->setActive(false);
108
            }
109
110 54
            $opener = $opener->getPrevious();
111
        }
112 291
    }
113
114
    /**
115
     * @param string|string[] $characters
116
     *
117
     * @return DelimiterInterface|null
118
     */
119 432
    public function searchByCharacter($characters): ?DelimiterInterface
120
    {
121 432
        if (!\is_array($characters)) {
122
            $characters = [$characters];
123
        }
124
125 432
        $opener = $this->top;
126 432
        while ($opener !== null) {
127 426
            if (\in_array($opener->getChar(), $characters)) {
128 423
                break;
129
            }
130 72
            $opener = $opener->getPrevious();
131
        }
132
133 432
        return $opener;
134
    }
135
136 2022
    public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterProcessorCollection $processors)
137
    {
138 2022
        $openersBottom = [];
139
140
        // Find first closer above stackBottom
141 2022
        $closer = $this->findEarliest($stackBottom);
142
143
        // Move forward, looking for closers, and handling each
144 2022
        while ($closer !== null) {
145 918
            $delimiterChar = $closer->getChar();
146
147 918
            $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
148 918
            if (!$closer->canClose() || $delimiterProcessor === null) {
149 870
                $closer = $closer->getNext();
150 870
                continue;
151
            }
152
153 495
            $openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
154
155 495
            $useDelims = 0;
156 495
            $openerFound = false;
157 495
            $potentialOpenerFound = false;
158 495
            $opener = $closer->getPrevious();
159 495
            while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
160 468
                if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
161 417
                    $potentialOpenerFound = true;
162 417
                    $useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
163 417
                    if ($useDelims > 0) {
164 414
                        $openerFound = true;
165 414
                        break;
166
                    }
167
                }
168 90
                $opener = $opener->getPrevious();
169
            }
170
171 495
            if (!$openerFound) {
172 153
                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 138
                    $openersBottom[$delimiterChar] = $closer->getPrevious();
181 138
                    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 96
                        $this->removeDelimiter($closer);
185
                    }
186
                }
187 153
                $closer = $closer->getNext();
188 153
                continue;
189
            }
190
191 414
            $openerNode = $opener->getInlineNode();
192 414
            $closerNode = $closer->getInlineNode();
193
194
            // Remove number of used delimiters from stack and inline nodes.
195 414
            $opener->setLength($opener->getLength() - $useDelims);
196 414
            $closer->setLength($closer->getLength() - $useDelims);
197
198 414
            $openerNode->setContent(\substr($openerNode->getContent(), 0, -$useDelims));
199 414
            $closerNode->setContent(\substr($closerNode->getContent(), 0, -$useDelims));
200
201 414
            $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...
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 414
            AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
205 414
            $delimiterProcessor->process($openerNode, $closerNode, $useDelims);
206
207
            // No delimiter characters left to process, so we can remove delimiter and the now empty node.
208 414
            if ($opener->getLength() === 0) {
209 390
                $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...
210
            }
211
212 414
            if ($closer->getLength() === 0) {
213 390
                $next = $closer->getNext();
214 390
                $this->removeDelimiterAndNode($closer);
215 390
                $closer = $next;
216
            }
217
        }
218
219
        // Remove all delimiters
220 2022
        $this->removeAll($stackBottom);
221 2022
    }
222
}
223