AlternativeText::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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