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

HtmlHandler::walkDomNodesAndReplaceOnlyTextNodes()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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