aboutsummaryrefslogtreecommitdiffhomepage
path: root/plugins/markdown
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2017-05-07 19:17:33 +0200
committerArthurHoaro <arthur@hoa.ro>2017-05-07 19:17:33 +0200
commit01e942d44c7194607649817216aeb5d65c6acad6 (patch)
tree15777aa1005251f119e6dd680291147117766b5b /plugins/markdown
parentbc22c9a0acb095970e9494cbe8954f0612e05dc0 (diff)
parent8868f3ca461011a8fb6dd9f90b60ed697ab52fc5 (diff)
downloadShaarli-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.php1528
-rw-r--r--plugins/markdown/README.md51
-rw-r--r--plugins/markdown/markdown.meta5
-rw-r--r--plugins/markdown/markdown.php105
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
16class 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('&amp;', '&lt;'), $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' => '&amp;',
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('&amp;', '&lt;'), $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
27To enable the plugin, just check it in the plugin administration page. 26To enable the plugin, just check it in the plugin administration page.
28 27
29You can also add `markdown` to your list of enabled plugins in `data/config.php` 28You 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
32This should look like: 31This 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
42Parsedown parsing library is imported using Composer. If you installed Shaarli using `git`,
43or the `master` branch, run
44
45 composer update --no-dev --prefer-dist
46
38### No Markdown tag 47### No Markdown tag
39 48
40If the tag `.nomarkdown` is set for a shaare, it won't be converted to Markdown syntax. 49If 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
55By default, HTML tags are escaped. You can enable HTML tags rendering
56by 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
66With this setting, Markdown support HTML tags. For example:
67
68 > <strong>strong</strong><strike>strike</strike>
69
70Will 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,
81enabling 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 @@
1description="Render shaare description with Markdown syntax." 1description="Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
2If your shaared descriptions containing HTML tags before enabling the markdown plugin,
3enabling it might break your page.
4See 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
9require_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 */
15define('NO_MD_TAG', '.nomarkdown'); 12define('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 */
24function hook_markdown_render_linklist($data) 22function 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 */
43function hook_markdown_render_feed($data) 42function 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 */
62function hook_markdown_render_daily($data) 63function 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 */
84function noMarkdownTag($tags) 89function 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 */
101function 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 */
139function reverse_text2clickable($description) 167function 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 */
211function process_markdown($description) 278function 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);