krz/php-blog
A PHP blogging system with built-in static comments.
clone: git clone https://gitbay.org/krz/php-blog.git
main: _classes/Parsedown.php · raw
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, _templates the LICENSE file that was distributed
12# with this source code.
13#
14#
15
16class Parsedown
17{
18 # ~
19
20 const version = '1.7.4';
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 function setSafeMode($safeMode)
79 {
80 $this->safeMode = (bool) $safeMode;
81
82 return $this;
83 }
84
85 protected $safeMode;
86
87 protected $safeLinksWhitelist = array(
88 'http://',
89 'https://',
90 'ftp://',
91 'ftps://',
92 'mailto:',
93 'data:image/png;base64,',
94 'data:image/gif;base64,',
95 'data:image/jpeg;base64,',
96 'irc:',
97 'ircs:',
98 'git:',
99 'ssh:',
100 'news:',
101 'steam:',
102 );
103
104 #
105 # Lines
106 #
107
108 protected $BlockTypes = array(
109 '#' => array('Header'),
110 '*' => array('Rule', 'List'),
111 '+' => array('List'),
112 '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
113 '0' => array('List'),
114 '1' => array('List'),
115 '2' => array('List'),
116 '3' => array('List'),
117 '4' => array('List'),
118 '5' => array('List'),
119 '6' => array('List'),
120 '7' => array('List'),
121 '8' => array('List'),
122 '9' => array('List'),
123 ':' => array('Table'),
124 '<' => array('Comment', 'Markup'),
125 '=' => array('SetextHeader'),
126 '>' => array('Quote'),
127 '[' => array('Reference'),
128 '_' => array('Rule'),
129 '`' => array('FencedCode'),
130 '|' => array('Table'),
131 '~' => array('FencedCode'),
132 );
133
134 # ~
135
136 protected $unmarkedBlockTypes = array(
137 'Code',
138 );
139
140 #
141 # Blocks
142 #
143
144 protected function lines(array $lines)
145 {
146 $CurrentBlock = null;
147
148 foreach ($lines as $line)
149 {
150 if (chop($line) === '')
151 {
152 if (isset($CurrentBlock))
153 {
154 $CurrentBlock['interrupted'] = true;
155 }
156
157 continue;
158 }
159
160 if (strpos($line, "\t") !== false)
161 {
162 $parts = explode("\t", $line);
163
164 $line = $parts[0];
165
166 unset($parts[0]);
167
168 foreach ($parts as $part)
169 {
170 $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
171
172 $line .= str_repeat(' ', $shortage);
173 $line .= $part;
174 }
175 }
176
177 $indent = 0;
178
179 while (isset($line[$indent]) and $line[$indent] === ' ')
180 {
181 $indent ++;
182 }
183
184 $text = $indent > 0 ? substr($line, $indent) : $line;
185
186 # ~
187
188 $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
189
190 # ~
191
192 if (isset($CurrentBlock['continuable']))
193 {
194 $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
195
196 if (isset($Block))
197 {
198 $CurrentBlock = $Block;
199
200 continue;
201 }
202 else
203 {
204 if ($this->isBlockCompletable($CurrentBlock['type']))
205 {
206 $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
207 }
208 }
209 }
210
211 # ~
212
213 $marker = $text[0];
214
215 # ~
216
217 $blockTypes = $this->unmarkedBlockTypes;
218
219 if (isset($this->BlockTypes[$marker]))
220 {
221 foreach ($this->BlockTypes[$marker] as $blockType)
222 {
223 $blockTypes []= $blockType;
224 }
225 }
226
227 #
228 # ~
229
230 foreach ($blockTypes as $blockType)
231 {
232 $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
233
234 if (isset($Block))
235 {
236 $Block['type'] = $blockType;
237
238 if ( ! isset($Block['identified']))
239 {
240 $Blocks []= $CurrentBlock;
241
242 $Block['identified'] = true;
243 }
244
245 if ($this->isBlockContinuable($blockType))
246 {
247 $Block['continuable'] = true;
248 }
249
250 $CurrentBlock = $Block;
251
252 continue 2;
253 }
254 }
255
256 # ~
257
258 if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
259 {
260 $CurrentBlock['element']['text'] .= "\n".$text;
261 }
262 else
263 {
264 $Blocks []= $CurrentBlock;
265
266 $CurrentBlock = $this->paragraph($Line);
267
268 $CurrentBlock['identified'] = true;
269 }
270 }
271
272 # ~
273
274 if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
275 {
276 $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
277 }
278
279 # ~
280
281 $Blocks []= $CurrentBlock;
282
283 unset($Blocks[0]);
284
285 # ~
286
287 $markup = '';
288
289 foreach ($Blocks as $Block)
290 {
291 if (isset($Block['hidden']))
292 {
293 continue;
294 }
295
296 $markup .= "\n";
297 $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
298 }
299
300 $markup .= "\n";
301
302 # ~
303
304 return $markup;
305 }
306
307 protected function isBlockContinuable($Type)
308 {
309 return method_exists($this, 'block'.$Type.'Continue');
310 }
311
312 protected function isBlockCompletable($Type)
313 {
314 return method_exists($this, 'block'.$Type.'Complete');
315 }
316
317 #
318 # Code
319
320 protected function blockCode($Line, $Block = null)
321 {
322 if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
323 {
324 return;
325 }
326
327 if ($Line['indent'] >= 4)
328 {
329 $text = substr($Line['body'], 4);
330
331 $Block = array(
332 'element' => array(
333 'name' => 'pre',
334 'handler' => 'element',
335 'text' => array(
336 'name' => 'code',
337 'text' => $text,
338 ),
339 ),
340 );
341
342 return $Block;
343 }
344 }
345
346 protected function blockCodeContinue($Line, $Block)
347 {
348 if ($Line['indent'] >= 4)
349 {
350 if (isset($Block['interrupted']))
351 {
352 $Block['element']['text']['text'] .= "\n";
353
354 unset($Block['interrupted']);
355 }
356
357 $Block['element']['text']['text'] .= "\n";
358
359 $text = substr($Line['body'], 4);
360
361 $Block['element']['text']['text'] .= $text;
362
363 return $Block;
364 }
365 }
366
367 protected function blockCodeComplete($Block)
368 {
369 $text = $Block['element']['text']['text'];
370
371 $Block['element']['text']['text'] = $text;
372
373 return $Block;
374 }
375
376 #
377 # Comment
378
379 protected function blockComment($Line)
380 {
381 if ($this->markupEscaped or $this->safeMode)
382 {
383 return;
384 }
385
386 if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
387 {
388 $Block = array(
389 'markup' => $Line['body'],
390 );
391
392 if (preg_match('/-->$/', $Line['text']))
393 {
394 $Block['closed'] = true;
395 }
396
397 return $Block;
398 }
399 }
400
401 protected function blockCommentContinue($Line, array $Block)
402 {
403 if (isset($Block['closed']))
404 {
405 return;
406 }
407
408 $Block['markup'] .= "\n" . $Line['body'];
409
410 if (preg_match('/-->$/', $Line['text']))
411 {
412 $Block['closed'] = true;
413 }
414
415 return $Block;
416 }
417
418 #
419 # Fenced Code
420
421 protected function blockFencedCode($Line)
422 {
423 if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches))
424 {
425 $Element = array(
426 'name' => 'code',
427 'text' => '',
428 );
429
430 if (isset($matches[1]))
431 {
432 /**
433 * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
434 * Every HTML element may have a class attribute specified.
435 * The attribute, if specified, must have a value that is a set
436 * of space-separated tokens representing the various classes
437 * that the element belongs to.
438 * [...]
439 * The space characters, for the purposes of this specification,
440 * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
441 * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
442 * U+000D CARRIAGE RETURN (CR).
443 */
444 $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r"));
445
446 $class = 'language-'.$language;
447
448 $Element['attributes'] = array(
449 'class' => $class,
450 );
451 }
452
453 $Block = array(
454 'char' => $Line['text'][0],
455 'element' => array(
456 'name' => 'pre',
457 'handler' => 'element',
458 'text' => $Element,
459 ),
460 );
461
462 return $Block;
463 }
464 }
465
466 protected function blockFencedCodeContinue($Line, $Block)
467 {
468 if (isset($Block['complete']))
469 {
470 return;
471 }
472
473 if (isset($Block['interrupted']))
474 {
475 $Block['element']['text']['text'] .= "\n";
476
477 unset($Block['interrupted']);
478 }
479
480 if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
481 {
482 $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
483
484 $Block['complete'] = true;
485
486 return $Block;
487 }
488
489 $Block['element']['text']['text'] .= "\n".$Line['body'];
490
491 return $Block;
492 }
493
494 protected function blockFencedCodeComplete($Block)
495 {
496 $text = $Block['element']['text']['text'];
497
498 $Block['element']['text']['text'] = $text;
499
500 return $Block;
501 }
502
503 #
504 # Header
505
506 protected function blockHeader($Line)
507 {
508 if (isset($Line['text'][1]))
509 {
510 $level = 1;
511
512 while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
513 {
514 $level ++;
515 }
516
517 if ($level > 6)
518 {
519 return;
520 }
521
522 $text = trim($Line['text'], '# ');
523
524 $Block = array(
525 'element' => array(
526 'name' => 'h' . min(6, $level),
527 'text' => $text,
528 'handler' => 'line',
529 ),
530 );
531
532 return $Block;
533 }
534 }
535
536 #
537 # List
538
539 protected function blockList($Line)
540 {
541 list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
542
543 if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
544 {
545 $Block = array(
546 'indent' => $Line['indent'],
547 'pattern' => $pattern,
548 'element' => array(
549 'name' => $name,
550 'handler' => 'elements',
551 ),
552 );
553
554 if($name === 'ol')
555 {
556 $listStart = stristr($matches[0], '.', true);
557
558 if($listStart !== '1')
559 {
560 $Block['element']['attributes'] = array('start' => $listStart);
561 }
562 }
563
564 $Block['li'] = array(
565 'name' => 'li',
566 'handler' => 'li',
567 'text' => array(
568 $matches[2],
569 ),
570 );
571
572 $Block['element']['text'] []= & $Block['li'];
573
574 return $Block;
575 }
576 }
577
578 protected function blockListContinue($Line, array $Block)
579 {
580 if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
581 {
582 if (isset($Block['interrupted']))
583 {
584 $Block['li']['text'] []= '';
585
586 $Block['loose'] = true;
587
588 unset($Block['interrupted']);
589 }
590
591 unset($Block['li']);
592
593 $text = isset($matches[1]) ? $matches[1] : '';
594
595 $Block['li'] = array(
596 'name' => 'li',
597 'handler' => 'li',
598 'text' => array(
599 $text,
600 ),
601 );
602
603 $Block['element']['text'] []= & $Block['li'];
604
605 return $Block;
606 }
607
608 if ($Line['text'][0] === '[' and $this->blockReference($Line))
609 {
610 return $Block;
611 }
612
613 if ( ! isset($Block['interrupted']))
614 {
615 $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
616
617 $Block['li']['text'] []= $text;
618
619 return $Block;
620 }
621
622 if ($Line['indent'] > 0)
623 {
624 $Block['li']['text'] []= '';
625
626 $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
627
628 $Block['li']['text'] []= $text;
629
630 unset($Block['interrupted']);
631
632 return $Block;
633 }
634 }
635
636 protected function blockListComplete(array $Block)
637 {
638 if (isset($Block['loose']))
639 {
640 foreach ($Block['element']['text'] as &$li)
641 {
642 if (end($li['text']) !== '')
643 {
644 $li['text'] []= '';
645 }
646 }
647 }
648
649 return $Block;
650 }
651
652 #
653 # Quote
654
655 protected function blockQuote($Line)
656 {
657 if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
658 {
659 $Block = array(
660 'element' => array(
661 'name' => 'blockquote',
662 'handler' => 'lines',
663 'text' => (array) $matches[1],
664 ),
665 );
666
667 return $Block;
668 }
669 }
670
671 protected function blockQuoteContinue($Line, array $Block)
672 {
673 if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
674 {
675 if (isset($Block['interrupted']))
676 {
677 $Block['element']['text'] []= '';
678
679 unset($Block['interrupted']);
680 }
681
682 $Block['element']['text'] []= $matches[1];
683
684 return $Block;
685 }
686
687 if ( ! isset($Block['interrupted']))
688 {
689 $Block['element']['text'] []= $Line['text'];
690
691 return $Block;
692 }
693 }
694
695 #
696 # Rule
697
698 protected function blockRule($Line)
699 {
700 if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
701 {
702 $Block = array(
703 'element' => array(
704 'name' => 'hr'
705 ),
706 );
707
708 return $Block;
709 }
710 }
711
712 #
713 # Setext
714
715 protected function blockSetextHeader($Line, array $Block = null)
716 {
717 if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
718 {
719 return;
720 }
721
722 if (chop($Line['text'], $Line['text'][0]) === '')
723 {
724 $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
725
726 return $Block;
727 }
728 }
729
730 #
731 # Markup
732
733 protected function blockMarkup($Line)
734 {
735 if ($this->markupEscaped or $this->safeMode)
736 {
737 return;
738 }
739
740 if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
741 {
742 $element = strtolower($matches[1]);
743
744 if (in_array($element, $this->textLevelElements))
745 {
746 return;
747 }
748
749 $Block = array(
750 'name' => $matches[1],
751 'depth' => 0,
752 'markup' => $Line['text'],
753 );
754
755 $length = strlen($matches[0]);
756
757 $remainder = substr($Line['text'], $length);
758
759 if (trim($remainder) === '')
760 {
761 if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
762 {
763 $Block['closed'] = true;
764
765 $Block['void'] = true;
766 }
767 }
768 else
769 {
770 if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
771 {
772 return;
773 }
774
775 if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
776 {
777 $Block['closed'] = true;
778 }
779 }
780
781 return $Block;
782 }
783 }
784
785 protected function blockMarkupContinue($Line, array $Block)
786 {
787 if (isset($Block['closed']))
788 {
789 return;
790 }
791
792 if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
793 {
794 $Block['depth'] ++;
795 }
796
797 if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
798 {
799 if ($Block['depth'] > 0)
800 {
801 $Block['depth'] --;
802 }
803 else
804 {
805 $Block['closed'] = true;
806 }
807 }
808
809 if (isset($Block['interrupted']))
810 {
811 $Block['markup'] .= "\n";
812
813 unset($Block['interrupted']);
814 }
815
816 $Block['markup'] .= "\n".$Line['body'];
817
818 return $Block;
819 }
820
821 #
822 # Reference
823
824 protected function blockReference($Line)
825 {
826 if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
827 {
828 $id = strtolower($matches[1]);
829
830 $Data = array(
831 'url' => $matches[2],
832 'title' => null,
833 );
834
835 if (isset($matches[3]))
836 {
837 $Data['title'] = $matches[3];
838 }
839
840 $this->DefinitionData['Reference'][$id] = $Data;
841
842 $Block = array(
843 'hidden' => true,
844 );
845
846 return $Block;
847 }
848 }
849
850 #
851 # Table
852
853 protected function blockTable($Line, array $Block = null)
854 {
855 if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
856 {
857 return;
858 }
859
860 if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
861 {
862 $alignments = array();
863
864 $divider = $Line['text'];
865
866 $divider = trim($divider);
867 $divider = trim($divider, '|');
868
869 $dividerCells = explode('|', $divider);
870
871 foreach ($dividerCells as $dividerCell)
872 {
873 $dividerCell = trim($dividerCell);
874
875 if ($dividerCell === '')
876 {
877 continue;
878 }
879
880 $alignment = null;
881
882 if ($dividerCell[0] === ':')
883 {
884 $alignment = 'left';
885 }
886
887 if (substr($dividerCell, - 1) === ':')
888 {
889 $alignment = $alignment === 'left' ? 'center' : 'right';
890 }
891
892 $alignments []= $alignment;
893 }
894
895 # ~
896
897 $HeaderElements = array();
898
899 $header = $Block['element']['text'];
900
901 $header = trim($header);
902 $header = trim($header, '|');
903
904 $headerCells = explode('|', $header);
905
906 foreach ($headerCells as $index => $headerCell)
907 {
908 $headerCell = trim($headerCell);
909
910 $HeaderElement = array(
911 'name' => 'th',
912 'text' => $headerCell,
913 'handler' => 'line',
914 );
915
916 if (isset($alignments[$index]))
917 {
918 $alignment = $alignments[$index];
919
920 $HeaderElement['attributes'] = array(
921 'style' => 'text-align: '.$alignment.';',
922 );
923 }
924
925 $HeaderElements []= $HeaderElement;
926 }
927
928 # ~
929
930 $Block = array(
931 'alignments' => $alignments,
932 'identified' => true,
933 'element' => array(
934 'name' => 'table',
935 'handler' => 'elements',
936 ),
937 );
938
939 $Block['element']['text'] []= array(
940 'name' => 'thead',
941 'handler' => 'elements',
942 );
943
944 $Block['element']['text'] []= array(
945 'name' => 'tbody',
946 'handler' => 'elements',
947 'text' => array(),
948 );
949
950 $Block['element']['text'][0]['text'] []= array(
951 'name' => 'tr',
952 'handler' => 'elements',
953 'text' => $HeaderElements,
954 );
955
956 return $Block;
957 }
958 }
959
960 protected function blockTableContinue($Line, array $Block)
961 {
962 if (isset($Block['interrupted']))
963 {
964 return;
965 }
966
967 if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
968 {
969 $Elements = array();
970
971 $row = $Line['text'];
972
973 $row = trim($row);
974 $row = trim($row, '|');
975
976 preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
977
978 foreach ($matches[0] as $index => $cell)
979 {
980 $cell = trim($cell);
981
982 $Element = array(
983 'name' => 'td',
984 'handler' => 'line',
985 'text' => $cell,
986 );
987
988 if (isset($Block['alignments'][$index]))
989 {
990 $Element['attributes'] = array(
991 'style' => 'text-align: '.$Block['alignments'][$index].';',
992 );
993 }
994
995 $Elements []= $Element;
996 }
997
998 $Element = array(
999 'name' => 'tr',
1000 'handler' => 'elements',
1001 'text' => $Elements,
1002 );
1003
1004 $Block['element']['text'][1]['text'] []= $Element;
1005
1006 return $Block;
1007 }
1008 }
1009
1010 #
1011 # ~
1012 #
1013
1014 protected function paragraph($Line)
1015 {
1016 $Block = array(
1017 'element' => array(
1018 'name' => 'p',
1019 'text' => $Line['text'],
1020 'handler' => 'line',
1021 ),
1022 );
1023
1024 return $Block;
1025 }
1026
1027 #
1028 # Inline Elements
1029 #
1030
1031 protected $InlineTypes = array(
1032 '"' => array('SpecialCharacter'),
1033 '!' => array('Image'),
1034 '&' => array('SpecialCharacter'),
1035 '*' => array('Emphasis'),
1036 ':' => array('Url'),
1037 '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
1038 '>' => array('SpecialCharacter'),
1039 '[' => array('Link'),
1040 '_' => array('Emphasis'),
1041 '`' => array('Code'),
1042 '~' => array('Strikethrough'),
1043 '\\' => array('EscapeSequence'),
1044 );
1045
1046 # ~
1047
1048 protected $inlineMarkerList = '!"*_&[:<>`~\\';
1049
1050 #
1051 # ~
1052 #
1053
1054 public function line($text, $nonNestables=array())
1055 {
1056 $markup = '';
1057
1058 # $excerpt is based on the first occurrence of a marker
1059
1060 while ($excerpt = strpbrk($text, $this->inlineMarkerList))
1061 {
1062 $marker = $excerpt[0];
1063
1064 $markerPosition = strpos($text, $marker);
1065
1066 $Excerpt = array('text' => $excerpt, 'context' => $text);
1067
1068 foreach ($this->InlineTypes[$marker] as $inlineType)
1069 {
1070 # check to see if the current inline type is nestable in the current context
1071
1072 if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables))
1073 {
1074 continue;
1075 }
1076
1077 $Inline = $this->{'inline'.$inlineType}($Excerpt);
1078
1079 if ( ! isset($Inline))
1080 {
1081 continue;
1082 }
1083
1084 # makes sure that the inline belongs to "our" marker
1085
1086 if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
1087 {
1088 continue;
1089 }
1090
1091 # sets a default inline position
1092
1093 if ( ! isset($Inline['position']))
1094 {
1095 $Inline['position'] = $markerPosition;
1096 }
1097
1098 # cause the new element to 'inherit' our non nestables
1099
1100 foreach ($nonNestables as $non_nestable)
1101 {
1102 $Inline['element']['nonNestables'][] = $non_nestable;
1103 }
1104
1105 # the text that comes before the inline
1106 $unmarkedText = substr($text, 0, $Inline['position']);
1107
1108 # compile the unmarked text
1109 $markup .= $this->unmarkedText($unmarkedText);
1110
1111 # compile the inline
1112 $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1113
1114 # remove the examined text
1115 $text = substr($text, $Inline['position'] + $Inline['extent']);
1116
1117 continue 2;
1118 }
1119
1120 # the marker does not belong to an inline
1121
1122 $unmarkedText = substr($text, 0, $markerPosition + 1);
1123
1124 $markup .= $this->unmarkedText($unmarkedText);
1125
1126 $text = substr($text, $markerPosition + 1);
1127 }
1128
1129 $markup .= $this->unmarkedText($text);
1130
1131 return $markup;
1132 }
1133
1134 #
1135 # ~
1136 #
1137
1138 protected function inlineCode($Excerpt)
1139 {
1140 $marker = $Excerpt['text'][0];
1141
1142 if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1143 {
1144 $text = $matches[2];
1145 $text = preg_replace("/[ ]*\n/", ' ', $text);
1146
1147 return array(
1148 'extent' => strlen($matches[0]),
1149 'element' => array(
1150 'name' => 'code',
1151 'text' => $text,
1152 ),
1153 );
1154 }
1155 }
1156
1157 protected function inlineEmailTag($Excerpt)
1158 {
1159 if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
1160 {
1161 $url = $matches[1];
1162
1163 if ( ! isset($matches[2]))
1164 {
1165 $url = 'mailto:' . $url;
1166 }
1167
1168 return array(
1169 'extent' => strlen($matches[0]),
1170 'element' => array(
1171 'name' => 'a',
1172 'text' => $matches[1],
1173 'attributes' => array(
1174 'href' => $url,
1175 'rel' => 'noopener,noreferrer',
1176 'target' => '_blank'
1177 ),
1178 ),
1179 );
1180 }
1181 }
1182
1183 protected function inlineEmphasis($Excerpt)
1184 {
1185 if ( ! isset($Excerpt['text'][1]))
1186 {
1187 return;
1188 }
1189
1190 $marker = $Excerpt['text'][0];
1191
1192 if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1193 {
1194 $emphasis = 'strong';
1195 }
1196 elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1197 {
1198 $emphasis = 'em';
1199 }
1200 else
1201 {
1202 return;
1203 }
1204
1205 return array(
1206 'extent' => strlen($matches[0]),
1207 'element' => array(
1208 'name' => $emphasis,
1209 'handler' => 'line',
1210 'text' => $matches[1],
1211 ),
1212 );
1213 }
1214
1215 protected function inlineEscapeSequence($Excerpt)
1216 {
1217 if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1218 {
1219 return array(
1220 'markup' => $Excerpt['text'][1],
1221 'extent' => 2,
1222 );
1223 }
1224 }
1225
1226 protected function inlineImage($Excerpt)
1227 {
1228 if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1229 {
1230 return;
1231 }
1232
1233 $Excerpt['text']= substr($Excerpt['text'], 1);
1234
1235 $Link = $this->inlineLink($Excerpt);
1236
1237 if ($Link === null)
1238 {
1239 return;
1240 }
1241
1242 $Inline = array(
1243 'extent' => $Link['extent'] + 1,
1244 'element' => array(
1245 'name' => 'img',
1246 'attributes' => array(
1247 'src' => $Link['element']['attributes']['href'],
1248 'alt' => $Link['element']['text'],
1249 'class' => 'img-fluid'
1250 ),
1251 ),
1252 );
1253
1254 $Inline['element']['attributes'] += $Link['element']['attributes'];
1255
1256 unset($Inline['element']['attributes']['href']);
1257
1258 return $Inline;
1259 }
1260
1261 protected function inlineLink($Excerpt)
1262 {
1263 $Element = array(
1264 'name' => 'a',
1265 'handler' => 'line',
1266 'nonNestables' => array('Url', 'Link'),
1267 'text' => null,
1268 'attributes' => array(
1269 'href' => null,
1270 'title' => null,
1271 ),
1272 );
1273
1274 $extent = 0;
1275
1276 $remainder = $Excerpt['text'];
1277
1278 if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
1279 {
1280 $Element['text'] = $matches[1];
1281
1282 $extent += strlen($matches[0]);
1283
1284 $remainder = substr($remainder, $extent);
1285 }
1286 else
1287 {
1288 return;
1289 }
1290
1291 if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches))
1292 {
1293 $Element['attributes']['href'] = $matches[1];
1294
1295 if (isset($matches[2]))
1296 {
1297 $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1298 }
1299
1300 $extent += strlen($matches[0]);
1301 }
1302 else
1303 {
1304 if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1305 {
1306 $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
1307 $definition = strtolower($definition);
1308
1309 $extent += strlen($matches[0]);
1310 }
1311 else
1312 {
1313 $definition = strtolower($Element['text']);
1314 }
1315
1316 if ( ! isset($this->DefinitionData['Reference'][$definition]))
1317 {
1318 return;
1319 }
1320
1321 $Definition = $this->DefinitionData['Reference'][$definition];
1322
1323 $Element['attributes']['href'] = $Definition['url'];
1324 $Element['attributes']['title'] = $Definition['title'];
1325 }
1326
1327 return array(
1328 'extent' => $extent,
1329 'element' => $Element,
1330 );
1331 }
1332
1333 protected function inlineMarkup($Excerpt)
1334 {
1335 if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
1336 {
1337 return;
1338 }
1339
1340 if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches))
1341 {
1342 return array(
1343 'markup' => $matches[0],
1344 'extent' => strlen($matches[0]),
1345 );
1346 }
1347
1348 if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
1349 {
1350 return array(
1351 'markup' => $matches[0],
1352 'extent' => strlen($matches[0]),
1353 );
1354 }
1355
1356 if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
1357 {
1358 return array(
1359 'markup' => $matches[0],
1360 'extent' => strlen($matches[0]),
1361 );
1362 }
1363 }
1364
1365 protected function inlineSpecialCharacter($Excerpt)
1366 {
1367 if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
1368 {
1369 return array(
1370 'markup' => '&',
1371 'extent' => 1,
1372 );
1373 }
1374
1375 $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1376
1377 if (isset($SpecialCharacter[$Excerpt['text'][0]]))
1378 {
1379 return array(
1380 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1381 'extent' => 1,
1382 );
1383 }
1384 }
1385
1386 protected function inlineStrikethrough($Excerpt)
1387 {
1388 if ( ! isset($Excerpt['text'][1]))
1389 {
1390 return;
1391 }
1392
1393 if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1394 {
1395 return array(
1396 'extent' => strlen($matches[0]),
1397 'element' => array(
1398 'name' => 'del',
1399 'text' => $matches[1],
1400 'handler' => 'line',
1401 ),
1402 );
1403 }
1404 }
1405
1406 protected function inlineUrl($Excerpt)
1407 {
1408 if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1409 {
1410 return;
1411 }
1412
1413 if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
1414 {
1415 $url = $matches[0][0];
1416
1417 $Inline = array(
1418 'extent' => strlen($matches[0][0]),
1419 'position' => $matches[0][1],
1420 'element' => array(
1421 'name' => 'a',
1422 'text' => $url,
1423 'attributes' => array(
1424 'href' => $url,
1425 'rel' => 'noopener,noreferrer',
1426 'target' => '_blank'
1427 ),
1428 ),
1429 );
1430
1431 return $Inline;
1432 }
1433 }
1434
1435 protected function inlineUrlTag($Excerpt)
1436 {
1437 if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
1438 {
1439 $url = $matches[1];
1440
1441 return array(
1442 'extent' => strlen($matches[0]),
1443 'element' => array(
1444 'name' => 'a',
1445 'text' => $url,
1446 'attributes' => array(
1447 'href' => $url,
1448 'rel' => 'noopener,noreferrer',
1449 'target' => '_blank'
1450 ),
1451 ),
1452 );
1453 }
1454 }
1455
1456 # ~
1457
1458 protected function unmarkedText($text)
1459 {
1460 if ($this->breaksEnabled)
1461 {
1462 $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1463 }
1464 else
1465 {
1466 $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1467 $text = str_replace(" \n", "\n", $text);
1468 }
1469
1470 return $text;
1471 }
1472
1473 #
1474 # Handlers
1475 #
1476
1477 protected function element(array $Element)
1478 {
1479 if ($this->safeMode)
1480 {
1481 $Element = $this->sanitiseElement($Element);
1482 }
1483
1484 $markup = '<'.$Element['name'];
1485
1486 if (isset($Element['attributes']))
1487 {
1488 foreach ($Element['attributes'] as $name => $value)
1489 {
1490 if ($value === null)
1491 {
1492 continue;
1493 }
1494
1495 $markup .= ' '.$name.'="'.self::escape($value).'"';
1496 }
1497 }
1498
1499 $permitRawHtml = false;
1500
1501 if (isset($Element['text']))
1502 {
1503 $text = $Element['text'];
1504 }
1505 // very strongly consider an alternative if you're writing an
1506 // extension
1507 elseif (isset($Element['rawHtml']))
1508 {
1509 $text = $Element['rawHtml'];
1510 $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
1511 $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
1512 }
1513
1514 if (isset($text))
1515 {
1516 $markup .= '>';
1517
1518 if (!isset($Element['nonNestables']))
1519 {
1520 $Element['nonNestables'] = array();
1521 }
1522
1523 if (isset($Element['handler']))
1524 {
1525 $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']);
1526 }
1527 elseif (!$permitRawHtml)
1528 {
1529 $markup .= self::escape($text, true);
1530 }
1531 else
1532 {
1533 $markup .= $text;
1534 }
1535
1536 $markup .= '</'.$Element['name'].'>';
1537 }
1538 else
1539 {
1540 $markup .= ' />';
1541 }
1542
1543 return $markup;
1544 }
1545
1546 protected function elements(array $Elements)
1547 {
1548 $markup = '';
1549
1550 foreach ($Elements as $Element)
1551 {
1552 $markup .= "\n" . $this->element($Element);
1553 }
1554
1555 $markup .= "\n";
1556
1557 return $markup;
1558 }
1559
1560 # ~
1561
1562 protected function li($lines)
1563 {
1564 $markup = $this->lines($lines);
1565
1566 $trimmedMarkup = trim($markup);
1567
1568 if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
1569 {
1570 $markup = $trimmedMarkup;
1571 $markup = substr($markup, 3);
1572
1573 $position = strpos($markup, "</p>");
1574
1575 $markup = substr_replace($markup, '', $position, 4);
1576 }
1577
1578 return $markup;
1579 }
1580
1581 #
1582 # Deprecated Methods
1583 #
1584
1585 function parse($text)
1586 {
1587 $markup = $this->text($text);
1588
1589 return $markup;
1590 }
1591
1592 protected function sanitiseElement(array $Element)
1593 {
1594 static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
1595 static $safeUrlNameToAtt = array(
1596 'a' => 'href',
1597 'img' => 'src',
1598 );
1599
1600 if (isset($safeUrlNameToAtt[$Element['name']]))
1601 {
1602 $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
1603 }
1604
1605 if ( ! empty($Element['attributes']))
1606 {
1607 foreach ($Element['attributes'] as $att => $val)
1608 {
1609 # filter out badly parsed attribute
1610 if ( ! preg_match($goodAttribute, $att))
1611 {
1612 unset($Element['attributes'][$att]);
1613 }
1614 # dump onevent attribute
1615 elseif (self::striAtStart($att, 'on'))
1616 {
1617 unset($Element['attributes'][$att]);
1618 }
1619 }
1620 }
1621
1622 return $Element;
1623 }
1624
1625 protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
1626 {
1627 foreach ($this->safeLinksWhitelist as $scheme)
1628 {
1629 if (self::striAtStart($Element['attributes'][$attribute], $scheme))
1630 {
1631 return $Element;
1632 }
1633 }
1634
1635 $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
1636
1637 return $Element;
1638 }
1639
1640 #
1641 # Static Methods
1642 #
1643
1644 protected static function escape($text, $allowQuotes = false)
1645 {
1646 return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
1647 }
1648
1649 protected static function striAtStart($string, $needle)
1650 {
1651 $len = strlen($needle);
1652
1653 if ($len > strlen($string))
1654 {
1655 return false;
1656 }
1657 else
1658 {
1659 return strtolower(substr($string, 0, $len)) === strtolower($needle);
1660 }
1661 }
1662
1663 static function instance($name = 'default')
1664 {
1665 if (isset(self::$instances[$name]))
1666 {
1667 return self::$instances[$name];
1668 }
1669
1670 $instance = new static();
1671
1672 self::$instances[$name] = $instance;
1673
1674 return $instance;
1675 }
1676
1677 private static $instances = array();
1678
1679 #
1680 # Fields
1681 #
1682
1683 protected $DefinitionData;
1684
1685 #
1686 # Read-Only
1687
1688 protected $specialCharacters = array(
1689 '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1690 );
1691
1692 protected $StrongRegex = array(
1693 '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1694 '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1695 );
1696
1697 protected $EmRegex = array(
1698 '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1699 '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1700 );
1701
1702 protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1703
1704 protected $voidElements = array(
1705 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1706 );
1707
1708 protected $textLevelElements = array(
1709 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1710 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1711 'i', 'rp', 'del', 'code', 'strike', 'marquee',
1712 'q', 'rt', 'ins', 'font', 'strong',
1713 's', 'tt', 'kbd', 'mark',
1714 'u', 'xm', 'sub', 'nobr',
1715 'sup', 'ruby',
1716 'var', 'span',
1717 'wbr', 'time',
1718 );
1719}