]> git.bts.cx Git - cx.git/blob - cx/third_party/parsedown/Parsedown.php
Basic draft support
[cx.git] / cx / third_party / parsedown / Parsedown.php
1 <?php
2
3 #
4 #
5 # Parsedown
6 # http://parsedown.org
7 #
8 # (c) Emanuil Rusev
9 # http://erusev.com
10 #
11 # For the full license information, view the LICENSE file that was distributed
12 # with this source code.
13 #
14 #
15
16 class Parsedown
17 {
18     # ~
19
20     const version = '1.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                     ),
1176                 ),
1177             );
1178         }
1179     }
1180
1181     protected function inlineEmphasis($Excerpt)
1182     {
1183         if ( ! isset($Excerpt['text'][1]))
1184         {
1185             return;
1186         }
1187
1188         $marker = $Excerpt['text'][0];
1189
1190         if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1191         {
1192             $emphasis = 'strong';
1193         }
1194         elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1195         {
1196             $emphasis = 'em';
1197         }
1198         else
1199         {
1200             return;
1201         }
1202
1203         return array(
1204             'extent' => strlen($matches[0]),
1205             'element' => array(
1206                 'name' => $emphasis,
1207                 'handler' => 'line',
1208                 'text' => $matches[1],
1209             ),
1210         );
1211     }
1212
1213     protected function inlineEscapeSequence($Excerpt)
1214     {
1215         if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1216         {
1217             return array(
1218                 'markup' => $Excerpt['text'][1],
1219                 'extent' => 2,
1220             );
1221         }
1222     }
1223
1224     protected function inlineImage($Excerpt)
1225     {
1226         if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1227         {
1228             return;
1229         }
1230
1231         $Excerpt['text']= substr($Excerpt['text'], 1);
1232
1233         $Link = $this->inlineLink($Excerpt);
1234
1235         if ($Link === null)
1236         {
1237             return;
1238         }
1239
1240         $Inline = array(
1241             'extent' => $Link['extent'] + 1,
1242             'element' => array(
1243                 'name' => 'img',
1244                 'attributes' => array(
1245                     'src' => $Link['element']['attributes']['href'],
1246                     'alt' => $Link['element']['text'],
1247                 ),
1248             ),
1249         );
1250
1251         $Inline['element']['attributes'] += $Link['element']['attributes'];
1252
1253         unset($Inline['element']['attributes']['href']);
1254
1255         return $Inline;
1256     }
1257
1258     protected function inlineLink($Excerpt)
1259     {
1260         $Element = array(
1261             'name' => 'a',
1262             'handler' => 'line',
1263             'nonNestables' => array('Url', 'Link'),
1264             'text' => null,
1265             'attributes' => array(
1266                 'href' => null,
1267                 'title' => null,
1268             ),
1269         );
1270
1271         $extent = 0;
1272
1273         $remainder = $Excerpt['text'];
1274
1275         if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
1276         {
1277             $Element['text'] = $matches[1];
1278
1279             $extent += strlen($matches[0]);
1280
1281             $remainder = substr($remainder, $extent);
1282         }
1283         else
1284         {
1285             return;
1286         }
1287
1288         if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches))
1289         {
1290             $Element['attributes']['href'] = $matches[1];
1291
1292             if (isset($matches[2]))
1293             {
1294                 $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1295             }
1296
1297             $extent += strlen($matches[0]);
1298         }
1299         else
1300         {
1301             if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1302             {
1303                 $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
1304                 $definition = strtolower($definition);
1305
1306                 $extent += strlen($matches[0]);
1307             }
1308             else
1309             {
1310                 $definition = strtolower($Element['text']);
1311             }
1312
1313             if ( ! isset($this->DefinitionData['Reference'][$definition]))
1314             {
1315                 return;
1316             }
1317
1318             $Definition = $this->DefinitionData['Reference'][$definition];
1319
1320             $Element['attributes']['href'] = $Definition['url'];
1321             $Element['attributes']['title'] = $Definition['title'];
1322         }
1323
1324         return array(
1325             'extent' => $extent,
1326             'element' => $Element,
1327         );
1328     }
1329
1330     protected function inlineMarkup($Excerpt)
1331     {
1332         if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
1333         {
1334             return;
1335         }
1336
1337         if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches))
1338         {
1339             return array(
1340                 'markup' => $matches[0],
1341                 'extent' => strlen($matches[0]),
1342             );
1343         }
1344
1345         if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
1346         {
1347             return array(
1348                 'markup' => $matches[0],
1349                 'extent' => strlen($matches[0]),
1350             );
1351         }
1352
1353         if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
1354         {
1355             return array(
1356                 'markup' => $matches[0],
1357                 'extent' => strlen($matches[0]),
1358             );
1359         }
1360     }
1361
1362     protected function inlineSpecialCharacter($Excerpt)
1363     {
1364         if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
1365         {
1366             return array(
1367                 'markup' => '&amp;',
1368                 'extent' => 1,
1369             );
1370         }
1371
1372         $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1373
1374         if (isset($SpecialCharacter[$Excerpt['text'][0]]))
1375         {
1376             return array(
1377                 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1378                 'extent' => 1,
1379             );
1380         }
1381     }
1382
1383     protected function inlineStrikethrough($Excerpt)
1384     {
1385         if ( ! isset($Excerpt['text'][1]))
1386         {
1387             return;
1388         }
1389
1390         if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1391         {
1392             return array(
1393                 'extent' => strlen($matches[0]),
1394                 'element' => array(
1395                     'name' => 'del',
1396                     'text' => $matches[1],
1397                     'handler' => 'line',
1398                 ),
1399             );
1400         }
1401     }
1402
1403     protected function inlineUrl($Excerpt)
1404     {
1405         if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1406         {
1407             return;
1408         }
1409
1410         if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
1411         {
1412             $url = $matches[0][0];
1413
1414             $Inline = array(
1415                 'extent' => strlen($matches[0][0]),
1416                 'position' => $matches[0][1],
1417                 'element' => array(
1418                     'name' => 'a',
1419                     'text' => $url,
1420                     'attributes' => array(
1421                         'href' => $url,
1422                     ),
1423                 ),
1424             );
1425
1426             return $Inline;
1427         }
1428     }
1429
1430     protected function inlineUrlTag($Excerpt)
1431     {
1432         if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
1433         {
1434             $url = $matches[1];
1435
1436             return array(
1437                 'extent' => strlen($matches[0]),
1438                 'element' => array(
1439                     'name' => 'a',
1440                     'text' => $url,
1441                     'attributes' => array(
1442                         'href' => $url,
1443                     ),
1444                 ),
1445             );
1446         }
1447     }
1448
1449     # ~
1450
1451     protected function unmarkedText($text)
1452     {
1453         if ($this->breaksEnabled)
1454         {
1455             $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1456         }
1457         else
1458         {
1459             $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1460             $text = str_replace(" \n", "\n", $text);
1461         }
1462
1463         return $text;
1464     }
1465
1466     #
1467     # Handlers
1468     #
1469
1470     protected function element(array $Element)
1471     {
1472         if ($this->safeMode)
1473         {
1474             $Element = $this->sanitiseElement($Element);
1475         }
1476
1477         $markup = '<'.$Element['name'];
1478
1479         if (isset($Element['attributes']))
1480         {
1481             foreach ($Element['attributes'] as $name => $value)
1482             {
1483                 if ($value === null)
1484                 {
1485                     continue;
1486                 }
1487
1488                 $markup .= ' '.$name.'="'.self::escape($value).'"';
1489             }
1490         }
1491
1492         $permitRawHtml = false;
1493
1494         if (isset($Element['text']))
1495         {
1496             $text = $Element['text'];
1497         }
1498         // very strongly consider an alternative if you're writing an
1499         // extension
1500         elseif (isset($Element['rawHtml']))
1501         {
1502             $text = $Element['rawHtml'];
1503             $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
1504             $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
1505         }
1506
1507         if (isset($text))
1508         {
1509             $markup .= '>';
1510
1511             if (!isset($Element['nonNestables']))
1512             {
1513                 $Element['nonNestables'] = array();
1514             }
1515
1516             if (isset($Element['handler']))
1517             {
1518                 $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']);
1519             }
1520             elseif (!$permitRawHtml)
1521             {
1522                 $markup .= self::escape($text, true);
1523             }
1524             else
1525             {
1526                 $markup .= $text;
1527             }
1528
1529             $markup .= '</'.$Element['name'].'>';
1530         }
1531         else
1532         {
1533             $markup .= ' />';
1534         }
1535
1536         return $markup;
1537     }
1538
1539     protected function elements(array $Elements)
1540     {
1541         $markup = '';
1542
1543         foreach ($Elements as $Element)
1544         {
1545             $markup .= "\n" . $this->element($Element);
1546         }
1547
1548         $markup .= "\n";
1549
1550         return $markup;
1551     }
1552
1553     # ~
1554
1555     protected function li($lines)
1556     {
1557         $markup = $this->lines($lines);
1558
1559         $trimmedMarkup = trim($markup);
1560
1561         if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
1562         {
1563             $markup = $trimmedMarkup;
1564             $markup = substr($markup, 3);
1565
1566             $position = strpos($markup, "</p>");
1567
1568             $markup = substr_replace($markup, '', $position, 4);
1569         }
1570
1571         return $markup;
1572     }
1573
1574     #
1575     # Deprecated Methods
1576     #
1577
1578     function parse($text)
1579     {
1580         $markup = $this->text($text);
1581
1582         return $markup;
1583     }
1584
1585     protected function sanitiseElement(array $Element)
1586     {
1587         static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
1588         static $safeUrlNameToAtt  = array(
1589             'a'   => 'href',
1590             'img' => 'src',
1591         );
1592
1593         if (isset($safeUrlNameToAtt[$Element['name']]))
1594         {
1595             $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
1596         }
1597
1598         if ( ! empty($Element['attributes']))
1599         {
1600             foreach ($Element['attributes'] as $att => $val)
1601             {
1602                 # filter out badly parsed attribute
1603                 if ( ! preg_match($goodAttribute, $att))
1604                 {
1605                     unset($Element['attributes'][$att]);
1606                 }
1607                 # dump onevent attribute
1608                 elseif (self::striAtStart($att, 'on'))
1609                 {
1610                     unset($Element['attributes'][$att]);
1611                 }
1612             }
1613         }
1614
1615         return $Element;
1616     }
1617
1618     protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
1619     {
1620         foreach ($this->safeLinksWhitelist as $scheme)
1621         {
1622             if (self::striAtStart($Element['attributes'][$attribute], $scheme))
1623             {
1624                 return $Element;
1625             }
1626         }
1627
1628         $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
1629
1630         return $Element;
1631     }
1632
1633     #
1634     # Static Methods
1635     #
1636
1637     protected static function escape($text, $allowQuotes = false)
1638     {
1639         return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
1640     }
1641
1642     protected static function striAtStart($string, $needle)
1643     {
1644         $len = strlen($needle);
1645
1646         if ($len > strlen($string))
1647         {
1648             return false;
1649         }
1650         else
1651         {
1652             return strtolower(substr($string, 0, $len)) === strtolower($needle);
1653         }
1654     }
1655
1656     static function instance($name = 'default')
1657     {
1658         if (isset(self::$instances[$name]))
1659         {
1660             return self::$instances[$name];
1661         }
1662
1663         $instance = new static();
1664
1665         self::$instances[$name] = $instance;
1666
1667         return $instance;
1668     }
1669
1670     private static $instances = array();
1671
1672     #
1673     # Fields
1674     #
1675
1676     protected $DefinitionData;
1677
1678     #
1679     # Read-Only
1680
1681     protected $specialCharacters = array(
1682         '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1683     );
1684
1685     protected $StrongRegex = array(
1686         '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1687         '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1688     );
1689
1690     protected $EmRegex = array(
1691         '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1692         '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1693     );
1694
1695     protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1696
1697     protected $voidElements = array(
1698         'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1699     );
1700
1701     protected $textLevelElements = array(
1702         'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1703         'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1704         'i', 'rp', 'del', 'code',          'strike', 'marquee',
1705         'q', 'rt', 'ins', 'font',          'strong',
1706         's', 'tt', 'kbd', 'mark',
1707         'u', 'xm', 'sub', 'nobr',
1708                    'sup', 'ruby',
1709                    'var', 'span',
1710                    'wbr', 'time',
1711     );
1712 }