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