Parsedown::lines()   F
last analyzed

Complexity

Conditions 26
Paths 3544

Size

Total Lines 139
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 139
rs 2
c 0
b 0
f 0
cc 26
eloc 62
nc 3544
nop 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
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
    public function text($text)
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
    public function setBreaksEnabled($breaksEnabled)
54
    {
55
        $this->breaksEnabled = $breaksEnabled;
56
57
        return $this;
58
    }
59
60
    protected $breaksEnabled;
61
62
    public function setMarkupEscaped($markupEscaped)
63
    {
64
        $this->markupEscaped = $markupEscaped;
65
66
        return $this;
67
    }
68
69
    protected $markupEscaped;
70
71
    public function setUrlsLinked($urlsLinked)
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
            if (chop($line) === '') {
126
                if (isset($CurrentBlock)) {
127
                    $CurrentBlock['interrupted'] = true;
128
                }
129
130
                continue;
131
            }
132
133
            if (strpos($line, "\t") !== false) {
134
                $parts = explode("\t", $line);
135
136
                $line = $parts[0];
137
138
                unset($parts[0]);
139
140
                foreach ($parts as $part) {
141
                    $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
142
143
                    $line .= str_repeat(' ', $shortage);
144
                    $line .= $part;
145
                }
146
            }
147
148
            $indent = 0;
149
150
            while (isset($line[$indent]) and $line[$indent] === ' ') {
151
                $indent ++;
152
            }
153
154
            $text = $indent > 0 ? substr($line, $indent) : $line;
155
156
            # ~
157
158
            $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
159
160
            # ~
161
162
            if (isset($CurrentBlock['continuable'])) {
163
                $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
164
165
                if (isset($Block)) {
166
                    $CurrentBlock = $Block;
167
168
                    continue;
169
                } else {
170
                    if ($this->isBlockCompletable($CurrentBlock['type'])) {
171
                        $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
172
                    }
173
                }
174
            }
175
176
            # ~
177
178
            $marker = $text[0];
179
180
            # ~
181
182
            $blockTypes = $this->unmarkedBlockTypes;
183
184
            if (isset($this->BlockTypes[$marker])) {
185
                foreach ($this->BlockTypes[$marker] as $blockType) {
186
                    $blockTypes []= $blockType;
187
                }
188
            }
189
190
            #
191
            # ~
192
193
            foreach ($blockTypes as $blockType) {
194
                $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
195
196
                if (isset($Block)) {
197
                    $Block['type'] = $blockType;
198
199
                    if (! isset($Block['identified'])) {
200
                        $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...
201
202
                        $Block['identified'] = true;
203
                    }
204
205
                    if ($this->isBlockContinuable($blockType)) {
206
                        $Block['continuable'] = true;
207
                    }
208
209
                    $CurrentBlock = $Block;
210
211
                    continue 2;
212
                }
213
            }
214
215
            # ~
216
217
            if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) {
218
                $CurrentBlock['element']['text'] .= "\n".$text;
219
            } else {
220
                $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...
221
222
                $CurrentBlock = $this->paragraph($Line);
223
224
                $CurrentBlock['identified'] = true;
225
            }
226
        }
227
228
        # ~
229
230
        if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) {
231
            $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
232
        }
233
234
        # ~
235
236
        $Blocks []= $CurrentBlock;
237
238
        unset($Blocks[0]);
239
240
        # ~
241
242
        $markup = '';
243
244
        foreach ($Blocks as $Block) {
245
            if (isset($Block['hidden'])) {
246
                continue;
247
            }
248
249
            $markup .= "\n";
250
            $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
251
        }
252
253
        $markup .= "\n";
254
255
        # ~
256
257
        return $markup;
258
    }
259
260
    protected function isBlockContinuable($Type)
261
    {
262
        return method_exists($this, 'block'.$Type.'Continue');
263
    }
264
265
    protected function isBlockCompletable($Type)
266
    {
267
        return method_exists($this, 'block'.$Type.'Complete');
268
    }
269
270
    #
271
    # Code
272
273
    protected function blockCode($Line, $Block = null)
274
    {
275
        if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) {
276
            return;
277
        }
278
279
        if ($Line['indent'] >= 4) {
280
            $text = substr($Line['body'], 4);
281
282
            $Block = array(
283
                'element' => array(
284
                    'name' => 'pre',
285
                    'handler' => 'element',
286
                    'text' => array(
287
                        'name' => 'code',
288
                        'text' => $text,
289
                    ),
290
                ),
291
            );
292
293
            return $Block;
294
        }
295
    }
296
297
    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...
298
    {
299
        if ($Line['indent'] >= 4) {
300
            if (isset($Block['interrupted'])) {
301
                $Block['element']['text']['text'] .= "\n";
302
303
                unset($Block['interrupted']);
304
            }
305
306
            $Block['element']['text']['text'] .= "\n";
307
308
            $text = substr($Line['body'], 4);
309
310
            $Block['element']['text']['text'] .= $text;
311
312
            return $Block;
313
        }
314
    }
315
316
    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...
317
    {
318
        $text = $Block['element']['text']['text'];
319
320
        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
321
322
        $Block['element']['text']['text'] = $text;
323
324
        return $Block;
325
    }
326
327
    #
328
    # Comment
329
330
    protected function blockComment($Line)
331
    {
332
        if ($this->markupEscaped) {
333
            return;
334
        }
335
336
        if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') {
337
            $Block = array(
338
                'markup' => $Line['body'],
339
            );
340
341
            if (preg_match('/-->$/', $Line['text'])) {
342
                $Block['closed'] = true;
343
            }
344
345
            return $Block;
346
        }
347
    }
348
349
    protected function blockCommentContinue($Line, array $Block)
350
    {
351
        if (isset($Block['closed'])) {
352
            return;
353
        }
354
355
        $Block['markup'] .= "\n" . $Line['body'];
356
357
        if (preg_match('/-->$/', $Line['text'])) {
358
            $Block['closed'] = true;
359
        }
360
361
        return $Block;
362
    }
363
364
    #
365
    # Fenced Code
366
367
    protected function blockFencedCode($Line)
368
    {
369
        if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) {
370
            $Element = array(
371
                'name' => 'code',
372
                'text' => '',
373
            );
374
375
            if (isset($matches[1])) {
376
                $class = 'language-'.$matches[1];
377
378
                $Element['attributes'] = array(
379
                    'class' => $class,
380
                );
381
            }
382
383
            $Block = array(
384
                'char' => $Line['text'][0],
385
                'element' => array(
386
                    'name' => 'pre',
387
                    'handler' => 'element',
388
                    'text' => $Element,
389
                ),
390
            );
391
392
            return $Block;
393
        }
394
    }
395
396
    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...
397
    {
398
        if (isset($Block['complete'])) {
399
            return;
400
        }
401
402
        if (isset($Block['interrupted'])) {
403
            $Block['element']['text']['text'] .= "\n";
404
405
            unset($Block['interrupted']);
406
        }
407
408
        if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) {
409
            $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
410
411
            $Block['complete'] = true;
412
413
            return $Block;
414
        }
415
416
        $Block['element']['text']['text'] .= "\n".$Line['body'];
417
        ;
418
419
        return $Block;
420
    }
421
422
    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...
423
    {
424
        $text = $Block['element']['text']['text'];
425
426
        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
427
428
        $Block['element']['text']['text'] = $text;
429
430
        return $Block;
431
    }
432
433
    #
434
    # Header
435
436
    protected function blockHeader($Line)
437
    {
438
        if (isset($Line['text'][1])) {
439
            $level = 1;
440
441
            while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') {
442
                $level ++;
443
            }
444
445
            if ($level > 6) {
446
                return;
447
            }
448
449
            $text = trim($Line['text'], '# ');
450
451
            $Block = array(
452
                'element' => array(
453
                    'name' => 'h' . min(6, $level),
454
                    'text' => $text,
455
                    'handler' => 'line',
456
                ),
457
            );
458
459
            return $Block;
460
        }
461
    }
462
463
    #
464
    # List
465
466
    protected function blockList($Line)
467
    {
468
        list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
469
470
        if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) {
471
            $Block = array(
472
                'indent' => $Line['indent'],
473
                'pattern' => $pattern,
474
                'element' => array(
475
                    'name' => $name,
476
                    'handler' => 'elements',
477
                ),
478
            );
479
480
            if ($name === 'ol') {
481
                $listStart = stristr($matches[0], '.', true);
482
483
                if ($listStart !== '1') {
484
                    $Block['element']['attributes'] = array('start' => $listStart);
485
                }
486
            }
487
488
            $Block['li'] = array(
489
                'name' => 'li',
490
                'handler' => 'li',
491
                'text' => array(
492
                    $matches[2],
493
                ),
494
            );
495
496
            $Block['element']['text'] []= & $Block['li'];
497
498
            return $Block;
499
        }
500
    }
501
502
    protected function blockListContinue($Line, array $Block)
503
    {
504
        if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) {
505
            if (isset($Block['interrupted'])) {
506
                $Block['li']['text'] []= '';
507
508
                unset($Block['interrupted']);
509
            }
510
511
            unset($Block['li']);
512
513
            $text = isset($matches[1]) ? $matches[1] : '';
514
515
            $Block['li'] = array(
516
                'name' => 'li',
517
                'handler' => 'li',
518
                'text' => array(
519
                    $text,
520
                ),
521
            );
522
523
            $Block['element']['text'] []= & $Block['li'];
524
525
            return $Block;
526
        }
527
528
        if ($Line['text'][0] === '[' and $this->blockReference($Line)) {
529
            return $Block;
530
        }
531
532
        if (! isset($Block['interrupted'])) {
533
            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
534
535
            $Block['li']['text'] []= $text;
536
537
            return $Block;
538
        }
539
540
        if ($Line['indent'] > 0) {
541
            $Block['li']['text'] []= '';
542
543
            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
544
545
            $Block['li']['text'] []= $text;
546
547
            unset($Block['interrupted']);
548
549
            return $Block;
550
        }
551
    }
552
553
    #
554
    # Quote
555
556
    protected function blockQuote($Line)
557
    {
558
        if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) {
559
            $Block = array(
560
                'element' => array(
561
                    'name' => 'blockquote',
562
                    'handler' => 'lines',
563
                    'text' => (array) $matches[1],
564
                ),
565
            );
566
567
            return $Block;
568
        }
569
    }
570
571
    protected function blockQuoteContinue($Line, array $Block)
572
    {
573
        if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) {
574
            if (isset($Block['interrupted'])) {
575
                $Block['element']['text'] []= '';
576
577
                unset($Block['interrupted']);
578
            }
579
580
            $Block['element']['text'] []= $matches[1];
581
582
            return $Block;
583
        }
584
585
        if (! isset($Block['interrupted'])) {
586
            $Block['element']['text'] []= $Line['text'];
587
588
            return $Block;
589
        }
590
    }
591
592
    #
593
    # Rule
594
595
    protected function blockRule($Line)
596
    {
597
        if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) {
598
            $Block = array(
599
                'element' => array(
600
                    'name' => 'hr'
601
                ),
602
            );
603
604
            return $Block;
605
        }
606
    }
607
608
    #
609
    # Setext
610
611
    protected function blockSetextHeader($Line, array $Block = null)
612
    {
613
        if (! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) {
614
            return;
615
        }
616
617
        if (chop($Line['text'], $Line['text'][0]) === '') {
618
            $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
619
620
            return $Block;
621
        }
622
    }
623
624
    #
625
    # Markup
626
627
    protected function blockMarkup($Line)
628
    {
629
        if ($this->markupEscaped) {
630
            return;
631
        }
632
633
        if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) {
634
            $element = strtolower($matches[1]);
635
636
            if (in_array($element, $this->textLevelElements)) {
637
                return;
638
            }
639
640
            $Block = array(
641
                'name' => $matches[1],
642
                'depth' => 0,
643
                'markup' => $Line['text'],
644
            );
645
646
            $length = strlen($matches[0]);
647
648
            $remainder = substr($Line['text'], $length);
649
650
            if (trim($remainder) === '') {
651
                if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) {
652
                    $Block['closed'] = true;
653
654
                    $Block['void'] = true;
655
                }
656
            } else {
657
                if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) {
658
                    return;
659
                }
660
661
                if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) {
662
                    $Block['closed'] = true;
663
                }
664
            }
665
666
            return $Block;
667
        }
668
    }
669
670
    protected function blockMarkupContinue($Line, array $Block)
671
    {
672
        if (isset($Block['closed'])) {
673
            return;
674
        }
675
676
        if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) { # open
677
            $Block['depth'] ++;
678
        }
679
680
        if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) { # close
681
            if ($Block['depth'] > 0) {
682
                $Block['depth'] --;
683
            } else {
684
                $Block['closed'] = true;
685
            }
686
        }
687
688
        if (isset($Block['interrupted'])) {
689
            $Block['markup'] .= "\n";
690
691
            unset($Block['interrupted']);
692
        }
693
694
        $Block['markup'] .= "\n".$Line['body'];
695
696
        return $Block;
697
    }
698
699
    #
700
    # Reference
701
702
    protected function blockReference($Line)
703
    {
704
        if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) {
705
            $id = strtolower($matches[1]);
706
707
            $Data = array(
708
                'url' => $matches[2],
709
                'title' => null,
710
            );
711
712
            if (isset($matches[3])) {
713
                $Data['title'] = $matches[3];
714
            }
715
716
            $this->DefinitionData['Reference'][$id] = $Data;
717
718
            $Block = array(
719
                'hidden' => true,
720
            );
721
722
            return $Block;
723
        }
724
    }
725
726
    #
727
    # Table
728
729
    protected function blockTable($Line, array $Block = null)
730
    {
731
        if (! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) {
732
            return;
733
        }
734
735
        if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') {
736
            $alignments = array();
737
738
            $divider = $Line['text'];
739
740
            $divider = trim($divider);
741
            $divider = trim($divider, '|');
742
743
            $dividerCells = explode('|', $divider);
744
745
            foreach ($dividerCells as $dividerCell) {
746
                $dividerCell = trim($dividerCell);
747
748
                if ($dividerCell === '') {
749
                    continue;
750
                }
751
752
                $alignment = null;
753
754
                if ($dividerCell[0] === ':') {
755
                    $alignment = 'left';
756
                }
757
758
                if (substr($dividerCell, - 1) === ':') {
759
                    $alignment = $alignment === 'left' ? 'center' : 'right';
760
                }
761
762
                $alignments []= $alignment;
763
            }
764
765
            # ~
766
767
            $HeaderElements = array();
768
769
            $header = $Block['element']['text'];
770
771
            $header = trim($header);
772
            $header = trim($header, '|');
773
774
            $headerCells = explode('|', $header);
775
776
            foreach ($headerCells as $index => $headerCell) {
777
                $headerCell = trim($headerCell);
778
779
                $HeaderElement = array(
780
                    'name' => 'th',
781
                    'text' => $headerCell,
782
                    'handler' => 'line',
783
                );
784
785
                if (isset($alignments[$index])) {
786
                    $alignment = $alignments[$index];
787
788
                    $HeaderElement['attributes'] = array(
789
                        'style' => 'text-align: '.$alignment.';',
790
                    );
791
                }
792
793
                $HeaderElements []= $HeaderElement;
794
            }
795
796
            # ~
797
798
            $Block = array(
799
                'alignments' => $alignments,
800
                'identified' => true,
801
                'element' => array(
802
                    'name' => 'table',
803
                    'handler' => 'elements',
804
                ),
805
            );
806
807
            $Block['element']['text'] []= array(
808
                'name' => 'thead',
809
                'handler' => 'elements',
810
            );
811
812
            $Block['element']['text'] []= array(
813
                'name' => 'tbody',
814
                'handler' => 'elements',
815
                'text' => array(),
816
            );
817
818
            $Block['element']['text'][0]['text'] []= array(
819
                'name' => 'tr',
820
                'handler' => 'elements',
821
                'text' => $HeaderElements,
822
            );
823
824
            return $Block;
825
        }
826
    }
827
828
    protected function blockTableContinue($Line, array $Block)
829
    {
830
        if (isset($Block['interrupted'])) {
831
            return;
832
        }
833
834
        if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) {
835
            $Elements = array();
836
837
            $row = $Line['text'];
838
839
            $row = trim($row);
840
            $row = trim($row, '|');
841
842
            preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
843
844
            foreach ($matches[0] as $index => $cell) {
845
                $cell = trim($cell);
846
847
                $Element = array(
848
                    'name' => 'td',
849
                    'handler' => 'line',
850
                    'text' => $cell,
851
                );
852
853
                if (isset($Block['alignments'][$index])) {
854
                    $Element['attributes'] = array(
855
                        'style' => 'text-align: '.$Block['alignments'][$index].';',
856
                    );
857
                }
858
859
                $Elements []= $Element;
860
            }
861
862
            $Element = array(
863
                'name' => 'tr',
864
                'handler' => 'elements',
865
                'text' => $Elements,
866
            );
867
868
            $Block['element']['text'][1]['text'] []= $Element;
869
870
            return $Block;
871
        }
872
    }
873
874
    #
875
    # ~
876
    #
877
878
    protected function paragraph($Line)
879
    {
880
        $Block = array(
881
            'element' => array(
882
                'name' => 'p',
883
                'text' => $Line['text'],
884
                'handler' => 'line',
885
            ),
886
        );
887
888
        return $Block;
889
    }
890
891
    #
892
    # Inline Elements
893
    #
894
895
    protected $InlineTypes = array(
896
        '"' => array('SpecialCharacter'),
897
        '!' => array('Image'),
898
        '&' => array('SpecialCharacter'),
899
        '*' => array('Emphasis'),
900
        ':' => array('Url'),
901
        '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
902
        '>' => array('SpecialCharacter'),
903
        '[' => array('Link'),
904
        '_' => array('Emphasis'),
905
        '`' => array('Code'),
906
        '~' => array('Strikethrough'),
907
        '\\' => array('EscapeSequence'),
908
    );
909
910
    # ~
911
912
    protected $inlineMarkerList = '!"*_&[:<>`~\\';
913
914
    #
915
    # ~
916
    #
917
918
    public function line($text)
919
    {
920
        $markup = '';
921
922
        # $excerpt is based on the first occurrence of a marker
923
924
        while ($excerpt = strpbrk($text, $this->inlineMarkerList)) {
925
            $marker = $excerpt[0];
926
927
            $markerPosition = strpos($text, $marker);
928
929
            $Excerpt = array('text' => $excerpt, 'context' => $text);
930
931
            foreach ($this->InlineTypes[$marker] as $inlineType) {
932
                $Inline = $this->{'inline'.$inlineType}($Excerpt);
933
934
                if (! isset($Inline)) {
935
                    continue;
936
                }
937
938
                # makes sure that the inline belongs to "our" marker
939
940
                if (isset($Inline['position']) and $Inline['position'] > $markerPosition) {
941
                    continue;
942
                }
943
944
                # sets a default inline position
945
946
                if (! isset($Inline['position'])) {
947
                    $Inline['position'] = $markerPosition;
948
                }
949
950
                # the text that comes before the inline
951
                $unmarkedText = substr($text, 0, $Inline['position']);
952
953
                # compile the unmarked text
954
                $markup .= $this->unmarkedText($unmarkedText);
955
956
                # compile the inline
957
                $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
958
959
                # remove the examined text
960
                $text = substr($text, $Inline['position'] + $Inline['extent']);
961
962
                continue 2;
963
            }
964
965
            # the marker does not belong to an inline
966
967
            $unmarkedText = substr($text, 0, $markerPosition + 1);
968
969
            $markup .= $this->unmarkedText($unmarkedText);
970
971
            $text = substr($text, $markerPosition + 1);
972
        }
973
974
        $markup .= $this->unmarkedText($text);
975
976
        return $markup;
977
    }
978
979
    #
980
    # ~
981
    #
982
983
    protected function inlineCode($Excerpt)
984
    {
985
        $marker = $Excerpt['text'][0];
986
987
        if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches)) {
988
            $text = $matches[2];
989
            $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
990
            $text = preg_replace("/[ ]*\n/", ' ', $text);
991
992
            return array(
993
                'extent' => strlen($matches[0]),
994
                'element' => array(
995
                    'name' => 'code',
996
                    'text' => $text,
997
                ),
998
            );
999
        }
1000
    }
1001
1002
    protected function inlineEmailTag($Excerpt)
1003
    {
1004
        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) {
1005
            $url = $matches[1];
1006
1007
            if (! isset($matches[2])) {
1008
                $url = 'mailto:' . $url;
1009
            }
1010
1011
            return array(
1012
                'extent' => strlen($matches[0]),
1013
                'element' => array(
1014
                    'name' => 'a',
1015
                    'text' => $matches[1],
1016
                    'attributes' => array(
1017
                        'href' => $url,
1018
                    ),
1019
                ),
1020
            );
1021
        }
1022
    }
1023
1024
    protected function inlineEmphasis($Excerpt)
1025
    {
1026
        if (! isset($Excerpt['text'][1])) {
1027
            return;
1028
        }
1029
1030
        $marker = $Excerpt['text'][0];
1031
1032
        if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) {
1033
            $emphasis = 'strong';
1034
        } elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) {
1035
            $emphasis = 'em';
1036
        } else {
1037
            return;
1038
        }
1039
1040
        return array(
1041
            'extent' => strlen($matches[0]),
1042
            'element' => array(
1043
                'name' => $emphasis,
1044
                'handler' => 'line',
1045
                'text' => $matches[1],
1046
            ),
1047
        );
1048
    }
1049
1050
    protected function inlineEscapeSequence($Excerpt)
1051
    {
1052
        if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) {
1053
            return array(
1054
                'markup' => $Excerpt['text'][1],
1055
                'extent' => 2,
1056
            );
1057
        }
1058
    }
1059
1060
    protected function inlineImage($Excerpt)
1061
    {
1062
        if (! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') {
1063
            return;
1064
        }
1065
1066
        $Excerpt['text']= substr($Excerpt['text'], 1);
1067
1068
        $Link = $this->inlineLink($Excerpt);
1069
1070
        if ($Link === null) {
1071
            return;
1072
        }
1073
1074
        $Inline = array(
1075
            'extent' => $Link['extent'] + 1,
1076
            'element' => array(
1077
                'name' => 'img',
1078
                'attributes' => array(
1079
                    'src' => $Link['element']['attributes']['href'],
1080
                    'alt' => $Link['element']['text'],
1081
                ),
1082
            ),
1083
        );
1084
1085
        $Inline['element']['attributes'] += $Link['element']['attributes'];
1086
1087
        unset($Inline['element']['attributes']['href']);
1088
1089
        return $Inline;
1090
    }
1091
1092
    protected function inlineLink($Excerpt)
1093
    {
1094
        $Element = array(
1095
            'name' => 'a',
1096
            'handler' => 'line',
1097
            'text' => null,
1098
            'attributes' => array(
1099
                'href' => null,
1100
                'title' => null,
1101
            ),
1102
        );
1103
1104
        $extent = 0;
1105
1106
        $remainder = $Excerpt['text'];
1107
1108
        if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
1109
            $Element['text'] = $matches[1];
1110
1111
            $extent += strlen($matches[0]);
1112
1113
            $remainder = substr($remainder, $extent);
1114
        } else {
1115
            return;
1116
        }
1117
1118
        if (preg_match('/^[(]((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches)) {
1119
            $Element['attributes']['href'] = $matches[1];
1120
1121
            if (isset($matches[2])) {
1122
                $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1123
            }
1124
1125
            $extent += strlen($matches[0]);
1126
        } else {
1127
            if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
1128
                $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
1129
                $definition = strtolower($definition);
1130
1131
                $extent += strlen($matches[0]);
1132
            } else {
1133
                $definition = strtolower($Element['text']);
1134
            }
1135
1136
            if (! isset($this->DefinitionData['Reference'][$definition])) {
1137
                return;
1138
            }
1139
1140
            $Definition = $this->DefinitionData['Reference'][$definition];
1141
1142
            $Element['attributes']['href'] = $Definition['url'];
1143
            $Element['attributes']['title'] = $Definition['title'];
1144
        }
1145
1146
        $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
1147
1148
        return array(
1149
            'extent' => $extent,
1150
            'element' => $Element,
1151
        );
1152
    }
1153
1154
    protected function inlineMarkup($Excerpt)
1155
    {
1156
        if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) {
1157
            return;
1158
        }
1159
1160
        if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches)) {
1161
            return array(
1162
                'markup' => $matches[0],
1163
                'extent' => strlen($matches[0]),
1164
            );
1165
        }
1166
1167
        if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches)) {
1168
            return array(
1169
                'markup' => $matches[0],
1170
                'extent' => strlen($matches[0]),
1171
            );
1172
        }
1173
1174
        if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) {
1175
            return array(
1176
                'markup' => $matches[0],
1177
                'extent' => strlen($matches[0]),
1178
            );
1179
        }
1180
    }
1181
1182
    protected function inlineSpecialCharacter($Excerpt)
1183
    {
1184
        if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) {
1185
            return array(
1186
                'markup' => '&amp;',
1187
                'extent' => 1,
1188
            );
1189
        }
1190
1191
        $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
1192
1193
        if (isset($SpecialCharacter[$Excerpt['text'][0]])) {
1194
            return array(
1195
                'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
1196
                'extent' => 1,
1197
            );
1198
        }
1199
    }
1200
1201
    protected function inlineStrikethrough($Excerpt)
1202
    {
1203
        if (! isset($Excerpt['text'][1])) {
1204
            return;
1205
        }
1206
1207
        if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) {
1208
            return array(
1209
                'extent' => strlen($matches[0]),
1210
                'element' => array(
1211
                    'name' => 'del',
1212
                    'text' => $matches[1],
1213
                    'handler' => 'line',
1214
                ),
1215
            );
1216
        }
1217
    }
1218
1219
    protected function inlineUrl($Excerpt)
1220
    {
1221
        if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') {
1222
            return;
1223
        }
1224
1225
        if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) {
1226
            $Inline = array(
1227
                'extent' => strlen($matches[0][0]),
1228
                'position' => $matches[0][1],
1229
                'element' => array(
1230
                    'name' => 'a',
1231
                    'text' => $matches[0][0],
1232
                    'attributes' => array(
1233
                        'href' => $matches[0][0],
1234
                    ),
1235
                ),
1236
            );
1237
1238
            return $Inline;
1239
        }
1240
    }
1241
1242
    protected function inlineUrlTag($Excerpt)
1243
    {
1244
        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) {
1245
            $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
1246
1247
            return array(
1248
                'extent' => strlen($matches[0]),
1249
                'element' => array(
1250
                    'name' => 'a',
1251
                    'text' => $url,
1252
                    'attributes' => array(
1253
                        'href' => $url,
1254
                    ),
1255
                ),
1256
            );
1257
        }
1258
    }
1259
1260
    # ~
1261
1262
    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...
1263
    {
1264
        if ($this->breaksEnabled) {
1265
            $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
1266
        } else {
1267
            $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
1268
            $text = str_replace(" \n", "\n", $text);
1269
        }
1270
1271
        return $text;
1272
    }
1273
1274
    #
1275
    # Handlers
1276
    #
1277
1278
    protected function element(array $Element)
1279
    {
1280
        $markup = '<'.$Element['name'];
1281
1282
        if (isset($Element['attributes'])) {
1283
            foreach ($Element['attributes'] as $name => $value) {
1284
                if ($value === null) {
1285
                    continue;
1286
                }
1287
1288
                $markup .= ' '.$name.'="'.$value.'"';
1289
            }
1290
        }
1291
1292
        if (isset($Element['text'])) {
1293
            $markup .= '>';
1294
1295
            if (isset($Element['handler'])) {
1296
                $markup .= $this->{$Element['handler']}($Element['text']);
1297
            } else {
1298
                $markup .= $Element['text'];
1299
            }
1300
1301
            $markup .= '</'.$Element['name'].'>';
1302
        } else {
1303
            $markup .= ' />';
1304
        }
1305
1306
        return $markup;
1307
    }
1308
1309
    protected function elements(array $Elements)
1310
    {
1311
        $markup = '';
1312
1313
        foreach ($Elements as $Element) {
1314
            $markup .= "\n" . $this->element($Element);
1315
        }
1316
1317
        $markup .= "\n";
1318
1319
        return $markup;
1320
    }
1321
1322
    # ~
1323
1324
    protected function li($lines)
1325
    {
1326
        $markup = $this->lines($lines);
1327
1328
        $trimmedMarkup = trim($markup);
1329
1330
        if (! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>') {
1331
            $markup = $trimmedMarkup;
1332
            $markup = substr($markup, 3);
1333
1334
            $position = strpos($markup, "</p>");
1335
1336
            $markup = substr_replace($markup, '', $position, 4);
1337
        }
1338
1339
        return $markup;
1340
    }
1341
1342
    #
1343
    # Deprecated Methods
1344
    #
1345
1346
    public function parse($text)
1347
    {
1348
        $markup = $this->text($text);
1349
1350
        return $markup;
1351
    }
1352
1353
    #
1354
    # Static Methods
1355
    #
1356
1357
    public 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...
1358
    {
1359
        if (isset(self::$instances[$name])) {
1360
            return self::$instances[$name];
1361
        }
1362
1363
        $instance = new static();
1364
1365
        self::$instances[$name] = $instance;
1366
1367
        return $instance;
1368
    }
1369
1370
    private static $instances = array();
1371
1372
    #
1373
    # Fields
1374
    #
1375
1376
    protected $DefinitionData;
1377
1378
    #
1379
    # Read-Only
1380
1381
    protected $specialCharacters = array(
1382
        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
1383
    );
1384
1385
    protected $StrongRegex = array(
1386
        '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
1387
        '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
1388
    );
1389
1390
    protected $EmRegex = array(
1391
        '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1392
        '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1393
    );
1394
1395
    protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
1396
1397
    protected $voidElements = array(
1398
        'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1399
    );
1400
1401
    protected $textLevelElements = array(
1402
        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1403
        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1404
        'i', 'rp', 'del', 'code',          'strike', 'marquee',
1405
        'q', 'rt', 'ins', 'font',          'strong',
1406
        's', 'tt', 'sub', 'mark',
1407
        'u', 'xm', 'sup', 'nobr',
1408
                   'var', 'ruby',
1409
                   'wbr', 'span',
1410
                          'time',
1411
    );
1412
}
1413