Version 1
[yaffs-website] / vendor / cebe / markdown / inline / LinkTrait.php
1 <?php
2 /**
3  * @copyright Copyright (c) 2014 Carsten Brandt
4  * @license https://github.com/cebe/markdown/blob/master/LICENSE
5  * @link https://github.com/cebe/markdown#readme
6  */
7
8 namespace cebe\markdown\inline;
9
10 // work around https://github.com/facebook/hhvm/issues/1120
11 defined('ENT_HTML401') || define('ENT_HTML401', 0);
12
13 /**
14  * Addes links and images as well as url markers.
15  *
16  * This trait conflicts with the HtmlTrait. If both are used together,
17  * you have to define a resolution, by defining the HtmlTrait::parseInlineHtml
18  * as private so it is not used directly:
19  *
20  * ```php
21  * use block\HtmlTrait {
22  *     parseInlineHtml as private parseInlineHtml;
23  * }
24  * ```
25  *
26  * If the method exists it is called internally by this trait.
27  *
28  * Also make sure to reset references on prepare():
29  *
30  * ```php
31  * protected function prepare()
32  * {
33  *     // reset references
34  *     $this->references = [];
35  * }
36  * ```
37  */
38 trait LinkTrait
39 {
40         /**
41          * @var array a list of defined references in this document.
42          */
43         protected $references = [];
44
45         /**
46          * Parses a link indicated by `[`.
47          * @marker [
48          */
49         protected function parseLink($markdown)
50         {
51                 if (!in_array('parseLink', array_slice($this->context, 1)) && ($parts = $this->parseLinkOrImage($markdown)) !== false) {
52                         list($text, $url, $title, $offset, $key) = $parts;
53                         return [
54                                 [
55                                         'link',
56                                         'text' => $this->parseInline($text),
57                                         'url' => $url,
58                                         'title' => $title,
59                                         'refkey' => $key,
60                                         'orig' => substr($markdown, 0, $offset),
61                                 ],
62                                 $offset
63                         ];
64                 } else {
65                         // remove all starting [ markers to avoid next one to be parsed as link
66                         $result = '[';
67                         $i = 1;
68                         while (isset($markdown[$i]) && $markdown[$i] == '[') {
69                                 $result .= '[';
70                                 $i++;
71                         }
72                         return [['text', $result], $i];
73                 }
74         }
75
76         /**
77          * Parses an image indicated by `![`.
78          * @marker ![
79          */
80         protected function parseImage($markdown)
81         {
82                 if (($parts = $this->parseLinkOrImage(substr($markdown, 1))) !== false) {
83                         list($text, $url, $title, $offset, $key) = $parts;
84
85                         return [
86                                 [
87                                         'image',
88                                         'text' => $text,
89                                         'url' => $url,
90                                         'title' => $title,
91                                         'refkey' => $key,
92                                         'orig' => substr($markdown, 0, $offset + 1),
93                                 ],
94                                 $offset + 1
95                         ];
96                 } else {
97                         // remove all starting [ markers to avoid next one to be parsed as link
98                         $result = '!';
99                         $i = 1;
100                         while (isset($markdown[$i]) && $markdown[$i] == '[') {
101                                 $result .= '[';
102                                 $i++;
103                         }
104                         return [['text', $result], $i];
105                 }
106         }
107
108         protected function parseLinkOrImage($markdown)
109         {
110                 if (strpos($markdown, ']') !== false && preg_match('/\[((?>[^\]\[]+|(?R))*)\]/', $markdown, $textMatches)) { // TODO improve bracket regex
111                         $text = $textMatches[1];
112                         $offset = strlen($textMatches[0]);
113                         $markdown = substr($markdown, $offset);
114
115                         $pattern = <<<REGEXP
116                                 /(?(R) # in case of recursion match parentheses
117                                          \(((?>[^\s()]+)|(?R))*\)
118                                 |      # else match a link with title
119                                         ^\((((?>[^\s()]+)|(?R))*)(\s+"(.*?)")?\)
120                                 )/x
121 REGEXP;
122                         if (preg_match($pattern, $markdown, $refMatches)) {
123                                 // inline link
124                                 return [
125                                         $text,
126                                         isset($refMatches[2]) ? $refMatches[2] : '', // url
127                                         empty($refMatches[5]) ? null: $refMatches[5], // title
128                                         $offset + strlen($refMatches[0]), // offset
129                                         null, // reference key
130                                 ];
131                         } elseif (preg_match('/^([ \n]?\[(.*?)\])?/s', $markdown, $refMatches)) {
132                                 // reference style link
133                                 if (empty($refMatches[2])) {
134                                         $key = strtolower($text);
135                                 } else {
136                                         $key = strtolower($refMatches[2]);
137                                 }
138                                 return [
139                                         $text,
140                                         null, // url
141                                         null, // title
142                                         $offset + strlen($refMatches[0]), // offset
143                                         $key,
144                                 ];
145                         }
146                 }
147                 return false;
148         }
149
150         /**
151          * Parses inline HTML.
152          * @marker <
153          */
154         protected function parseLt($text)
155         {
156                 if (strpos($text, '>') !== false) {
157                         if (!in_array('parseLink', $this->context)) { // do not allow links in links
158                                 if (preg_match('/^<([^\s]*?@[^\s]*?\.\w+?)>/', $text, $matches)) {
159                                         // email address
160                                         return [
161                                                 ['email', $matches[1]],
162                                                 strlen($matches[0])
163                                         ];
164                                 } elseif (preg_match('/^<([a-z]{3,}:\/\/[^\s]+?)>/', $text, $matches)) {
165                                         // URL
166                                         return [
167                                                 ['url', $matches[1]],
168                                                 strlen($matches[0])
169                                         ];
170                                 }
171                         }
172                         // try inline HTML if it was neither a URL nor email if HtmlTrait is included.
173                         if (method_exists($this, 'parseInlineHtml')) {
174                                 return $this->parseInlineHtml($text);
175                         }
176                 }
177                 return [['text', '&lt;'], 1];
178         }
179
180         protected function renderEmail($block)
181         {
182                 $email = htmlspecialchars($block[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
183                 return "<a href=\"mailto:$email\">$email</a>";
184         }
185
186         protected function renderUrl($block)
187         {
188                 $url = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
189                 $text = htmlspecialchars(urldecode($block[1]), ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
190                 return "<a href=\"$url\">$text</a>";
191         }
192
193         protected function lookupReference($key)
194         {
195                 $normalizedKey = preg_replace('/\s+/', ' ', $key);
196                 if (isset($this->references[$key]) || isset($this->references[$key = $normalizedKey])) {
197                         return $this->references[$key];
198                 }
199                 return false;
200         }
201
202         protected function renderLink($block)
203         {
204                 if (isset($block['refkey'])) {
205                         if (($ref = $this->lookupReference($block['refkey'])) !== false) {
206                                 $block = array_merge($block, $ref);
207                         } else {
208                                 return $block['orig'];
209                         }
210                 }
211                 return '<a href="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
212                         . (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
213                         . '>' . $this->renderAbsy($block['text']) . '</a>';
214         }
215
216         protected function renderImage($block)
217         {
218                 if (isset($block['refkey'])) {
219                         if (($ref = $this->lookupReference($block['refkey'])) !== false) {
220                                 $block = array_merge($block, $ref);
221                         } else {
222                                 return $block['orig'];
223                         }
224                 }
225                 return '<img src="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
226                         . ' alt="' . htmlspecialchars($block['text'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"'
227                         . (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
228                         . ($this->html5 ? '>' : ' />');
229         }
230
231         // references
232
233         protected function identifyReference($line)
234         {
235                 return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[(.+?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*$/', $line);
236         }
237
238         /**
239          * Consume link references
240          */
241         protected function consumeReference($lines, $current)
242         {
243                 while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*$/', $lines[$current], $matches)) {
244                         $label = strtolower($matches[1]);
245
246                         $this->references[$label] = [
247                                 'url' => $matches[2],
248                         ];
249                         if (isset($matches[3])) {
250                                 $this->references[$label]['title'] = $matches[3];
251                         } else {
252                                 // title may be on the next line
253                                 if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
254                                         $this->references[$label]['title'] = $matches[1];
255                                         $current++;
256                                 }
257                         }
258                         $current++;
259                 }
260                 return [false, --$current];
261         }
262 }