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