]> git.mxchange.org Git - friendica.git/blob - library/parsedown/Parsedown.php
New routines for markdown to html and html to markdown.
[friendica.git] / library / 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.5.1';
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 $DefinitionTypes = array(
111         '[' => array('Reference'),
112     );
113
114     # ~
115
116     protected $unmarkedBlockTypes = array(
117         'Code',
118     );
119
120     #
121     # Blocks
122     #
123
124     private function lines(array $lines)
125     {
126         $CurrentBlock = null;
127
128         foreach ($lines as $line)
129         {
130             if (chop($line) === '')
131             {
132                 if (isset($CurrentBlock))
133                 {
134                     $CurrentBlock['interrupted'] = true;
135                 }
136
137                 continue;
138             }
139
140             if (strpos($line, "\t") !== false)
141             {
142                 $parts = explode("\t", $line);
143
144                 $line = $parts[0];
145
146                 unset($parts[0]);
147
148                 foreach ($parts as $part)
149                 {
150                     $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
151
152                     $line .= str_repeat(' ', $shortage);
153                     $line .= $part;
154                 }
155             }
156
157             $indent = 0;
158
159             while (isset($line[$indent]) and $line[$indent] === ' ')
160             {
161                 $indent ++;
162             }
163
164             $text = $indent > 0 ? substr($line, $indent) : $line;
165
166             # ~
167
168             $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
169
170             # ~
171
172             if (isset($CurrentBlock['incomplete']))
173             {
174                 $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
175
176                 if (isset($Block))
177                 {
178                     $CurrentBlock = $Block;
179
180                     continue;
181                 }
182                 else
183                 {
184                     if (method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
185                     {
186                         $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
187                     }
188
189                     unset($CurrentBlock['incomplete']);
190                 }
191             }
192
193             # ~
194
195             $marker = $text[0];
196
197             # ~
198
199             $blockTypes = $this->unmarkedBlockTypes;
200
201             if (isset($this->BlockTypes[$marker]))
202             {
203                 foreach ($this->BlockTypes[$marker] as $blockType)
204                 {
205                     $blockTypes []= $blockType;
206                 }
207             }
208
209             #
210             # ~
211
212             foreach ($blockTypes as $blockType)
213             {
214                 $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
215
216                 if (isset($Block))
217                 {
218                     $Block['type'] = $blockType;
219
220                     if ( ! isset($Block['identified']))
221                     {
222                         $Blocks []= $CurrentBlock;
223
224                         $Block['identified'] = true;
225                     }
226
227                     if (method_exists($this, 'block'.$blockType.'Continue'))
228                     {
229                         $Block['incomplete'] = true;
230                     }
231
232                     $CurrentBlock = $Block;
233
234                     continue 2;
235                 }
236             }
237
238             # ~
239
240             if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
241             {
242                 $CurrentBlock['element']['text'] .= "\n".$text;
243             }
244             else
245             {
246                 $Blocks []= $CurrentBlock;
247
248                 $CurrentBlock = $this->paragraph($Line);
249
250                 $CurrentBlock['identified'] = true;
251             }
252         }
253
254         # ~
255
256         if (isset($CurrentBlock['incomplete']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
257         {
258             $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
259         }
260
261         # ~
262
263         $Blocks []= $CurrentBlock;
264
265         unset($Blocks[0]);
266
267         # ~
268
269         $markup = '';
270
271         foreach ($Blocks as $Block)
272         {
273             if (isset($Block['hidden']))
274             {
275                 continue;
276             }
277
278             $markup .= "\n";
279             $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
280         }
281
282         $markup .= "\n";
283
284         # ~
285
286         return $markup;
287     }
288
289     #
290     # Code
291
292     protected function blockCode($Line, $Block = null)
293     {
294         if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
295         {
296             return;
297         }
298
299         if ($Line['indent'] >= 4)
300         {
301             $text = substr($Line['body'], 4);
302
303             $Block = array(
304                 'element' => array(
305                     'name' => 'pre',
306                     'handler' => 'element',
307                     'text' => array(
308                         'name' => 'code',
309                         'text' => $text,
310                     ),
311                 ),
312             );
313
314             return $Block;
315         }
316     }
317
318     protected function blockCodeContinue($Line, $Block)
319     {
320         if ($Line['indent'] >= 4)
321         {
322             if (isset($Block['interrupted']))
323             {
324                 $Block['element']['text']['text'] .= "\n";
325
326                 unset($Block['interrupted']);
327             }
328
329             $Block['element']['text']['text'] .= "\n";
330
331             $text = substr($Line['body'], 4);
332
333             $Block['element']['text']['text'] .= $text;
334
335             return $Block;
336         }
337     }
338
339     protected function blockCodeComplete($Block)
340     {
341         $text = $Block['element']['text']['text'];
342
343         $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
344
345         $Block['element']['text']['text'] = $text;
346
347         return $Block;
348     }
349
350     #
351     # Comment
352
353     protected function blockComment($Line)
354     {
355         if ($this->markupEscaped)
356         {
357             return;
358         }
359
360         if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
361         {
362             $Block = array(
363                 'markup' => $Line['body'],
364             );
365
366             if (preg_match('/-->$/', $Line['text']))
367             {
368                 $Block['closed'] = true;
369             }
370
371             return $Block;
372         }
373     }
374
375     protected function blockCommentContinue($Line, array $Block)
376     {
377         if (isset($Block['closed']))
378         {
379             return;
380         }
381
382         $Block['markup'] .= "\n" . $Line['body'];
383
384         if (preg_match('/-->$/', $Line['text']))
385         {
386             $Block['closed'] = true;
387         }
388
389         return $Block;
390     }
391
392     #
393     # Fenced Code
394
395     protected function blockFencedCode($Line)
396     {
397         if (preg_match('/^(['.$Line['text'][0].']{3,})[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
398         {
399             $Element = array(
400                 'name' => 'code',
401                 'text' => '',
402             );
403
404             if (isset($matches[2]))
405             {
406                 $class = 'language-'.$matches[2];
407
408                 $Element['attributes'] = array(
409                     'class' => $class,
410                 );
411             }
412
413             $Block = array(
414                 'char' => $Line['text'][0],
415                 'element' => array(
416                     'name' => 'pre',
417                     'handler' => 'element',
418                     'text' => $Element,
419                 ),
420             );
421
422             return $Block;
423         }
424     }
425
426     protected function blockFencedCodeContinue($Line, $Block)
427     {
428         if (isset($Block['complete']))
429         {
430             return;
431         }
432
433         if (isset($Block['interrupted']))
434         {
435             $Block['element']['text']['text'] .= "\n";
436
437             unset($Block['interrupted']);
438         }
439
440         if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
441         {
442             $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
443
444             $Block['complete'] = true;
445
446             return $Block;
447         }
448
449         $Block['element']['text']['text'] .= "\n".$Line['body'];;
450
451         return $Block;
452     }
453
454     protected function blockFencedCodeComplete($Block)
455     {
456         $text = $Block['element']['text']['text'];
457
458         $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
459
460         $Block['element']['text']['text'] = $text;
461
462         return $Block;
463     }
464
465     #
466     # Header
467
468     protected function blockHeader($Line)
469     {
470         if (isset($Line['text'][1]))
471         {
472             $level = 1;
473
474             while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
475             {
476                 $level ++;
477             }
478
479             if ($level > 6)
480             {
481                 return;
482             }
483
484             $text = trim($Line['text'], '# ');
485
486             $Block = array(
487                 'element' => array(
488                     'name' => 'h' . min(6, $level),
489                     'text' => $text,
490                     'handler' => 'line',
491                 ),
492             );
493
494             return $Block;
495         }
496     }
497
498     #
499     # List
500
501     protected function blockList($Line)
502     {
503         list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
504
505         if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
506         {
507             $Block = array(
508                 'indent' => $Line['indent'],
509                 'pattern' => $pattern,
510                 'element' => array(
511                     'name' => $name,
512                     'handler' => 'elements',
513                 ),
514             );
515
516             $Block['li'] = array(
517                 'name' => 'li',
518                 'handler' => 'li',
519                 'text' => array(
520                     $matches[2],
521                 ),
522             );
523
524             $Block['element']['text'] []= & $Block['li'];
525
526             return $Block;
527         }
528     }
529
530     protected function blockListContinue($Line, array $Block)
531     {
532         if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
533         {
534             if (isset($Block['interrupted']))
535             {
536                 $Block['li']['text'] []= '';
537
538                 unset($Block['interrupted']);
539             }
540
541             unset($Block['li']);
542
543             $text = isset($matches[1]) ? $matches[1] : '';
544
545             $Block['li'] = array(
546                 'name' => 'li',
547                 'handler' => 'li',
548                 'text' => array(
549                     $text,
550                 ),
551             );
552
553             $Block['element']['text'] []= & $Block['li'];
554
555             return $Block;
556         }
557
558         if ($Line['text'][0] === '[' and $this->blockReference($Line))
559         {
560             return $Block;
561         }
562
563         if ( ! isset($Block['interrupted']))
564         {
565             $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
566
567             $Block['li']['text'] []= $text;
568
569             return $Block;
570         }
571
572         if ($Line['indent'] > 0)
573         {
574             $Block['li']['text'] []= '';
575
576             $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
577
578             $Block['li']['text'] []= $text;
579
580             unset($Block['interrupted']);
581
582             return $Block;
583         }
584     }
585
586     #
587     # Quote
588
589     protected function blockQuote($Line)
590     {
591         if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
592         {
593             $Block = array(
594                 'element' => array(
595                     'name' => 'blockquote',
596                     'handler' => 'lines',
597                     'text' => (array) $matches[1],
598                 ),
599             );
600
601             return $Block;
602         }
603     }
604
605     protected function blockQuoteContinue($Line, array $Block)
606     {
607         if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
608         {
609             if (isset($Block['interrupted']))
610             {
611                 $Block['element']['text'] []= '';
612
613                 unset($Block['interrupted']);
614             }
615
616             $Block['element']['text'] []= $matches[1];
617
618             return $Block;
619         }
620
621         if ( ! isset($Block['interrupted']))
622         {
623             $Block['element']['text'] []= $Line['text'];
624
625             return $Block;
626         }
627     }
628
629     #
630     # Rule
631
632     protected function blockRule($Line)
633     {
634         if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
635         {
636             $Block = array(
637                 'element' => array(
638                     'name' => 'hr'
639                 ),
640             );
641
642             return $Block;
643         }
644     }
645
646     #
647     # Setext
648
649     protected function blockSetextHeader($Line, array $Block = null)
650     {
651         if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
652         {
653             return;
654         }
655
656         if (chop($Line['text'], $Line['text'][0]) === '')
657         {
658             $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
659
660             return $Block;
661         }
662     }
663
664     #
665     # Markup
666
667     protected function blockMarkup($Line)
668     {
669         if ($this->markupEscaped)
670         {
671             return;
672         }
673
674         if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
675         {
676             if (in_array($matches[1], $this->textLevelElements))
677             {
678                 return;
679             }
680
681             $Block = array(
682                 'name' => $matches[1],
683                 'depth' => 0,
684                 'markup' => $Line['text'],
685             );
686
687             $length = strlen($matches[0]);
688
689             $remainder = substr($Line['text'], $length);
690
691             if (trim($remainder) === '')
692             {
693                 if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
694                 {
695                     $Block['closed'] = true;
696
697                     $Block['void'] = true;
698                 }
699             }
700             else
701             {
702                 if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
703                 {
704                     return;
705                 }
706
707                 if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
708                 {
709                     $Block['closed'] = true;
710                 }
711             }
712
713             return $Block;
714         }
715     }
716
717     protected function blockMarkupContinue($Line, array $Block)
718     {
719         if (isset($Block['closed']))
720         {
721             return;
722         }
723
724         if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
725         {
726             $Block['depth'] ++;
727         }
728
729         if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
730         {
731             if ($Block['depth'] > 0)
732             {
733                 $Block['depth'] --;
734             }
735             else
736             {
737                 $Block['closed'] = true;
738             }
739         }
740
741         if (isset($Block['interrupted']))
742         {
743             $Block['markup'] .= "\n";
744
745             unset($Block['interrupted']);
746         }
747
748         $Block['markup'] .= "\n".$Line['body'];
749
750         return $Block;
751     }
752
753     #
754     # Reference
755
756     protected function blockReference($Line)
757     {
758         if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
759         {
760             $id = strtolower($matches[1]);
761
762             $Data = array(
763                 'url' => $matches[2],
764                 'title' => null,
765             );
766
767             if (isset($matches[3]))
768             {
769                 $Data['title'] = $matches[3];
770             }
771
772             $this->DefinitionData['Reference'][$id] = $Data;
773
774             $Block = array(
775                 'hidden' => true,
776             );
777
778             return $Block;
779         }
780     }
781
782     #
783     # Table
784
785     protected function blockTable($Line, array $Block = null)
786     {
787         if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
788         {
789             return;
790         }
791
792         if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
793         {
794             $alignments = array();
795
796             $divider = $Line['text'];
797
798             $divider = trim($divider);
799             $divider = trim($divider, '|');
800
801             $dividerCells = explode('|', $divider);
802
803             foreach ($dividerCells as $dividerCell)
804             {
805                 $dividerCell = trim($dividerCell);
806
807                 if ($dividerCell === '')
808                 {
809                     continue;
810                 }
811
812                 $alignment = null;
813
814                 if ($dividerCell[0] === ':')
815                 {
816                     $alignment = 'left';
817                 }
818
819                 if (substr($dividerCell, - 1) === ':')
820                 {
821                     $alignment = $alignment === 'left' ? 'center' : 'right';
822                 }
823
824                 $alignments []= $alignment;
825             }
826
827             # ~
828
829             $HeaderElements = array();
830
831             $header = $Block['element']['text'];
832
833             $header = trim($header);
834             $header = trim($header, '|');
835
836             $headerCells = explode('|', $header);
837
838             foreach ($headerCells as $index => $headerCell)
839             {
840                 $headerCell = trim($headerCell);
841
842                 $HeaderElement = array(
843                     'name' => 'th',
844                     'text' => $headerCell,
845                     'handler' => 'line',
846                 );
847
848                 if (isset($alignments[$index]))
849                 {
850                     $alignment = $alignments[$index];
851
852                     $HeaderElement['attributes'] = array(
853                         'style' => 'text-align: '.$alignment.';',
854                     );
855                 }
856
857                 $HeaderElements []= $HeaderElement;
858             }
859
860             # ~
861
862             $Block = array(
863                 'alignments' => $alignments,
864                 'identified' => true,
865                 'element' => array(
866                     'name' => 'table',
867                     'handler' => 'elements',
868                 ),
869             );
870
871             $Block['element']['text'] []= array(
872                 'name' => 'thead',
873                 'handler' => 'elements',
874             );
875
876             $Block['element']['text'] []= array(
877                 'name' => 'tbody',
878                 'handler' => 'elements',
879                 'text' => array(),
880             );
881
882             $Block['element']['text'][0]['text'] []= array(
883                 'name' => 'tr',
884                 'handler' => 'elements',
885                 'text' => $HeaderElements,
886             );
887
888             return $Block;
889         }
890     }
891
892     protected function blockTableContinue($Line, array $Block)
893     {
894         if (isset($Block['interrupted']))
895         {
896             return;
897         }
898
899         if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
900         {
901             $Elements = array();
902
903             $row = $Line['text'];
904
905             $row = trim($row);
906             $row = trim($row, '|');
907
908             preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
909
910             foreach ($matches[0] as $index => $cell)
911             {
912                 $cell = trim($cell);
913
914                 $Element = array(
915                     'name' => 'td',
916                     'handler' => 'line',
917                     'text' => $cell,
918                 );
919
920                 if (isset($Block['alignments'][$index]))
921                 {
922                     $Element['attributes'] = array(
923                         'style' => 'text-align: '.$Block['alignments'][$index].';',
924                     );
925                 }
926
927                 $Elements []= $Element;
928             }
929
930             $Element = array(
931                 'name' => 'tr',
932                 'handler' => 'elements',
933                 'text' => $Elements,
934             );
935
936             $Block['element']['text'][1]['text'] []= $Element;
937
938             return $Block;
939         }
940     }
941
942     #
943     # ~
944     #
945
946     protected function paragraph($Line)
947     {
948         $Block = array(
949             'element' => array(
950                 'name' => 'p',
951                 'text' => $Line['text'],
952                 'handler' => 'line',
953             ),
954         );
955
956         return $Block;
957     }
958
959     #
960     # Inline Elements
961     #
962
963     protected $InlineTypes = array(
964         '"' => array('SpecialCharacter'),
965         '!' => array('Image'),
966         '&' => array('SpecialCharacter'),
967         '*' => array('Emphasis'),
968         ':' => array('Url'),
969         '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
970         '>' => array('SpecialCharacter'),
971         '[' => array('Link'),
972         '_' => array('Emphasis'),
973         '`' => array('Code'),
974         '~' => array('Strikethrough'),
975         '\\' => array('EscapeSequence'),
976     );
977
978     # ~
979
980     protected $inlineMarkerList = '!"*_&[:<>`~\\';
981
982     #
983     # ~
984     #
985
986     public function line($text)
987     {
988         $markup = '';
989
990         $unexaminedText = $text;
991
992         $markerPosition = 0;
993
994         while ($excerpt = strpbrk($unexaminedText, $this->inlineMarkerList))
995         {
996             $marker = $excerpt[0];
997
998             $markerPosition += strpos($unexaminedText, $marker);
999
1000             $Excerpt = array('text' => $excerpt, 'context' => $text);
1001
1002             foreach ($this->InlineTypes[$marker] as $inlineType)
1003             {
1004                 $Inline = $this->{'inline'.$inlineType}($Excerpt);
1005
1006                 if ( ! isset($Inline))
1007                 {
1008                     continue;
1009                 }
1010
1011                 if (isset($Inline['position']) and $Inline['position'] > $markerPosition) # position is ahead of marker
1012                 {
1013                     continue;
1014                 }
1015
1016                 if ( ! isset($Inline['position']))
1017                 {
1018                     $Inline['position'] = $markerPosition;
1019                 }
1020
1021                 $unmarkedText = substr($text, 0, $Inline['position']);
1022
1023                 $markup .= $this->unmarkedText($unmarkedText);
1024
1025                 $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1026
1027                 $text = substr($text, $Inline['position'] + $Inline['extent']);
1028
1029                 $unexaminedText = $text;
1030
1031                 $markerPosition = 0;
1032
1033                 continue 2;
1034             }
1035
1036             $unexaminedText = substr($excerpt, 1);
1037
1038             $markerPosition ++;
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 = $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 self();
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 }