Completed
Branch development (b1b115)
by Johannes
10:28
created

Parsedown::lines()   F

Complexity

Conditions 26
Paths 3160

Size

Total Lines 161

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 161
rs 0
cc 26
nc 3160
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
}
1680