Parsedown::escape()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
#
4
#
5
# Parsedown
6
# http://parsedown.org
7
#
8
# (c) Emanuil Rusev
9
# http://erusev.com
10
#
11
# For the full license information, view the LICENSE file that was distributed
12
# with this source code.
13
#
14
#
15
16
class Parsedown
17
{
18
    # ~
19
20
    const version = '1.7.4';
21
22
    # ~
23
24
    function text($text)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
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)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

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

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1579
    {
1580
        $markup = $this->text($text);
1581
1582
        return $markup;
1583
    }
1584
1585
    protected function sanitiseElement(array $Element)
1586
    {
1587
        static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
1588
        static $safeUrlNameToAtt  = array(
1589
            'a'   => 'href',
1590
            'img' => 'src',
1591
        );
1592
1593
        if (isset($safeUrlNameToAtt[$Element['name']]))
1594
        {
1595
            $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
1596
        }
1597
1598
        if ( ! empty($Element['attributes']))
1599
        {
1600
            foreach ($Element['attributes'] as $att => $val)
1601
            {
1602
                # filter out badly parsed attribute
1603
                if ( ! preg_match($goodAttribute, $att))
1604
                {
1605
                    unset($Element['attributes'][$att]);
1606
                }
1607
                # dump onevent attribute
1608
                elseif (self::striAtStart($att, 'on'))
1609
                {
1610
                    unset($Element['attributes'][$att]);
1611
                }
1612
            }
1613
        }
1614
1615
        return $Element;
1616
    }
1617
1618
    protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
1619
    {
1620
        foreach ($this->safeLinksWhitelist as $scheme)
1621
        {
1622
            if (self::striAtStart($Element['attributes'][$attribute], $scheme))
1623
            {
1624
                return $Element;
1625
            }
1626
        }
1627
1628
        $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
1629
1630
        return $Element;
1631
    }
1632
1633
    #
1634
    # Static Methods
1635
    #
1636
1637
    protected static function escape($text, $allowQuotes = false)
1638
    {
1639
        return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
1640
    }
1641
1642
    protected static function striAtStart($string, $needle)
1643
    {
1644
        $len = strlen($needle);
1645
1646
        if ($len > strlen($string))
1647
        {
1648
            return false;
1649
        }
1650
        else
1651
        {
1652
            return strtolower(substr($string, 0, $len)) === strtolower($needle);
1653
        }
1654
    }
1655
1656
    static function instance($name = 'default')
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1657
    {
1658
        if (isset(self::$instances[$name]))
1659
        {
1660
            return self::$instances[$name];
1661
        }
1662
1663
        $instance = new static();
1664
1665
        self::$instances[$name] = $instance;
1666
1667
        return $instance;
1668
    }
1669
1670
    private static $instances = array();
1671
1672
    #
1673
    # Fields
1674
    #
1675
1676
    protected $DefinitionData;
1677
1678
    #
1679
    # Read-Only
1680
1681
    protected $specialCharacters = array(
1682
        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1683
    );
1684
1685
    protected $StrongRegex = array(
1686
        '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1687
        '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1688
    );
1689
1690
    protected $EmRegex = array(
1691
        '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1692
        '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1693
    );
1694
1695
    protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1696
1697
    protected $voidElements = array(
1698
        'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1699
    );
1700
1701
    protected $textLevelElements = array(
1702
        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1703
        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1704
        'i', 'rp', 'del', 'code',          'strike', 'marquee',
1705
        'q', 'rt', 'ins', 'font',          'strong',
1706
        's', 'tt', 'kbd', 'mark',
1707
        'u', 'xm', 'sub', 'nobr',
1708
                   'sup', 'ruby',
1709
                   'var', 'span',
1710
                   'wbr', 'time',
1711
    );
1712
}
1713