Subversion Repositories cheapmusic

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
103 - 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.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
    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
                $class = 'language-'.$matches[1];
433
 
434
                $Element['attributes'] = array(
435
                    'class' => $class,
436
                );
437
            }
438
 
439
            $Block = array(
440
                'char' => $Line['text'][0],
441
                'element' => array(
442
                    'name' => 'pre',
443
                    'handler' => 'element',
444
                    'text' => $Element,
445
                ),
446
            );
447
 
448
            return $Block;
449
        }
450
    }
451
 
452
    protected function blockFencedCodeContinue($Line, $Block)
453
    {
454
        if (isset($Block['complete']))
455
        {
456
            return;
457
        }
458
 
459
        if (isset($Block['interrupted']))
460
        {
461
            $Block['element']['text']['text'] .= "\n";
462
 
463
            unset($Block['interrupted']);
464
        }
465
 
466
        if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
467
        {
468
            $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
469
 
470
            $Block['complete'] = true;
471
 
472
            return $Block;
473
        }
474
 
475
        $Block['element']['text']['text'] .= "\n".$Line['body'];
476
 
477
        return $Block;
478
    }
479
 
480
    protected function blockFencedCodeComplete($Block)
481
    {
482
        $text = $Block['element']['text']['text'];
483
 
484
        $Block['element']['text']['text'] = $text;
485
 
486
        return $Block;
487
    }
488
 
489
    #
490
    # Header
491
 
492
    protected function blockHeader($Line)
493
    {
494
        if (isset($Line['text'][1]))
495
        {
496
            $level = 1;
497
 
498
            while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
499
            {
500
                $level ++;
501
            }
502
 
503
            if ($level > 6)
504
            {
505
                return;
506
            }
507
 
508
            $text = trim($Line['text'], '# ');
509
 
510
            $Block = array(
511
                'element' => array(
512
                    'name' => 'h' . min(6, $level),
513
                    'text' => $text,
514
                    'handler' => 'line',
515
                ),
516
            );
517
 
518
            return $Block;
519
        }
520
    }
521
 
522
    #
523
    # List
524
 
525
    protected function blockList($Line)
526
    {
527
        list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
528
 
529
        if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
530
        {
531
            $Block = array(
532
                'indent' => $Line['indent'],
533
                'pattern' => $pattern,
534
                'element' => array(
535
                    'name' => $name,
536
                    'handler' => 'elements',
537
                ),
538
            );
539
 
540
            if($name === 'ol')
541
            {
542
                $listStart = stristr($matches[0], '.', true);
543
 
544
                if($listStart !== '1')
545
                {
546
                    $Block['element']['attributes'] = array('start' => $listStart);
547
                }
548
            }
549
 
550
            $Block['li'] = array(
551
                'name' => 'li',
552
                'handler' => 'li',
553
                'text' => array(
554
                    $matches[2],
555
                ),
556
            );
557
 
558
            $Block['element']['text'] []= & $Block['li'];
559
 
560
            return $Block;
561
        }
562
    }
563
 
564
    protected function blockListContinue($Line, array $Block)
565
    {
566
        if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
567
        {
568
            if (isset($Block['interrupted']))
569
            {
570
                $Block['li']['text'] []= '';
571
 
572
                $Block['loose'] = true;
573
 
574
                unset($Block['interrupted']);
575
            }
576
 
577
            unset($Block['li']);
578
 
579
            $text = isset($matches[1]) ? $matches[1] : '';
580
 
581
            $Block['li'] = array(
582
                'name' => 'li',
583
                'handler' => 'li',
584
                'text' => array(
585
                    $text,
586
                ),
587
            );
588
 
589
            $Block['element']['text'] []= & $Block['li'];
590
 
591
            return $Block;
592
        }
593
 
594
        if ($Line['text'][0] === '[' and $this->blockReference($Line))
595
        {
596
            return $Block;
597
        }
598
 
599
        if ( ! isset($Block['interrupted']))
600
        {
601
            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
602
 
603
            $Block['li']['text'] []= $text;
604
 
605
            return $Block;
606
        }
607
 
608
        if ($Line['indent'] > 0)
609
        {
610
            $Block['li']['text'] []= '';
611
 
612
            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
613
 
614
            $Block['li']['text'] []= $text;
615
 
616
            unset($Block['interrupted']);
617
 
618
            return $Block;
619
        }
620
    }
621
 
622
    protected function blockListComplete(array $Block)
623
    {
624
        if (isset($Block['loose']))
625
        {
626
            foreach ($Block['element']['text'] as &$li)
627
            {
628
                if (end($li['text']) !== '')
629
                {
630
                    $li['text'] []= '';
631
                }
632
            }
633
        }
634
 
635
        return $Block;
636
    }
637
 
638
    #
639
    # Quote
640
 
641
    protected function blockQuote($Line)
642
    {
643
        if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
644
        {
645
            $Block = array(
646
                'element' => array(
647
                    'name' => 'blockquote',
648
                    'handler' => 'lines',
649
                    'text' => (array) $matches[1],
650
                ),
651
            );
652
 
653
            return $Block;
654
        }
655
    }
656
 
657
    protected function blockQuoteContinue($Line, array $Block)
658
    {
659
        if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
660
        {
661
            if (isset($Block['interrupted']))
662
            {
663
                $Block['element']['text'] []= '';
664
 
665
                unset($Block['interrupted']);
666
            }
667
 
668
            $Block['element']['text'] []= $matches[1];
669
 
670
            return $Block;
671
        }
672
 
673
        if ( ! isset($Block['interrupted']))
674
        {
675
            $Block['element']['text'] []= $Line['text'];
676
 
677
            return $Block;
678
        }
679
    }
680
 
681
    #
682
    # Rule
683
 
684
    protected function blockRule($Line)
685
    {
686
        if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
687
        {
688
            $Block = array(
689
                'element' => array(
690
                    'name' => 'hr'
691
                ),
692
            );
693
 
694
            return $Block;
695
        }
696
    }
697
 
698
    #
699
    # Setext
700
 
701
    protected function blockSetextHeader($Line, array $Block = null)
702
    {
703
        if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
704
        {
705
            return;
706
        }
707
 
708
        if (chop($Line['text'], $Line['text'][0]) === '')
709
        {
710
            $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
711
 
712
            return $Block;
713
        }
714
    }
715
 
716
    #
717
    # Markup
718
 
719
    protected function blockMarkup($Line)
720
    {
721
        if ($this->markupEscaped or $this->safeMode)
722
        {
723
            return;
724
        }
725
 
726
        if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
727
        {
728
            $element = strtolower($matches[1]);
729
 
730
            if (in_array($element, $this->textLevelElements))
731
            {
732
                return;
733
            }
734
 
735
            $Block = array(
736
                'name' => $matches[1],
737
                'depth' => 0,
738
                'markup' => $Line['text'],
739
            );
740
 
741
            $length = strlen($matches[0]);
742
 
743
            $remainder = substr($Line['text'], $length);
744
 
745
            if (trim($remainder) === '')
746
            {
747
                if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
748
                {
749
                    $Block['closed'] = true;
750
 
751
                    $Block['void'] = true;
752
                }
753
            }
754
            else
755
            {
756
                if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
757
                {
758
                    return;
759
                }
760
 
761
                if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
762
                {
763
                    $Block['closed'] = true;
764
                }
765
            }
766
 
767
            return $Block;
768
        }
769
    }
770
 
771
    protected function blockMarkupContinue($Line, array $Block)
772
    {
773
        if (isset($Block['closed']))
774
        {
775
            return;
776
        }
777
 
778
        if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
779
        {
780
            $Block['depth'] ++;
781
        }
782
 
783
        if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
784
        {
785
            if ($Block['depth'] > 0)
786
            {
787
                $Block['depth'] --;
788
            }
789
            else
790
            {
791
                $Block['closed'] = true;
792
            }
793
        }
794
 
795
        if (isset($Block['interrupted']))
796
        {
797
            $Block['markup'] .= "\n";
798
 
799
            unset($Block['interrupted']);
800
        }
801
 
802
        $Block['markup'] .= "\n".$Line['body'];
803
 
804
        return $Block;
805
    }
806
 
807
    #
808
    # Reference
809
 
810
    protected function blockReference($Line)
811
    {
812
        if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
813
        {
814
            $id = strtolower($matches[1]);
815
 
816
            $Data = array(
817
                'url' => $matches[2],
818
                'title' => null,
819
            );
820
 
821
            if (isset($matches[3]))
822
            {
823
                $Data['title'] = $matches[3];
824
            }
825
 
826
            $this->DefinitionData['Reference'][$id] = $Data;
827
 
828
            $Block = array(
829
                'hidden' => true,
830
            );
831
 
832
            return $Block;
833
        }
834
    }
835
 
836
    #
837
    # Table
838
 
839
    protected function blockTable($Line, array $Block = null)
840
    {
841
        if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
842
        {
843
            return;
844
        }
845
 
846
        if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
847
        {
848
            $alignments = array();
849
 
850
            $divider = $Line['text'];
851
 
852
            $divider = trim($divider);
853
            $divider = trim($divider, '|');
854
 
855
            $dividerCells = explode('|', $divider);
856
 
857
            foreach ($dividerCells as $dividerCell)
858
            {
859
                $dividerCell = trim($dividerCell);
860
 
861
                if ($dividerCell === '')
862
                {
863
                    continue;
864
                }
865
 
866
                $alignment = null;
867
 
868
                if ($dividerCell[0] === ':')
869
                {
870
                    $alignment = 'left';
871
                }
872
 
873
                if (substr($dividerCell, - 1) === ':')
874
                {
875
                    $alignment = $alignment === 'left' ? 'center' : 'right';
876
                }
877
 
878
                $alignments []= $alignment;
879
            }
880
 
881
            # ~
882
 
883
            $HeaderElements = array();
884
 
885
            $header = $Block['element']['text'];
886
 
887
            $header = trim($header);
888
            $header = trim($header, '|');
889
 
890
            $headerCells = explode('|', $header);
891
 
892
            foreach ($headerCells as $index => $headerCell)
893
            {
894
                $headerCell = trim($headerCell);
895
 
896
                $HeaderElement = array(
897
                    'name' => 'th',
898
                    'text' => $headerCell,
899
                    'handler' => 'line',
900
                );
901
 
902
                if (isset($alignments[$index]))
903
                {
904
                    $alignment = $alignments[$index];
905
 
906
                    $HeaderElement['attributes'] = array(
907
                        'style' => 'text-align: '.$alignment.';',
908
                    );
909
                }
910
 
911
                $HeaderElements []= $HeaderElement;
912
            }
913
 
914
            # ~
915
 
916
            $Block = array(
917
                'alignments' => $alignments,
918
                'identified' => true,
919
                'element' => array(
920
                    'name' => 'table',
921
                    'handler' => 'elements',
922
                ),
923
            );
924
 
925
            $Block['element']['text'] []= array(
926
                'name' => 'thead',
927
                'handler' => 'elements',
928
            );
929
 
930
            $Block['element']['text'] []= array(
931
                'name' => 'tbody',
932
                'handler' => 'elements',
933
                'text' => array(),
934
            );
935
 
936
            $Block['element']['text'][0]['text'] []= array(
937
                'name' => 'tr',
938
                'handler' => 'elements',
939
                'text' => $HeaderElements,
940
            );
941
 
942
            return $Block;
943
        }
944
    }
945
 
946
    protected function blockTableContinue($Line, array $Block)
947
    {
948
        if (isset($Block['interrupted']))
949
        {
950
            return;
951
        }
952
 
953
        if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
954
        {
955
            $Elements = array();
956
 
957
            $row = $Line['text'];
958
 
959
            $row = trim($row);
960
            $row = trim($row, '|');
961
 
962
            preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
963
 
964
            foreach ($matches[0] as $index => $cell)
965
            {
966
                $cell = trim($cell);
967
 
968
                $Element = array(
969
                    'name' => 'td',
970
                    'handler' => 'line',
971
                    'text' => $cell,
972
                );
973
 
974
                if (isset($Block['alignments'][$index]))
975
                {
976
                    $Element['attributes'] = array(
977
                        'style' => 'text-align: '.$Block['alignments'][$index].';',
978
                    );
979
                }
980
 
981
                $Elements []= $Element;
982
            }
983
 
984
            $Element = array(
985
                'name' => 'tr',
986
                'handler' => 'elements',
987
                'text' => $Elements,
988
            );
989
 
990
            $Block['element']['text'][1]['text'] []= $Element;
991
 
992
            return $Block;
993
        }
994
    }
995
 
996
    #
997
    # ~
998
    #
999
 
1000
    protected function paragraph($Line)
1001
    {
1002
        $Block = array(
1003
            'element' => array(
1004
                'name' => 'p',
1005
                'text' => $Line['text'],
1006
                'handler' => 'line',
1007
            ),
1008
        );
1009
 
1010
        return $Block;
1011
    }
1012
 
1013
    #
1014
    # Inline Elements
1015
    #
1016
 
1017
    protected $InlineTypes = array(
1018
        '"' => array('SpecialCharacter'),
1019
        '!' => array('Image'),
1020
        '&' => array('SpecialCharacter'),
1021
        '*' => array('Emphasis'),
1022
        ':' => array('Url'),
1023
        '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
1024
        '>' => array('SpecialCharacter'),
1025
        '[' => array('Link'),
1026
        '_' => array('Emphasis'),
1027
        '`' => array('Code'),
1028
        '~' => array('Strikethrough'),
1029
        '\\' => array('EscapeSequence'),
1030
    );
1031
 
1032
    # ~
1033
 
1034
    protected $inlineMarkerList = '!"*_&[:<>`~\\';
1035
 
1036
    #
1037
    # ~
1038
    #
1039
 
1040
    public function line($text, $nonNestables=array())
1041
    {
1042
        $markup = '';
1043
 
1044
        # $excerpt is based on the first occurrence of a marker
1045
 
1046
        while ($excerpt = strpbrk($text, $this->inlineMarkerList))
1047
        {
1048
            $marker = $excerpt[0];
1049
 
1050
            $markerPosition = strpos($text, $marker);
1051
 
1052
            $Excerpt = array('text' => $excerpt, 'context' => $text);
1053
 
1054
            foreach ($this->InlineTypes[$marker] as $inlineType)
1055
            {
1056
                # check to see if the current inline type is nestable in the current context
1057
 
1058
                if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables))
1059
                {
1060
                    continue;
1061
                }
1062
 
1063
                $Inline = $this->{'inline'.$inlineType}($Excerpt);
1064
 
1065
                if ( ! isset($Inline))
1066
                {
1067
                    continue;
1068
                }
1069
 
1070
                # makes sure that the inline belongs to "our" marker
1071
 
1072
                if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
1073
                {
1074
                    continue;
1075
                }
1076
 
1077
                # sets a default inline position
1078
 
1079
                if ( ! isset($Inline['position']))
1080
                {
1081
                    $Inline['position'] = $markerPosition;
1082
                }
1083
 
1084
                # cause the new element to 'inherit' our non nestables
1085
 
1086
                foreach ($nonNestables as $non_nestable)
1087
                {
1088
                    $Inline['element']['nonNestables'][] = $non_nestable;
1089
                }
1090
 
1091
                # the text that comes before the inline
1092
                $unmarkedText = substr($text, 0, $Inline['position']);
1093
 
1094
                # compile the unmarked text
1095
                $markup .= $this->unmarkedText($unmarkedText);
1096
 
1097
                # compile the inline
1098
                $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1099
 
1100
                # remove the examined text
1101
                $text = substr($text, $Inline['position'] + $Inline['extent']);
1102
 
1103
                continue 2;
1104
            }
1105
 
1106
            # the marker does not belong to an inline
1107
 
1108
            $unmarkedText = substr($text, 0, $markerPosition + 1);
1109
 
1110
            $markup .= $this->unmarkedText($unmarkedText);
1111
 
1112
            $text = substr($text, $markerPosition + 1);
1113
        }
1114
 
1115
        $markup .= $this->unmarkedText($text);
1116
 
1117
        return $markup;
1118
    }
1119
 
1120
    #
1121
    # ~
1122
    #
1123
 
1124
    protected function inlineCode($Excerpt)
1125
    {
1126
        $marker = $Excerpt['text'][0];
1127
 
1128
        if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1129
        {
1130
            $text = $matches[2];
1131
            $text = preg_replace("/[ ]*\n/", ' ', $text);
1132
 
1133
            return array(
1134
                'extent' => strlen($matches[0]),
1135
                'element' => array(
1136
                    'name' => 'code',
1137
                    'text' => $text,
1138
                ),
1139
            );
1140
        }
1141
    }
1142
 
1143
    protected function inlineEmailTag($Excerpt)
1144
    {
1145
        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
1146
        {
1147
            $url = $matches[1];
1148
 
1149
            if ( ! isset($matches[2]))
1150
            {
1151
                $url = 'mailto:' . $url;
1152
            }
1153
 
1154
            return array(
1155
                'extent' => strlen($matches[0]),
1156
                'element' => array(
1157
                    'name' => 'a',
1158
                    'text' => $matches[1],
1159
                    'attributes' => array(
1160
                        'href' => $url,
1161
                    ),
1162
                ),
1163
            );
1164
        }
1165
    }
1166
 
1167
    protected function inlineEmphasis($Excerpt)
1168
    {
1169
        if ( ! isset($Excerpt['text'][1]))
1170
        {
1171
            return;
1172
        }
1173
 
1174
        $marker = $Excerpt['text'][0];
1175
 
1176
        if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1177
        {
1178
            $emphasis = 'strong';
1179
        }
1180
        elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1181
        {
1182
            $emphasis = 'em';
1183
        }
1184
        else
1185
        {
1186
            return;
1187
        }
1188
 
1189
        return array(
1190
            'extent' => strlen($matches[0]),
1191
            'element' => array(
1192
                'name' => $emphasis,
1193
                'handler' => 'line',
1194
                'text' => $matches[1],
1195
            ),
1196
        );
1197
    }
1198
 
1199
    protected function inlineEscapeSequence($Excerpt)
1200
    {
1201
        if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1202
        {
1203
            return array(
1204
                'markup' => $Excerpt['text'][1],
1205
                'extent' => 2,
1206
            );
1207
        }
1208
    }
1209
 
1210
    protected function inlineImage($Excerpt)
1211
    {
1212
        if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1213
        {
1214
            return;
1215
        }
1216
 
1217
        $Excerpt['text']= substr($Excerpt['text'], 1);
1218
 
1219
        $Link = $this->inlineLink($Excerpt);
1220
 
1221
        if ($Link === null)
1222
        {
1223
            return;
1224
        }
1225
 
1226
        $Inline = array(
1227
            'extent' => $Link['extent'] + 1,
1228
            'element' => array(
1229
                'name' => 'img',
1230
                'attributes' => array(
1231
                    'src' => $Link['element']['attributes']['href'],
1232
                    'alt' => $Link['element']['text'],
1233
                ),
1234
            ),
1235
        );
1236
 
1237
        $Inline['element']['attributes'] += $Link['element']['attributes'];
1238
 
1239
        unset($Inline['element']['attributes']['href']);
1240
 
1241
        return $Inline;
1242
    }
1243
 
1244
    protected function inlineLink($Excerpt)
1245
    {
1246
        $Element = array(
1247
            'name' => 'a',
1248
            'handler' => 'line',
1249
            'nonNestables' => array('Url', 'Link'),
1250
            'text' => null,
1251
            'attributes' => array(
1252
                'href' => null,
1253
                'title' => null,
1254
            ),
1255
        );
1256
 
1257
        $extent = 0;
1258
 
1259
        $remainder = $Excerpt['text'];
1260
 
1261
        if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
1262
        {
1263
            $Element['text'] = $matches[1];
1264
 
1265
            $extent += strlen($matches[0]);
1266
 
1267
            $remainder = substr($remainder, $extent);
1268
        }
1269
        else
1270
        {
1271
            return;
1272
        }
1273
 
1274
        if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches))
1275
        {
1276
            $Element['attributes']['href'] = $matches[1];
1277
 
1278
            if (isset($matches[2]))
1279
            {
1280
                $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1281
            }
1282
 
1283
            $extent += strlen($matches[0]);
1284
        }
1285
        else
1286
        {
1287
            if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1288
            {
1289
                $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
1290
                $definition = strtolower($definition);
1291
 
1292
                $extent += strlen($matches[0]);
1293
            }
1294
            else
1295
            {
1296
                $definition = strtolower($Element['text']);
1297
            }
1298
 
1299
            if ( ! isset($this->DefinitionData['Reference'][$definition]))
1300
            {
1301
                return;
1302
            }
1303
 
1304
            $Definition = $this->DefinitionData['Reference'][$definition];
1305
 
1306
            $Element['attributes']['href'] = $Definition['url'];
1307
            $Element['attributes']['title'] = $Definition['title'];
1308
        }
1309
 
1310
        return array(
1311
            'extent' => $extent,
1312
            'element' => $Element,
1313
        );
1314
    }
1315
 
1316
    protected function inlineMarkup($Excerpt)
1317
    {
1318
        if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
1319
        {
1320
            return;
1321
        }
1322
 
1323
        if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches))
1324
        {
1325
            return array(
1326
                'markup' => $matches[0],
1327
                'extent' => strlen($matches[0]),
1328
            );
1329
        }
1330
 
1331
        if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
1332
        {
1333
            return array(
1334
                'markup' => $matches[0],
1335
                'extent' => strlen($matches[0]),
1336
            );
1337
        }
1338
 
1339
        if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
1340
        {
1341
            return array(
1342
                'markup' => $matches[0],
1343
                'extent' => strlen($matches[0]),
1344
            );
1345
        }
1346
    }
1347
 
1348
    protected function inlineSpecialCharacter($Excerpt)
1349
    {
1350
        if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
1351
        {
1352
            return array(
1353
                'markup' => '&amp;',
1354
                'extent' => 1,
1355
            );
1356
        }
1357
 
1358
        $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1359
 
1360
        if (isset($SpecialCharacter[$Excerpt['text'][0]]))
1361
        {
1362
            return array(
1363
                'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1364
                'extent' => 1,
1365
            );
1366
        }
1367
    }
1368
 
1369
    protected function inlineStrikethrough($Excerpt)
1370
    {
1371
        if ( ! isset($Excerpt['text'][1]))
1372
        {
1373
            return;
1374
        }
1375
 
1376
        if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1377
        {
1378
            return array(
1379
                'extent' => strlen($matches[0]),
1380
                'element' => array(
1381
                    'name' => 'del',
1382
                    'text' => $matches[1],
1383
                    'handler' => 'line',
1384
                ),
1385
            );
1386
        }
1387
    }
1388
 
1389
    protected function inlineUrl($Excerpt)
1390
    {
1391
        if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1392
        {
1393
            return;
1394
        }
1395
 
1396
        if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
1397
        {
1398
            $url = $matches[0][0];
1399
 
1400
            $Inline = array(
1401
                'extent' => strlen($matches[0][0]),
1402
                'position' => $matches[0][1],
1403
                'element' => array(
1404
                    'name' => 'a',
1405
                    'text' => $url,
1406
                    'attributes' => array(
1407
                        'href' => $url,
1408
                    ),
1409
                ),
1410
            );
1411
 
1412
            return $Inline;
1413
        }
1414
    }
1415
 
1416
    protected function inlineUrlTag($Excerpt)
1417
    {
1418
        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
1419
        {
1420
            $url = $matches[1];
1421
 
1422
            return array(
1423
                'extent' => strlen($matches[0]),
1424
                'element' => array(
1425
                    'name' => 'a',
1426
                    'text' => $url,
1427
                    'attributes' => array(
1428
                        'href' => $url,
1429
                    ),
1430
                ),
1431
            );
1432
        }
1433
    }
1434
 
1435
    # ~
1436
 
1437
    protected function unmarkedText($text)
1438
    {
1439
        if ($this->breaksEnabled)
1440
        {
1441
            $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1442
        }
1443
        else
1444
        {
1445
            $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1446
            $text = str_replace(" \n", "\n", $text);
1447
        }
1448
 
1449
        return $text;
1450
    }
1451
 
1452
    #
1453
    # Handlers
1454
    #
1455
 
1456
    protected function element(array $Element)
1457
    {
1458
        if ($this->safeMode)
1459
        {
1460
            $Element = $this->sanitiseElement($Element);
1461
        }
1462
 
1463
        $markup = '<'.$Element['name'];
1464
 
1465
        if (isset($Element['attributes']))
1466
        {
1467
            foreach ($Element['attributes'] as $name => $value)
1468
            {
1469
                if ($value === null)
1470
                {
1471
                    continue;
1472
                }
1473
 
1474
                $markup .= ' '.$name.'="'.self::escape($value).'"';
1475
            }
1476
        }
1477
 
1478
        if (isset($Element['text']))
1479
        {
1480
            $markup .= '>';
1481
 
1482
            if (!isset($Element['nonNestables']))
1483
            {
1484
                $Element['nonNestables'] = array();
1485
            }
1486
 
1487
            if (isset($Element['handler']))
1488
            {
1489
                $markup .= $this->{$Element['handler']}($Element['text'], $Element['nonNestables']);
1490
            }
1491
            else
1492
            {
1493
                $markup .= self::escape($Element['text'], true);
1494
            }
1495
 
1496
            $markup .= '</'.$Element['name'].'>';
1497
        }
1498
        else
1499
        {
1500
            $markup .= ' />';
1501
        }
1502
 
1503
        return $markup;
1504
    }
1505
 
1506
    protected function elements(array $Elements)
1507
    {
1508
        $markup = '';
1509
 
1510
        foreach ($Elements as $Element)
1511
        {
1512
            $markup .= "\n" . $this->element($Element);
1513
        }
1514
 
1515
        $markup .= "\n";
1516
 
1517
        return $markup;
1518
    }
1519
 
1520
    # ~
1521
 
1522
    protected function li($lines)
1523
    {
1524
        $markup = $this->lines($lines);
1525
 
1526
        $trimmedMarkup = trim($markup);
1527
 
1528
        if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
1529
        {
1530
            $markup = $trimmedMarkup;
1531
            $markup = substr($markup, 3);
1532
 
1533
            $position = strpos($markup, "</p>");
1534
 
1535
            $markup = substr_replace($markup, '', $position, 4);
1536
        }
1537
 
1538
        return $markup;
1539
    }
1540
 
1541
    #
1542
    # Deprecated Methods
1543
    #
1544
 
1545
    function parse($text)
1546
    {
1547
        $markup = $this->text($text);
1548
 
1549
        return $markup;
1550
    }
1551
 
1552
    protected function sanitiseElement(array $Element)
1553
    {
1554
        static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
1555
        static $safeUrlNameToAtt  = array(
1556
            'a'   => 'href',
1557
            'img' => 'src',
1558
        );
1559
 
1560
        if (isset($safeUrlNameToAtt[$Element['name']]))
1561
        {
1562
            $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
1563
        }
1564
 
1565
        if ( ! empty($Element['attributes']))
1566
        {
1567
            foreach ($Element['attributes'] as $att => $val)
1568
            {
1569
                # filter out badly parsed attribute
1570
                if ( ! preg_match($goodAttribute, $att))
1571
                {
1572
                    unset($Element['attributes'][$att]);
1573
                }
1574
                # dump onevent attribute
1575
                elseif (self::striAtStart($att, 'on'))
1576
                {
1577
                    unset($Element['attributes'][$att]);
1578
                }
1579
            }
1580
        }
1581
 
1582
        return $Element;
1583
    }
1584
 
1585
    protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
1586
    {
1587
        foreach ($this->safeLinksWhitelist as $scheme)
1588
        {
1589
            if (self::striAtStart($Element['attributes'][$attribute], $scheme))
1590
            {
1591
                return $Element;
1592
            }
1593
        }
1594
 
1595
        $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
1596
 
1597
        return $Element;
1598
    }
1599
 
1600
    #
1601
    # Static Methods
1602
    #
1603
 
1604
    protected static function escape($text, $allowQuotes = false)
1605
    {
1606
        return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
1607
    }
1608
 
1609
    protected static function striAtStart($string, $needle)
1610
    {
1611
        $len = strlen($needle);
1612
 
1613
        if ($len > strlen($string))
1614
        {
1615
            return false;
1616
        }
1617
        else
1618
        {
1619
            return strtolower(substr($string, 0, $len)) === strtolower($needle);
1620
        }
1621
    }
1622
 
1623
    static function instance($name = 'default')
1624
    {
1625
        if (isset(self::$instances[$name]))
1626
        {
1627
            return self::$instances[$name];
1628
        }
1629
 
1630
        $instance = new static();
1631
 
1632
        self::$instances[$name] = $instance;
1633
 
1634
        return $instance;
1635
    }
1636
 
1637
    private static $instances = array();
1638
 
1639
    #
1640
    # Fields
1641
    #
1642
 
1643
    protected $DefinitionData;
1644
 
1645
    #
1646
    # Read-Only
1647
 
1648
    protected $specialCharacters = array(
1649
        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1650
    );
1651
 
1652
    protected $StrongRegex = array(
1653
        '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1654
        '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1655
    );
1656
 
1657
    protected $EmRegex = array(
1658
        '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1659
        '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1660
    );
1661
 
1662
    protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1663
 
1664
    protected $voidElements = array(
1665
        'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1666
    );
1667
 
1668
    protected $textLevelElements = array(
1669
        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1670
        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1671
        'i', 'rp', 'del', 'code',          'strike', 'marquee',
1672
        'q', 'rt', 'ins', 'font',          'strong',
1673
        's', 'tt', 'kbd', 'mark',
1674
        'u', 'xm', 'sub', 'nobr',
1675
                   'sup', 'ruby',
1676
                   'var', 'span',
1677
                   'wbr', 'time',
1678
    );
1679
}