Parsedown::blockListContinue()   C
last analyzed

Complexity

Conditions 17
Paths 11

Size

Total Lines 77
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 17
eloc 41
c 1
b 0
f 1
nc 11
nop 2
dl 0
loc 77
rs 5.2166

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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