Completed
Pull Request — master (#50)
by Frederik
02:02
created

AlternativeText   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 356
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 94.26%

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 0
dl 0
loc 356
ccs 115
cts 122
cp 0.9426
rs 8.3157
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A isEmpty() 0 4 1
A getRaw() 0 4 1
A __toString() 0 4 1
A normalizeSpace() 0 10 1
B fromHtml() 0 32 4
A updateParagraphsAndBreaksToNewLine() 0 21 3
B wrapSymbols() 0 30 3
B updateLists() 0 58 7
A updateImages() 0 12 2
A updateHorizontalRule() 0 9 2
B updateLinks() 0 42 3
A removeHead() 0 8 2
C wrap() 0 51 12

How to fix   Complexity   

Complex Class

Complex classes like AlternativeText 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 AlternativeText, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace Genkgo\Mail;
5
6
final class AlternativeText
7
{
8
    /**
9
     * @var string
10
     */
11
    private $text;
12
13
    /**
14
     * @param string $text
15
     */
16 19
    public function __construct(string $text)
17
    {
18 19
        $this->text = $text;
19 19
    }
20
21
    /**
22
     * @return bool
23
     */
24 8
    public function isEmpty(): bool
25
    {
26 8
        return $this->text === '';
27
    }
28
29
    /**
30
     * @return string
31
     */
32 12
    public function getRaw(): string
33
    {
34 12
        return $this->text;
35
    }
36
37
    /**
38
     * @return string
39
     */
40
    public function __toString(): string
41 12
    {
42
        return $this->normalizeSpace($this->text);
43 12
    }
44 12
45 12
    /**
46 12
     * @param string $string
47 12
     * @return string
48
     */
49
    private function normalizeSpace(string $string): string
50
    {
51
        return $this->wrap(
52
            \str_replace(
53
                ["  ", "\n ", " \n", " \r\n", "\t"],
54
                [" ", "\n", "\n", "\r\n", "    "],
55
                \trim($string)
56 19
            )
57
        );
58 19
    }
59 17
60
    /**
61
     * @param string $html AlternativeText
62 8
     * @return AlternativeText
63 8
     */
64 8
    public static function fromHtml(string $html): AlternativeText
65
    {
66
        if ($html === '') {
67 8
            return new self($html);
68 8
        }
69 8
70
        $html = \preg_replace('/\h\h+/', ' ', $html);
71
        $html = \preg_replace('/\v/', '', $html);
72
        $text = new self($html);
73 8
74 8
        try {
75 8
            $document = new \DOMDocument();
76 8
            $result = @$document->loadHTML($text->text);
77 8
            if ($result === false) {
78 8
                throw new \DOMException('Incorrect HTML');
79 8
            }
80
81 8
            $text->wrapSymbols($document);
82
            $text->updateHorizontalRule($document);
83
            $text->updateLists($document);
84
            $text->updateImages($document);
85
            $text->updateLinks($document);
86 8
            $text->removeHead($document);
87
            $text->updateParagraphsAndBreaksToNewLine($document);
88
89
            $text->text = $document->textContent;
90
        } catch (\DOMException $e) {
91
            $text->text = \strip_tags($text->text);
92 8
        }
93
94 8
        return $text;
95
    }
96 8
97
    /**
98
     * @param \DOMDocument $document
99
     */
100
    private function updateParagraphsAndBreaksToNewLine(\DOMDocument $document): void
101
    {
102
        $xpath = new \DOMXPath($document);
103 8
        $break = [
104 6
            'br' => "\r\n",
105 1
            'ul' => "\r\n",
106
            'ol' => "\r\n",
107 6
            'dl' => "\r\n",
108
        ];
109
110 6
        /** @var \DOMElement $element */
111
        foreach ($xpath->query('//p|//br|//h1|//h2|//h3|//h4|//h5|//h6|//ul|//ol|//dl|//hr') as $element) {
112 8
            if (isset($break[$element->nodeName])) {
113
                $textNode = $document->createTextNode($break[$element->nodeName]);
114
            } else {
115
                $textNode = $document->createTextNode("\r\n\r\n");
116
            }
117 8
118
            $element->appendChild($textNode);
119 8
        }
120
    }
121 8
122
    /**
123
     * @param \DOMDocument $document
124
     */
125
    private function wrapSymbols(\DOMDocument $document): void
126
    {
127
        $xpath = new \DOMXPath($document);
128
        $wrap = [
129
            'h1' => "*",
130
            'h2' => "**",
131
            'h3' => "***",
132
            'h4' => "****",
133
            'h5' => "*****",
134 8
            'h6' => "******",
135 1
            'strong' => "*",
136 1
            'b' => "*",
137
            'em' => "*",
138
            'i' => "*",
139 1
        ];
140 1
141 1
        /** @var \DOMElement $element */
142 1
        foreach ($xpath->query('//h1|//h2|//h3|//h4|//h5|//h6|//strong|//b|//em|//i') as $element) {
143
            $element->appendChild(
144
                $document->createTextNode($wrap[$element->nodeName])
145
            );
146 8
147
            if ($element->firstChild !== null) {
148
                $element->insertBefore(
149
                    $document->createTextNode($wrap[$element->nodeName]),
150
                    $element->firstChild
151 8
                );
152
            }
153 8
        }
154
    }
155
156 8
    /**
157 1
     * @param \DOMDocument $document
158 1
     */
159 1
    private function updateLists(\DOMDocument $document): void
160 1
    {
161
        $xpath = new \DOMXPath($document);
162
163
        /** @var \DOMElement $element */
164
        foreach ($xpath->query('//ul/li') as $element) {
165
            if ($element->firstChild !== null) {
166
                $element->insertBefore(
167
                    $document->createTextNode("\t- "),
168 1
                    $element->firstChild
169 1
                );
170
            } else {
171
                $element->appendChild(
172
                    $document->createTextNode("\t- ")
173
                );
174 8
            }
175 1
176 1
            $element->appendChild(
177 1
                $document->createTextNode("\r\n")
178
            );
179 1
        }
180 1
181 1
        /** @var \DOMElement $element */
182 1
        foreach ($xpath->query('//ol/li') as $element) {
183
            $itemPath = new \DOMXPath($document);
184
            $itemNumber = (int)$itemPath->evaluate('string(count(preceding-sibling::li))', $element) + 1;
185
            $text = \sprintf("\t%d. ", $itemNumber);
186
187
            if ($element->firstChild !== null) {
188
                $element->insertBefore(
189
                    $document->createTextNode($text),
190 1
                    $element->firstChild
191 1
                );
192
            } else {
193
                $element->appendChild(
194
                    $document->createTextNode($text)
195
                );
196 8
            }
197 1
198 1
            $element->appendChild(
199
                $document->createTextNode("\r\n")
200
            );
201
        }
202
203 8
        /** @var \DOMElement $element */
204 1
        foreach ($xpath->query('//dl/dt') as $element) {
205 1
            $element->appendChild(
206
                $document->createTextNode(': ')
207
            );
208 8
        }
209
210
        /** @var \DOMElement $element */
211
        foreach ($xpath->query('//dl/dd') as $element) {
212
            $element->appendChild(
213 8
                $document->createTextNode("\r\n")
214
            );
215 8
        }
216
    }
217
218 8
    /**
219 1
     * @param \DOMDocument $document
220 1
     */
221 1
    private function updateImages(\DOMDocument $document): void
222 1
    {
223
        $xpath = new \DOMXPath($document);
224 8
225
        /** @var \DOMElement $element */
226
        foreach ($xpath->query('//img[@src and @alt]') as $element) {
227
            $link = $document->createElement('a');
228
            $link->setAttribute('href', $element->getAttribute('src'));
229 8
            $link->textContent = $element->getAttribute('alt');
230
            $element->parentNode->replaceChild($link, $element);
231 8
        }
232
    }
233
234 8
    /**
235 1
     * @param \DOMDocument $document
236
     */
237 8
    private function updateHorizontalRule(\DOMDocument $document): void
238
    {
239
        $xpath = new \DOMXPath($document);
240
241
        /** @var \DOMElement $element */
242 8
        foreach ($xpath->query('//hr') as $element) {
243
            $element->textContent = \str_repeat('=', 78);
244 8
        }
245 8
    }
246
247 8
    /**
248
     * @param \DOMDocument $document
249
     */
250
    private function updateLinks(\DOMDocument $document): void
251
    {
252
        $xpath = new \DOMXPath($document);
253
        $item = 1;
254
        $conversion = [
255
            '0' => "\u{2070}",
256
            '1' => "\u{2071}",
257
            '2' => "\u{00B2}",
258
            '3' => "\u{00B3}",
259
            '4' => "\u{2074}",
260 8
            '5' => "\u{2075}",
261 1
            '6' => "\u{2076}",
262 1
            '7' => "\u{2077}",
263 1
            '8' => "\u{2078}",
264 1
            '9' => "\u{2079}",
265
        ];
266
267 1
        /** @var \DOMElement $element */
268 1
        foreach ($xpath->query('//a[@href and @href != .]') as $element) {
269 1
            $itemString = (string) $item;
270 1
            $itemUnicode = '';
271 1
            for ($i = 0, $j = \strlen($itemString); $i < $j; $i++) {
272 1
                $itemUnicode .= $conversion[$itemString[$i]];
273
            }
274
275
            $document->documentElement->appendChild(
276
                $document->createTextNode(
277 1
                    \sprintf(
278 1
                        "[%s] %s\r\n",
279
                        $itemUnicode,
280
                        $element->getAttribute('href')
281 1
                    )
282
                )
283 8
            );
284
285
            $element->appendChild(
286
                $document->createTextNode($itemUnicode)
287
            );
288 8
289
            $item++;
290 8
        }
291 8
    }
292 2
293 2
    /**
294
     * @param \DOMDocument $document
295 8
     */
296
    private function removeHead(\DOMDocument $document): void
297
    {
298
        $heads = $document->getElementsByTagName('head');
299
        while ($heads->length > 0) {
300
            $head = $heads->item(0);
301
            $head->parentNode->removeChild($head);
302
        }
303
    }
304
305
    /**
306
     * @param string $unwrappedText
307
     * @param int $width
308
     * @return string
309
     */
310
    private function wrap(string $unwrappedText, int $width = 75): string
311
    {
312
        $result = [];
313
        $carriageReturn = false;
314
        $lineChars = -1;
315
        $quote = false;
316
        $quoteLength = 0;
317
318
        $iterator = \IntlBreakIterator::createCharacterInstance(\Locale::getDefault());
319
        $iterator->setText($unwrappedText);
320
        foreach ($iterator->getPartsIterator() as $char) {
321
            if ($char === "\r\n") {
322
                $lineChars = -1;
323
                $quoteLength = 0;
324
                $quote = false;
325
            } elseif ($char === "\r") {
326
                $carriageReturn = true;
327
            } elseif ($char === "\n") {
328
                if (!$carriageReturn) {
329
                    $char = "\r\n";
330
                }
331
332
                $lineChars = -1;
333
                $quoteLength = 0;
334
                $quote = false;
335
            }
336
337
            if ($lineChars >= $width && \IntlChar::isWhitespace($char)) {
338
                $char = "\r\n" . \str_pad('', $quoteLength - 1, '>');
339
                $lineChars = -1;
340
                $quoteLength = 0;
341
                $quote = false;
342
            }
343
344
            $result[] = $char;
345
            $lineChars++;
346
347
            if ($lineChars === 1 && $char === ">") {
348
                $quote = true;
349
                $quoteLength = 1;
350
            }
351
352
            if ($quote && $char === ">") {
353
                $quoteLength++;
354
            } else {
355
                $quote = false;
356
            }
357
        }
358
359
        return \implode('', $result);
360
    }
361
}
362