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

AlternativeText::updateLinks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 12
cts 12
cp 1
rs 9.3142
c 0
b 0
f 0
cc 2
eloc 11
nc 2
nop 1
crap 2
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
254
        /** @var \DOMElement $element */
255 18
        foreach ($xpath->query('//a[@href and @href != .]') as $element) {
256 1
            $element->insertBefore(
257 1
                $document->createTextNode('>> '),
258 1
                $element->firstChild
259
            );
260
261 1
            $element->appendChild(
262 1
                $document->createTextNode(
263 1
                    \sprintf(
264 1
                        " <%s>",
265 1
                        $element->getAttribute('href')
266
                    )
267
                )
268
            );
269
        }
270 18
    }
271
272
    /**
273
     * @param \DOMDocument $document
274
     */
275 18
    private function removeHead(\DOMDocument $document): void
276
    {
277 18
        $heads = $document->getElementsByTagName('head');
278 18
        while ($heads->length > 0) {
279 4
            $head = $heads->item(0);
280 4
            $head->parentNode->removeChild($head);
281
        }
282 18
    }
283
284
    /**
285
     * @param string $unwrappedText
286
     * @param int $width
287
     * @return string
288
     */
289 24
    private function wrap(string $unwrappedText, int $width = 75): string
290
    {
291 24
        $result = [];
292 24
        $carriageReturn = false;
293 24
        $lineChars = -1;
294 24
        $quote = false;
295 24
        $quoteLength = 0;
296
297 24
        $iterator = \IntlBreakIterator::createCharacterInstance(\Locale::getDefault());
298 24
        $iterator->setText($unwrappedText);
299 24
        foreach ($iterator->getPartsIterator() as $char) {
300 22
            if ($char === "\r\n") {
301 3
                $lineChars = -1;
302 3
                $quoteLength = 0;
303 3
                $quote = false;
304 22
            } elseif ($char === "\r") {
305
                $carriageReturn = true;
306 22
            } elseif ($char === "\n") {
307 11
                if (!$carriageReturn) {
308 11
                    $char = "\r\n";
309
                }
310
311 11
                $lineChars = -1;
312 11
                $quoteLength = 0;
313 11
                $quote = false;
314
            }
315
316 22
            if ($char !== "\r") {
317 22
                $carriageReturn = false;
318
            }
319
320 22
            if ($lineChars >= $width && \IntlChar::isWhitespace($char)) {
321 2
                $char = "\r\n" . \str_pad('', $quoteLength, '>');
322 2
                $lineChars = -1;
323 2
                $quoteLength = 0;
324 2
                $quote = false;
325
            }
326
327 22
            $result[] = $char;
328 22
            $lineChars++;
329
330 22
            if ($lineChars === 1 && $char === ">") {
331 12
                $quote = true;
332 12
                $quoteLength = 1;
333 22
            } elseif ($quote && $char === ">") {
334 3
                $quoteLength++;
335
            } else {
336 22
                $quote = false;
337
            }
338
        }
339
340 24
        return \implode('', $result);
341
    }
342
}
343