Completed
Push — master ( 990cee...3d5add )
by Colin
13s queued 11s
created

DelimiterStack   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 98.95%

Importance

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

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
D processDelimiters() 0 88 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 996
    public function push(Delimiter $newDelimiter)
32
    {
33 996
        $newDelimiter->setPrevious($this->top);
34
35 996
        if ($this->top !== null) {
36 597
            $this->top->setNext($newDelimiter);
37
        }
38
39 996
        $this->top = $newDelimiter;
40 996
    }
41
42
    /**
43
     * @param Delimiter|null $stackBottom
44
     *
45
     * @return Delimiter|null
46
     */
47 2025
    private function findEarliest(Delimiter $stackBottom = null): ?Delimiter
48
    {
49 2025
        $delimiter = $this->top;
50 2025
        while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
51 555
            $delimiter = $delimiter->getPrevious();
52
        }
53
54 2025
        return $delimiter;
55
    }
56
57
    /**
58
     * @param Delimiter $delimiter
59
     */
60 996
    public function removeDelimiter(Delimiter $delimiter)
61
    {
62 996
        if ($delimiter->getPrevious() !== null) {
63 366
            $delimiter->getPrevious()->setNext($delimiter->getNext());
64
        }
65
66 996
        if ($delimiter->getNext() === null) {
67
            // top of stack
68 996
            $this->top = $delimiter->getPrevious();
69
        } else {
70 408
            $delimiter->getNext()->setPrevious($delimiter->getPrevious());
71
        }
72 996
    }
73
74 414
    private function removeDelimiterAndNode(Delimiter $delimiter)
75
    {
76 414
        $delimiter->getInlineNode()->detach();
77 414
        $this->removeDelimiter($delimiter);
78 414
    }
79
80 414
    private function removeDelimitersBetween(Delimiter $opener, Delimiter $closer)
81
    {
82 414
        $delimiter = $closer->getPrevious();
83 414
        while ($delimiter !== null && $delimiter !== $opener) {
84 27
            $previous = $delimiter->getPrevious();
85 27
            $this->removeDelimiter($delimiter);
86 27
            $delimiter = $previous;
87
        }
88 414
    }
89
90
    /**
91
     * @param Delimiter|null $stackBottom
92
     */
93 2025
    public function removeAll(Delimiter $stackBottom = null)
94
    {
95 2025
        while ($this->top && $this->top !== $stackBottom) {
96 573
            $this->removeDelimiter($this->top);
97
        }
98 2025
    }
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 2025
    public function processDelimiters(?Delimiter $stackBottom, DelimiterProcessorCollection $processors)
138
    {
139 2025
        $openersBottom = [];
140
141
        // Find first closer above stackBottom
142 2025
        $closer = $this->findEarliest($stackBottom);
143
144
        // Move forward, looking for closers, and handling each
145 2025
        while ($closer !== null) {
146 924
            $delimiterChar = $closer->getChar();
147
148 924
            $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
149 924
            if (!$closer->canClose() || $delimiterProcessor === null) {
150 876
                $closer = $closer->getNext();
151 876
                continue;
152
            }
153
154 495
            $openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
155
156 495
            $useDelims = 0;
157 495
            $openerFound = false;
158 495
            $potentialOpenerFound = false;
159 495
            $opener = $closer->getPrevious();
160 495
            while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
161 468
                if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
162 417
                    $potentialOpenerFound = true;
163 417
                    $useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
164 417
                    if ($useDelims > 0) {
165 414
                        $openerFound = true;
166 414
                        break;
167
                    }
168
                }
169 90
                $opener = $opener->getPrevious();
170
            }
171
172 495
            if (!$openerFound) {
173 153
                if (!$potentialOpenerFound) {
174
                    // Only do this when we didn't even have a potential
175
                    // opener (one that matches the character and can open).
176
                    // If an opener was rejected because of the number of
177
                    // delimiters (e.g. because of the "multiple of 3"
178
                    // Set lower bound for future searches for openersrule),
179
                    // we want to consider it next time because the number
180
                    // of delimiters can change as we continue processing.
181 138
                    $openersBottom[$delimiterChar] = $closer->getPrevious();
182 138
                    if (!$closer->canOpen()) {
183
                        // We can remove a closer that can't be an opener,
184
                        // once we've seen there's no matching opener.
185 96
                        $this->removeDelimiter($closer);
186
                    }
187
                }
188 153
                $closer = $closer->getNext();
189 153
                continue;
190
            }
191
192
            /** @var Text $openerNode */
193 414
            $openerNode = $opener->getInlineNode();
194
            /** @var Text $closerNode */
195 414
            $closerNode = $closer->getInlineNode();
196
197
            // Remove number of used delimiters from stack and inline nodes.
198 414
            $opener->setNumDelims($opener->getNumDelims() - $useDelims);
199 414
            $closer->setNumDelims($closer->getNumDelims() - $useDelims);
200
201 414
            $openerNode->setContent(\substr($openerNode->getContent(), 0, -$useDelims));
202 414
            $closerNode->setContent(\substr($closerNode->getContent(), 0, -$useDelims));
203
204 414
            $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...
205
            // The delimiter processor can re-parent the nodes between opener and closer,
206
            // so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
207 414
            AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
208 414
            $delimiterProcessor->process($openerNode, $closerNode, $useDelims);
209
210
            // No delimiter characters left to process, so we can remove delimiter and the now empty node.
211 414
            if ($opener->getNumDelims() === 0) {
212 390
                $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...
213
            }
214
215 414
            if ($closer->getNumDelims() === 0) {
216 390
                $next = $closer->getNext();
217 390
                $this->removeDelimiterAndNode($closer);
218 390
                $closer = $next;
219
            }
220
        }
221
222
        // Remove all delimiters
223 2025
        $this->removeAll($stackBottom);
224 2025
    }
225
}
226