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

AlternativeText::updateLinks()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 42
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 3

Importance

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