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

HtmlHandler::replaceText()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 1
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 5
    public function initialize($html)
46
    {
47 5
        $this->crawler->clear();
48 5
        $this->crawler->addHtmlContent($html);
49
50 5
        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 5
    public function replaceHtml(array $replacements)
67
    {
68 5
        $this->ensureInitialized();
69
70 4
        $html = $this->crawler->html();
71
72 4
        foreach ($replacements as $pattern => $replacement) {
73 4
            $html = preg_replace($pattern, $replacement, $html);
74
        }
75
76 4
        $this->initialize($html);
77 4
    }
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 1
        $this->crawler->clear();
91 1
        $this->crawler->addNode($domNode);
92 1
    }
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 1
            $text = $node->textContent;
102
103 1
            foreach ($replacements as $pattern => $replacement) {
104
                $text = preg_replace($pattern, $replacement, $text);
105
            }
106
107 1
            $node->textContent = $text;
108
109 1
            return;
110
        }
111
112
//        if (!$node->hasChildNodes()) {
113
//            return;
114
//        }
115
//
116 14
        foreach ($node->childNodes as $childNode) {
117 14
            $this->walkDomNodesAndReplaceOnlyTextNodes($childNode, $replacements);
118
        }
119 1
    }
120
121
    /**
122
     * Replace links to other post with links to see the post on esaba.
123
     *
124
     * @param string $routeName
125
     * @param string $routeVariableName
126
     */
127 2
    public function replacePostUrls($routeName, $routeVariableName)
128
    {
129 2
        $backReferenceNumberForPostId = null;
130 2
        $backReferenceNumberForAnchorHash = null;
131 2
        $pattern = $this->getPostUrlPattern($backReferenceNumberForPostId, $backReferenceNumberForAnchorHash);
132 2
        $walker = $this->getATagWalkerForPostUrls($pattern, $backReferenceNumberForPostId, $backReferenceNumberForAnchorHash, $routeName, $routeVariableName);
133
134 2
        $this->replaceATagWithWalker($pattern, $walker);
135 2
    }
136
137
    /**
138
     * Disable @mention links.
139
     */
140 2
    public function disableMentionLinks()
141
    {
142 2
        $pattern = $this->getMentionLinkPattern();
143 2
        $walker = $this->getATagWalkerForMentionLinks($pattern);
144
145 2
        $this->replaceATagWithWalker($pattern, $walker);
146 2
    }
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 4
    public function replaceATagWithWalker($pattern, \Closure $walker)
155
    {
156 4
        $this->ensureInitialized();
157
158 3
        $targetATags = $this->crawler->filter('a')->reduce($this->getATagReducer($pattern));
159 3
        $replacements = $targetATags->each($walker);
160 3
        $replacements = array_combine(array_column($replacements, 'pattern'), array_column($replacements, 'replacement'));
161
162 3
        $this->replaceHtml($replacements);
163 3
    }
164
165
    /**
166
     * @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
     */
170 28
    public function getPostUrlPattern(&$backReferenceNumberForPostId, &$backReferenceNumberForAnchorHash)
171
    {
172 28
        $backReferenceNumberForPostId = 3;
173 28
        $backReferenceNumberForAnchorHash = 5;
174
175 28
        return sprintf('#^((https?:)?//%s\.esa\.io)?/posts/(\d+)(/|/edit/?)?(\#.+)?$#', $this->teamName);
176
    }
177
178
    /**
179
     * @return string
180
     */
181 3
    public function getMentionLinkPattern()
182
    {
183 3
        return '#/members/([^\'"]+)#';
184
    }
185
186
    /**
187
     * Return closure reduces ATags Crawler with regexp pattern for href value.
188
     *
189
     * @param string $pattern
190
     * @return \Closure
191
     */
192
    public function getATagReducer($pattern)
193
    {
194 28
        $reducer = function (Crawler $node) use ($pattern) {
195 26
            preg_match($pattern, $node->attr('href'), $matches);
196
197 26
            return boolval($matches);
198 28
        };
199
200 28
        return $reducer;
201
    }
202
203
    /**
204
     * Return closure returns map of ['pattern' => regexp pattern, 'replacement' => replacement] for href value of post urls.
205
     *
206
     * @param string $pattern
207
     * @param int $backReferenceNumberForPostId
208
     * @param int $backReferenceNumberForAnchorHash
209
     * @param string $routeName
210
     * @param string $routeVariableName
211
     * @return \Closure
212
     */
213 3
    public function getATagWalkerForPostUrls($pattern, $backReferenceNumberForPostId, $backReferenceNumberForAnchorHash, $routeName, $routeVariableName)
214
    {
215 3
        $that = $this;
216
217 3
        $walker = function (Crawler $node) use ($pattern, $backReferenceNumberForPostId, $backReferenceNumberForAnchorHash, $routeName, $routeVariableName, $that) {
218 1
            preg_match($pattern, $node->attr('href'), $matches);
219 1
            $href = $matches[0];
220 1
            $postId = $matches[$backReferenceNumberForPostId];
221 1
            $anchorHash = isset($matches[$backReferenceNumberForAnchorHash]) ? $matches[$backReferenceNumberForAnchorHash] : '';
222
223 1
            $pattern = sprintf('/href=(\'|")%s\1/', str_replace('/', '\/', $href));
224 1
            $replacement = sprintf('href="%s%s"', $that->urlGenerator->generate($routeName, [$routeVariableName => $postId]), $anchorHash);
225
226
            return [
227 1
                'pattern' => $pattern,
228 1
                'replacement' => $replacement,
229
            ];
230 3
        };
231
232 3
        return $walker;
233
    }
234
235
    /**
236
     * 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 1
            preg_match($pattern, $node->attr('href'), $matches);
245 1
            $href = $matches[0];
246
247 1
            $pattern = sprintf('/href=(\'|")%s\1/', str_replace('/', '\/', $href));
248 1
            $replacement = '';
249
250
            return [
251 1
                'pattern' => $pattern,
252 1
                'replacement' => $replacement,
253
            ];
254 3
        };
255
256 3
        return $walker;
257
    }
258
259
    /**
260
     * Replace emoji codes only in text content of each nodes with img tags.
261
     */
262 15
    public function replaceEmojiCodes()
263
    {
264
        // find emoji codes.
265 15
        preg_match_all('/:([^\s:<>\'"]+):/', $this->crawler->text(), $matches);
266
267 13
        $tempReplacements = [];
268 13
        foreach (array_unique($matches[1]) as $name) {
269 12
            $pattern = sprintf('/:%s:/', preg_quote($name));
270 12
            $replacement = sprintf('__ESABA_IMG_TAG__%s__ESABA_IMG_TAG__', $name);
271
272 12
            $tempReplacements[$pattern] = $replacement;
273
        }
274
275
        // set temporarily replaced html content.
276 13
        $this->replaceText($tempReplacements);
277
278 1
        $replacements = [];
279 1
        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 1
        $this->replaceHtml($replacements);
290 1
    }
291
292
    /**
293
     * Return map of ['id' => id, 'text' => text] of headings as TOC.
294
     *
295
     * @return array
296
     */
297 3
    public function getToc()
298
    {
299 3
        $this->ensureInitialized();
300
301 2
        $toc = $this->crawler->filter('h1, h2, h3')->each($this->getWalkerForToc());
302
303 2
        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 3
        $walker = function (Crawler $node) {
314
            return [
315 2
                'id' => $node->attr('id'),
316 2
                'text' => trim(str_replace($node->filter('a')->text(), '', $node->text())),
317
            ];
318 3
        };
319
320 3
        return $walker;
321
    }
322
323 22
    private function ensureInitialized()
324
    {
325 22
        if (!$this->crawler->count()) {
326 4
            throw new \LogicException('Initialize before using.');
327
        }
328 18
    }
329
}
330