Passed
Pull Request — master (#12)
by Takashi
02:44
created

HtmlHandler::walkDomNodesAndReplaceOnlyTextNodes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 19
ccs 6
cts 6
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Ttskch\Esa;
4
5
use Symfony\Component\DomCrawler\Crawler;
6
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
7
8
class HtmlHandler
9
{
10
    /**
11
     * @var Crawler
12
     */
13
    private $crawler;
14
15
    /**
16
     * @var UrlGeneratorInterface
17
     */
18
    private $urlGenerator;
19
20
    /**
21
     * @var EmojiManager
22
     */
23
    private $emojiManager;
24
25
    /**
26
     * @var string
27
     */
28
    private $teamName;
29
30
    /**
31
     * @param array $replacements
32
     */
33 57
    public function __construct(Crawler $crawler, UrlGeneratorInterface $urlGenerator, EmojiManager $emojiManager, $teamName)
34
    {
35 57
        $this->crawler = $crawler;
36 57
        $this->urlGenerator = $urlGenerator;
37 57
        $this->emojiManager = $emojiManager;
38 57
        $this->teamName = $teamName;
39 57
    }
40
41
    /**
42
     * @param string $html
43
     * @return $this
44
     */
45 17
    public function initialize($html)
46
    {
47 17
        $this->crawler->clear();
48 17
        $this->crawler->addHtmlContent($html);
49
50 17
        return $this;
51
    }
52
53
    /**
54
     * @return string
55
     */
56 3
    public function dumpHtml()
57
    {
58 3
        $this->ensureInitialized();
59
60 2
        return $this->crawler->html();
61
    }
62
63
    /**
64
     * @param array $replacements map of [regexp pattern => replacement].
65
     */
66 17
    public function replaceHtml(array $replacements)
67
    {
68 17
        $this->ensureInitialized();
69
70 16
        $html = $this->crawler->html();
71
72 16
        foreach ($replacements as $pattern => $replacement) {
73 16
            $html = preg_replace($pattern, $replacement, $html);
74
        }
75
76 16
        $this->initialize($html);
77 16
    }
78
79
    /**
80
     * @param array $replacements map of [regexp pattern => replacement].
81
     */
82 13
    public function replaceText(array $replacements)
83
    {
84 13
        $this->ensureInitialized();
85
86 13
        $domNode = $this->crawler->getNode(0);
87
88 13
        $this->walkDomNodesAndReplaceOnlyTextNodes($domNode, $replacements);
89
90 13
        $this->crawler->clear();
91 13
        $this->crawler->addNode($domNode);
92 13
    }
93
94
    /**
95
     * @param \DOMNode $node
96
     * @param array $replacements map of [regexp pattern => replacement].
97
     */
98 14
    public function walkDomNodesAndReplaceOnlyTextNodes(\DOMNode $node, array $replacements)
99
    {
100 14
        if ($node->nodeType === XML_TEXT_NODE) {
101 14
            foreach ($replacements as $pattern => $replacement) {
102 13
                $node->textContent = preg_replace($pattern, $replacement, $node->textContent);
103
            }
104
105 14
            return;
106
        }
107
108
//        if (!$node->hasChildNodes()) {
109
//            return;
110
//        }
111
//
112
//        foreach ($node->childNodes as $childNode) {
113
//            $this->walkDomNodesAndReplaceOnlyTextNodes($childNode, $replacements);
114
//        }
115
116 14
        $this->replaceTextChildren($node, $replacements);
117 14
    }
118
119 14
    public function replaceTextChildren(\DOMNode $node, array $replacements)
120
    {
121 14
        if (!$node->hasChildNodes()) {
122 14
            return;
123
        }
124
125 14
        foreach ($node->childNodes as $childNode) {
126 14
            $this->walkDomNodesAndReplaceOnlyTextNodes($childNode, $replacements);
127
        }
128 14
    }
129
130
    /**
131
     * Replace links to other post with links to see the post on esaba.
132
     *
133
     * @param string $routeName
134
     * @param string $routeVariableName
135
     */
136 2
    public function replacePostUrls($routeName, $routeVariableName)
137
    {
138 2
        $backReferenceNumberForPostId = null;
139 2
        $backReferenceNumberForAnchorHash = null;
140 2
        $pattern = $this->getPostUrlPattern($backReferenceNumberForPostId, $backReferenceNumberForAnchorHash);
141 2
        $walker = $this->getATagWalkerForPostUrls($pattern, $backReferenceNumberForPostId, $backReferenceNumberForAnchorHash, $routeName, $routeVariableName);
142
143 2
        $this->replaceATagWithWalker($pattern, $walker);
144 2
    }
145
146
    /**
147
     * Disable @mention links.
148
     */
149 2
    public function disableMentionLinks()
150
    {
151 2
        $pattern = $this->getMentionLinkPattern();
152 2
        $walker = $this->getATagWalkerForMentionLinks($pattern);
153
154 2
        $this->replaceATagWithWalker($pattern, $walker);
155 2
    }
156
157
    /**
158
     * Replace <a> tag href values for specified regexp pattern with closure returns map of ['pattern' => regexp pattern, 'replacement' => replacement].
159
     *
160
     * @param string $pattern
161
     * @param \Closure $walker
162
     */
163 4
    public function replaceATagWithWalker($pattern, \Closure $walker)
164
    {
165 4
        $this->ensureInitialized();
166
167 3
        $targetATags = $this->crawler->filter('a')->reduce($this->getATagReducer($pattern));
168 3
        $replacements = $targetATags->each($walker);
169 3
        $replacements = array_combine(array_column($replacements, 'pattern'), array_column($replacements, 'replacement'));
170
171 3
        $this->replaceHtml($replacements);
172 3
    }
173
174
    /**
175
     * @param string $backReferenceNumberForPostId For returning position of post id in regexp pattern.
176
     * @param string $backReferenceNumberForAnchorHash For returning position of anchor hash regexp pattern.
177
     * @return string
178
     */
179 28
    public function getPostUrlPattern(&$backReferenceNumberForPostId, &$backReferenceNumberForAnchorHash)
180
    {
181 28
        $backReferenceNumberForPostId = 3;
182 28
        $backReferenceNumberForAnchorHash = 5;
183
184 28
        return sprintf('#^((https?:)?//%s\.esa\.io)?/posts/(\d+)(/|/edit/?)?(\#.+)?$#', $this->teamName);
185
    }
186
187
    /**
188
     * @return string
189
     */
190 3
    public function getMentionLinkPattern()
191
    {
192 3
        return '#/members/([^\'"]+)#';
193
    }
194
195
    /**
196
     * Return closure reduces ATags Crawler with regexp pattern for href value.
197
     *
198
     * @param string $pattern
199
     * @return \Closure
200
     */
201
    public function getATagReducer($pattern)
202
    {
203 28
        $reducer = function (Crawler $node) use ($pattern) {
204 26
            preg_match($pattern, $node->attr('href'), $matches);
205
206 26
            return boolval($matches);
207 28
        };
208
209 28
        return $reducer;
210
    }
211
212
    /**
213
     * Return closure returns map of ['pattern' => regexp pattern, 'replacement' => replacement] for href value of post urls.
214
     *
215
     * @param string $pattern
216
     * @param int $backReferenceNumberForPostId
217
     * @param int $backReferenceNumberForAnchorHash
218
     * @param string $routeName
219
     * @param string $routeVariableName
220
     * @return \Closure
221
     */
222 3
    public function getATagWalkerForPostUrls($pattern, $backReferenceNumberForPostId, $backReferenceNumberForAnchorHash, $routeName, $routeVariableName)
223
    {
224 3
        $that = $this;
225
226 3
        $walker = function (Crawler $node) use ($pattern, $backReferenceNumberForPostId, $backReferenceNumberForAnchorHash, $routeName, $routeVariableName, $that) {
227 1
            preg_match($pattern, $node->attr('href'), $matches);
228 1
            $href = $matches[0];
229 1
            $postId = $matches[$backReferenceNumberForPostId];
230 1
            $anchorHash = isset($matches[$backReferenceNumberForAnchorHash]) ? $matches[$backReferenceNumberForAnchorHash] : '';
231
232 1
            $pattern = sprintf('/href=(\'|")%s\1/', str_replace('/', '\/', $href));
233 1
            $replacement = sprintf('href="%s%s"', $that->urlGenerator->generate($routeName, [$routeVariableName => $postId]), $anchorHash);
234
235
            return [
236 1
                'pattern' => $pattern,
237 1
                'replacement' => $replacement,
238
            ];
239 3
        };
240
241 3
        return $walker;
242
    }
243
244
    /**
245
     * Return closure returns map of ['pattern' => regexp pattern, 'replacement' => replacement] for href value of mention links.
246
     *
247
     * @param string $pattern
248
     * @return \Closure
249
     */
250
    public function getATagWalkerForMentionLinks($pattern)
251
    {
252 3
        $walker = function (Crawler $node) use ($pattern) {
253 1
            preg_match($pattern, $node->attr('href'), $matches);
254 1
            $href = $matches[0];
255
256 1
            $pattern = sprintf('/href=(\'|")%s\1/', str_replace('/', '\/', $href));
257 1
            $replacement = '';
258
259
            return [
260 1
                'pattern' => $pattern,
261 1
                'replacement' => $replacement,
262
            ];
263 3
        };
264
265 3
        return $walker;
266
    }
267
268
    /**
269
     * Replace emoji codes only in text content of each nodes with img tags.
270
     */
271 15
    public function replaceEmojiCodes()
272
    {
273
        // find emoji codes.
274 15
        preg_match_all('/:([^\s:<>\'"]+):/', $this->crawler->text(), $matches);
275
276 13
        $tempReplacements = [];
277 13
        foreach (array_unique($matches[1]) as $name) {
278 12
            $pattern = sprintf('/:%s:/', preg_quote($name));
279 12
            $replacement = sprintf('__ESABA_IMG_TAG__%s__ESABA_IMG_TAG__', $name);
280
281 12
            $tempReplacements[$pattern] = $replacement;
282
        }
283
284
        // set temporarily replaced html content.
285 13
        $this->replaceText($tempReplacements);
286
287 13
        $replacements = [];
288 13
        foreach (array_values($tempReplacements) as $tempReplacement) {
289 12
            preg_match('/__ESABA_IMG_TAG__(.+)__ESABA_IMG_TAG__/', $tempReplacement, $matches);
290 12
            $name = $matches[1];
291
292 12
            $pattern = sprintf('/%s/', preg_quote($tempReplacement));
293 12
            $replacement = sprintf('<img src="%s" class="emoji" title=":%s:" alt=":%s:">', $this->emojiManager->getImageUrl($name), $name, $name);
294
295 12
            $replacements[$pattern] = $replacement;
296
        }
297
298 13
        $this->replaceHtml($replacements);
299 13
    }
300
301
    /**
302
     * Return map of ['id' => id, 'text' => text] of headings as TOC.
303
     *
304
     * @return array
305
     */
306 3
    public function getToc()
307
    {
308 3
        $this->ensureInitialized();
309
310 2
        $toc = $this->crawler->filter('h1, h2, h3')->each($this->getWalkerForToc());
311
312 2
        return $toc;
313
    }
314
315
    /**
316
     * Return closure returns map of ['id' => id, 'text' => text] of h tags.
317
     *
318
     * @return \Closure
319
     */
320
    public function getWalkerForToc()
321
    {
322 3
        $walker = function (Crawler $node) {
323
            return [
324 2
                'id' => $node->attr('id'),
325 2
                'text' => trim(str_replace($node->filter('a')->text(), '', $node->text())),
326
            ];
327 3
        };
328
329 3
        return $walker;
330
    }
331
332 22
    private function ensureInitialized()
333
    {
334 22
        if (!$this->crawler->count()) {
335 4
            throw new \LogicException('Initialize before using.');
336
        }
337 18
    }
338
}
339