Completed
Branch master (8e0976)
by Adam
04:13
created

Markdown::inlineHash()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 25
rs 9.52
c 0
b 0
f 0
1
<?php
2
3
namespace Coyote\Services\Parser\Parsers;
4
5
use Coyote\Repositories\Contracts\UserRepositoryInterface as User;
6
7
// dziedziczymy po Parsedown a nie Parsedown Extra z uwagi na buga. Parsedown Extra wycina reszte linii jezeli
8
// w danej linii znajdzie sie tag. np. <ort>ktory</ort> w ogle => <ort>ktory</ort>
9
class Markdown extends \Parsedown implements ParserInterface
10
{
11
    /**
12
     * @var User
13
     */
14
    private $user;
15
16
    /**
17
     * @var bool
18
     */
19
    private $enableHashParser = false;
20
21
    /**
22
     * @var bool
23
     */
24
    private $enableUserTagParser = true;
25
26
    /**
27
     * @var string
28
     */
29
    private $hashRoute = 'microblog.tag';
30
31
    /**
32
     * @param User $user
33
     * @throws \Exception
34
     */
35
    public function __construct(User $user)
36
    {
37
        $this->InlineTypes['@'][] = 'UserTag';
38
        $this->inlineMarkerList .= '@';
39
40
        $this->InlineTypes['#'][] = 'Hash';
41
        $this->inlineMarkerList .= '#';
42
43
        $this->user = $user;
44
    }
45
46
    /**
47
     * @param bool $flag
48
     * @return Markdown
49
     */
50
    public function setEnableHashParser(bool $flag)
51
    {
52
        $this->enableHashParser = $flag;
53
54
        return $this;
55
    }
56
57
    /**
58
     * @param boolean $flag
59
     * @return Markdown
60
     */
61
    public function setEnableUserTagParser(bool $flag)
62
    {
63
        $this->enableUserTagParser = $flag;
64
65
        return $this;
66
    }
67
68
    /**
69
     * @param string $text
70
     * @return string
71
     */
72
    public function parse($text)
73
    {
74
        // @see https://github.com/erusev/parsedown/issues/432
75
        // trzeba wylaczyc zamiane linkow na URL poniewaz nie dziala to prawidlowo (bug parsera)
76
        // robimy to osobno, w parserze Autolink
77
        $this->setUrlsLinked(false);
78
79
        return $this->text($text);
80
    }
81
82
    /**
83
     * @param array $excerpt
84
     * @return array|void
85
     */
86
    protected function inlineHash($excerpt)
87
    {
88
        if (!$this->enableHashParser) {
89
            return;
90
        }
91
92
        if (!$this->isSingleWord($excerpt)) {
93
            return;
94
        }
95
96
        if (preg_match('~#([\p{L}\p{Mn}0-9\._+-]+)~u', $excerpt['text'], $matches)) {
97
            $tag = mb_strtolower($matches[1]);
98
99
            return [
100
                'extent' => strlen($matches[0]),
101
                'element' => [
102
                    'name' => 'a',
103
                    'text' => $matches[0],
104
                    'attributes' => [
105
                        'href' => route($this->hashRoute, [$tag])
106
                    ]
107
                ]
108
            ];
109
        }
110
    }
111
112
    /**
113
     * We don't want <h1> in our text
114
     * We also need to disable this syntax due to parsedown error. #foo should NOT be parser
115
     * because markdown needs extra space after "#". Otherwise it is hashtag.
116
     *
117
     * DO NOT REMOVE THIS METHOD.
118
     *
119
     * @param $line
120
     * @return array|null
121
     */
122
    protected function blockHeader($line)
123
    {
124
        $block = parent::blockHeader($line);
125
126
        if (isset($block['element'])) {
127
            if ($block['element']['name'] == 'h1') {
128
                return null;
129
            }
130
        }
131
132
        return $block;
133
    }
134
135
    /**
136
     * Parse users login
137
     *
138
     * @param array $excerpt
139
     * @return array|null
140
     */
141
    protected function inlineUserTag($excerpt)
142
    {
143
        if (!$this->enableUserTagParser) {
144
            return null;
145
        }
146
147
        $text = &$excerpt['text'];
148
        $start = strpos($text, '@');
149
150
        if ($this->isSingleWord($excerpt)) {
151
            if (!isset($text[$start + 1])) {
152
                return null;
153
            }
154
155
            $exitChar = $text[$start + 1] === '{' ? '}' : ":,.\'\n "; // <-- space at the end
156
            $end = $this->strpos($text, $exitChar, $start);
157
158
            if ($end === false) {
159
                $end = mb_strlen($text) - $start;
160
            }
161
162
            $length = $end - $start;
163
            $start += 1;
164
            $end -= 1;
165
166
            if ($exitChar == '}') {
167
                $start += 1;
168
                $end -= 1;
169
170
                $length += 1;
171
            }
172
173
            $name = substr($text, $start, $end);
174
175
            // user name ends with ")" -- we strip if login is within bracket
176
            if (strlen($name) > 0 && $name[mb_strlen($name) - 1] === ')' && mb_strpos($name, '(') === false) {
177
                $name = mb_substr($name, 0, -1);
178
                $length -= 1;
179
            }
180
181
            $user = $this->user->findByName($name);
182
183
            $replacement = [
184
                'extent' => $length,
185
                'element' => [
186
                    'name' => 'a',
187
                    'text' => '@' . $name
188
                ]
189
            ];
190
191
            if ($user) {
192
                $replacement['element']['attributes'] = [
193
                    'href' => route('profile', [$user->id]),
194
                    'data-user-id' => $user->id,
195
                    'class' => 'mention'
196
                ];
197
            } else {
198
                $replacement['element']['name'] = 'strong';
199
            }
200
201
            return $replacement;
202
        }
203
    }
204
205
    /**
206
     * @param array $excerpt
207
     * @return bool
208
     */
209
    private function isSingleWord(&$excerpt)
210
    {
211
        // the whole text
212
        $context = &$excerpt['context'];
213
        // "@" or "#" position
214
        $start = mb_strpos($context, $excerpt['text']);
215
        // previous character (before "@" or "#")
216
        $preceding = mb_substr($context, $start - 1, 1);
217
        // next character (after "@" or "#")
218
        $following = mb_substr($context, $start + 1, 1);
219
220
        return mb_substr($excerpt['text'], $start + 1, 1) !== false
221
            && ($start === 0 || in_array($preceding, [' ', '(', "\n"]))
222
                && $following !== ' ';
223
    }
224
225
    /**
226
     * Find the position of the first occurrence of a character in a string
227
     *
228
     * @param $haystack
229
     * @param string $needle
230
     * @param int $offset
231
     * @return bool|mixed
232
     */
233
    private function strpos($haystack, $needle, $offset = 0)
234
    {
235
        $result = [];
236
237
        foreach (str_split($needle) as $char) {
238
            if (($pos = strpos($haystack, $char, $offset)) !== false) {
239
                $result[] = $pos;
240
            }
241
        }
242
243
        return $result ? min($result) : false;
244
    }
245
}
246