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