Completed
Push — master ( 423d33...296a97 )
by Renato
12s
created

Parsedown::li()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 2
nop 1
1
<?php
2
3
namespace GitScrum\Classes;
4
5
#
6
#
7
# Parsedown
8
# http://parsedown.org
9
#
10
# (c) Emanuil Rusev
11
# http://erusev.com
12
#
13
# For the full license information, view the LICENSE file that was distributed
14
# with this source code.
15
#
16
#
17
18
class Parsedown
19
{
20
    # ~
21
22
    const version = '1.6.0';
23
24
    # ~
25
26
    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...
27
    {
28
        # make sure no definitions are set
29
        $this->DefinitionData = array();
30
31
        # standardize line breaks
32
        $text = str_replace(array("\r\n", "\r"), "\n", $text);
33
34
        # remove surrounding line breaks
35
        $text = trim($text, "\n");
36
37
        # split text into lines
38
        $lines = explode("\n", $text);
39
40
        # iterate through lines to identify blocks
41
        $markup = $this->lines($lines);
42
43
        # trim line breaks
44
        $markup = trim($markup, "\n");
45
46
        return $markup;
47
    }
48
49
    #
50
    # Setters
51
    #
52
53
    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...
54
    {
55
        $this->breaksEnabled = $breaksEnabled;
56
57
        return $this;
58
    }
59
60
    protected $breaksEnabled;
61
62
    function setMarkupEscaped($markupEscaped)
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...
63
    {
64
        $this->markupEscaped = $markupEscaped;
65
66
        return $this;
67
    }
68
69
    protected $markupEscaped;
70
71
    function setUrlsLinked($urlsLinked)
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...
72
    {
73
        $this->urlsLinked = $urlsLinked;
74
75
        return $this;
76
    }
77
78
    protected $urlsLinked = true;
79
80
    #
81
    # Lines
82
    #
83
84
    protected $BlockTypes = array(
85
        '#' => array('Header'),
86
        '*' => array('Rule', 'List'),
87
        '+' => array('List'),
88
        '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
89
        '0' => array('List'),
90
        '1' => array('List'),
91
        '2' => array('List'),
92
        '3' => array('List'),
93
        '4' => array('List'),
94
        '5' => array('List'),
95
        '6' => array('List'),
96
        '7' => array('List'),
97
        '8' => array('List'),
98
        '9' => array('List'),
99
        ':' => array('Table'),
100
        '<' => array('Comment', 'Markup'),
101
        '=' => array('SetextHeader'),
102
        '>' => array('Quote'),
103
        '[' => array('Reference'),
104
        '_' => array('Rule'),
105
        '`' => array('FencedCode'),
106
        '|' => array('Table'),
107
        '~' => array('FencedCode'),
108
    );
109
110
    # ~
111
112
    protected $unmarkedBlockTypes = array(
113
        'Code',
114
    );
115
116
    #
117
    # Blocks
118
    #
119
120
    protected function lines(array $lines)
121
    {
122
        $CurrentBlock = null;
123
124
        foreach ($lines as $line)
125
        {
126
            if (chop($line) === '')
127
            {
128
                if (isset($CurrentBlock))
129
                {
130
                    $CurrentBlock['interrupted'] = true;
131
                }
132
133
                continue;
134
            }
135
136
            if (strpos($line, "\t") !== false)
137
            {
138
                $parts = explode("\t", $line);
139
140
                $line = $parts[0];
141
142
                unset($parts[0]);
143
144
                foreach ($parts as $part)
145
                {
146
                    $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
147
148
                    $line .= str_repeat(' ', $shortage);
149
                    $line .= $part;
150
                }
151
            }
152
153
            $indent = 0;
154
155
            while (isset($line[$indent]) and $line[$indent] === ' ')
156
            {
157
                $indent ++;
158
            }
159
160
            $text = $indent > 0 ? substr($line, $indent) : $line;
161
162
            # ~
163
164
            $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
165
166
            # ~
167
168
            if (isset($CurrentBlock['continuable']))
169
            {
170
                $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
171
172
                if (isset($Block))
173
                {
174
                    $CurrentBlock = $Block;
175
176
                    continue;
177
                }
178
                else
179
                {
180
                    if ($this->isBlockCompletable($CurrentBlock['type']))
181
                    {
182
                        $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
183
                    }
184
                }
185
            }
186
187
            # ~
188
189
            $marker = $text[0];
190
191
            # ~
192
193
            $blockTypes = $this->unmarkedBlockTypes;
194
195
            if (isset($this->BlockTypes[$marker]))
196
            {
197
                foreach ($this->BlockTypes[$marker] as $blockType)
198
                {
199
                    $blockTypes []= $blockType;
200
                }
201
            }
202
203
            #
204
            # ~
205
206
            foreach ($blockTypes as $blockType)
207
            {
208
                $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
209
210
                if (isset($Block))
211
                {
212
                    $Block['type'] = $blockType;
213
214
                    if ( ! isset($Block['identified']))
215
                    {
216
                        $Blocks []= $CurrentBlock;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$Blocks was never initialized. Although not strictly required by PHP, it is generally a good practice to add $Blocks = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
217
218
                        $Block['identified'] = true;
219
                    }
220
221
                    if ($this->isBlockContinuable($blockType))
222
                    {
223
                        $Block['continuable'] = true;
224
                    }
225
226
                    $CurrentBlock = $Block;
227
228
                    continue 2;
229
                }
230
            }
231
232
            # ~
233
234
            if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
235
            {
236
                $CurrentBlock['element']['text'] .= "\n".$text;
237
            }
238
            else
239
            {
240
                $Blocks []= $CurrentBlock;
0 ignored issues
show
Bug introduced by
The variable $Blocks does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
241
242
                $CurrentBlock = $this->paragraph($Line);
243
244
                $CurrentBlock['identified'] = true;
245
            }
246
        }
247
248
        # ~
249
250
        if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
251
        {
252
            $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
253
        }
254
255
        # ~
256
257
        $Blocks []= $CurrentBlock;
258
259
        unset($Blocks[0]);
260
261
        # ~
262
263
        $markup = '';
264
265
        foreach ($Blocks as $Block)
266
        {
267
            if (isset($Block['hidden']))
268
            {
269
                continue;
270
            }
271
272
            $markup .= "\n";
273
            $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
274
        }
275
276
        $markup .= "\n";
277
278
        # ~
279
280
        return $markup;
281
    }
282
283
    protected function isBlockContinuable($Type)
284
    {
285
        return method_exists($this, 'block'.$Type.'Continue');
286
    }
287
288
    protected function isBlockCompletable($Type)
289
    {
290
        return method_exists($this, 'block'.$Type.'Complete');
291
    }
292
293
    #
294
    # Code
295
296
    protected function blockCode($Line, $Block = null)
297
    {
298
        if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
299
        {
300
            return;
301
        }
302
303
        if ($Line['indent'] >= 4)
304
        {
305
            $text = substr($Line['body'], 4);
306
307
            $Block = array(
308
                'element' => array(
309
                    'name' => 'pre',
310
                    'handler' => 'element',
311
                    'text' => array(
312
                        'name' => 'code',
313
                        'text' => $text,
314
                    ),
315
                ),
316
            );
317
318
            return $Block;
319
        }
320
    }
321
322
    protected function blockCodeContinue($Line, $Block)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
323
    {
324
        if ($Line['indent'] >= 4)
325
        {
326
            if (isset($Block['interrupted']))
327
            {
328
                $Block['element']['text']['text'] .= "\n";
329
330
                unset($Block['interrupted']);
331
            }
332
333
            $Block['element']['text']['text'] .= "\n";
334
335
            $text = substr($Line['body'], 4);
336
337
            $Block['element']['text']['text'] .= $text;
338
339
            return $Block;
340
        }
341
    }
342
343
    protected function blockCodeComplete($Block)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
344
    {
345
        $text = $Block['element']['text']['text'];
346
347
        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
348
349
        $Block['element']['text']['text'] = $text;
350
351
        return $Block;
352
    }
353
354
    #
355
    # Comment
356
357
    protected function blockComment($Line)
358
    {
359
        if ($this->markupEscaped)
360
        {
361
            return;
362
        }
363
364
        if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
365
        {
366
            $Block = array(
367
                'markup' => $Line['body'],
368
            );
369
370
            if (preg_match('/-->$/', $Line['text']))
371
            {
372
                $Block['closed'] = true;
373
            }
374
375
            return $Block;
376
        }
377
    }
378
379
    protected function blockCommentContinue($Line, array $Block)
380
    {
381
        if (isset($Block['closed']))
382
        {
383
            return;
384
        }
385
386
        $Block['markup'] .= "\n" . $Line['body'];
387
388
        if (preg_match('/-->$/', $Line['text']))
389
        {
390
            $Block['closed'] = true;
391
        }
392
393
        return $Block;
394
    }
395
396
    #
397
    # Fenced Code
398
399
    protected function blockFencedCode($Line)
400
    {
401
        if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
402
        {
403
            $Element = array(
404
                'name' => 'code',
405
                'text' => '',
406
            );
407
408
            if (isset($matches[1]))
409
            {
410
                $class = 'language-'.$matches[1];
411
412
                $Element['attributes'] = array(
413
                    'class' => $class,
414
                );
415
            }
416
417
            $Block = array(
418
                'char' => $Line['text'][0],
419
                'element' => array(
420
                    'name' => 'pre',
421
                    'handler' => 'element',
422
                    'text' => $Element,
423
                ),
424
            );
425
426
            return $Block;
427
        }
428
    }
429
430
    protected function blockFencedCodeContinue($Line, $Block)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
431
    {
432
        if (isset($Block['complete']))
433
        {
434
            return;
435
        }
436
437
        if (isset($Block['interrupted']))
438
        {
439
            $Block['element']['text']['text'] .= "\n";
440
441
            unset($Block['interrupted']);
442
        }
443
444
        if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
445
        {
446
            $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
447
448
            $Block['complete'] = true;
449
450
            return $Block;
451
        }
452
453
        $Block['element']['text']['text'] .= "\n".$Line['body'];;
454
455
        return $Block;
456
    }
457
458
    protected function blockFencedCodeComplete($Block)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
459
    {
460
        $text = $Block['element']['text']['text'];
461
462
        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
463
464
        $Block['element']['text']['text'] = $text;
465
466
        return $Block;
467
    }
468
469
    #
470
    # Header
471
472
    protected function blockHeader($Line)
473
    {
474
        if (isset($Line['text'][1]))
475
        {
476
            $level = 1;
477
478
            while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
479
            {
480
                $level ++;
481
            }
482
483
            if ($level > 6)
484
            {
485
                return;
486
            }
487
488
            $text = trim($Line['text'], '# ');
489
490
            $Block = array(
491
                'element' => array(
492
                    'name' => 'h' . min(6, $level),
493
                    'text' => $text,
494
                    'handler' => 'line',
495
                ),
496
            );
497
498
            return $Block;
499
        }
500
    }
501
502
    #
503
    # List
504
505
    protected function blockList($Line)
506
    {
507
        list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
508
509
        if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
510
        {
511
            $Block = array(
512
                'indent' => $Line['indent'],
513
                'pattern' => $pattern,
514
                'element' => array(
515
                    'name' => $name,
516
                    'handler' => 'elements',
517
                ),
518
            );
519
520
            if($name === 'ol')
521
            {
522
                $listStart = stristr($matches[0], '.', true);
523
524
                if($listStart !== '1')
525
                {
526
                    $Block['element']['attributes'] = array('start' => $listStart);
527
                }
528
            }
529
530
            $Block['li'] = array(
531
                'name' => 'li',
532
                'handler' => 'li',
533
                'text' => array(
534
                    $matches[2],
535
                ),
536
            );
537
538
            $Block['element']['text'] []= & $Block['li'];
539
540
            return $Block;
541
        }
542
    }
543
544
    protected function blockListContinue($Line, array $Block)
545
    {
546
        if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
547
        {
548
            if (isset($Block['interrupted']))
549
            {
550
                $Block['li']['text'] []= '';
551
552
                unset($Block['interrupted']);
553
            }
554
555
            unset($Block['li']);
556
557
            $text = isset($matches[1]) ? $matches[1] : '';
558
559
            $Block['li'] = array(
560
                'name' => 'li',
561
                'handler' => 'li',
562
                'text' => array(
563
                    $text,
564
                ),
565
            );
566
567
            $Block['element']['text'] []= & $Block['li'];
568
569
            return $Block;
570
        }
571
572
        if ($Line['text'][0] === '[' and $this->blockReference($Line))
573
        {
574
            return $Block;
575
        }
576
577
        if ( ! isset($Block['interrupted']))
578
        {
579
            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
580
581
            $Block['li']['text'] []= $text;
582
583
            return $Block;
584
        }
585
586
        if ($Line['indent'] > 0)
587
        {
588
            $Block['li']['text'] []= '';
589
590
            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
591
592
            $Block['li']['text'] []= $text;
593
594
            unset($Block['interrupted']);
595
596
            return $Block;
597
        }
598
    }
599
600
    #
601
    # Quote
602
603
    protected function blockQuote($Line)
604
    {
605
        if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
606
        {
607
            $Block = array(
608
                'element' => array(
609
                    'name' => 'blockquote',
610
                    'handler' => 'lines',
611
                    'text' => (array) $matches[1],
612
                ),
613
            );
614
615
            return $Block;
616
        }
617
    }
618
619
    protected function blockQuoteContinue($Line, array $Block)
620
    {
621
        if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
622
        {
623
            if (isset($Block['interrupted']))
624
            {
625
                $Block['element']['text'] []= '';
626
627
                unset($Block['interrupted']);
628
            }
629
630
            $Block['element']['text'] []= $matches[1];
631
632
            return $Block;
633
        }
634
635
        if ( ! isset($Block['interrupted']))
636
        {
637
            $Block['element']['text'] []= $Line['text'];
638
639
            return $Block;
640
        }
641
    }
642
643
    #
644
    # Rule
645
646
    protected function blockRule($Line)
647
    {
648
        if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
649
        {
650
            $Block = array(
651
                'element' => array(
652
                    'name' => 'hr'
653
                ),
654
            );
655
656
            return $Block;
657
        }
658
    }
659
660
    #
661
    # Setext
662
663
    protected function blockSetextHeader($Line, array $Block = null)
664
    {
665
        if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
666
        {
667
            return;
668
        }
669
670
        if (chop($Line['text'], $Line['text'][0]) === '')
671
        {
672
            $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
673
674
            return $Block;
675
        }
676
    }
677
678
    #
679
    # Markup
680
681
    protected function blockMarkup($Line)
682
    {
683
        if ($this->markupEscaped)
684
        {
685
            return;
686
        }
687
688
        if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
689
        {
690
            $element = strtolower($matches[1]);
691
692
            if (in_array($element, $this->textLevelElements))
693
            {
694
                return;
695
            }
696
697
            $Block = array(
698
                'name' => $matches[1],
699
                'depth' => 0,
700
                'markup' => $Line['text'],
701
            );
702
703
            $length = strlen($matches[0]);
704
705
            $remainder = substr($Line['text'], $length);
706
707
            if (trim($remainder) === '')
708
            {
709
                if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
710
                {
711
                    $Block['closed'] = true;
712
713
                    $Block['void'] = true;
714
                }
715
            }
716
            else
717
            {
718
                if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
719
                {
720
                    return;
721
                }
722
723
                if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
724
                {
725
                    $Block['closed'] = true;
726
                }
727
            }
728
729
            return $Block;
730
        }
731
    }
732
733
    protected function blockMarkupContinue($Line, array $Block)
734
    {
735
        if (isset($Block['closed']))
736
        {
737
            return;
738
        }
739
740
        if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
741
        {
742
            $Block['depth'] ++;
743
        }
744
745
        if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
746
        {
747
            if ($Block['depth'] > 0)
748
            {
749
                $Block['depth'] --;
750
            }
751
            else
752
            {
753
                $Block['closed'] = true;
754
            }
755
        }
756
757
        if (isset($Block['interrupted']))
758
        {
759
            $Block['markup'] .= "\n";
760
761
            unset($Block['interrupted']);
762
        }
763
764
        $Block['markup'] .= "\n".$Line['body'];
765
766
        return $Block;
767
    }
768
769
    #
770
    # Reference
771
772
    protected function blockReference($Line)
773
    {
774
        if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
775
        {
776
            $id = strtolower($matches[1]);
777
778
            $Data = array(
779
                'url' => $matches[2],
780
                'title' => null,
781
            );
782
783
            if (isset($matches[3]))
784
            {
785
                $Data['title'] = $matches[3];
786
            }
787
788
            $this->DefinitionData['Reference'][$id] = $Data;
789
790
            $Block = array(
791
                'hidden' => true,
792
            );
793
794
            return $Block;
795
        }
796
    }
797
798
    #
799
    # Table
800
801
    protected function blockTable($Line, array $Block = null)
802
    {
803
        if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
804
        {
805
            return;
806
        }
807
808
        if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
809
        {
810
            $alignments = array();
811
812
            $divider = $Line['text'];
813
814
            $divider = trim($divider);
815
            $divider = trim($divider, '|');
816
817
            $dividerCells = explode('|', $divider);
818
819
            foreach ($dividerCells as $dividerCell)
820
            {
821
                $dividerCell = trim($dividerCell);
822
823
                if ($dividerCell === '')
824
                {
825
                    continue;
826
                }
827
828
                $alignment = null;
829
830
                if ($dividerCell[0] === ':')
831
                {
832
                    $alignment = 'left';
833
                }
834
835
                if (substr($dividerCell, - 1) === ':')
836
                {
837
                    $alignment = $alignment === 'left' ? 'center' : 'right';
838
                }
839
840
                $alignments []= $alignment;
841
            }
842
843
            # ~
844
845
            $HeaderElements = array();
846
847
            $header = $Block['element']['text'];
848
849
            $header = trim($header);
850
            $header = trim($header, '|');
851
852
            $headerCells = explode('|', $header);
853
854
            foreach ($headerCells as $index => $headerCell)
855
            {
856
                $headerCell = trim($headerCell);
857
858
                $HeaderElement = array(
859
                    'name' => 'th',
860
                    'text' => $headerCell,
861
                    'handler' => 'line',
862
                );
863
864
                if (isset($alignments[$index]))
865
                {
866
                    $alignment = $alignments[$index];
867
868
                    $HeaderElement['attributes'] = array(
869
                        'style' => 'text-align: '.$alignment.';',
870
                    );
871
                }
872
873
                $HeaderElements []= $HeaderElement;
874
            }
875
876
            # ~
877
878
            $Block = array(
879
                'alignments' => $alignments,
880
                'identified' => true,
881
                'element' => array(
882
                    'name' => 'table',
883
                    'handler' => 'elements',
884
                ),
885
            );
886
887
            $Block['element']['text'] []= array(
888
                'name' => 'thead',
889
                'handler' => 'elements',
890
            );
891
892
            $Block['element']['text'] []= array(
893
                'name' => 'tbody',
894
                'handler' => 'elements',
895
                'text' => array(),
896
            );
897
898
            $Block['element']['text'][0]['text'] []= array(
899
                'name' => 'tr',
900
                'handler' => 'elements',
901
                'text' => $HeaderElements,
902
            );
903
904
            return $Block;
905
        }
906
    }
907
908
    protected function blockTableContinue($Line, array $Block)
909
    {
910
        if (isset($Block['interrupted']))
911
        {
912
            return;
913
        }
914
915
        if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
916
        {
917
            $Elements = array();
918
919
            $row = $Line['text'];
920
921
            $row = trim($row);
922
            $row = trim($row, '|');
923
924
            preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
925
926
            foreach ($matches[0] as $index => $cell)
927
            {
928
                $cell = trim($cell);
929
930
                $Element = array(
931
                    'name' => 'td',
932
                    'handler' => 'line',
933
                    'text' => $cell,
934
                );
935
936
                if (isset($Block['alignments'][$index]))
937
                {
938
                    $Element['attributes'] = array(
939
                        'style' => 'text-align: '.$Block['alignments'][$index].';',
940
                    );
941
                }
942
943
                $Elements []= $Element;
944
            }
945
946
            $Element = array(
947
                'name' => 'tr',
948
                'handler' => 'elements',
949
                'text' => $Elements,
950
            );
951
952
            $Block['element']['text'][1]['text'] []= $Element;
953
954
            return $Block;
955
        }
956
    }
957
958
    #
959
    # ~
960
    #
961
962
    protected function paragraph($Line)
963
    {
964
        $Block = array(
965
            'element' => array(
966
                'name' => 'p',
967
                'text' => $Line['text'],
968
                'handler' => 'line',
969
            ),
970
        );
971
972
        return $Block;
973
    }
974
975
    #
976
    # Inline Elements
977
    #
978
979
    protected $InlineTypes = array(
980
        '"' => array('SpecialCharacter'),
981
        '!' => array('Image'),
982
        '&' => array('SpecialCharacter'),
983
        '*' => array('Emphasis'),
984
        ':' => array('Url'),
985
        '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
986
        '>' => array('SpecialCharacter'),
987
        '[' => array('Link'),
988
        '_' => array('Emphasis'),
989
        '`' => array('Code'),
990
        '~' => array('Strikethrough'),
991
        '\\' => array('EscapeSequence'),
992
    );
993
994
    # ~
995
996
    protected $inlineMarkerList = '!"*_&[:<>`~\\';
997
998
    #
999
    # ~
1000
    #
1001
1002
    public function line($text)
1003
    {
1004
        $markup = '';
1005
1006
        # $excerpt is based on the first occurrence of a marker
1007
1008
        while ($excerpt = strpbrk($text, $this->inlineMarkerList))
1009
        {
1010
            $marker = $excerpt[0];
1011
1012
            $markerPosition = strpos($text, $marker);
1013
1014
            $Excerpt = array('text' => $excerpt, 'context' => $text);
1015
1016
            foreach ($this->InlineTypes[$marker] as $inlineType)
1017
            {
1018
                $Inline = $this->{'inline'.$inlineType}($Excerpt);
1019
1020
                if ( ! isset($Inline))
1021
                {
1022
                    continue;
1023
                }
1024
1025
                # makes sure that the inline belongs to "our" marker
1026
1027
                if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
1028
                {
1029
                    continue;
1030
                }
1031
1032
                # sets a default inline position
1033
1034
                if ( ! isset($Inline['position']))
1035
                {
1036
                    $Inline['position'] = $markerPosition;
1037
                }
1038
1039
                # the text that comes before the inline
1040
                $unmarkedText = substr($text, 0, $Inline['position']);
1041
1042
                # compile the unmarked text
1043
                $markup .= $this->unmarkedText($unmarkedText);
1044
1045
                # compile the inline
1046
                $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
1047
1048
                # remove the examined text
1049
                $text = substr($text, $Inline['position'] + $Inline['extent']);
1050
1051
                continue 2;
1052
            }
1053
1054
            # the marker does not belong to an inline
1055
1056
            $unmarkedText = substr($text, 0, $markerPosition + 1);
1057
1058
            $markup .= $this->unmarkedText($unmarkedText);
1059
1060
            $text = substr($text, $markerPosition + 1);
1061
        }
1062
1063
        $markup .= $this->unmarkedText($text);
1064
1065
        return $markup;
1066
    }
1067
1068
    #
1069
    # ~
1070
    #
1071
1072
    protected function inlineCode($Excerpt)
1073
    {
1074
        $marker = $Excerpt['text'][0];
1075
1076
        if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1077
        {
1078
            $text = $matches[2];
1079
            $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
1080
            $text = preg_replace("/[ ]*\n/", ' ', $text);
1081
1082
            return array(
1083
                'extent' => strlen($matches[0]),
1084
                'element' => array(
1085
                    'name' => 'code',
1086
                    'text' => $text,
1087
                ),
1088
            );
1089
        }
1090
    }
1091
1092
    protected function inlineEmailTag($Excerpt)
1093
    {
1094
        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
1095
        {
1096
            $url = $matches[1];
1097
1098
            if ( ! isset($matches[2]))
1099
            {
1100
                $url = 'mailto:' . $url;
1101
            }
1102
1103
            return array(
1104
                'extent' => strlen($matches[0]),
1105
                'element' => array(
1106
                    'name' => 'a',
1107
                    'text' => $matches[1],
1108
                    'attributes' => array(
1109
                        'href' => $url,
1110
                    ),
1111
                ),
1112
            );
1113
        }
1114
    }
1115
1116
    protected function inlineEmphasis($Excerpt)
1117
    {
1118
        if ( ! isset($Excerpt['text'][1]))
1119
        {
1120
            return;
1121
        }
1122
1123
        $marker = $Excerpt['text'][0];
1124
1125
        if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1126
        {
1127
            $emphasis = 'strong';
1128
        }
1129
        elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1130
        {
1131
            $emphasis = 'em';
1132
        }
1133
        else
1134
        {
1135
            return;
1136
        }
1137
1138
        return array(
1139
            'extent' => strlen($matches[0]),
1140
            'element' => array(
1141
                'name' => $emphasis,
1142
                'handler' => 'line',
1143
                'text' => $matches[1],
1144
            ),
1145
        );
1146
    }
1147
1148
    protected function inlineEscapeSequence($Excerpt)
1149
    {
1150
        if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1151
        {
1152
            return array(
1153
                'markup' => $Excerpt['text'][1],
1154
                'extent' => 2,
1155
            );
1156
        }
1157
    }
1158
1159
    protected function inlineImage($Excerpt)
1160
    {
1161
        if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1162
        {
1163
            return;
1164
        }
1165
1166
        $Excerpt['text']= substr($Excerpt['text'], 1);
1167
1168
        $Link = $this->inlineLink($Excerpt);
1169
1170
        if ($Link === null)
1171
        {
1172
            return;
1173
        }
1174
1175
        $Inline = array(
1176
            'extent' => $Link['extent'] + 1,
1177
            'element' => array(
1178
                'name' => 'img',
1179
                'attributes' => array(
1180
                    'src' => $Link['element']['attributes']['href'],
1181
                    'alt' => $Link['element']['text'],
1182
                ),
1183
            ),
1184
        );
1185
1186
        $Inline['element']['attributes'] += $Link['element']['attributes'];
1187
1188
        unset($Inline['element']['attributes']['href']);
1189
1190
        return $Inline;
1191
    }
1192
1193
    protected function inlineLink($Excerpt)
1194
    {
1195
        $Element = array(
1196
            'name' => 'a',
1197
            'handler' => 'line',
1198
            'text' => null,
1199
            'attributes' => array(
1200
                'href' => null,
1201
                'title' => null,
1202
            ),
1203
        );
1204
1205
        $extent = 0;
1206
1207
        $remainder = $Excerpt['text'];
1208
1209
        if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
1210
        {
1211
            $Element['text'] = $matches[1];
1212
1213
            $extent += strlen($matches[0]);
1214
1215
            $remainder = substr($remainder, $extent);
1216
        }
1217
        else
1218
        {
1219
            return;
1220
        }
1221
1222
        if (preg_match('/^[(]((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches))
1223
        {
1224
            $Element['attributes']['href'] = $matches[1];
1225
1226
            if (isset($matches[2]))
1227
            {
1228
                $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1229
            }
1230
1231
            $extent += strlen($matches[0]);
1232
        }
1233
        else
1234
        {
1235
            if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1236
            {
1237
                $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
1238
                $definition = strtolower($definition);
1239
1240
                $extent += strlen($matches[0]);
1241
            }
1242
            else
1243
            {
1244
                $definition = strtolower($Element['text']);
1245
            }
1246
1247
            if ( ! isset($this->DefinitionData['Reference'][$definition]))
1248
            {
1249
                return;
1250
            }
1251
1252
            $Definition = $this->DefinitionData['Reference'][$definition];
1253
1254
            $Element['attributes']['href'] = $Definition['url'];
1255
            $Element['attributes']['title'] = $Definition['title'];
1256
        }
1257
1258
        $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
1259
1260
        return array(
1261
            'extent' => $extent,
1262
            'element' => $Element,
1263
        );
1264
    }
1265
1266
    protected function inlineMarkup($Excerpt)
1267
    {
1268
        if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
1269
        {
1270
            return;
1271
        }
1272
1273
        if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
1274
        {
1275
            return array(
1276
                'markup' => $matches[0],
1277
                'extent' => strlen($matches[0]),
1278
            );
1279
        }
1280
1281
        if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
1282
        {
1283
            return array(
1284
                'markup' => $matches[0],
1285
                'extent' => strlen($matches[0]),
1286
            );
1287
        }
1288
1289
        if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
1290
        {
1291
            return array(
1292
                'markup' => $matches[0],
1293
                'extent' => strlen($matches[0]),
1294
            );
1295
        }
1296
    }
1297
1298
    protected function inlineSpecialCharacter($Excerpt)
1299
    {
1300
        if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
1301
        {
1302
            return array(
1303
                'markup' => '&amp;',
1304
                'extent' => 1,
1305
            );
1306
        }
1307
1308
        $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1309
1310
        if (isset($SpecialCharacter[$Excerpt['text'][0]]))
1311
        {
1312
            return array(
1313
                'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1314
                'extent' => 1,
1315
            );
1316
        }
1317
    }
1318
1319
    protected function inlineStrikethrough($Excerpt)
1320
    {
1321
        if ( ! isset($Excerpt['text'][1]))
1322
        {
1323
            return;
1324
        }
1325
1326
        if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1327
        {
1328
            return array(
1329
                'extent' => strlen($matches[0]),
1330
                'element' => array(
1331
                    'name' => 'del',
1332
                    'text' => $matches[1],
1333
                    'handler' => 'line',
1334
                ),
1335
            );
1336
        }
1337
    }
1338
1339
    protected function inlineUrl($Excerpt)
1340
    {
1341
        if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1342
        {
1343
            return;
1344
        }
1345
1346
        if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
1347
        {
1348
            $Inline = array(
1349
                'extent' => strlen($matches[0][0]),
1350
                'position' => $matches[0][1],
1351
                'element' => array(
1352
                    'name' => 'a',
1353
                    'text' => $matches[0][0],
1354
                    'attributes' => array(
1355
                        'href' => $matches[0][0],
1356
                    ),
1357
                ),
1358
            );
1359
1360
            return $Inline;
1361
        }
1362
    }
1363
1364
    protected function inlineUrlTag($Excerpt)
1365
    {
1366
        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
1367
        {
1368
            $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
1369
1370
            return array(
1371
                'extent' => strlen($matches[0]),
1372
                'element' => array(
1373
                    'name' => 'a',
1374
                    'text' => $url,
1375
                    'attributes' => array(
1376
                        'href' => $url,
1377
                    ),
1378
                ),
1379
            );
1380
        }
1381
    }
1382
1383
    # ~
1384
1385
    protected function unmarkedText($text)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
1386
    {
1387
        if ($this->breaksEnabled)
1388
        {
1389
            $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1390
        }
1391
        else
1392
        {
1393
            $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1394
            $text = str_replace(" \n", "\n", $text);
1395
        }
1396
1397
        return $text;
1398
    }
1399
1400
    #
1401
    # Handlers
1402
    #
1403
1404
    protected function element(array $Element)
1405
    {
1406
        $markup = '<'.$Element['name'];
1407
1408
        if (isset($Element['attributes']))
1409
        {
1410
            foreach ($Element['attributes'] as $name => $value)
1411
            {
1412
                if ($value === null)
1413
                {
1414
                    continue;
1415
                }
1416
1417
                $markup .= ' '.$name.'="'.$value.'"';
1418
            }
1419
        }
1420
1421
        if (isset($Element['text']))
1422
        {
1423
            $markup .= '>';
1424
1425
            if (isset($Element['handler']))
1426
            {
1427
                $markup .= $this->{$Element['handler']}($Element['text']);
1428
            }
1429
            else
1430
            {
1431
                $markup .= $Element['text'];
1432
            }
1433
1434
            $markup .= '</'.$Element['name'].'>';
1435
        }
1436
        else
1437
        {
1438
            $markup .= ' />';
1439
        }
1440
1441
        return $markup;
1442
    }
1443
1444
    protected function elements(array $Elements)
1445
    {
1446
        $markup = '';
1447
1448
        foreach ($Elements as $Element)
1449
        {
1450
            $markup .= "\n" . $this->element($Element);
1451
        }
1452
1453
        $markup .= "\n";
1454
1455
        return $markup;
1456
    }
1457
1458
    # ~
1459
1460
    protected function li($lines)
1461
    {
1462
        $markup = $this->lines($lines);
1463
1464
        $trimmedMarkup = trim($markup);
1465
1466
        if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
1467
        {
1468
            $markup = $trimmedMarkup;
1469
            $markup = substr($markup, 3);
1470
1471
            $position = strpos($markup, "</p>");
1472
1473
            $markup = substr_replace($markup, '', $position, 4);
1474
        }
1475
1476
        return $markup;
1477
    }
1478
1479
    #
1480
    # Deprecated Methods
1481
    #
1482
1483
    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...
1484
    {
1485
        $markup = $this->text($text);
1486
1487
        return $markup;
1488
    }
1489
1490
    #
1491
    # Static Methods
1492
    #
1493
1494
    static function instance($name = 'default')
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
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...
1495
    {
1496
        if (isset(self::$instances[$name]))
1497
        {
1498
            return self::$instances[$name];
1499
        }
1500
1501
        $instance = new static();
1502
1503
        self::$instances[$name] = $instance;
1504
1505
        return $instance;
1506
    }
1507
1508
    private static $instances = array();
1509
1510
    #
1511
    # Fields
1512
    #
1513
1514
    protected $DefinitionData;
1515
1516
    #
1517
    # Read-Only
1518
1519
    protected $specialCharacters = array(
1520
        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1521
    );
1522
1523
    protected $StrongRegex = array(
1524
        '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1525
        '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1526
    );
1527
1528
    protected $EmRegex = array(
1529
        '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1530
        '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1531
    );
1532
1533
    protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1534
1535
    protected $voidElements = array(
1536
        'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1537
    );
1538
1539
    protected $textLevelElements = array(
1540
        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1541
        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1542
        'i', 'rp', 'del', 'code',          'strike', 'marquee',
1543
        'q', 'rt', 'ins', 'font',          'strong',
1544
        's', 'tt', 'sub', 'mark',
1545
        'u', 'xm', 'sup', 'nobr',
1546
                   'var', 'ruby',
1547
                   'wbr', 'span',
1548
                          'time',
1549
    );
1550
}
1551