Completed
Pull Request — master (#64)
by Frederik
03:38 queued 01:09
created

AlternativeText::wrapSymbols()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

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