diff options
author | ArthurHoaro <arthur@hoa.ro> | 2017-05-07 19:17:33 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2017-05-07 19:17:33 +0200 |
commit | 01e942d44c7194607649817216aeb5d65c6acad6 (patch) | |
tree | 15777aa1005251f119e6dd680291147117766b5b /plugins/markdown | |
parent | bc22c9a0acb095970e9494cbe8954f0612e05dc0 (diff) | |
parent | 8868f3ca461011a8fb6dd9f90b60ed697ab52fc5 (diff) | |
download | Shaarli-01e942d44c7194607649817216aeb5d65c6acad6.tar.gz Shaarli-01e942d44c7194607649817216aeb5d65c6acad6.tar.zst Shaarli-01e942d44c7194607649817216aeb5d65c6acad6.zip |
Merge tag 'v0.8.4' into stable
Release v0.8.4
Diffstat (limited to 'plugins/markdown')
-rw-r--r-- | plugins/markdown/Parsedown.php | 1528 | ||||
-rw-r--r-- | plugins/markdown/README.md | 51 | ||||
-rw-r--r-- | plugins/markdown/markdown.meta | 5 | ||||
-rw-r--r-- | plugins/markdown/markdown.php | 105 |
4 files changed, 135 insertions, 1554 deletions
diff --git a/plugins/markdown/Parsedown.php b/plugins/markdown/Parsedown.php deleted file mode 100644 index 91e05dcc..00000000 --- a/plugins/markdown/Parsedown.php +++ /dev/null | |||
@@ -1,1528 +0,0 @@ | |||
1 | <?php | ||
2 | |||
3 | # | ||
4 | # | ||
5 | # Parsedown | ||
6 | # http://parsedown.org | ||
7 | # | ||
8 | # (c) Emanuil Rusev | ||
9 | # http://erusev.com | ||
10 | # | ||
11 | # For the full license information, view the LICENSE file that was distributed | ||
12 | # with this source code. | ||
13 | # | ||
14 | # | ||
15 | |||
16 | class Parsedown | ||
17 | { | ||
18 | # ~ | ||
19 | |||
20 | const version = '1.6.0'; | ||
21 | |||
22 | # ~ | ||
23 | |||
24 | function text($text) | ||
25 | { | ||
26 | # make sure no definitions are set | ||
27 | $this->DefinitionData = array(); | ||
28 | |||
29 | # standardize line breaks | ||
30 | $text = str_replace(array("\r\n", "\r"), "\n", $text); | ||
31 | |||
32 | # remove surrounding line breaks | ||
33 | $text = trim($text, "\n"); | ||
34 | |||
35 | # split text into lines | ||
36 | $lines = explode("\n", $text); | ||
37 | |||
38 | # iterate through lines to identify blocks | ||
39 | $markup = $this->lines($lines); | ||
40 | |||
41 | # trim line breaks | ||
42 | $markup = trim($markup, "\n"); | ||
43 | |||
44 | return $markup; | ||
45 | } | ||
46 | |||
47 | # | ||
48 | # Setters | ||
49 | # | ||
50 | |||
51 | function setBreaksEnabled($breaksEnabled) | ||
52 | { | ||
53 | $this->breaksEnabled = $breaksEnabled; | ||
54 | |||
55 | return $this; | ||
56 | } | ||
57 | |||
58 | protected $breaksEnabled; | ||
59 | |||
60 | function setMarkupEscaped($markupEscaped) | ||
61 | { | ||
62 | $this->markupEscaped = $markupEscaped; | ||
63 | |||
64 | return $this; | ||
65 | } | ||
66 | |||
67 | protected $markupEscaped; | ||
68 | |||
69 | function setUrlsLinked($urlsLinked) | ||
70 | { | ||
71 | $this->urlsLinked = $urlsLinked; | ||
72 | |||
73 | return $this; | ||
74 | } | ||
75 | |||
76 | protected $urlsLinked = true; | ||
77 | |||
78 | # | ||
79 | # Lines | ||
80 | # | ||
81 | |||
82 | protected $BlockTypes = array( | ||
83 | '#' => array('Header'), | ||
84 | '*' => array('Rule', 'List'), | ||
85 | '+' => array('List'), | ||
86 | '-' => array('SetextHeader', 'Table', 'Rule', 'List'), | ||
87 | '0' => array('List'), | ||
88 | '1' => array('List'), | ||
89 | '2' => array('List'), | ||
90 | '3' => array('List'), | ||
91 | '4' => array('List'), | ||
92 | '5' => array('List'), | ||
93 | '6' => array('List'), | ||
94 | '7' => array('List'), | ||
95 | '8' => array('List'), | ||
96 | '9' => array('List'), | ||
97 | ':' => array('Table'), | ||
98 | '<' => array('Comment', 'Markup'), | ||
99 | '=' => array('SetextHeader'), | ||
100 | '>' => array('Quote'), | ||
101 | '[' => array('Reference'), | ||
102 | '_' => array('Rule'), | ||
103 | '`' => array('FencedCode'), | ||
104 | '|' => array('Table'), | ||
105 | '~' => array('FencedCode'), | ||
106 | ); | ||
107 | |||
108 | # ~ | ||
109 | |||
110 | protected $unmarkedBlockTypes = array( | ||
111 | 'Code', | ||
112 | ); | ||
113 | |||
114 | # | ||
115 | # Blocks | ||
116 | # | ||
117 | |||
118 | private function lines(array $lines) | ||
119 | { | ||
120 | $CurrentBlock = null; | ||
121 | |||
122 | foreach ($lines as $line) | ||
123 | { | ||
124 | if (chop($line) === '') | ||
125 | { | ||
126 | if (isset($CurrentBlock)) | ||
127 | { | ||
128 | $CurrentBlock['interrupted'] = true; | ||
129 | } | ||
130 | |||
131 | continue; | ||
132 | } | ||
133 | |||
134 | if (strpos($line, "\t") !== false) | ||
135 | { | ||
136 | $parts = explode("\t", $line); | ||
137 | |||
138 | $line = $parts[0]; | ||
139 | |||
140 | unset($parts[0]); | ||
141 | |||
142 | foreach ($parts as $part) | ||
143 | { | ||
144 | $shortage = 4 - mb_strlen($line, 'utf-8') % 4; | ||
145 | |||
146 | $line .= str_repeat(' ', $shortage); | ||
147 | $line .= $part; | ||
148 | } | ||
149 | } | ||
150 | |||
151 | $indent = 0; | ||
152 | |||
153 | while (isset($line[$indent]) and $line[$indent] === ' ') | ||
154 | { | ||
155 | $indent ++; | ||
156 | } | ||
157 | |||
158 | $text = $indent > 0 ? substr($line, $indent) : $line; | ||
159 | |||
160 | # ~ | ||
161 | |||
162 | $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); | ||
163 | |||
164 | # ~ | ||
165 | |||
166 | if (isset($CurrentBlock['continuable'])) | ||
167 | { | ||
168 | $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); | ||
169 | |||
170 | if (isset($Block)) | ||
171 | { | ||
172 | $CurrentBlock = $Block; | ||
173 | |||
174 | continue; | ||
175 | } | ||
176 | else | ||
177 | { | ||
178 | if (method_exists($this, 'block'.$CurrentBlock['type'].'Complete')) | ||
179 | { | ||
180 | $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); | ||
181 | } | ||
182 | } | ||
183 | } | ||
184 | |||
185 | # ~ | ||
186 | |||
187 | $marker = $text[0]; | ||
188 | |||
189 | # ~ | ||
190 | |||
191 | $blockTypes = $this->unmarkedBlockTypes; | ||
192 | |||
193 | if (isset($this->BlockTypes[$marker])) | ||
194 | { | ||
195 | foreach ($this->BlockTypes[$marker] as $blockType) | ||
196 | { | ||
197 | $blockTypes []= $blockType; | ||
198 | } | ||
199 | } | ||
200 | |||
201 | # | ||
202 | # ~ | ||
203 | |||
204 | foreach ($blockTypes as $blockType) | ||
205 | { | ||
206 | $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); | ||
207 | |||
208 | if (isset($Block)) | ||
209 | { | ||
210 | $Block['type'] = $blockType; | ||
211 | |||
212 | if ( ! isset($Block['identified'])) | ||
213 | { | ||
214 | $Blocks []= $CurrentBlock; | ||
215 | |||
216 | $Block['identified'] = true; | ||
217 | } | ||
218 | |||
219 | if (method_exists($this, 'block'.$blockType.'Continue')) | ||
220 | { | ||
221 | $Block['continuable'] = true; | ||
222 | } | ||
223 | |||
224 | $CurrentBlock = $Block; | ||
225 | |||
226 | continue 2; | ||
227 | } | ||
228 | } | ||
229 | |||
230 | # ~ | ||
231 | |||
232 | if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) | ||
233 | { | ||
234 | $CurrentBlock['element']['text'] .= "\n".$text; | ||
235 | } | ||
236 | else | ||
237 | { | ||
238 | $Blocks []= $CurrentBlock; | ||
239 | |||
240 | $CurrentBlock = $this->paragraph($Line); | ||
241 | |||
242 | $CurrentBlock['identified'] = true; | ||
243 | } | ||
244 | } | ||
245 | |||
246 | # ~ | ||
247 | |||
248 | if (isset($CurrentBlock['continuable']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete')) | ||
249 | { | ||
250 | $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); | ||
251 | } | ||
252 | |||
253 | # ~ | ||
254 | |||
255 | $Blocks []= $CurrentBlock; | ||
256 | |||
257 | unset($Blocks[0]); | ||
258 | |||
259 | # ~ | ||
260 | |||
261 | $markup = ''; | ||
262 | |||
263 | foreach ($Blocks as $Block) | ||
264 | { | ||
265 | if (isset($Block['hidden'])) | ||
266 | { | ||
267 | continue; | ||
268 | } | ||
269 | |||
270 | $markup .= "\n"; | ||
271 | $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); | ||
272 | } | ||
273 | |||
274 | $markup .= "\n"; | ||
275 | |||
276 | # ~ | ||
277 | |||
278 | return $markup; | ||
279 | } | ||
280 | |||
281 | # | ||
282 | # Code | ||
283 | |||
284 | protected function blockCode($Line, $Block = null) | ||
285 | { | ||
286 | if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) | ||
287 | { | ||
288 | return; | ||
289 | } | ||
290 | |||
291 | if ($Line['indent'] >= 4) | ||
292 | { | ||
293 | $text = substr($Line['body'], 4); | ||
294 | |||
295 | $Block = array( | ||
296 | 'element' => array( | ||
297 | 'name' => 'pre', | ||
298 | 'handler' => 'element', | ||
299 | 'text' => array( | ||
300 | 'name' => 'code', | ||
301 | 'text' => $text, | ||
302 | ), | ||
303 | ), | ||
304 | ); | ||
305 | |||
306 | return $Block; | ||
307 | } | ||
308 | } | ||
309 | |||
310 | protected function blockCodeContinue($Line, $Block) | ||
311 | { | ||
312 | if ($Line['indent'] >= 4) | ||
313 | { | ||
314 | if (isset($Block['interrupted'])) | ||
315 | { | ||
316 | $Block['element']['text']['text'] .= "\n"; | ||
317 | |||
318 | unset($Block['interrupted']); | ||
319 | } | ||
320 | |||
321 | $Block['element']['text']['text'] .= "\n"; | ||
322 | |||
323 | $text = substr($Line['body'], 4); | ||
324 | |||
325 | $Block['element']['text']['text'] .= $text; | ||
326 | |||
327 | return $Block; | ||
328 | } | ||
329 | } | ||
330 | |||
331 | protected function blockCodeComplete($Block) | ||
332 | { | ||
333 | $text = $Block['element']['text']['text']; | ||
334 | |||
335 | $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); | ||
336 | |||
337 | $Block['element']['text']['text'] = $text; | ||
338 | |||
339 | return $Block; | ||
340 | } | ||
341 | |||
342 | # | ||
343 | # Comment | ||
344 | |||
345 | protected function blockComment($Line) | ||
346 | { | ||
347 | if ($this->markupEscaped) | ||
348 | { | ||
349 | return; | ||
350 | } | ||
351 | |||
352 | if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') | ||
353 | { | ||
354 | $Block = array( | ||
355 | 'markup' => $Line['body'], | ||
356 | ); | ||
357 | |||
358 | if (preg_match('/-->$/', $Line['text'])) | ||
359 | { | ||
360 | $Block['closed'] = true; | ||
361 | } | ||
362 | |||
363 | return $Block; | ||
364 | } | ||
365 | } | ||
366 | |||
367 | protected function blockCommentContinue($Line, array $Block) | ||
368 | { | ||
369 | if (isset($Block['closed'])) | ||
370 | { | ||
371 | return; | ||
372 | } | ||
373 | |||
374 | $Block['markup'] .= "\n" . $Line['body']; | ||
375 | |||
376 | if (preg_match('/-->$/', $Line['text'])) | ||
377 | { | ||
378 | $Block['closed'] = true; | ||
379 | } | ||
380 | |||
381 | return $Block; | ||
382 | } | ||
383 | |||
384 | # | ||
385 | # Fenced Code | ||
386 | |||
387 | protected function blockFencedCode($Line) | ||
388 | { | ||
389 | if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) | ||
390 | { | ||
391 | $Element = array( | ||
392 | 'name' => 'code', | ||
393 | 'text' => '', | ||
394 | ); | ||
395 | |||
396 | if (isset($matches[1])) | ||
397 | { | ||
398 | $class = 'language-'.$matches[1]; | ||
399 | |||
400 | $Element['attributes'] = array( | ||
401 | 'class' => $class, | ||
402 | ); | ||
403 | } | ||
404 | |||
405 | $Block = array( | ||
406 | 'char' => $Line['text'][0], | ||
407 | 'element' => array( | ||
408 | 'name' => 'pre', | ||
409 | 'handler' => 'element', | ||
410 | 'text' => $Element, | ||
411 | ), | ||
412 | ); | ||
413 | |||
414 | return $Block; | ||
415 | } | ||
416 | } | ||
417 | |||
418 | protected function blockFencedCodeContinue($Line, $Block) | ||
419 | { | ||
420 | if (isset($Block['complete'])) | ||
421 | { | ||
422 | return; | ||
423 | } | ||
424 | |||
425 | if (isset($Block['interrupted'])) | ||
426 | { | ||
427 | $Block['element']['text']['text'] .= "\n"; | ||
428 | |||
429 | unset($Block['interrupted']); | ||
430 | } | ||
431 | |||
432 | if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) | ||
433 | { | ||
434 | $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); | ||
435 | |||
436 | $Block['complete'] = true; | ||
437 | |||
438 | return $Block; | ||
439 | } | ||
440 | |||
441 | $Block['element']['text']['text'] .= "\n".$Line['body'];; | ||
442 | |||
443 | return $Block; | ||
444 | } | ||
445 | |||
446 | protected function blockFencedCodeComplete($Block) | ||
447 | { | ||
448 | $text = $Block['element']['text']['text']; | ||
449 | |||
450 | $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); | ||
451 | |||
452 | $Block['element']['text']['text'] = $text; | ||
453 | |||
454 | return $Block; | ||
455 | } | ||
456 | |||
457 | # | ||
458 | # Header | ||
459 | |||
460 | protected function blockHeader($Line) | ||
461 | { | ||
462 | if (isset($Line['text'][1])) | ||
463 | { | ||
464 | $level = 1; | ||
465 | |||
466 | while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') | ||
467 | { | ||
468 | $level ++; | ||
469 | } | ||
470 | |||
471 | if ($level > 6) | ||
472 | { | ||
473 | return; | ||
474 | } | ||
475 | |||
476 | $text = trim($Line['text'], '# '); | ||
477 | |||
478 | $Block = array( | ||
479 | 'element' => array( | ||
480 | 'name' => 'h' . min(6, $level), | ||
481 | 'text' => $text, | ||
482 | 'handler' => 'line', | ||
483 | ), | ||
484 | ); | ||
485 | |||
486 | return $Block; | ||
487 | } | ||
488 | } | ||
489 | |||
490 | # | ||
491 | # List | ||
492 | |||
493 | protected function blockList($Line) | ||
494 | { | ||
495 | list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); | ||
496 | |||
497 | if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) | ||
498 | { | ||
499 | $Block = array( | ||
500 | 'indent' => $Line['indent'], | ||
501 | 'pattern' => $pattern, | ||
502 | 'element' => array( | ||
503 | 'name' => $name, | ||
504 | 'handler' => 'elements', | ||
505 | ), | ||
506 | ); | ||
507 | |||
508 | $Block['li'] = array( | ||
509 | 'name' => 'li', | ||
510 | 'handler' => 'li', | ||
511 | 'text' => array( | ||
512 | $matches[2], | ||
513 | ), | ||
514 | ); | ||
515 | |||
516 | $Block['element']['text'] []= & $Block['li']; | ||
517 | |||
518 | return $Block; | ||
519 | } | ||
520 | } | ||
521 | |||
522 | protected function blockListContinue($Line, array $Block) | ||
523 | { | ||
524 | if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) | ||
525 | { | ||
526 | if (isset($Block['interrupted'])) | ||
527 | { | ||
528 | $Block['li']['text'] []= ''; | ||
529 | |||
530 | unset($Block['interrupted']); | ||
531 | } | ||
532 | |||
533 | unset($Block['li']); | ||
534 | |||
535 | $text = isset($matches[1]) ? $matches[1] : ''; | ||
536 | |||
537 | $Block['li'] = array( | ||
538 | 'name' => 'li', | ||
539 | 'handler' => 'li', | ||
540 | 'text' => array( | ||
541 | $text, | ||
542 | ), | ||
543 | ); | ||
544 | |||
545 | $Block['element']['text'] []= & $Block['li']; | ||
546 | |||
547 | return $Block; | ||
548 | } | ||
549 | |||
550 | if ($Line['text'][0] === '[' and $this->blockReference($Line)) | ||
551 | { | ||
552 | return $Block; | ||
553 | } | ||
554 | |||
555 | if ( ! isset($Block['interrupted'])) | ||
556 | { | ||
557 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); | ||
558 | |||
559 | $Block['li']['text'] []= $text; | ||
560 | |||
561 | return $Block; | ||
562 | } | ||
563 | |||
564 | if ($Line['indent'] > 0) | ||
565 | { | ||
566 | $Block['li']['text'] []= ''; | ||
567 | |||
568 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); | ||
569 | |||
570 | $Block['li']['text'] []= $text; | ||
571 | |||
572 | unset($Block['interrupted']); | ||
573 | |||
574 | return $Block; | ||
575 | } | ||
576 | } | ||
577 | |||
578 | # | ||
579 | # Quote | ||
580 | |||
581 | protected function blockQuote($Line) | ||
582 | { | ||
583 | if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) | ||
584 | { | ||
585 | $Block = array( | ||
586 | 'element' => array( | ||
587 | 'name' => 'blockquote', | ||
588 | 'handler' => 'lines', | ||
589 | 'text' => (array) $matches[1], | ||
590 | ), | ||
591 | ); | ||
592 | |||
593 | return $Block; | ||
594 | } | ||
595 | } | ||
596 | |||
597 | protected function blockQuoteContinue($Line, array $Block) | ||
598 | { | ||
599 | if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) | ||
600 | { | ||
601 | if (isset($Block['interrupted'])) | ||
602 | { | ||
603 | $Block['element']['text'] []= ''; | ||
604 | |||
605 | unset($Block['interrupted']); | ||
606 | } | ||
607 | |||
608 | $Block['element']['text'] []= $matches[1]; | ||
609 | |||
610 | return $Block; | ||
611 | } | ||
612 | |||
613 | if ( ! isset($Block['interrupted'])) | ||
614 | { | ||
615 | $Block['element']['text'] []= $Line['text']; | ||
616 | |||
617 | return $Block; | ||
618 | } | ||
619 | } | ||
620 | |||
621 | # | ||
622 | # Rule | ||
623 | |||
624 | protected function blockRule($Line) | ||
625 | { | ||
626 | if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) | ||
627 | { | ||
628 | $Block = array( | ||
629 | 'element' => array( | ||
630 | 'name' => 'hr' | ||
631 | ), | ||
632 | ); | ||
633 | |||
634 | return $Block; | ||
635 | } | ||
636 | } | ||
637 | |||
638 | # | ||
639 | # Setext | ||
640 | |||
641 | protected function blockSetextHeader($Line, array $Block = null) | ||
642 | { | ||
643 | if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) | ||
644 | { | ||
645 | return; | ||
646 | } | ||
647 | |||
648 | if (chop($Line['text'], $Line['text'][0]) === '') | ||
649 | { | ||
650 | $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; | ||
651 | |||
652 | return $Block; | ||
653 | } | ||
654 | } | ||
655 | |||
656 | # | ||
657 | # Markup | ||
658 | |||
659 | protected function blockMarkup($Line) | ||
660 | { | ||
661 | if ($this->markupEscaped) | ||
662 | { | ||
663 | return; | ||
664 | } | ||
665 | |||
666 | if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) | ||
667 | { | ||
668 | $element = strtolower($matches[1]); | ||
669 | |||
670 | if (in_array($element, $this->textLevelElements)) | ||
671 | { | ||
672 | return; | ||
673 | } | ||
674 | |||
675 | $Block = array( | ||
676 | 'name' => $matches[1], | ||
677 | 'depth' => 0, | ||
678 | 'markup' => $Line['text'], | ||
679 | ); | ||
680 | |||
681 | $length = strlen($matches[0]); | ||
682 | |||
683 | $remainder = substr($Line['text'], $length); | ||
684 | |||
685 | if (trim($remainder) === '') | ||
686 | { | ||
687 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) | ||
688 | { | ||
689 | $Block['closed'] = true; | ||
690 | |||
691 | $Block['void'] = true; | ||
692 | } | ||
693 | } | ||
694 | else | ||
695 | { | ||
696 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) | ||
697 | { | ||
698 | return; | ||
699 | } | ||
700 | |||
701 | if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) | ||
702 | { | ||
703 | $Block['closed'] = true; | ||
704 | } | ||
705 | } | ||
706 | |||
707 | return $Block; | ||
708 | } | ||
709 | } | ||
710 | |||
711 | protected function blockMarkupContinue($Line, array $Block) | ||
712 | { | ||
713 | if (isset($Block['closed'])) | ||
714 | { | ||
715 | return; | ||
716 | } | ||
717 | |||
718 | if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open | ||
719 | { | ||
720 | $Block['depth'] ++; | ||
721 | } | ||
722 | |||
723 | if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close | ||
724 | { | ||
725 | if ($Block['depth'] > 0) | ||
726 | { | ||
727 | $Block['depth'] --; | ||
728 | } | ||
729 | else | ||
730 | { | ||
731 | $Block['closed'] = true; | ||
732 | } | ||
733 | } | ||
734 | |||
735 | if (isset($Block['interrupted'])) | ||
736 | { | ||
737 | $Block['markup'] .= "\n"; | ||
738 | |||
739 | unset($Block['interrupted']); | ||
740 | } | ||
741 | |||
742 | $Block['markup'] .= "\n".$Line['body']; | ||
743 | |||
744 | return $Block; | ||
745 | } | ||
746 | |||
747 | # | ||
748 | # Reference | ||
749 | |||
750 | protected function blockReference($Line) | ||
751 | { | ||
752 | if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) | ||
753 | { | ||
754 | $id = strtolower($matches[1]); | ||
755 | |||
756 | $Data = array( | ||
757 | 'url' => $matches[2], | ||
758 | 'title' => null, | ||
759 | ); | ||
760 | |||
761 | if (isset($matches[3])) | ||
762 | { | ||
763 | $Data['title'] = $matches[3]; | ||
764 | } | ||
765 | |||
766 | $this->DefinitionData['Reference'][$id] = $Data; | ||
767 | |||
768 | $Block = array( | ||
769 | 'hidden' => true, | ||
770 | ); | ||
771 | |||
772 | return $Block; | ||
773 | } | ||
774 | } | ||
775 | |||
776 | # | ||
777 | # Table | ||
778 | |||
779 | protected function blockTable($Line, array $Block = null) | ||
780 | { | ||
781 | if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) | ||
782 | { | ||
783 | return; | ||
784 | } | ||
785 | |||
786 | if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') | ||
787 | { | ||
788 | $alignments = array(); | ||
789 | |||
790 | $divider = $Line['text']; | ||
791 | |||
792 | $divider = trim($divider); | ||
793 | $divider = trim($divider, '|'); | ||
794 | |||
795 | $dividerCells = explode('|', $divider); | ||
796 | |||
797 | foreach ($dividerCells as $dividerCell) | ||
798 | { | ||
799 | $dividerCell = trim($dividerCell); | ||
800 | |||
801 | if ($dividerCell === '') | ||
802 | { | ||
803 | continue; | ||
804 | } | ||
805 | |||
806 | $alignment = null; | ||
807 | |||
808 | if ($dividerCell[0] === ':') | ||
809 | { | ||
810 | $alignment = 'left'; | ||
811 | } | ||
812 | |||
813 | if (substr($dividerCell, - 1) === ':') | ||
814 | { | ||
815 | $alignment = $alignment === 'left' ? 'center' : 'right'; | ||
816 | } | ||
817 | |||
818 | $alignments []= $alignment; | ||
819 | } | ||
820 | |||
821 | # ~ | ||
822 | |||
823 | $HeaderElements = array(); | ||
824 | |||
825 | $header = $Block['element']['text']; | ||
826 | |||
827 | $header = trim($header); | ||
828 | $header = trim($header, '|'); | ||
829 | |||
830 | $headerCells = explode('|', $header); | ||
831 | |||
832 | foreach ($headerCells as $index => $headerCell) | ||
833 | { | ||
834 | $headerCell = trim($headerCell); | ||
835 | |||
836 | $HeaderElement = array( | ||
837 | 'name' => 'th', | ||
838 | 'text' => $headerCell, | ||
839 | 'handler' => 'line', | ||
840 | ); | ||
841 | |||
842 | if (isset($alignments[$index])) | ||
843 | { | ||
844 | $alignment = $alignments[$index]; | ||
845 | |||
846 | $HeaderElement['attributes'] = array( | ||
847 | 'style' => 'text-align: '.$alignment.';', | ||
848 | ); | ||
849 | } | ||
850 | |||
851 | $HeaderElements []= $HeaderElement; | ||
852 | } | ||
853 | |||
854 | # ~ | ||
855 | |||
856 | $Block = array( | ||
857 | 'alignments' => $alignments, | ||
858 | 'identified' => true, | ||
859 | 'element' => array( | ||
860 | 'name' => 'table', | ||
861 | 'handler' => 'elements', | ||
862 | ), | ||
863 | ); | ||
864 | |||
865 | $Block['element']['text'] []= array( | ||
866 | 'name' => 'thead', | ||
867 | 'handler' => 'elements', | ||
868 | ); | ||
869 | |||
870 | $Block['element']['text'] []= array( | ||
871 | 'name' => 'tbody', | ||
872 | 'handler' => 'elements', | ||
873 | 'text' => array(), | ||
874 | ); | ||
875 | |||
876 | $Block['element']['text'][0]['text'] []= array( | ||
877 | 'name' => 'tr', | ||
878 | 'handler' => 'elements', | ||
879 | 'text' => $HeaderElements, | ||
880 | ); | ||
881 | |||
882 | return $Block; | ||
883 | } | ||
884 | } | ||
885 | |||
886 | protected function blockTableContinue($Line, array $Block) | ||
887 | { | ||
888 | if (isset($Block['interrupted'])) | ||
889 | { | ||
890 | return; | ||
891 | } | ||
892 | |||
893 | if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) | ||
894 | { | ||
895 | $Elements = array(); | ||
896 | |||
897 | $row = $Line['text']; | ||
898 | |||
899 | $row = trim($row); | ||
900 | $row = trim($row, '|'); | ||
901 | |||
902 | preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); | ||
903 | |||
904 | foreach ($matches[0] as $index => $cell) | ||
905 | { | ||
906 | $cell = trim($cell); | ||
907 | |||
908 | $Element = array( | ||
909 | 'name' => 'td', | ||
910 | 'handler' => 'line', | ||
911 | 'text' => $cell, | ||
912 | ); | ||
913 | |||
914 | if (isset($Block['alignments'][$index])) | ||
915 | { | ||
916 | $Element['attributes'] = array( | ||
917 | 'style' => 'text-align: '.$Block['alignments'][$index].';', | ||
918 | ); | ||
919 | } | ||
920 | |||
921 | $Elements []= $Element; | ||
922 | } | ||
923 | |||
924 | $Element = array( | ||
925 | 'name' => 'tr', | ||
926 | 'handler' => 'elements', | ||
927 | 'text' => $Elements, | ||
928 | ); | ||
929 | |||
930 | $Block['element']['text'][1]['text'] []= $Element; | ||
931 | |||
932 | return $Block; | ||
933 | } | ||
934 | } | ||
935 | |||
936 | # | ||
937 | # ~ | ||
938 | # | ||
939 | |||
940 | protected function paragraph($Line) | ||
941 | { | ||
942 | $Block = array( | ||
943 | 'element' => array( | ||
944 | 'name' => 'p', | ||
945 | 'text' => $Line['text'], | ||
946 | 'handler' => 'line', | ||
947 | ), | ||
948 | ); | ||
949 | |||
950 | return $Block; | ||
951 | } | ||
952 | |||
953 | # | ||
954 | # Inline Elements | ||
955 | # | ||
956 | |||
957 | protected $InlineTypes = array( | ||
958 | '"' => array('SpecialCharacter'), | ||
959 | '!' => array('Image'), | ||
960 | '&' => array('SpecialCharacter'), | ||
961 | '*' => array('Emphasis'), | ||
962 | ':' => array('Url'), | ||
963 | '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), | ||
964 | '>' => array('SpecialCharacter'), | ||
965 | '[' => array('Link'), | ||
966 | '_' => array('Emphasis'), | ||
967 | '`' => array('Code'), | ||
968 | '~' => array('Strikethrough'), | ||
969 | '\\' => array('EscapeSequence'), | ||
970 | ); | ||
971 | |||
972 | # ~ | ||
973 | |||
974 | protected $inlineMarkerList = '!"*_&[:<>`~\\'; | ||
975 | |||
976 | # | ||
977 | # ~ | ||
978 | # | ||
979 | |||
980 | public function line($text) | ||
981 | { | ||
982 | $markup = ''; | ||
983 | |||
984 | # $excerpt is based on the first occurrence of a marker | ||
985 | |||
986 | while ($excerpt = strpbrk($text, $this->inlineMarkerList)) | ||
987 | { | ||
988 | $marker = $excerpt[0]; | ||
989 | |||
990 | $markerPosition = strpos($text, $marker); | ||
991 | |||
992 | $Excerpt = array('text' => $excerpt, 'context' => $text); | ||
993 | |||
994 | foreach ($this->InlineTypes[$marker] as $inlineType) | ||
995 | { | ||
996 | $Inline = $this->{'inline'.$inlineType}($Excerpt); | ||
997 | |||
998 | if ( ! isset($Inline)) | ||
999 | { | ||
1000 | continue; | ||
1001 | } | ||
1002 | |||
1003 | # makes sure that the inline belongs to "our" marker | ||
1004 | |||
1005 | if (isset($Inline['position']) and $Inline['position'] > $markerPosition) | ||
1006 | { | ||
1007 | continue; | ||
1008 | } | ||
1009 | |||
1010 | # sets a default inline position | ||
1011 | |||
1012 | if ( ! isset($Inline['position'])) | ||
1013 | { | ||
1014 | $Inline['position'] = $markerPosition; | ||
1015 | } | ||
1016 | |||
1017 | # the text that comes before the inline | ||
1018 | $unmarkedText = substr($text, 0, $Inline['position']); | ||
1019 | |||
1020 | # compile the unmarked text | ||
1021 | $markup .= $this->unmarkedText($unmarkedText); | ||
1022 | |||
1023 | # compile the inline | ||
1024 | $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); | ||
1025 | |||
1026 | # remove the examined text | ||
1027 | $text = substr($text, $Inline['position'] + $Inline['extent']); | ||
1028 | |||
1029 | continue 2; | ||
1030 | } | ||
1031 | |||
1032 | # the marker does not belong to an inline | ||
1033 | |||
1034 | $unmarkedText = substr($text, 0, $markerPosition + 1); | ||
1035 | |||
1036 | $markup .= $this->unmarkedText($unmarkedText); | ||
1037 | |||
1038 | $text = substr($text, $markerPosition + 1); | ||
1039 | } | ||
1040 | |||
1041 | $markup .= $this->unmarkedText($text); | ||
1042 | |||
1043 | return $markup; | ||
1044 | } | ||
1045 | |||
1046 | # | ||
1047 | # ~ | ||
1048 | # | ||
1049 | |||
1050 | protected function inlineCode($Excerpt) | ||
1051 | { | ||
1052 | $marker = $Excerpt['text'][0]; | ||
1053 | |||
1054 | if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches)) | ||
1055 | { | ||
1056 | $text = $matches[2]; | ||
1057 | $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); | ||
1058 | $text = preg_replace("/[ ]*\n/", ' ', $text); | ||
1059 | |||
1060 | return array( | ||
1061 | 'extent' => strlen($matches[0]), | ||
1062 | 'element' => array( | ||
1063 | 'name' => 'code', | ||
1064 | 'text' => $text, | ||
1065 | ), | ||
1066 | ); | ||
1067 | } | ||
1068 | } | ||
1069 | |||
1070 | protected function inlineEmailTag($Excerpt) | ||
1071 | { | ||
1072 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) | ||
1073 | { | ||
1074 | $url = $matches[1]; | ||
1075 | |||
1076 | if ( ! isset($matches[2])) | ||
1077 | { | ||
1078 | $url = 'mailto:' . $url; | ||
1079 | } | ||
1080 | |||
1081 | return array( | ||
1082 | 'extent' => strlen($matches[0]), | ||
1083 | 'element' => array( | ||
1084 | 'name' => 'a', | ||
1085 | 'text' => $matches[1], | ||
1086 | 'attributes' => array( | ||
1087 | 'href' => $url, | ||
1088 | ), | ||
1089 | ), | ||
1090 | ); | ||
1091 | } | ||
1092 | } | ||
1093 | |||
1094 | protected function inlineEmphasis($Excerpt) | ||
1095 | { | ||
1096 | if ( ! isset($Excerpt['text'][1])) | ||
1097 | { | ||
1098 | return; | ||
1099 | } | ||
1100 | |||
1101 | $marker = $Excerpt['text'][0]; | ||
1102 | |||
1103 | if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) | ||
1104 | { | ||
1105 | $emphasis = 'strong'; | ||
1106 | } | ||
1107 | elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) | ||
1108 | { | ||
1109 | $emphasis = 'em'; | ||
1110 | } | ||
1111 | else | ||
1112 | { | ||
1113 | return; | ||
1114 | } | ||
1115 | |||
1116 | return array( | ||
1117 | 'extent' => strlen($matches[0]), | ||
1118 | 'element' => array( | ||
1119 | 'name' => $emphasis, | ||
1120 | 'handler' => 'line', | ||
1121 | 'text' => $matches[1], | ||
1122 | ), | ||
1123 | ); | ||
1124 | } | ||
1125 | |||
1126 | protected function inlineEscapeSequence($Excerpt) | ||
1127 | { | ||
1128 | if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) | ||
1129 | { | ||
1130 | return array( | ||
1131 | 'markup' => $Excerpt['text'][1], | ||
1132 | 'extent' => 2, | ||
1133 | ); | ||
1134 | } | ||
1135 | } | ||
1136 | |||
1137 | protected function inlineImage($Excerpt) | ||
1138 | { | ||
1139 | if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') | ||
1140 | { | ||
1141 | return; | ||
1142 | } | ||
1143 | |||
1144 | $Excerpt['text']= substr($Excerpt['text'], 1); | ||
1145 | |||
1146 | $Link = $this->inlineLink($Excerpt); | ||
1147 | |||
1148 | if ($Link === null) | ||
1149 | { | ||
1150 | return; | ||
1151 | } | ||
1152 | |||
1153 | $Inline = array( | ||
1154 | 'extent' => $Link['extent'] + 1, | ||
1155 | 'element' => array( | ||
1156 | 'name' => 'img', | ||
1157 | 'attributes' => array( | ||
1158 | 'src' => $Link['element']['attributes']['href'], | ||
1159 | 'alt' => $Link['element']['text'], | ||
1160 | ), | ||
1161 | ), | ||
1162 | ); | ||
1163 | |||
1164 | $Inline['element']['attributes'] += $Link['element']['attributes']; | ||
1165 | |||
1166 | unset($Inline['element']['attributes']['href']); | ||
1167 | |||
1168 | return $Inline; | ||
1169 | } | ||
1170 | |||
1171 | protected function inlineLink($Excerpt) | ||
1172 | { | ||
1173 | $Element = array( | ||
1174 | 'name' => 'a', | ||
1175 | 'handler' => 'line', | ||
1176 | 'text' => null, | ||
1177 | 'attributes' => array( | ||
1178 | 'href' => null, | ||
1179 | 'title' => null, | ||
1180 | ), | ||
1181 | ); | ||
1182 | |||
1183 | $extent = 0; | ||
1184 | |||
1185 | $remainder = $Excerpt['text']; | ||
1186 | |||
1187 | if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches)) | ||
1188 | { | ||
1189 | $Element['text'] = $matches[1]; | ||
1190 | |||
1191 | $extent += strlen($matches[0]); | ||
1192 | |||
1193 | $remainder = substr($remainder, $extent); | ||
1194 | } | ||
1195 | else | ||
1196 | { | ||
1197 | return; | ||
1198 | } | ||
1199 | |||
1200 | if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches)) | ||
1201 | { | ||
1202 | $Element['attributes']['href'] = $matches[1]; | ||
1203 | |||
1204 | if (isset($matches[2])) | ||
1205 | { | ||
1206 | $Element['attributes']['title'] = substr($matches[2], 1, - 1); | ||
1207 | } | ||
1208 | |||
1209 | $extent += strlen($matches[0]); | ||
1210 | } | ||
1211 | else | ||
1212 | { | ||
1213 | if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) | ||
1214 | { | ||
1215 | $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; | ||
1216 | $definition = strtolower($definition); | ||
1217 | |||
1218 | $extent += strlen($matches[0]); | ||
1219 | } | ||
1220 | else | ||
1221 | { | ||
1222 | $definition = strtolower($Element['text']); | ||
1223 | } | ||
1224 | |||
1225 | if ( ! isset($this->DefinitionData['Reference'][$definition])) | ||
1226 | { | ||
1227 | return; | ||
1228 | } | ||
1229 | |||
1230 | $Definition = $this->DefinitionData['Reference'][$definition]; | ||
1231 | |||
1232 | $Element['attributes']['href'] = $Definition['url']; | ||
1233 | $Element['attributes']['title'] = $Definition['title']; | ||
1234 | } | ||
1235 | |||
1236 | $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']); | ||
1237 | |||
1238 | return array( | ||
1239 | 'extent' => $extent, | ||
1240 | 'element' => $Element, | ||
1241 | ); | ||
1242 | } | ||
1243 | |||
1244 | protected function inlineMarkup($Excerpt) | ||
1245 | { | ||
1246 | if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) | ||
1247 | { | ||
1248 | return; | ||
1249 | } | ||
1250 | |||
1251 | if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches)) | ||
1252 | { | ||
1253 | return array( | ||
1254 | 'markup' => $matches[0], | ||
1255 | 'extent' => strlen($matches[0]), | ||
1256 | ); | ||
1257 | } | ||
1258 | |||
1259 | if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches)) | ||
1260 | { | ||
1261 | return array( | ||
1262 | 'markup' => $matches[0], | ||
1263 | 'extent' => strlen($matches[0]), | ||
1264 | ); | ||
1265 | } | ||
1266 | |||
1267 | if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) | ||
1268 | { | ||
1269 | return array( | ||
1270 | 'markup' => $matches[0], | ||
1271 | 'extent' => strlen($matches[0]), | ||
1272 | ); | ||
1273 | } | ||
1274 | } | ||
1275 | |||
1276 | protected function inlineSpecialCharacter($Excerpt) | ||
1277 | { | ||
1278 | if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) | ||
1279 | { | ||
1280 | return array( | ||
1281 | 'markup' => '&', | ||
1282 | 'extent' => 1, | ||
1283 | ); | ||
1284 | } | ||
1285 | |||
1286 | $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); | ||
1287 | |||
1288 | if (isset($SpecialCharacter[$Excerpt['text'][0]])) | ||
1289 | { | ||
1290 | return array( | ||
1291 | 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', | ||
1292 | 'extent' => 1, | ||
1293 | ); | ||
1294 | } | ||
1295 | } | ||
1296 | |||
1297 | protected function inlineStrikethrough($Excerpt) | ||
1298 | { | ||
1299 | if ( ! isset($Excerpt['text'][1])) | ||
1300 | { | ||
1301 | return; | ||
1302 | } | ||
1303 | |||
1304 | if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) | ||
1305 | { | ||
1306 | return array( | ||
1307 | 'extent' => strlen($matches[0]), | ||
1308 | 'element' => array( | ||
1309 | 'name' => 'del', | ||
1310 | 'text' => $matches[1], | ||
1311 | 'handler' => 'line', | ||
1312 | ), | ||
1313 | ); | ||
1314 | } | ||
1315 | } | ||
1316 | |||
1317 | protected function inlineUrl($Excerpt) | ||
1318 | { | ||
1319 | if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') | ||
1320 | { | ||
1321 | return; | ||
1322 | } | ||
1323 | |||
1324 | if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) | ||
1325 | { | ||
1326 | $Inline = array( | ||
1327 | 'extent' => strlen($matches[0][0]), | ||
1328 | 'position' => $matches[0][1], | ||
1329 | 'element' => array( | ||
1330 | 'name' => 'a', | ||
1331 | 'text' => $matches[0][0], | ||
1332 | 'attributes' => array( | ||
1333 | 'href' => $matches[0][0], | ||
1334 | ), | ||
1335 | ), | ||
1336 | ); | ||
1337 | |||
1338 | return $Inline; | ||
1339 | } | ||
1340 | } | ||
1341 | |||
1342 | protected function inlineUrlTag($Excerpt) | ||
1343 | { | ||
1344 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) | ||
1345 | { | ||
1346 | $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]); | ||
1347 | |||
1348 | return array( | ||
1349 | 'extent' => strlen($matches[0]), | ||
1350 | 'element' => array( | ||
1351 | 'name' => 'a', | ||
1352 | 'text' => $url, | ||
1353 | 'attributes' => array( | ||
1354 | 'href' => $url, | ||
1355 | ), | ||
1356 | ), | ||
1357 | ); | ||
1358 | } | ||
1359 | } | ||
1360 | |||
1361 | # ~ | ||
1362 | |||
1363 | protected function unmarkedText($text) | ||
1364 | { | ||
1365 | if ($this->breaksEnabled) | ||
1366 | { | ||
1367 | $text = preg_replace('/[ ]*\n/', "<br />\n", $text); | ||
1368 | } | ||
1369 | else | ||
1370 | { | ||
1371 | $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text); | ||
1372 | $text = str_replace(" \n", "\n", $text); | ||
1373 | } | ||
1374 | |||
1375 | return $text; | ||
1376 | } | ||
1377 | |||
1378 | # | ||
1379 | # Handlers | ||
1380 | # | ||
1381 | |||
1382 | protected function element(array $Element) | ||
1383 | { | ||
1384 | $markup = '<'.$Element['name']; | ||
1385 | |||
1386 | if (isset($Element['attributes'])) | ||
1387 | { | ||
1388 | foreach ($Element['attributes'] as $name => $value) | ||
1389 | { | ||
1390 | if ($value === null) | ||
1391 | { | ||
1392 | continue; | ||
1393 | } | ||
1394 | |||
1395 | $markup .= ' '.$name.'="'.$value.'"'; | ||
1396 | } | ||
1397 | } | ||
1398 | |||
1399 | if (isset($Element['text'])) | ||
1400 | { | ||
1401 | $markup .= '>'; | ||
1402 | |||
1403 | if (isset($Element['handler'])) | ||
1404 | { | ||
1405 | $markup .= $this->{$Element['handler']}($Element['text']); | ||
1406 | } | ||
1407 | else | ||
1408 | { | ||
1409 | $markup .= $Element['text']; | ||
1410 | } | ||
1411 | |||
1412 | $markup .= '</'.$Element['name'].'>'; | ||
1413 | } | ||
1414 | else | ||
1415 | { | ||
1416 | $markup .= ' />'; | ||
1417 | } | ||
1418 | |||
1419 | return $markup; | ||
1420 | } | ||
1421 | |||
1422 | protected function elements(array $Elements) | ||
1423 | { | ||
1424 | $markup = ''; | ||
1425 | |||
1426 | foreach ($Elements as $Element) | ||
1427 | { | ||
1428 | $markup .= "\n" . $this->element($Element); | ||
1429 | } | ||
1430 | |||
1431 | $markup .= "\n"; | ||
1432 | |||
1433 | return $markup; | ||
1434 | } | ||
1435 | |||
1436 | # ~ | ||
1437 | |||
1438 | protected function li($lines) | ||
1439 | { | ||
1440 | $markup = $this->lines($lines); | ||
1441 | |||
1442 | $trimmedMarkup = trim($markup); | ||
1443 | |||
1444 | if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>') | ||
1445 | { | ||
1446 | $markup = $trimmedMarkup; | ||
1447 | $markup = substr($markup, 3); | ||
1448 | |||
1449 | $position = strpos($markup, "</p>"); | ||
1450 | |||
1451 | $markup = substr_replace($markup, '', $position, 4); | ||
1452 | } | ||
1453 | |||
1454 | return $markup; | ||
1455 | } | ||
1456 | |||
1457 | # | ||
1458 | # Deprecated Methods | ||
1459 | # | ||
1460 | |||
1461 | function parse($text) | ||
1462 | { | ||
1463 | $markup = $this->text($text); | ||
1464 | |||
1465 | return $markup; | ||
1466 | } | ||
1467 | |||
1468 | # | ||
1469 | # Static Methods | ||
1470 | # | ||
1471 | |||
1472 | static function instance($name = 'default') | ||
1473 | { | ||
1474 | if (isset(self::$instances[$name])) | ||
1475 | { | ||
1476 | return self::$instances[$name]; | ||
1477 | } | ||
1478 | |||
1479 | $instance = new static(); | ||
1480 | |||
1481 | self::$instances[$name] = $instance; | ||
1482 | |||
1483 | return $instance; | ||
1484 | } | ||
1485 | |||
1486 | private static $instances = array(); | ||
1487 | |||
1488 | # | ||
1489 | # Fields | ||
1490 | # | ||
1491 | |||
1492 | protected $DefinitionData; | ||
1493 | |||
1494 | # | ||
1495 | # Read-Only | ||
1496 | |||
1497 | protected $specialCharacters = array( | ||
1498 | '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', | ||
1499 | ); | ||
1500 | |||
1501 | protected $StrongRegex = array( | ||
1502 | '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', | ||
1503 | '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', | ||
1504 | ); | ||
1505 | |||
1506 | protected $EmRegex = array( | ||
1507 | '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', | ||
1508 | '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', | ||
1509 | ); | ||
1510 | |||
1511 | protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; | ||
1512 | |||
1513 | protected $voidElements = array( | ||
1514 | 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', | ||
1515 | ); | ||
1516 | |||
1517 | protected $textLevelElements = array( | ||
1518 | 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', | ||
1519 | 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', | ||
1520 | 'i', 'rp', 'del', 'code', 'strike', 'marquee', | ||
1521 | 'q', 'rt', 'ins', 'font', 'strong', | ||
1522 | 's', 'tt', 'sub', 'mark', | ||
1523 | 'u', 'xm', 'sup', 'nobr', | ||
1524 | 'var', 'ruby', | ||
1525 | 'wbr', 'span', | ||
1526 | 'time', | ||
1527 | ); | ||
1528 | } \ No newline at end of file | ||
diff --git a/plugins/markdown/README.md b/plugins/markdown/README.md index 4f021871..bc9427e2 100644 --- a/plugins/markdown/README.md +++ b/plugins/markdown/README.md | |||
@@ -20,26 +20,65 @@ The directory structure should look like: | |||
20 | |--- markdown.css | 20 | |--- markdown.css |
21 | |--- markdown.meta | 21 | |--- markdown.meta |
22 | |--- markdown.php | 22 | |--- markdown.php |
23 | |--- Parsedown.php | ||
24 | |--- README.md | 23 | |--- README.md |
25 | ``` | 24 | ``` |
26 | 25 | ||
27 | To enable the plugin, just check it in the plugin administration page. | 26 | To enable the plugin, just check it in the plugin administration page. |
28 | 27 | ||
29 | You can also add `markdown` to your list of enabled plugins in `data/config.php` | 28 | You can also add `markdown` to your list of enabled plugins in `data/config.json.php` |
30 | (`ENABLED_PLUGINS` array). | 29 | (`general.enabled_plugins` list). |
31 | 30 | ||
32 | This should look like: | 31 | This should look like: |
33 | 32 | ||
34 | ``` | 33 | ``` |
35 | $GLOBALS['config']['ENABLED_PLUGINS'] = array('qrcode', 'any_other_plugin', 'markdown') | 34 | "general": { |
35 | "enabled_plugins": [ | ||
36 | "markdown", | ||
37 | [...] | ||
38 | ], | ||
39 | } | ||
36 | ``` | 40 | ``` |
37 | 41 | ||
42 | Parsedown parsing library is imported using Composer. If you installed Shaarli using `git`, | ||
43 | or the `master` branch, run | ||
44 | |||
45 | composer update --no-dev --prefer-dist | ||
46 | |||
38 | ### No Markdown tag | 47 | ### No Markdown tag |
39 | 48 | ||
40 | If the tag `.nomarkdown` is set for a shaare, it won't be converted to Markdown syntax. | 49 | If the tag `nomarkdown` is set for a shaare, it won't be converted to Markdown syntax. |
41 | 50 | ||
42 | > Note: it's a private tag (leading dot), so it won't be displayed to visitors. | 51 | > Note: this is a special tag, so it won't be displayed in link list. |
52 | |||
53 | ### HTML escape | ||
54 | |||
55 | By default, HTML tags are escaped. You can enable HTML tags rendering | ||
56 | by setting `security.markdwon_escape` to `false` in `data/config.json.php`: | ||
57 | |||
58 | ```json | ||
59 | { | ||
60 | "security": { | ||
61 | "markdown_escape": false | ||
62 | } | ||
63 | } | ||
64 | ``` | ||
65 | |||
66 | With this setting, Markdown support HTML tags. For example: | ||
67 | |||
68 | > <strong>strong</strong><strike>strike</strike> | ||
69 | |||
70 | Will render as: | ||
71 | |||
72 | > <strong>strong</strong><strike>strike</strike> | ||
73 | |||
74 | |||
75 | **Warning:** | ||
76 | |||
77 | * This setting might present **security risks** (XSS) on shared instances, even though tags | ||
78 | such as script, iframe, etc should be disabled. | ||
79 | * If you want to shaare HTML code, it is necessary to use inline code or code blocks. | ||
80 | * If your shaared descriptions contained HTML tags before enabling the markdown plugin, | ||
81 | enabling it might break your page. | ||
43 | 82 | ||
44 | ### Known issue | 83 | ### Known issue |
45 | 84 | ||
diff --git a/plugins/markdown/markdown.meta b/plugins/markdown/markdown.meta index e3904ed8..8df2ed0b 100644 --- a/plugins/markdown/markdown.meta +++ b/plugins/markdown/markdown.meta | |||
@@ -1 +1,4 @@ | |||
1 | description="Render shaare description with Markdown syntax." | 1 | description="Render shaare description with Markdown syntax.<br><strong>Warning</strong>: |
2 | If your shaared descriptions containing HTML tags before enabling the markdown plugin, | ||
3 | enabling it might break your page. | ||
4 | See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering\">README</a>." | ||
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php index 57fcce32..de7c823d 100644 --- a/plugins/markdown/markdown.php +++ b/plugins/markdown/markdown.php | |||
@@ -6,30 +6,28 @@ | |||
6 | * Shaare's descriptions are parsed with Markdown. | 6 | * Shaare's descriptions are parsed with Markdown. |
7 | */ | 7 | */ |
8 | 8 | ||
9 | require_once 'Parsedown.php'; | ||
10 | |||
11 | /* | 9 | /* |
12 | * If this tag is used on a shaare, the description won't be processed by Parsedown. | 10 | * If this tag is used on a shaare, the description won't be processed by Parsedown. |
13 | * Using a private tag so it won't appear for visitors. | ||
14 | */ | 11 | */ |
15 | define('NO_MD_TAG', '.nomarkdown'); | 12 | define('NO_MD_TAG', 'nomarkdown'); |
16 | 13 | ||
17 | /** | 14 | /** |
18 | * Parse linklist descriptions. | 15 | * Parse linklist descriptions. |
19 | * | 16 | * |
20 | * @param array $data linklist data. | 17 | * @param array $data linklist data. |
18 | * @param ConfigManager $conf instance. | ||
21 | * | 19 | * |
22 | * @return mixed linklist data parsed in markdown (and converted to HTML). | 20 | * @return mixed linklist data parsed in markdown (and converted to HTML). |
23 | */ | 21 | */ |
24 | function hook_markdown_render_linklist($data) | 22 | function hook_markdown_render_linklist($data, $conf) |
25 | { | 23 | { |
26 | foreach ($data['links'] as &$value) { | 24 | foreach ($data['links'] as &$value) { |
27 | if (!empty($value['tags']) && noMarkdownTag($value['tags'])) { | 25 | if (!empty($value['tags']) && noMarkdownTag($value['tags'])) { |
26 | $value = stripNoMarkdownTag($value); | ||
28 | continue; | 27 | continue; |
29 | } | 28 | } |
30 | $value['description'] = process_markdown($value['description']); | 29 | $value['description'] = process_markdown($value['description'], $conf->get('security.markdown_escape', true)); |
31 | } | 30 | } |
32 | |||
33 | return $data; | 31 | return $data; |
34 | } | 32 | } |
35 | 33 | ||
@@ -37,16 +35,18 @@ function hook_markdown_render_linklist($data) | |||
37 | * Parse feed linklist descriptions. | 35 | * Parse feed linklist descriptions. |
38 | * | 36 | * |
39 | * @param array $data linklist data. | 37 | * @param array $data linklist data. |
38 | * @param ConfigManager $conf instance. | ||
40 | * | 39 | * |
41 | * @return mixed linklist data parsed in markdown (and converted to HTML). | 40 | * @return mixed linklist data parsed in markdown (and converted to HTML). |
42 | */ | 41 | */ |
43 | function hook_markdown_render_feed($data) | 42 | function hook_markdown_render_feed($data, $conf) |
44 | { | 43 | { |
45 | foreach ($data['links'] as &$value) { | 44 | foreach ($data['links'] as &$value) { |
46 | if (!empty($value['tags']) && noMarkdownTag($value['tags'])) { | 45 | if (!empty($value['tags']) && noMarkdownTag($value['tags'])) { |
46 | $value = stripNoMarkdownTag($value); | ||
47 | continue; | 47 | continue; |
48 | } | 48 | } |
49 | $value['description'] = process_markdown($value['description']); | 49 | $value['description'] = process_markdown($value['description'], $conf->get('security.markdown_escape', true)); |
50 | } | 50 | } |
51 | 51 | ||
52 | return $data; | 52 | return $data; |
@@ -55,19 +55,24 @@ function hook_markdown_render_feed($data) | |||
55 | /** | 55 | /** |
56 | * Parse daily descriptions. | 56 | * Parse daily descriptions. |
57 | * | 57 | * |
58 | * @param array $data daily data. | 58 | * @param array $data daily data. |
59 | * @param ConfigManager $conf instance. | ||
59 | * | 60 | * |
60 | * @return mixed daily data parsed in markdown (and converted to HTML). | 61 | * @return mixed daily data parsed in markdown (and converted to HTML). |
61 | */ | 62 | */ |
62 | function hook_markdown_render_daily($data) | 63 | function hook_markdown_render_daily($data, $conf) |
63 | { | 64 | { |
64 | // Manipulate columns data | 65 | // Manipulate columns data |
65 | foreach ($data['cols'] as &$value) { | 66 | foreach ($data['cols'] as &$value) { |
66 | foreach ($value as &$value2) { | 67 | foreach ($value as &$value2) { |
67 | if (!empty($value2['tags']) && noMarkdownTag($value2['tags'])) { | 68 | if (!empty($value2['tags']) && noMarkdownTag($value2['tags'])) { |
69 | $value2 = stripNoMarkdownTag($value2); | ||
68 | continue; | 70 | continue; |
69 | } | 71 | } |
70 | $value2['formatedDescription'] = process_markdown($value2['formatedDescription']); | 72 | $value2['formatedDescription'] = process_markdown( |
73 | $value2['formatedDescription'], | ||
74 | $conf->get('security.markdown_escape', true) | ||
75 | ); | ||
71 | } | 76 | } |
72 | } | 77 | } |
73 | 78 | ||
@@ -83,7 +88,30 @@ function hook_markdown_render_daily($data) | |||
83 | */ | 88 | */ |
84 | function noMarkdownTag($tags) | 89 | function noMarkdownTag($tags) |
85 | { | 90 | { |
86 | return strpos($tags, NO_MD_TAG) !== false; | 91 | return preg_match('/(^|\s)'. NO_MD_TAG .'(\s|$)/', $tags); |
92 | } | ||
93 | |||
94 | /** | ||
95 | * Remove the no-markdown meta tag so it won't be displayed. | ||
96 | * | ||
97 | * @param array $link Link data. | ||
98 | * | ||
99 | * @return array Updated link without no markdown tag. | ||
100 | */ | ||
101 | function stripNoMarkdownTag($link) | ||
102 | { | ||
103 | if (! empty($link['taglist'])) { | ||
104 | $offset = array_search(NO_MD_TAG, $link['taglist']); | ||
105 | if ($offset !== false) { | ||
106 | unset($link['taglist'][$offset]); | ||
107 | } | ||
108 | } | ||
109 | |||
110 | if (!empty($link['tags'])) { | ||
111 | str_replace(NO_MD_TAG, '', $link['tags']); | ||
112 | } | ||
113 | |||
114 | return $link; | ||
87 | } | 115 | } |
88 | 116 | ||
89 | /** | 117 | /** |
@@ -138,7 +166,45 @@ function hook_markdown_render_editlink($data) | |||
138 | */ | 166 | */ |
139 | function reverse_text2clickable($description) | 167 | function reverse_text2clickable($description) |
140 | { | 168 | { |
141 | return preg_replace('!<a +href="([^ ]*)">[^ ]+</a>!m', '$1', $description); | 169 | $descriptionLines = explode(PHP_EOL, $description); |
170 | $descriptionOut = ''; | ||
171 | $codeBlockOn = false; | ||
172 | $lineCount = 0; | ||
173 | |||
174 | foreach ($descriptionLines as $descriptionLine) { | ||
175 | // Detect line of code: starting with 4 spaces, | ||
176 | // except lists which can start with +/*/- or `2.` after spaces. | ||
177 | $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0; | ||
178 | // Detect and toggle block of code | ||
179 | if (!$codeBlockOn) { | ||
180 | $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0; | ||
181 | } | ||
182 | elseif (preg_match('/^```/', $descriptionLine) > 0) { | ||
183 | $codeBlockOn = false; | ||
184 | } | ||
185 | |||
186 | $hashtagTitle = ' title="Hashtag [^"]+"'; | ||
187 | // Reverse `inline code` hashtags. | ||
188 | $descriptionLine = preg_replace( | ||
189 | '!(`[^`\n]*)<a href="[^ ]*"'. $hashtagTitle .'>([^<]+)</a>([^`\n]*`)!m', | ||
190 | '$1$2$3', | ||
191 | $descriptionLine | ||
192 | ); | ||
193 | |||
194 | // Reverse all links in code blocks, only non hashtag elsewhere. | ||
195 | $hashtagFilter = (!$codeBlockOn && !$codeLineOn) ? '(?!'. $hashtagTitle .')': '(?:'. $hashtagTitle .')?'; | ||
196 | $descriptionLine = preg_replace( | ||
197 | '#<a href="[^ ]*"'. $hashtagFilter .'>([^<]+)</a>#m', | ||
198 | '$1', | ||
199 | $descriptionLine | ||
200 | ); | ||
201 | |||
202 | $descriptionOut .= $descriptionLine; | ||
203 | if ($lineCount++ < count($descriptionLines) - 1) { | ||
204 | $descriptionOut .= PHP_EOL; | ||
205 | } | ||
206 | } | ||
207 | return $descriptionOut; | ||
142 | } | 208 | } |
143 | 209 | ||
144 | /** | 210 | /** |
@@ -190,7 +256,7 @@ function sanitize_html($description) | |||
190 | $description); | 256 | $description); |
191 | } | 257 | } |
192 | $description = preg_replace( | 258 | $description = preg_replace( |
193 | '#(<[^>]+)on[a-z]*="[^"]*"#is', | 259 | '#(<[^>]+)on[a-z]*="?[^ "]*"?#is', |
194 | '$1', | 260 | '$1', |
195 | $description); | 261 | $description); |
196 | return $description; | 262 | return $description; |
@@ -205,20 +271,21 @@ function sanitize_html($description) | |||
205 | * 5. Wrap description in 'markdown' CSS class. | 271 | * 5. Wrap description in 'markdown' CSS class. |
206 | * | 272 | * |
207 | * @param string $description input description text. | 273 | * @param string $description input description text. |
274 | * @param bool $escape escape HTML entities | ||
208 | * | 275 | * |
209 | * @return string HTML processed $description. | 276 | * @return string HTML processed $description. |
210 | */ | 277 | */ |
211 | function process_markdown($description) | 278 | function process_markdown($description, $escape = true) |
212 | { | 279 | { |
213 | $parsedown = new Parsedown(); | 280 | $parsedown = new Parsedown(); |
214 | 281 | ||
215 | $processedDescription = $description; | 282 | $processedDescription = $description; |
216 | $processedDescription = reverse_text2clickable($processedDescription); | ||
217 | $processedDescription = reverse_nl2br($processedDescription); | 283 | $processedDescription = reverse_nl2br($processedDescription); |
218 | $processedDescription = reverse_space2nbsp($processedDescription); | 284 | $processedDescription = reverse_space2nbsp($processedDescription); |
285 | $processedDescription = reverse_text2clickable($processedDescription); | ||
219 | $processedDescription = unescape($processedDescription); | 286 | $processedDescription = unescape($processedDescription); |
220 | $processedDescription = $parsedown | 287 | $processedDescription = $parsedown |
221 | ->setMarkupEscaped(false) | 288 | ->setMarkupEscaped($escape) |
222 | ->setBreaksEnabled(true) | 289 | ->setBreaksEnabled(true) |
223 | ->text($processedDescription); | 290 | ->text($processedDescription); |
224 | $processedDescription = sanitize_html($processedDescription); | 291 | $processedDescription = sanitize_html($processedDescription); |