Processor   A
last analyzed

Complexity

Total Complexity 41

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Test Coverage

Coverage 91.15%

Importance

Changes 4
Bugs 2 Features 0
Metric Value
eloc 104
c 4
b 2
f 0
dl 0
loc 240
ccs 103
cts 113
cp 0.9115
rs 9.1199
wmc 41

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A hasAutoTOC() 0 3 3
A escaped() 0 8 1
B ensureHeadingHasId() 0 31 7
A getUniqueId() 0 17 4
B processDocument() 0 34 11
A generate() 0 31 6
A cloneChildren() 0 18 2
A setNull() 0 5 1
A render() 0 33 5

How to fix   Complexity   

Complex Class

Complex classes like Processor 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 Processor, and based on these observations, apply Extract Interface, too.

1
<?php namespace Todaymade\Daux\Format\HTML\ContentTypes\Markdown\TOC;
2
3
use DeepCopy\DeepCopy;
4
use League\CommonMark\Block\Element\Document;
5
use League\CommonMark\Block\Element\Heading;
6
use League\CommonMark\Block\Element\ListBlock;
7
use League\CommonMark\Block\Element\ListData;
8
use League\CommonMark\Block\Element\ListItem;
9
use League\CommonMark\Block\Element\Paragraph;
10
use League\CommonMark\DocumentProcessorInterface;
11
use League\CommonMark\Inline\Element\Link;
12
use League\CommonMark\Inline\Element\Text;
13
use League\CommonMark\Node\Node;
14
use ReflectionMethod;
15
use Todaymade\Daux\Config;
16
use Todaymade\Daux\ContentTypes\Markdown\TableOfContents;
17
18
class Processor implements DocumentProcessorInterface
19
{
20
    protected $config;
21
22 5
    public function __construct(Config $config)
23
    {
24 5
        $this->config = $config;
25 5
    }
26
27 1
    public function hasAutoTOC()
28
    {
29 1
        return array_key_exists('html', $this->config) && array_key_exists('auto_toc', $this->config['html']) && $this->config['html']['auto_toc'];
0 ignored issues
show
Bug introduced by Stéphane Goetz
$this->config of type Todaymade\Daux\Config is incompatible with the type array expected by parameter $search of array_key_exists(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

29
        return array_key_exists('html', /** @scrutinizer ignore-type */ $this->config) && array_key_exists('auto_toc', $this->config['html']) && $this->config['html']['auto_toc'];
Loading history...
30
    }
31
32
    /**
33
     * @param Document $document
34
     *
35
     * @return void
36
     */
37 5
    public function processDocument(Document $document)
38
    {
39
        /** @var TableOfContents[] $tocs */
40 5
        $tocs = [];
41
42 5
        $headings = [];
43
44 5
        $document->heading_ids = [];
0 ignored issues
show
Bug introduced by Stéphane Goetz
The property heading_ids does not seem to exist on League\CommonMark\Block\Element\Document.
Loading history...
45 5
        $walker = $document->walker();
46 5
        while ($event = $walker->next()) {
47 5
            $node = $event->getNode();
48
49 5
            if ($node instanceof TableOfContents && !$event->isEntering()) {
50 4
                $tocs[] = $node;
51 4
                continue;
52
            }
53
54 5
            if (!($node instanceof Heading) || !$event->isEntering()) {
55 5
                continue;
56
            }
57
58 5
            $this->ensureHeadingHasId($document, $node);
59 5
            $headings[] = new Entry($node);
60
        }
61
62 5
        if (count($headings) && (count($tocs) || $this->hasAutoTOC())) {
63 4
            $generated = $this->generate($headings);
64
65 4
            if (count($tocs)) {
66 4
                foreach ($tocs as $toc) {
67 4
                    $toc->appendChild($this->render($generated->getChildren()));
68
                }
69
            } else {
70
                $document->prependChild($this->render($generated->getChildren()));
71
            }
72
        }
73 5
    }
74
75
    /**
76
     * Get an escaped version of the link
77
     * @param string $url
78
     * @return string
79
     */
80 5
    protected function escaped($url) {
81 5
        $url = trim($url);
82 5
        $url = preg_replace('~[^\\pL0-9_]+~u', '-', $url);
83 5
        $url = trim($url, "-");
84 5
        $url = iconv("utf-8", "ASCII//TRANSLIT//IGNORE", $url);
85 5
        $url = preg_replace('~[^-a-zA-Z0-9_]+~', '', $url);
86
87 5
        return $url;
88
    }
89
90 5
    protected function getUniqueId(Document $document, $proposed) {
91 5
        if ($proposed == "page_") {
92 1
            $proposed = "page_section_" . (count($document->heading_ids) + 1);
0 ignored issues
show
Bug introduced by Stéphane Goetz
The property heading_ids does not seem to exist on League\CommonMark\Block\Element\Document.
Loading history...
93
        }
94
95
        // Quick path, it's a unique ID
96 5
        if (!in_array($proposed, $document->heading_ids)) {
97 5
            $document->heading_ids[] = $proposed;
98 5
            return $proposed;
99
        }
100
101 1
        $extension = 1; // Initialize the variable at one, so on the first iteration we have 2
102
        do {
103 1
            $extension++;
104 1
        } while (in_array("$proposed-$extension", $document->heading_ids));
105
106 1
        return "$proposed-$extension";
107
    }
108
109
    /**
110
     * @param Heading $node
111
     */
112 5
    protected function ensureHeadingHasId(Document $document, Heading $node)
113
    {
114
        // If the node has an ID, no need to generate it, just check it's unique
115 5
        $attributes = $node->getData('attributes', []);
116 5
        if (array_key_exists('id', $attributes) && !empty($attributes['id'])) {
117
            $node->data['attributes']['id'] = $this->getUniqueId($document, $attributes['id']);
118
119
            return;
120
        }
121
122
        // Well, seems we have to generate an ID
123 5
        $walker = $node->walker();
124 5
        $inside = [];
125 5
        while ($event = $walker->next()) {
126 5
            $insideNode = $event->getNode();
127
128 5
            if ($insideNode instanceof Heading) {
129 5
                continue;
130
            }
131
132 5
            $inside[] = $insideNode;
133
        }
134
135 5
        $text = '';
136 5
        foreach ($inside as $other) {
137 5
            if ($other instanceof Text) {
138 5
                $text .= ' ' . $other->getContent();
139
            }
140
        }
141
142 5
        $node->data['attributes']['id'] = $this->getUniqueId($document,'page_'. $this->escaped($text));
143 5
    }
144
145
    /**
146
     * Make a tree of the list of headings
147
     *
148
     * @param Entry[] $headings
149
     * @return RootEntry
150
     */
151 4
    public function generate($headings)
152
    {
153
        /** @var Entry $previous */
154 4
        $root = $previous = new RootEntry();
155 4
        foreach ($headings as $heading) {
156 4
            if ($heading->getLevel() < $previous->getLevel()) {
157
                $parent = $previous;
158
                do {
159
                    $parent = $parent->getParent();
160
                } while ($heading->getLevel() <= $parent->getLevel() && $parent->getLevel() != 0);
161
162
                $parent->addChild($heading);
163
                $previous = $heading;
164
                continue;
165
            }
166
167
168 4
            if ($heading->getLevel() > $previous->getLevel()) {
169 4
                $previous->addChild($heading);
170 4
                $previous = $heading;
171 4
                continue;
172
            }
173
174
            //if ($heading->getLevel() == $previous->getLevel()) {
175 2
            $previous->getParent()->addChild($heading);
176 2
            $previous = $heading;
177 2
            continue;
178
            //}
179
        }
180
181 4
        return $root;
182
    }
183
184
    /**
185
     * @param Entry[] $entries
186
     * @return ListBlock
187
     */
188 4
    protected function render(array $entries)
189
    {
190 4
        $data = new ListData();
191 4
        $data->type = ListBlock::TYPE_UNORDERED;
192
193 4
        $list = new ListBlock($data);
194 4
        $list->data['attributes']['class'] = 'TableOfContents';
195
196 4
        foreach ($entries as $entry) {
197 4
            $item = new ListItem($data);
198
199 4
            $a = new Link('#' . $entry->getId());
200
201 4
            $content = $entry->getContent();
202 4
            if ($content != null) {
203 4
                foreach ($this->cloneChildren($content) as $node) {
204 4
                    $a->appendChild($node);
205
                }
206
            }
207
208 4
            $p = new Paragraph();
209 4
            $p->appendChild($a);
210
211 4
            $item->appendChild($p);
212
213 4
            if (!empty($entry->getChildren())) {
214
                $item->appendChild($this->render($entry->getChildren()));
215
            }
216
217 4
            $list->appendChild($item);
218
        }
219
220 4
        return $list;
221
    }
222
223
    /**
224
     * Set the specified property to null on the object.
225
     *
226
     * @param Heading $object The object to modify
227
     * @param string $property The property to nullify
228
     */
229 4
    protected function setNull(Heading $object, $property)
230
    {
231 4
        $prop = new \ReflectionProperty(get_class($object), $property);
232 4
        $prop->setAccessible(true);
233 4
        $prop->setValue($object, null);
234 4
    }
235
236
    /**
237
     * @param Heading $node
238
     * @return Node[]
239
     */
240 4
    protected function cloneChildren(Heading $node)
241
    {
242 4
        $firstClone = clone $node;
243
244
        // We have no choice but to hack into the
245
        // system to reset the parent, previous and next
246 4
        $this->setNull($firstClone, 'parent');
247 4
        $this->setNull($firstClone, 'previous');
248 4
        $this->setNull($firstClone, 'next');
249
250
        // Also, the child elements need to know the next parents
251 4
        foreach ($firstClone->children() as $subnode) {
252 4
            $method = new ReflectionMethod(get_class($subnode), 'setParent');
253 4
            $method->setAccessible(true);
254 4
            $method->invoke($subnode, $firstClone);
255
        }
256
257 4
        return (new DeepCopy())->copy($firstClone)->children();
258
    }
259
}
260