Element   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 229
Duplicated Lines 0 %

Test Coverage

Coverage 96.67%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 89
dl 0
loc 229
ccs 87
cts 90
cp 0.9667
rs 3.44
c 3
b 0
f 0
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 isWhitespace() 0 3 2
A hasParent() 0 3 1
C isBlock() 0 21 16
A getValue() 0 3 1
A __construct() 0 5 1
A getPreviousSibling() 0 3 2
A isText() 0 3 1
A getParent() 0 3 2
A setFinalMarkdown() 0 12 3
A getSiblingPosition() 0 23 5
A getChildren() 0 10 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 isDescendantOf() 0 17 5

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
            /** @psalm-suppress RedundantCondition */
102
            \assert($node instanceof \DOMNode);
103
            $ret[] = new self($node);
104
        }
105
106 18
        return $ret;
107
    }
108 18
109
    public function getNext(): ?ElementInterface
110
    {
111
        if ($this->nextCached === null) {
112
            $nextNode = $this->getNextNode($this->node);
113
            if ($nextNode !== null) {
114 96
                $this->nextCached = new self($nextNode);
115
            }
116 96
        }
117
118
        return $this->nextCached;
119
    }
120
121
    private function getNextNode(\DOMNode $node, bool $checkChildren = true): ?\DOMNode
122 96
    {
123
        if ($checkChildren && $node->firstChild) {
124 96
            return $node->firstChild;
125
        }
126 96
127 96
        if ($node->nextSibling) {
128 64
            return $node->nextSibling;
129
        }
130 96
131
        if ($node->parentNode) {
132
            return $this->getNextNode($node->parentNode, false);
133
        }
134
135
        return null;
136 24
    }
137
138 24
    /**
139 24
     * @param string[]|string $tagNames
140 24
     */
141 24
    public function isDescendantOf($tagNames): bool
142 16
    {
143 16
        if (! \is_array($tagNames)) {
144
            $tagNames = [$tagNames];
145 24
        }
146
147
        for ($p = $this->node->parentNode; $p !== false; $p = $p->parentNode) {
148
            if ($p === null) {
149
                return false;
150
            }
151
152
            if (\in_array($p->nodeName, $tagNames, true)) {
153
                return true;
154 24
            }
155
        }
156 24
157
        return false;
158
    }
159
160 24
    public function setFinalMarkdown(string $markdown): void
161 24
    {
162
        if ($this->node->ownerDocument === null) {
163
            throw new \RuntimeException('Unowned node');
164 6
        }
165 6
166
        if ($this->node->parentNode === null) {
167
            throw new \RuntimeException('Cannot setFinalMarkdown() on a node without a parent');
168
        }
169
170
        $markdownNode = $this->node->ownerDocument->createTextNode($markdown);
171
        $this->node->parentNode->replaceChild($markdownNode, $this->node);
172
    }
173
174 96
    public function getChildrenAsString(): string
175
    {
176 96
        return $this->node->C14N();
177 6
    }
178 4
179
    public function getSiblingPosition(): int
180 96
    {
181 96
        $position = 0;
182 96
183
        $parent = $this->getParent();
184
        if ($parent === null) {
185 96
            return $position;
186 24
        }
187
188 64
        // Loop through all nodes and find the given $node
189
        foreach ($parent->getChildren() as $currentNode) {
190
            if (! $currentNode->isWhitespace()) {
191
                $position++;
192
            }
193
194
            // TODO: Need a less-buggy way of comparing these
195
            // Perhaps we can somehow ensure that we always have the exact same object and use === instead?
196 96
            if ($this->equals($currentNode)) {
197
                break;
198 96
            }
199 96
        }
200 96
201
        return $position;
202
    }
203
204
    public function getListItemLevel(): int
205 87
    {
206
        $level  = 0;
207 87
        $parent = $this->getParent();
208
209
        while ($parent !== null && $parent->hasParent()) {
210
            if ($parent->getTagName() === 'li') {
211
                $level++;
212
            }
213 9
214
            $parent = $parent->getParent();
215 9
        }
216
217
        return $level;
218 9
    }
219 9
220 9
    public function getAttribute(string $name): string
221 6
    {
222
        if ($this->node instanceof \DOMElement) {
223
            return $this->node->getAttribute($name);
224
        }
225 9
226 9
        return '';
227
    }
228 6
229
    public function equals(ElementInterface $element): bool
230 9
    {
231
        if ($element instanceof self) {
232
            return $element->node === $this->node;
233
        }
234
235
        return false;
236 12
    }
237
}
238