Element   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 227
Duplicated Lines 0 %

Test Coverage

Coverage 96.67%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 88
c 3
b 0
f 0
dl 0
loc 227
ccs 87
cts 90
cp 0.9667
rs 3.44
wmc 62

21 Methods

Rating   Name   Duplication   Size   Complexity  
A hasChildren() 0 3 1
A getTagName() 0 3 1
A getNextSibling() 0 3 2
A setFinalMarkdown() 0 12 3
A isWhitespace() 0 3 2
A getSiblingPosition() 0 23 5
A getChildren() 0 8 2
A getNext() 0 10 3
A getAttribute() 0 7 2
A getListItemLevel() 0 14 4
A getNextNode() 0 15 5
A getChildrenAsString() 0 3 1
A equals() 0 7 2
A hasParent() 0 3 1
C isBlock() 0 21 16
A getValue() 0 3 1
A isDescendantOf() 0 17 5
A __construct() 0 5 1
A getPreviousSibling() 0 3 2
A isText() 0 3 1
A getParent() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like Element often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Element, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace League\HTMLToMarkdown;
6
7
class Element implements ElementInterface
8
{
9
    /** @var \DOMNode */
10
    protected $node;
11
12
    /** @var ElementInterface|null */
13
    private $nextCached;
14
15
    /** @var \DOMNode|null */
16
    private $previousSiblingCached;
17
18
    public function __construct(\DOMNode $node)
19
    {
20
        $this->node = $node;
21
22 96
        $this->previousSiblingCached = $this->node->previousSibling;
23
    }
24 96
25 96
    public function isBlock(): bool
26 96
    {
27
        switch ($this->getTagName()) {
28
            case 'blockquote':
29
            case 'body':
30
            case 'div':
31 18
            case 'h1':
32
            case 'h2':
33 18
            case 'h3':
34 18
            case 'h4':
35 18
            case 'h5':
36 18
            case 'h6':
37 18
            case 'hr':
38 18
            case 'html':
39 18
            case 'li':
40 18
            case 'p':
41 18
            case 'ol':
42 18
            case 'ul':
43 18
                return true;
44 18
            default:
45 18
                return false;
46 17
        }
47 17
    }
48 17
49 3
    public function isText(): bool
50 10
    {
51 15
        return $this->getTagName() === '#text';
52 10
    }
53
54
    public function isWhitespace(): bool
55
    {
56
        return $this->getTagName() === '#text' && \trim($this->getValue()) === '';
57
    }
58 18
59
    public function getTagName(): string
60 18
    {
61
        return $this->node->nodeName;
62
    }
63
64
    public function getValue(): string
65
    {
66 9
        return $this->node->nodeValue;
67
    }
68 9
69
    public function hasParent(): bool
70
    {
71
        return $this->node->parentNode !== null;
72
    }
73
74 96
    public function getParent(): ?ElementInterface
75
    {
76 96
        return $this->node->parentNode ? new self($this->node->parentNode) : null;
77
    }
78
79
    public function getNextSibling(): ?ElementInterface
80
    {
81
        return $this->node->nextSibling !== null ? new self($this->node->nextSibling) : null;
82 84
    }
83
84 84
    public function getPreviousSibling(): ?ElementInterface
85
    {
86
        return $this->previousSiblingCached !== null ? new self($this->previousSiblingCached) : null;
87
    }
88
89
    public function hasChildren(): bool
90 81
    {
91
        return $this->node->hasChildNodes();
92 81
    }
93
94
    /**
95
     * @return ElementInterface[]
96
     */
97
    public function getChildren(): array
98 18
    {
99
        $ret = [];
100 18
        foreach ($this->node->childNodes as $node) {
101
            $ret[] = new self($node);
102
        }
103
104
        return $ret;
105
    }
106 18
107
    public function getNext(): ?ElementInterface
108 18
    {
109
        if ($this->nextCached === null) {
110
            $nextNode = $this->getNextNode($this->node);
111
            if ($nextNode !== null) {
112
                $this->nextCached = new self($nextNode);
113
            }
114 96
        }
115
116 96
        return $this->nextCached;
117
    }
118
119
    private function getNextNode(\DomNode $node, bool $checkChildren = true): ?\DomNode
120
    {
121
        if ($checkChildren && $node->firstChild) {
122 96
            return $node->firstChild;
123
        }
124 96
125
        if ($node->nextSibling) {
126 96
            return $node->nextSibling;
127 96
        }
128 64
129
        if ($node->parentNode) {
130 96
            return $this->getNextNode($node->parentNode, false);
131
        }
132
133
        return null;
134
    }
135
136 24
    /**
137
     * @param string[]|string $tagNames
138 24
     */
139 24
    public function isDescendantOf($tagNames): bool
140 24
    {
141 24
        if (! \is_array($tagNames)) {
142 16
            $tagNames = [$tagNames];
143 16
        }
144
145 24
        for ($p = $this->node->parentNode; $p !== false; $p = $p->parentNode) {
146
            if ($p === null) {
147
                return false;
148
            }
149
150
            if (\in_array($p->nodeName, $tagNames, true)) {
151
                return true;
152
            }
153
        }
154 24
155
        return false;
156 24
    }
157
158
    public function setFinalMarkdown(string $markdown): void
159
    {
160 24
        if ($this->node->ownerDocument === null) {
161 24
            throw new \RuntimeException('Unowned node');
162
        }
163
164 6
        if ($this->node->parentNode === null) {
165 6
            throw new \RuntimeException('Cannot setFinalMarkdown() on a node without a parent');
166
        }
167
168
        $markdownNode = $this->node->ownerDocument->createTextNode($markdown);
169
        $this->node->parentNode->replaceChild($markdownNode, $this->node);
170
    }
171
172
    public function getChildrenAsString(): string
173
    {
174 96
        return $this->node->C14N();
175
    }
176 96
177 6
    public function getSiblingPosition(): int
178 4
    {
179
        $position = 0;
180 96
181 96
        $parent = $this->getParent();
182 96
        if ($parent === null) {
183
            return $position;
184
        }
185 96
186 24
        // Loop through all nodes and find the given $node
187
        foreach ($parent->getChildren() as $currentNode) {
188 64
            if (! $currentNode->isWhitespace()) {
189
                $position++;
190
            }
191
192
            // TODO: Need a less-buggy way of comparing these
193
            // Perhaps we can somehow ensure that we always have the exact same object and use === instead?
194
            if ($this->equals($currentNode)) {
195
                break;
196 96
            }
197
        }
198 96
199 96
        return $position;
200 96
    }
201
202
    public function getListItemLevel(): int
203
    {
204
        $level  = 0;
205 87
        $parent = $this->getParent();
206
207 87
        while ($parent !== null && $parent->hasParent()) {
208
            if ($parent->getTagName() === 'li') {
209
                $level++;
210
            }
211
212
            $parent = $parent->getParent();
213 9
        }
214
215 9
        return $level;
216
    }
217
218 9
    public function getAttribute(string $name): string
219 9
    {
220 9
        if ($this->node instanceof \DOMElement) {
221 6
            return $this->node->getAttribute($name);
222
        }
223
224
        return '';
225 9
    }
226 9
227
    public function equals(ElementInterface $element): bool
228 6
    {
229
        if ($element instanceof self) {
230 9
            return $element->node === $this->node;
231
        }
232
233
        return false;
234
    }
235
}
236