aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorVirtualTam <virtualtam+github@flibidi.net>2016-10-22 12:53:15 +0200
committerGitHub <noreply@github.com>2016-10-22 12:53:15 +0200
commit761b4e283737a48934457855448df98e8014ba73 (patch)
tree3a98fc6dee7edd3a13f335f97fb9bc4faf95f064
parent3d5e0aede31e07f060e3ca470d36374c54d5040c (diff)
parentc5941f316a49c94eff354b63e75b3add98ac4aea (diff)
downloadShaarli-761b4e283737a48934457855448df98e8014ba73.tar.gz
Shaarli-761b4e283737a48934457855448df98e8014ba73.tar.zst
Shaarli-761b4e283737a48934457855448df98e8014ba73.zip
Merge pull request #674 from ArthurHoaro/parsedown-composer
Use Composer for Parsedown and fix an issue with links
-rw-r--r--COPYING4
-rw-r--r--composer.json3
-rw-r--r--plugins/markdown/Parsedown.php1528
-rw-r--r--plugins/markdown/markdown.php13
-rw-r--r--tests/plugins/PluginMarkdownTest.php13
-rw-r--r--tests/plugins/resources/markdown.html24
-rw-r--r--tests/plugins/resources/markdown.md24
7 files changed, 69 insertions, 1540 deletions
diff --git a/COPYING b/COPYING
index 28939100..22929463 100644
--- a/COPYING
+++ b/COPYING
@@ -72,10 +72,6 @@ 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
79---------------------------------------------------- 75----------------------------------------------------
80ZLIB/LIBPNG LICENSE 76ZLIB/LIBPNG LICENSE
81 77
diff --git a/composer.json b/composer.json
index 89a7e446..f7d26a31 100644
--- a/composer.json
+++ b/composer.json
@@ -11,7 +11,8 @@
11 "keywords": ["bookmark", "link", "share", "web"], 11 "keywords": ["bookmark", "link", "share", "web"],
12 "require": { 12 "require": {
13 "php": ">=5.3.4", 13 "php": ">=5.3.4",
14 "shaarli/netscape-bookmark-parser": "1.*" 14 "shaarli/netscape-bookmark-parser": "1.*",
15 "erusev/parsedown": "1.6"
15 }, 16 },
16 "require-dev": { 17 "require-dev": {
17 "phpmd/phpmd" : "@stable", 18 "phpmd/phpmd" : "@stable",
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/markdown.php b/plugins/markdown/markdown.php
index 6b1c1d44..a764b6fa 100644
--- a/plugins/markdown/markdown.php
+++ b/plugins/markdown/markdown.php
@@ -6,8 +6,6 @@
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 */ 11 */
@@ -157,8 +155,9 @@ function reverse_text2clickable($description)
157 $lineCount = 0; 155 $lineCount = 0;
158 156
159 foreach ($descriptionLines as $descriptionLine) { 157 foreach ($descriptionLines as $descriptionLine) {
160 // Detect line of code 158 // Detect line of code: starting with 4 spaces,
161 $codeLineOn = preg_match('/^ /', $descriptionLine) > 0; 159 // except lists which can start with +/*/- or `2.` after spaces.
160 $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0;
162 // Detect and toggle block of code 161 // Detect and toggle block of code
163 if (!$codeBlockOn) { 162 if (!$codeBlockOn) {
164 $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0; 163 $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0;
@@ -175,10 +174,10 @@ function reverse_text2clickable($description)
175 $descriptionLine 174 $descriptionLine
176 ); 175 );
177 176
178 // Reverse hashtag links if we're in a code block. 177 // Reverse all links in code blocks, only non hashtag elsewhere.
179 $hashtagFilter = ($codeBlockOn || $codeLineOn) ? $hashtagTitle : ''; 178 $hashtagFilter = (!$codeBlockOn && !$codeLineOn) ? '(?!'. $hashtagTitle .')': '(?:'. $hashtagTitle .')?';
180 $descriptionLine = preg_replace( 179 $descriptionLine = preg_replace(
181 '!<a href="[^ ]*"'. $hashtagFilter .'>([^<]+)</a>!m', 180 '#<a href="[^ ]*"'. $hashtagFilter .'>([^<]+)</a>#m',
182 '$1', 181 '$1',
183 $descriptionLine 182 $descriptionLine
184 ); 183 );
diff --git a/tests/plugins/PluginMarkdownTest.php b/tests/plugins/PluginMarkdownTest.php
index 3593a556..12bdda24 100644
--- a/tests/plugins/PluginMarkdownTest.php
+++ b/tests/plugins/PluginMarkdownTest.php
@@ -151,4 +151,17 @@ class PluginMarkdownTest extends PHPUnit_Framework_TestCase
151 $data = hook_markdown_render_daily($data); 151 $data = hook_markdown_render_daily($data);
152 $this->assertEquals($str, $data['cols'][0][0]['formatedDescription']); 152 $this->assertEquals($str, $data['cols'][0][0]['formatedDescription']);
153 } 153 }
154
155 /**
156 * Test hashtag links processed with markdown.
157 */
158 function testMarkdownHashtagLinks()
159 {
160 $md = file_get_contents('tests/plugins/resources/markdown.md');
161 $md = format_description($md);
162 $html = file_get_contents('tests/plugins/resources/markdown.html');
163
164 $data = process_markdown($md);
165 $this->assertEquals($html, $data);
166 }
154} 167}
diff --git a/tests/plugins/resources/markdown.html b/tests/plugins/resources/markdown.html
new file mode 100644
index 00000000..c0fbe7f4
--- /dev/null
+++ b/tests/plugins/resources/markdown.html
@@ -0,0 +1,24 @@
1<div class="markdown"><ul>
2<li>test:
3<ul>
4<li><a href="http://link.tld">zero</a></li>
5<li><a href="http://link.tld">two</a></li>
6<li><a href="http://link.tld">three</a></li>
7</ul></li>
8</ul>
9<ol>
10<li><a href="http://link.tld">zero</a>
11<ol>
12<li><a href="http://link.tld">two</a></li>
13<li><a href="http://link.tld">three</a></li>
14<li><a href="http://link.tld">four</a></li>
15<li>foo <a href="?addtag=foobar" title="Hashtag foobar">#foobar</a></li>
16</ol></li>
17</ol>
18<p><a href="?addtag=foobar" title="Hashtag foobar">#foobar</a> foo <code>lol #foo</code> <a href="?addtag=bar" title="Hashtag bar">#bar</a></p>
19<p>fsdfs <a href="http://link.tld">http://link.tld</a> <a href="?addtag=foobar" title="Hashtag foobar">#foobar</a> <code>http://link.tld</code></p>
20<pre><code>http://link.tld #foobar
21next #foo</code></pre>
22<p>Block:</p>
23<pre><code>lorem ipsum #foobar http://link.tld
24#foobar http://link.tld</code></pre></div> \ No newline at end of file
diff --git a/tests/plugins/resources/markdown.md b/tests/plugins/resources/markdown.md
new file mode 100644
index 00000000..0b8be7c5
--- /dev/null
+++ b/tests/plugins/resources/markdown.md
@@ -0,0 +1,24 @@
1* test:
2 * [zero](http://link.tld)
3 + [two](http://link.tld)
4 - [three](http://link.tld)
5
61. [zero](http://link.tld)
7 2. [two](http://link.tld)
8 3. [three](http://link.tld)
9 4. [four](http://link.tld)
10 5. foo #foobar
11
12#foobar foo `lol #foo` #bar
13
14fsdfs http://link.tld #foobar `http://link.tld`
15
16 http://link.tld #foobar
17 next #foo
18
19Block:
20
21```
22lorem ipsum #foobar http://link.tld
23#foobar http://link.tld
24``` \ No newline at end of file