Version 1
[yaffs-website] / vendor / cebe / markdown / Parser.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;
9 use ReflectionMethod;
10
11 /**
12  * A generic parser for markdown-like languages.
13  *
14  * @author Carsten Brandt <mail@cebe.cc>
15  */
16 abstract class Parser
17 {
18         /**
19          * @var integer the maximum nesting level for language elements.
20          */
21         public $maximumNestingLevel = 32;
22
23         /**
24          * @var string the current context the parser is in.
25          * TODO remove in favor of absy
26          */
27         protected $context = [];
28         /**
29          * @var array these are "escapeable" characters. When using one of these prefixed with a
30          * backslash, the character will be outputted without the backslash and is not interpreted
31          * as markdown.
32          */
33         protected $escapeCharacters = [
34                 '\\', // backslash
35         ];
36
37         private $_depth = 0;
38
39
40         /**
41          * Parses the given text considering the full language.
42          *
43          * This includes parsing block elements as well as inline elements.
44          *
45          * @param string $text the text to parse
46          * @return string parsed markup
47          */
48         public function parse($text)
49         {
50                 $this->prepare();
51                 
52                 if (empty($text)) {
53                         return '';
54                 }
55
56                 $text = str_replace(["\r\n", "\n\r", "\r"], "\n", $text);
57
58                 $this->prepareMarkers($text);
59
60                 $absy = $this->parseBlocks(explode("\n", $text));
61                 $markup = $this->renderAbsy($absy);
62
63                 $this->cleanup();
64                 return $markup;
65         }
66
67         /**
68          * Parses a paragraph without block elements (block elements are ignored).
69          *
70          * @param string $text the text to parse
71          * @return string parsed markup
72          */
73         public function parseParagraph($text)
74         {
75                 $this->prepare();
76
77                 if (empty($text)) {
78                         return '';
79                 }
80
81                 $text = str_replace(["\r\n", "\n\r", "\r"], "\n", $text);
82
83                 $this->prepareMarkers($text);
84
85                 $absy = $this->parseInline($text);
86                 $markup = $this->renderAbsy($absy);
87
88                 $this->cleanup();
89                 return $markup;
90         }
91
92         /**
93          * This method will be called before `parse()` and `parseParagraph()`.
94          * You can override it to do some initialization work.
95          */
96         protected function prepare()
97         {
98         }
99
100         /**
101          * This method will be called after `parse()` and `parseParagraph()`.
102          * You can override it to do cleanup.
103          */
104         protected function cleanup()
105         {
106         }
107
108
109         // block parsing
110
111         private $_blockTypes;
112
113         /**
114          * @return array a list of block element types available.
115          */
116         protected function blockTypes()
117         {
118                 if ($this->_blockTypes === null) {
119                         // detect block types via "identify" functions
120                         $reflection = new \ReflectionClass($this);
121                         $this->_blockTypes = array_filter(array_map(function($method) {
122                                 $name = $method->getName();
123                                 return strncmp($name, 'identify', 8) === 0 ? strtolower(substr($name, 8)) : false;
124                         }, $reflection->getMethods(ReflectionMethod::IS_PROTECTED)));
125
126                         sort($this->_blockTypes);
127                 }
128                 return $this->_blockTypes;
129         }
130
131         /**
132          * Given a set of lines and an index of a current line it uses the registed block types to
133          * detect the type of this line.
134          * @param array $lines
135          * @param integer $current
136          * @return string name of the block type in lower case
137          */
138         protected function detectLineType($lines, $current)
139         {
140                 $line = $lines[$current];
141                 $blockTypes = $this->blockTypes();
142                 foreach($blockTypes as $blockType) {
143                         if ($this->{'identify' . $blockType}($line, $lines, $current)) {
144                                 return $blockType;
145                         }
146                 }
147                 return 'paragraph';
148         }
149
150         /**
151          * Parse block elements by calling `identifyLine()` to identify them
152          * and call consume function afterwards.
153          * The blocks are then rendered by the corresponding rendering methods.
154          */
155         protected function parseBlocks($lines)
156         {
157                 if ($this->_depth >= $this->maximumNestingLevel) {
158                         // maximum depth is reached, do not parse input
159                         return [['text', implode("\n", $lines)]];
160                 }
161                 $this->_depth++;
162
163                 $blocks = [];
164
165                 $blockTypes = $this->blockTypes();
166
167                 // convert lines to blocks
168                 for ($i = 0, $count = count($lines); $i < $count; $i++) {
169                         $line = $lines[$i];
170                         if (!empty($line) && rtrim($line) !== '') { // skip empty lines
171                                 // identify a blocks beginning
172                                 $identified = false;
173                                 foreach($blockTypes as $blockType) {
174                                         if ($this->{'identify' . $blockType}($line, $lines, $i)) {
175                                                 // call consume method for the detected block type to consume further lines
176                                                 list($block, $i) = $this->{'consume' . $blockType}($lines, $i);
177                                                 if ($block !== false) {
178                                                         $blocks[] = $block;
179                                                 }
180                                                 $identified = true;
181                                                 break 1;
182                                         }
183                                 }
184                                 // consider the line a normal paragraph
185                                 if (!$identified) {
186                                         list($block, $i) = $this->consumeParagraph($lines, $i);
187                                         $blocks[] = $block;
188                                 }
189                         }
190                 }
191
192                 $this->_depth--;
193
194                 return $blocks;
195         }
196
197         protected function renderAbsy($blocks)
198         {
199                 $output = '';
200                 foreach ($blocks as $block) {
201                         array_unshift($this->context, $block[0]);
202                         $output .= $this->{'render' . $block[0]}($block);
203                         array_shift($this->context);
204                 }
205                 return $output;
206         }
207
208         /**
209          * Consume lines for a paragraph
210          *
211          * @param $lines
212          * @param $current
213          * @return array
214          */
215         protected function consumeParagraph($lines, $current)
216         {
217                 // consume until newline
218                 $content = [];
219                 for ($i = $current, $count = count($lines); $i < $count; $i++) {
220                         if (ltrim($lines[$i]) !== '') {
221                                 $content[] = $lines[$i];
222                         } else {
223                                 break;
224                         }
225                 }
226                 $block = [
227                         'paragraph',
228                         'content' => $this->parseInline(implode("\n", $content)),
229                 ];
230                 return [$block, --$i];
231         }
232
233         /**
234          * Render a paragraph block
235          *
236          * @param $block
237          * @return string
238          */
239         protected function renderParagraph($block)
240         {
241                 return '<p>' . $this->renderAbsy($block['content']) . "</p>\n";
242         }
243
244
245         // inline parsing
246
247
248         /**
249          * @var array the set of inline markers to use in different contexts.
250          */
251         private $_inlineMarkers = [];
252
253         /**
254          * Returns a map of inline markers to the corresponding parser methods.
255          *
256          * This array defines handler methods for inline markdown markers.
257          * When a marker is found in the text, the handler method is called with the text
258          * starting at the position of the marker.
259          *
260          * Note that markers starting with whitespace may slow down the parser,
261          * you may want to use [[renderText]] to deal with them.
262          *
263          * You may override this method to define a set of markers and parsing methods.
264          * The default implementation looks for protected methods starting with `parse` that
265          * also have an `@marker` annotation in PHPDoc.
266          *
267          * @return array a map of markers to parser methods
268          */
269         protected function inlineMarkers()
270         {
271                 $markers = [];
272                 // detect "parse" functions
273                 $reflection = new \ReflectionClass($this);
274                 foreach($reflection->getMethods(ReflectionMethod::IS_PROTECTED) as $method) {
275                         $methodName = $method->getName();
276                         if (strncmp($methodName, 'parse', 5) === 0) {
277                                 preg_match_all('/@marker ([^\s]+)/', $method->getDocComment(), $matches);
278                                 foreach($matches[1] as $match) {
279                                         $markers[$match] = $methodName;
280                                 }
281                         }
282                 }
283                 return $markers;
284         }
285
286         /**
287          * Prepare markers that are used in the text to parse
288          *
289          * Add all markers that are present in markdown.
290          * Check is done to avoid iterations in parseInline(), good for huge markdown files
291          * @param string $text
292          */
293         private function prepareMarkers($text)
294         {
295                 $this->_inlineMarkers = [];
296                 foreach ($this->inlineMarkers() as $marker => $method) {
297                         if (strpos($text, $marker) !== false) {
298                                 $m = $marker[0];
299                                 // put the longest marker first
300                                 if (isset($this->_inlineMarkers[$m])) {
301                                         reset($this->_inlineMarkers[$m]);
302                                         if (strlen($marker) > strlen(key($this->_inlineMarkers[$m]))) {
303                                                 $this->_inlineMarkers[$m] = array_merge([$marker => $method], $this->_inlineMarkers[$m]);
304                                                 continue;
305                                         }
306                                 }
307                                 $this->_inlineMarkers[$m][$marker] = $method;
308                         }
309                 }
310         }
311
312         /**
313          * Parses inline elements of the language.
314          *
315          * @param string $text the inline text to parse.
316          * @return array
317          */
318         protected function parseInline($text)
319         {
320                 if ($this->_depth >= $this->maximumNestingLevel) {
321                         // maximum depth is reached, do not parse input
322                         return [['text', $text]];
323                 }
324                 $this->_depth++;
325
326                 $markers = implode('', array_keys($this->_inlineMarkers));
327
328                 $paragraph = [];
329
330                 while (!empty($markers) && ($found = strpbrk($text, $markers)) !== false) {
331
332                         $pos = strpos($text, $found);
333
334                         // add the text up to next marker to the paragraph
335                         if ($pos !== 0) {
336                                 $paragraph[] = ['text', substr($text, 0, $pos)];
337                         }
338                         $text = $found;
339
340                         $parsed = false;
341                         foreach ($this->_inlineMarkers[$text[0]] as $marker => $method) {
342                                 if (strncmp($text, $marker, strlen($marker)) === 0) {
343                                         // parse the marker
344                                         array_unshift($this->context, $method);
345                                         list($output, $offset) = $this->$method($text);
346                                         array_shift($this->context);
347
348                                         $paragraph[] = $output;
349                                         $text = substr($text, $offset);
350                                         $parsed = true;
351                                         break;
352                                 }
353                         }
354                         if (!$parsed) {
355                                 $paragraph[] = ['text', substr($text, 0, 1)];
356                                 $text = substr($text, 1);
357                         }
358                 }
359
360                 $paragraph[] = ['text', $text];
361
362                 $this->_depth--;
363
364                 return $paragraph;
365         }
366
367         /**
368          * Parses escaped special characters.
369          * @marker \
370          */
371         protected function parseEscape($text)
372         {
373                 if (isset($text[1]) && in_array($text[1], $this->escapeCharacters)) {
374                         return [['text', $text[1]], 2];
375                 }
376                 return [['text', $text[0]], 1];
377         }
378
379         /**
380          * This function renders plain text sections in the markdown text.
381          * It can be used to work on normal text sections for example to highlight keywords or
382          * do special escaping.
383          */
384         protected function renderText($block)
385         {
386                 return $block[1];
387         }
388 }