Parsedown::blockList()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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