aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--COPYING4
-rw-r--r--application/Utils.php8
-rw-r--r--inc/shaarli.css1
-rw-r--r--plugins/markdown/Parsedown.php1528
-rw-r--r--plugins/markdown/README.md51
-rw-r--r--plugins/markdown/help.html5
-rw-r--r--plugins/markdown/markdown.css137
-rw-r--r--plugins/markdown/markdown.meta1
-rw-r--r--plugins/markdown/markdown.php158
-rw-r--r--tests/plugins/PluginMarkdownTest.php112
10 files changed, 1996 insertions, 9 deletions
diff --git a/COPYING b/COPYING
index 22929463..28939100 100644
--- a/COPYING
+++ b/COPYING
@@ -72,6 +72,10 @@ Files: plugins/wallabag/wallabag.png
72License: MIT License (http://opensource.org/licenses/MIT) 72License: MIT License (http://opensource.org/licenses/MIT)
73Copyright: (C) 2015 Nicolas LÅ“uillet - https://github.com/wallabag/wallabag 73Copyright: (C) 2015 Nicolas LÅ“uillet - https://github.com/wallabag/wallabag
74 74
75Files: plugins/markdown/Parsedown.php
76License: MIT License (http://opensource.org/licenses/MIT)
77Copyright: (C) 2015 Emanuil Rusev - https://github.com/erusev/parsedown
78
75---------------------------------------------------- 79----------------------------------------------------
76ZLIB/LIBPNG LICENSE 80ZLIB/LIBPNG LICENSE
77 81
diff --git a/application/Utils.php b/application/Utils.php
index f84f70e4..ac8bfbfc 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -44,14 +44,6 @@ function endsWith($haystack, $needle, $case=true)
44} 44}
45 45
46/** 46/**
47 * Same as nl2br(), but escapes < and >
48 */
49function nl2br_escaped($html)
50{
51 return str_replace('>', '&gt;', str_replace('<', '&lt;', nl2br($html)));
52}
53
54/**
55 * htmlspecialchars wrapper 47 * htmlspecialchars wrapper
56 */ 48 */
57function escape($str) 49function escape($str)
diff --git a/inc/shaarli.css b/inc/shaarli.css
index 451f0481..79ba1d69 100644
--- a/inc/shaarli.css
+++ b/inc/shaarli.css
@@ -467,7 +467,6 @@ h1 {
467 margin-top: 0; 467 margin-top: 0;
468 margin-bottom: 12px; 468 margin-bottom: 12px;
469 font-weight: normal; 469 font-weight: normal;
470 max-height: 400px;
471 overflow: auto; 470 overflow: auto;
472} 471}
473 472
diff --git a/plugins/markdown/Parsedown.php b/plugins/markdown/Parsedown.php
new file mode 100644
index 00000000..91e05dcc
--- /dev/null
+++ b/plugins/markdown/Parsedown.php
@@ -0,0 +1,1528 @@
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
new file mode 100644
index 00000000..22d0af35
--- /dev/null
+++ b/plugins/markdown/README.md
@@ -0,0 +1,51 @@
1## Markdown Shaarli plugin
2
3Convert all your shaares description to HTML formatted Markdown.
4
5Read more about Markdown syntax here.
6
7### Installation
8
9Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there.
10The directory structure should look like:
11
12```
13??? plugins
14    ??? markdown
15 ??? help.html
16 ??? markdown.css
17 ??? markdown.meta
18 ??? markdown.php
19 ??? Parsedown.php
20    ??? README.md
21```
22
23To enable the plugin, add `markdown` to your list of enabled plugins in `data/config.php`
24(`ENABLED_PLUGINS` array).
25
26This should look like:
27
28```
29$GLOBALS['config']['ENABLED_PLUGINS'] = array('qrcode', 'any_other_plugin', 'markdown')
30```
31
32### Known issue
33
34#### Redirector
35
36If you're using a redirector, you *need* to add a space after a link,
37otherwise the rest of the line will be `urlencode`.
38
39```
40[link](http://domain.tld)-->test
41```
42
43Will consider `http://domain.tld)-->test` as URL.
44
45Instead, add an additional space.
46
47```
48[link](http://domain.tld) -->test
49```
50
51> Won't fix because a `)` is a valid part of an URL.
diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html
new file mode 100644
index 00000000..a6797838
--- /dev/null
+++ b/plugins/markdown/help.html
@@ -0,0 +1,5 @@
1<div class="md_help">
2 Description will be rendered with
3 <a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation">
4 Markdown syntax</a>.
5</div>
diff --git a/plugins/markdown/markdown.css b/plugins/markdown/markdown.css
new file mode 100644
index 00000000..6d666dcf
--- /dev/null
+++ b/plugins/markdown/markdown.css
@@ -0,0 +1,137 @@
1/**
2 * Credit to Simon Laroche <https://github.com/simonlc/Markdown-CSS>
3 * whom created the CSS which this file is based on.
4 * License: Unlicense <http://unlicense.org/>
5 */
6
7.markdown p{
8 margin:0.75em 0;
9}
10
11.markdown img{
12 max-width:100%;
13}
14
15.markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6{
16 font-weight:normal;
17 font-style:normal;
18 line-height:1em;
19 margin:0.75em 0;
20}
21.markdown h4, .markdown h5, .markdown h6{ font-weight: bold; }
22.markdown h1{ font-size:2.5em; }
23.markdown h2{ font-size:2em; }
24.markdown h3{ font-size:1.5em; }
25.markdown h4{ font-size:1.2em; }
26.markdown h5{ font-size:1em; }
27.markdown h6{ font-size:0.9em; }
28
29.markdown blockquote{
30 color:#666666;
31 padding-left: 3em;
32 border-left: 0.5em #EEE solid;
33 margin:0.75em 0;
34}
35.markdown hr { display: block; height: 2px; border: 0; border-top: 1px solid #aaa;border-bottom: 1px solid #eee; margin: 1em 0; padding: 0; }
36.markdown pre, .markdown code, .markdown kbd, .markdown samp {
37 font-family: monospace, 'courier new';
38 font-size: 0.98em;
39}
40.markdown pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
41
42.markdown b, .markdown strong { font-weight: bold; }
43
44.markdown dfn, .markdown em { font-style: italic; }
45
46.markdown ins { background: #ff9; color: #000; text-decoration: none; }
47
48.markdown mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
49
50.markdown sub, .markdown sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
51.markdown sup { top: -0.5em; }
52.markdown sub { bottom: -0.25em; }
53
54.markdown ul, .markdown ol { margin: 1em 0; padding: 0 0 0 2em; }
55.markdown li p:last-child { margin:0 }
56.markdown dd { margin: 0 0 0 2em; }
57
58.markdown img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
59
60.markdown table { border-collapse: collapse; border-spacing: 0; }
61.markdown td { vertical-align: top; }
62
63@media only screen and (min-width: 480px) {
64 .markdown {font-size:0.9em;}
65}
66
67@media only screen and (min-width: 768px) {
68 .markdown {font-size:1em;}
69}
70
71#linklist .markdown li {
72 padding: 0;
73 border: none;
74 background: none;
75}
76
77#linklist .markdown ul li {
78 list-style: circle;
79}
80
81#linklist .markdown ol li {
82 list-style: decimal;
83}
84
85.markdown table {
86 padding: 0;
87}
88.markdown table tr {
89 border-top: 1px solid #cccccc;
90 background-color: white;
91 margin: 0;
92 padding: 0;
93}
94.markdown table tr:nth-child(2n) {
95 background-color: #f8f8f8;
96}
97.markdown table tr th {
98 font-weight: bold;
99 border: 1px solid #cccccc;
100 text-align: left;
101 margin: 0;
102 padding: 6px 13px;
103}
104.markdown table tr td {
105 border: 1px solid #cccccc;
106 text-align: left;
107 margin: 0;
108 padding: 6px 13px;
109}
110.markdown table tr th :first-child, .markdown table tr td :first-child {
111 margin-top: 0;
112}
113.markdown table tr th :last-child, table tr td :last-child {
114 margin-bottom: 0;
115}
116
117.md_help {
118 color: white;
119}
120
121/*
122 Remove header links style
123 */
124#pageheader .md_help a {
125 color: lightgray;
126 font-weight: bold;
127 text-decoration: underline;
128
129 background: none;
130 box-shadow: none;
131 padding: 0;
132 margin: 0;
133}
134
135#pageheader .md_help a:hover {
136 color: white;
137}
diff --git a/plugins/markdown/markdown.meta b/plugins/markdown/markdown.meta
new file mode 100644
index 00000000..af8c0dbe
--- /dev/null
+++ b/plugins/markdown/markdown.meta
@@ -0,0 +1 @@
description="Render shaare description with Markdown syntax."
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php
new file mode 100644
index 00000000..3630ef14
--- /dev/null
+++ b/plugins/markdown/markdown.php
@@ -0,0 +1,158 @@
1<?php
2
3/**
4 * Plugin Markdown.
5 *
6 * Shaare's descriptions are parsed with Markdown.
7 */
8
9require_once 'Parsedown.php';
10
11/**
12 * Parse linklist descriptions.
13 *
14 * @param array $data linklist data.
15 *
16 * @return mixed linklist data parsed in markdown (and converted to HTML).
17 */
18function hook_markdown_render_linklist($data)
19{
20 foreach ($data['links'] as &$value) {
21 $value['description'] = process_markdown($value['description']);
22 }
23
24 return $data;
25}
26
27/**
28 * Parse daily descriptions.
29 *
30 * @param array $data daily data.
31 *
32 * @return mixed daily data parsed in markdown (and converted to HTML).
33 */
34function hook_markdown_render_daily($data)
35{
36 // Manipulate columns data
37 foreach ($data['cols'] as &$value) {
38 foreach ($value as &$value2) {
39 $value2['formatedDescription'] = process_markdown($value2['formatedDescription']);
40 }
41 }
42
43 return $data;
44}
45
46/**
47 * When link list is displayed, include markdown CSS.
48 *
49 * @param array $data includes data.
50 *
51 * @return mixed - includes data with markdown CSS file added.
52 */
53function hook_markdown_render_includes($data)
54{
55 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST
56 || $data['_PAGE_'] == Router::$PAGE_DAILY
57 || $data['_PAGE_'] == Router::$PAGE_EDITLINK
58 ) {
59
60 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/markdown/markdown.css';
61 }
62
63 return $data;
64}
65
66/**
67 * Hook render_editlink.
68 * Adds an help link to markdown syntax.
69 *
70 * @param array $data data passed to plugin
71 *
72 * @return array altered $data.
73 */
74function hook_markdown_render_editlink($data)
75{
76 // Load help HTML into a string
77 $data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
78 return $data;
79}
80
81
82/**
83 * Remove HTML links auto generated by Shaarli core system.
84 * Keeps HREF attributes.
85 *
86 * @param string $description input description text.
87 *
88 * @return string $description without HTML links.
89 */
90function reverse_text2clickable($description)
91{
92 return preg_replace('!<a +href="([^ ]*)">[^ ]+</a>!m', '$1', $description);
93}
94
95/**
96 * Remove <br> tag to let markdown handle it.
97 *
98 * @param string $description input description text.
99 *
100 * @return string $description without <br> tags.
101 */
102function reverse_nl2br($description)
103{
104 return preg_replace('!<br */?>!im', '', $description);
105}
106
107/**
108 * Remove HTML spaces '&nbsp;' auto generated by Shaarli core system.
109 *
110 * @param string $description input description text.
111 *
112 * @return string $description without HTML links.
113 */
114function reverse_space2nbsp($description)
115{
116 return preg_replace('/(^| )&nbsp;/m', '$1 ', $description);
117}
118
119/**
120 * Remove '&gt;' at start of line auto generated by Shaarli core system
121 * to allow markdown blockquotes.
122 *
123 * @param string $description input description text.
124 *
125 * @return string $description without HTML links.
126 */
127function reset_quote_tags($description)
128{
129 return preg_replace('/^( *)&gt; /m', '$1> ', $description);
130}
131
132/**
133 * Render shaare contents through Markdown parser.
134 * 1. Remove HTML generated by Shaarli core.
135 * 2. Generate markdown descriptions.
136 * 3. Wrap description in 'markdown' CSS class.
137 *
138 * @param string $description input description text.
139 *
140 * @return string HTML processed $description.
141 */
142function process_markdown($description)
143{
144 $parsedown = new Parsedown();
145
146 $processedDescription = $description;
147 $processedDescription = reverse_text2clickable($processedDescription);
148 $processedDescription = reverse_nl2br($processedDescription);
149 $processedDescription = reverse_space2nbsp($processedDescription);
150 $processedDescription = reset_quote_tags($processedDescription);
151 $processedDescription = $parsedown
152 ->setMarkupEscaped(false)
153 ->setBreaksEnabled(true)
154 ->text($processedDescription);
155 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
156
157 return $processedDescription;
158}
diff --git a/tests/plugins/PluginMarkdownTest.php b/tests/plugins/PluginMarkdownTest.php
new file mode 100644
index 00000000..455f5ba7
--- /dev/null
+++ b/tests/plugins/PluginMarkdownTest.php
@@ -0,0 +1,112 @@
1<?php
2
3/**
4 * PluginMarkdownTest.php
5 */
6
7require_once 'application/Utils.php';
8require_once 'plugins/markdown/markdown.php';
9
10/**
11 * Class PlugQrcodeTest
12 * Unit test for the QR-Code plugin
13 */
14class PluginMarkdownTest extends PHPUnit_Framework_TestCase
15{
16 /**
17 * Reset plugin path
18 */
19 function setUp()
20 {
21 PluginManager::$PLUGINS_PATH = 'plugins';
22 }
23
24 /**
25 * Test render_linklist hook.
26 * Only check that there is basic markdown rendering.
27 */
28 function testMarkdownLinklist()
29 {
30 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
31 $data = array(
32 'links' => array(
33 0 => array(
34 'description' => $markdown,
35 ),
36 ),
37 );
38
39 $data = hook_markdown_render_linklist($data);
40 $this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
41 $this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
42 }
43
44 /**
45 * Test render_daily hook.
46 * Only check that there is basic markdown rendering.
47 */
48 function testMarkdownDaily()
49 {
50 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
51 $data = array(
52 // Columns data
53 'cols' => array(
54 // First, second, third.
55 0 => array(
56 // nth link
57 0 => array(
58 'formatedDescription' => $markdown,
59 ),
60 ),
61 ),
62 );
63
64 $data = hook_markdown_render_daily($data);
65 $this->assertNotFalse(strpos($data['cols'][0][0]['formatedDescription'], '<h1>'));
66 $this->assertNotFalse(strpos($data['cols'][0][0]['formatedDescription'], '<p>'));
67 }
68
69 /**
70 * Test reverse_text2clickable().
71 */
72 function testReverseText2clickable()
73 {
74 $text = 'stuff http://hello.there/is=someone#here otherstuff';
75 $clickableText = text2clickable($text, '');
76 $reversedText = reverse_text2clickable($clickableText);
77 $this->assertEquals($text, $reversedText);
78 }
79
80 /**
81 * Test reverse_nl2br().
82 */
83 function testReverseNl2br()
84 {
85 $text = 'stuff' . PHP_EOL . 'otherstuff';
86 $processedText = nl2br($text);
87 $reversedText = reverse_nl2br($processedText);
88 $this->assertEquals($text, $reversedText);
89 }
90
91 /**
92 * Test reverse_space2nbsp().
93 */
94 function testReverseSpace2nbsp()
95 {
96 $text = ' stuff' . PHP_EOL . ' otherstuff and another';
97 $processedText = space2nbsp($text);
98 $reversedText = reverse_space2nbsp($processedText);
99 $this->assertEquals($text, $reversedText);
100 }
101
102 /**
103 * Test reset_quote_tags()
104 */
105 function testResetQuoteTags()
106 {
107 $text = '> quote1'. PHP_EOL . ' > quote2 ' . PHP_EOL . 'noquote';
108 $processedText = escape($text);
109 $reversedText = reset_quote_tags($processedText);
110 $this->assertEquals($text, $reversedText);
111 }
112}