Parsedown::parse_span_elements()   F
last analyzed

Complexity

Conditions 57
Paths 6

Size

Total Lines 257
Code Lines 151

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 57
eloc 151
nc 6
nop 2
dl 0
loc 257
rs 3.3333
c 1
b 0
f 0

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
#
4
#
5
# Parsedown
6
# http://parsedown.org
7
#
8
# (c) Emanuil Rusev
9
# http://erusev.com
10
#
11
# For the full license information, please view the LICENSE file that was
12
# distributed with this source code.
13
#
14
#
15
16
/**
17
 * Class Parsedown
18
 */
19
class Parsedown
20
{
21
    #
22
    # Multiton (http://en.wikipedia.org/wiki/Multiton_pattern)
23
    #
24
25
    /**
26
     * @param string $name
27
     * @return mixed|Parsedown
28
     */
29
    public static function instance($name = 'default')
30
    {
31
        if (isset(self::$instances[$name])) {
32
            return self::$instances[$name];
33
        }
34
35
        $instance = new self();
36
37
        self::$instances[$name] = $instance;
38
39
        return $instance;
40
    }
41
42
    private static $instances = [];
43
44
    #
45
    # Setters
46
    #
47
48
    private $break_marker = "  \n";
49
50
    /**
51
     * @param $breaks_enabled
52
     * @return $this
53
     */
54
    public function set_breaks_enabled($breaks_enabled)
55
    {
56
        $this->break_marker = $breaks_enabled ? "\n" : "  \n";
57
58
        return $this;
59
    }
60
61
    #
62
    # Fields
63
    #
64
65
    private $reference_map       = [];
66
    private $escape_sequence_map = [];
67
68
    #
69
    # Public Methods
70
    #
71
72
    /**
73
     * @param $text
74
     * @return mixed|string
75
     */
76
    public function parse($text)
77
    {
78
        # removes \r characters
79
        $text = str_replace("\r\n", "\n", $text);
80
        $text = str_replace("\r", "\n", $text);
81
82
        # replaces tabs with spaces
83
        $text = str_replace("\t", '    ', $text);
84
85
        # encodes escape sequences
86
87
        if (false !== mb_strpos($text, '\\')) {
88
            $escape_sequences = ['\\\\', '\`', '\*', '\_', '\{', '\}', '\[', '\]', '\(', '\)', '\>', '\#', '\+', '\-', '\.', '\!'];
89
90
            foreach ($escape_sequences as $index => $escape_sequence) {
91
                if (false !== mb_strpos($text, $escape_sequence)) {
92
                    $code = "\x1A" . '\\' . $index . ';';
93
94
                    $text = str_replace($escape_sequence, $code, $text);
95
96
                    $this->escape_sequence_map[$code] = $escape_sequence;
97
                }
98
            }
99
        }
100
101
        # ~
102
103
        $text = trim($text, "\n");
104
105
        $lines = explode("\n", $text);
106
107
        $text = $this->parse_block_elements($lines);
108
109
        # decodes escape sequences
110
111
        foreach ($this->escape_sequence_map as $code => $escape_sequence) {
112
            $text = str_replace($code, $escape_sequence[1], $text);
113
        }
114
115
        # ~
116
117
        $text = rtrim($text, "\n");
118
119
        return $text;
120
    }
121
122
    #
123
    # Private Methods
124
    #
125
126
    /**
127
     * @param array  $lines
128
     * @param string $context
129
     * @return string
130
     */
131
    private function parse_block_elements(array $lines, $context = '')
132
    {
133
        $elements = [];
134
135
        $element = [
136
            'type' => '',
137
        ];
138
139
        foreach ($lines as $line) {
140
            # fenced elements
141
142
            switch ($element['type']) {
143
                case 'fenced block':
144
145
                    if (!isset($element['closed'])) {
146
                        if (preg_match('/^[ ]*' . $element['fence'][0] . '{3,}[ ]*$/', $line)) {
147
                            $element['closed'] = true;
148
                        } else {
149
                            '' !== $element['text'] and $element['text'] .= "\n";
150
151
                            $element['text'] .= $line;
152
                        }
153
154
                        continue 2;
155
                    }
156
157
                    break;
158
                case 'block-level markup':
159
160
                    if (!isset($element['closed'])) {
161
                        if (false !== mb_strpos($line, $element['start'])) { # opening tag
162
                            $element['depth']++;
163
                        }
164
165
                        if (false !== mb_strpos($line, $element['end'])) { # closing tag
166
                            $element['depth'] > 0 ? $element['depth']-- : $element['closed'] = true;
167
                        }
168
169
                        $element['text'] .= "\n" . $line;
170
171
                        continue 2;
172
                    }
173
174
                    break;
175
            }
176
177
            # *
178
179
            $deindented_line = ltrim($line);
180
181
            if ('' === $deindented_line) {
182
                $element['interrupted'] = true;
183
184
                continue;
185
            }
186
187
            # composite elements
188
189
            switch ($element['type']) {
190
                case 'blockquote':
191
192
                    if (!isset($element['interrupted'])) {
193
                        $line = preg_replace('/^[ ]*>[ ]?/', '', $line);
194
195
                        $element['lines'] [] = $line;
196
197
                        continue 2;
198
                    }
199
200
                    break;
201
                case 'li':
202
203
                    if (preg_match('/^([ ]{0,3})(\d+[.]|[*+-])[ ](.*)/', $line, $matches)) {
204
                        if ($element['indentation'] !== $matches[1]) {
205
                            $element['lines'] [] = $line;
206
                        } else {
207
                            unset($element['last']);
208
209
                            $elements [] = $element;
210
211
                            $element = [
212
                                'type'        => 'li',
213
                                'indentation' => $matches[1],
214
                                'last'        => true,
215
                                'lines'       => [
216
                                    preg_replace('/^[ ]{0,4}/', '', $matches[3]),
217
                                ],
218
                            ];
219
                        }
220
221
                        continue 2;
222
                    }
223
224
                    if (isset($element['interrupted'])) {
225
                        if (' ' === $line[0]) {
226
                            $element['lines'] [] = '';
227
228
                            $line = preg_replace('/^[ ]{0,4}/', '', $line);
229
230
                            $element['lines'] [] = $line;
231
232
                            unset($element['interrupted']);
233
234
                            continue 2;
235
                        }
236
                    } else {
237
                        $line = preg_replace('/^[ ]{0,4}/', '', $line);
238
239
                        $element['lines'] [] = $line;
240
241
                        continue 2;
242
                    }
243
244
                    break;
245
            }
246
247
            # indentation sensitive types
248
249
            switch ($line[0]) {
250
                case ' ':
251
252
                    # code block
253
254
                    if (isset($line[3]) and ' ' === $line[3] and ' ' === $line[2] and ' ' === $line[1]) {
255
                        $code_line = mb_substr($line, 4);
256
257
                        if ('code block' === $element['type']) {
258
                            if (isset($element['interrupted'])) {
259
                                $element['text'] .= "\n";
260
261
                                unset($element['interrupted']);
262
                            }
263
264
                            $element['text'] .= "\n" . $code_line;
265
                        } else {
266
                            $elements [] = $element;
267
268
                            $element = [
269
                                'type' => 'code block',
270
                                'text' => $code_line,
271
                            ];
272
                        }
273
274
                        continue 2;
275
                    }
276
277
                    break;
278
                case '#':
279
280
                    # atx heading (#)
281
282
                    if (isset($line[1])) {
283
                        $elements [] = $element;
284
285
                        $level = 1;
286
287
                        while (isset($line[$level]) and '#' === $line[$level]) {
288
                            ++$level;
289
                        }
290
291
                        $element = [
292
                            'type'  => 'heading',
293
                            'text'  => trim($line, '# '),
294
                            'level' => $level,
295
                        ];
296
297
                        continue 2;
298
                    }
299
300
                    break;
301
                case '-':
302
                case '=':
303
304
                    # setext heading
305
306
                    if ('paragraph' === $element['type'] and false === isset($element['interrupted'])) {
307
                        $chopped_line = rtrim($line);
308
309
                        $i = 1;
310
311
                        while (isset($chopped_line[$i])) {
312
                            if ($chopped_line[$i] !== $line[0]) {
313
                                break 2;
314
                            }
315
316
                            ++$i;
317
                        }
318
319
                        $element['type']  = 'heading';
320
                        $element['level'] = '-' === $line[0] ? 2 : 1;
321
322
                        continue 2;
323
                    }
324
325
                    break;
326
            }
327
328
            # indentation insensitive types
329
330
            switch ($deindented_line[0]) {
331
                case '<':
332
333
                    $position = mb_strpos($deindented_line, '>');
334
335
                    if ($position > 1) { # tag
336
                        $name = mb_substr($deindented_line, 1, $position - 1);
337
                        $name = rtrim($name);
338
339
                        if ('/' === mb_substr($name, -1)) {
340
                            $self_closing = true;
341
342
                            $name = mb_substr($name, 0, -1);
343
                        }
344
345
                        $position = mb_strpos($name, ' ');
346
347
                        if ($position) {
348
                            $name = mb_substr($name, 0, $position);
349
                        }
350
351
                        if (!ctype_alpha($name)) {
352
                            break;
353
                        }
354
355
                        if (in_array($name, $this->inline_tags, true)) {
356
                            break;
357
                        }
358
359
                        $elements [] = $element;
360
361
                        if (isset($self_closing)) {
362
                            $element = [
363
                                'type' => 'self-closing tag',
364
                                'text' => $deindented_line,
365
                            ];
366
367
                            unset($self_closing);
368
369
                            continue 2;
370
                        }
371
372
                        $element = [
373
                            'type'  => 'block-level markup',
374
                            'text'  => $deindented_line,
375
                            'start' => '<' . $name . '>',
376
                            'end'   => '</' . $name . '>',
377
                            'depth' => 0,
378
                        ];
379
380
                        if (mb_strpos($deindented_line, $element['end'])) {
381
                            $element['closed'] = true;
382
                        }
383
384
                        continue 2;
385
                    }
386
387
                    break;
388
                case '>':
389
390
                    # quote
391
392
                    if (preg_match('/^>[ ]?(.*)/', $deindented_line, $matches)) {
393
                        $elements [] = $element;
394
395
                        $element = [
396
                            'type'  => 'blockquote',
397
                            'lines' => [
398
                                $matches[1],
399
                            ],
400
                        ];
401
402
                        continue 2;
403
                    }
404
405
                    break;
406
                case '[':
407
408
                    # reference
409
410
                    if (preg_match('/^\[(.+?)\]:[ ]*(.+?)(?:[ ]+[\'"](.+?)[\'"])?[ ]*$/', $deindented_line, $matches)) {
411
                        $label = mb_strtolower($matches[1]);
412
413
                        $this->reference_map[$label] = [
414
                            '»' => trim($matches[2], '<>'),
415
                        ];
416
417
                        if (isset($matches[3])) {
418
                            $this->reference_map[$label]['#'] = $matches[3];
419
                        }
420
421
                        continue 2;
422
                    }
423
424
                    break;
425
                case '`':
426
                case '~':
427
428
                    # fenced code block
429
430
                    if (preg_match('/^([`]{3,}|[~]{3,})[ ]*(\S+)?[ ]*$/', $deindented_line, $matches)) {
431
                        $elements [] = $element;
432
433
                        $element = [
434
                            'type'  => 'fenced block',
435
                            'text'  => '',
436
                            'fence' => $matches[1],
437
                        ];
438
439
                        isset($matches[2]) and $element['language'] = $matches[2];
440
441
                        continue 2;
442
                    }
443
444
                    break;
445
                case '*':
446
                case '+':
447
                case '-':
448
                case '_':
449
450
                    # hr
451
452
                    if (preg_match('/^([-*_])([ ]{0,2}\1){2,}[ ]*$/', $deindented_line)) {
453
                        $elements [] = $element;
454
455
                        $element = [
456
                            'type' => 'hr',
457
                        ];
458
459
                        continue 2;
460
                    }
461
462
                    # li
463
464
                    if (preg_match('/^([ ]*)[*+-][ ](.*)/', $line, $matches)) {
465
                        $elements [] = $element;
466
467
                        $element = [
468
                            'type'        => 'li',
469
                            'ordered'     => false,
470
                            'indentation' => $matches[1],
471
                            'last'        => true,
472
                            'lines'       => [
473
                                preg_replace('/^[ ]{0,4}/', '', $matches[2]),
474
                            ],
475
                        ];
476
477
                        continue 2;
478
                    }
479
            }
480
481
            # li
482
483
            if ($deindented_line[0] <= '9' and $deindented_line[0] >= '0' and preg_match('/^([ ]*)\d+[.][ ](.*)/', $line, $matches)) {
484
                $elements [] = $element;
485
486
                $element = [
487
                    'type'        => 'li',
488
                    'ordered'     => true,
489
                    'indentation' => $matches[1],
490
                    'last'        => true,
491
                    'lines'       => [
492
                        preg_replace('/^[ ]{0,4}/', '', $matches[2]),
493
                    ],
494
                ];
495
496
                continue;
497
            }
498
499
            # paragraph
500
501
            if ('paragraph' === $element['type']) {
502
                if (isset($element['interrupted'])) {
503
                    $elements [] = $element;
504
505
                    $element['text'] = $line;
506
507
                    unset($element['interrupted']);
508
                } else {
509
                    $element['text'] .= "\n" . $line;
510
                }
511
            } else {
512
                $elements [] = $element;
513
514
                $element = [
515
                    'type' => 'paragraph',
516
                    'text' => $line,
517
                ];
518
            }
519
        }
520
521
        $elements [] = $element;
522
523
        unset($elements[0]);
524
525
        #
526
        # ~
527
        #
528
529
        $markup = '';
530
531
        foreach ($elements as $element) {
532
            switch ($element['type']) {
533
                case 'paragraph':
534
535
                    $text = $this->parse_span_elements($element['text']);
536
537
                    if ('li' === $context and '' === $markup) {
538
                        if (isset($element['interrupted'])) {
539
                            $markup .= "\n" . '<p>' . $text . '</p>' . "\n";
540
                        } else {
541
                            $markup .= $text;
542
                        }
543
                    } else {
544
                        $markup .= '<p>' . $text . '</p>' . "\n";
545
                    }
546
547
                    break;
548
                case 'blockquote':
549
550
                    $text = $this->parse_block_elements($element['lines']);
551
552
                    $markup .= '<blockquote>' . "\n" . $text . '</blockquote>' . "\n";
553
554
                    break;
555
                case 'code block':
556
557
                    $text = htmlspecialchars($element['text'], ENT_QUOTES | ENT_NOQUOTES, 'UTF-8');
558
559
                    false !== mb_strpos($text, "\x1A\\") and $text = strtr($text, $this->escape_sequence_map);
560
561
                    $markup .= isset($element['language']) ? '<pre><code class="language-' . $element['language'] . '">' . $text . '</code></pre>' : '<pre><code>' . $text . '</code></pre>';
562
563
                    $markup .= "\n";
564
565
                    break;
566
                case 'fenced block':
567
568
                    $text = $element['text'];
569
570
                    false !== mb_strpos($text, "\x1A\\") and $text = strtr($text, $this->escape_sequence_map);
571
572
                    $markup .= rex_highlight_string($text, true) . "\n";
0 ignored issues
show
Bug introduced by
The function rex_highlight_string was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

572
                    $markup .= /** @scrutinizer ignore-call */ rex_highlight_string($text, true) . "\n";
Loading history...
573
574
                    $markup .= "\n";
575
576
                    break;
577
                case 'heading':
578
579
                    $text = $this->parse_span_elements($element['text']);
580
581
                    $markup .= '<h' . $element['level'] . '>' . $text . '</h' . $element['level'] . '>' . "\n";
582
583
                    break;
584
                case 'hr':
585
586
                    $markup .= '<hr >' . "\n";
587
588
                    break;
589
                case 'li':
590
591
                    if (isset($element['ordered'])) { # first
592
                        $list_type = $element['ordered'] ? 'ol' : 'ul';
593
594
                        $markup .= '<' . $list_type . '>' . "\n";
595
                    }
596
597
                    if (isset($element['interrupted']) and !isset($element['last'])) {
598
                        $element['lines'] [] = '';
599
                    }
600
601
                    $text = $this->parse_block_elements($element['lines'], 'li');
602
603
                    $markup .= '<li>' . $text . '</li>' . "\n";
604
605
                    isset($element['last']) and $markup .= '</' . $list_type . '>' . "\n";
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $list_type does not seem to be defined for all execution paths leading up to this point.
Loading history...
606
607
                    break;
608
                case 'block-level markup':
609
610
                    $markup .= $element['text'] . "\n";
611
612
                    break;
613
                default:
614
615
                    $markup .= $element['text'] . "\n";
616
            }
617
        }
618
619
        return $markup;
620
    }
621
622
    /**
623
     * @param       $text
624
     * @param array $markers
625
     * @return string
626
     */
627
    private function parse_span_elements($text, $markers = ['![', '&', '*', '<', '[', '_', '`', 'http', '~~'])
628
    {
629
        if (false === isset($text[2]) or $markers === []) {
630
            return $text;
631
        }
632
633
        # ~
634
635
        $markup = '';
636
637
        while ($markers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $markers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
638
            $closest_marker          = null;
639
            $closest_marker_index    = 0;
640
            $closest_marker_position = null;
641
642
            foreach ($markers as $index => $marker) {
643
                $marker_position = mb_strpos($text, $marker);
644
645
                if (false === $marker_position) {
646
                    unset($markers[$index]);
647
648
                    continue;
649
                }
650
651
                if (null === $closest_marker or $marker_position < $closest_marker_position) {
652
                    $closest_marker          = $marker;
653
                    $closest_marker_index    = $index;
654
                    $closest_marker_position = $marker_position;
655
                }
656
            }
657
658
            # ~
659
660
            if (null === $closest_marker or false === isset($text[$closest_marker_position + 2])) {
661
                $markup .= $text;
662
663
                break;
664
            }
665
            $markup .= mb_substr($text, 0, $closest_marker_position);
666
667
            $text = mb_substr($text, $closest_marker_position);
0 ignored issues
show
Bug introduced by
It seems like $closest_marker_position can also be of type null; however, parameter $start of mb_substr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

667
            $text = mb_substr($text, /** @scrutinizer ignore-type */ $closest_marker_position);
Loading history...
668
669
            # ~
670
671
            unset($markers[$closest_marker_index]);
672
673
            # ~
674
675
            switch ($closest_marker) {
676
                case '![':
677
                case '[':
678
679
                    if (mb_strpos($text, ']') and preg_match('/\[((?:[^][]|(?R))*)\]/', $text, $matches)) {
680
                        $element = [
681
                            '!' => '!' === $text[0],
682
                            'a' => $matches[1],
683
                        ];
684
685
                        $offset = mb_strlen($matches[0]);
686
687
                        $element['!'] and ++$offset;
688
689
                        $remaining_text = mb_substr($text, $offset);
690
691
                        if ('(' === $remaining_text[0] and preg_match('/\([ ]*(.*?)(?:[ ]+[\'"](.+?)[\'"])?[ ]*\)/', $remaining_text, $matches)) {
692
                            $element['»'] = $matches[1];
693
694
                            if (isset($matches[2])) {
695
                                $element['#'] = $matches[2];
696
                            }
697
698
                            $offset += mb_strlen($matches[0]);
699
                        } elseif ($this->reference_map) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->reference_map of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
700
                            $reference = $element['a'];
701
702
                            if (preg_match('/^\s*\[(.*?)\]/', $remaining_text, $matches)) {
703
                                $reference = $matches[1] ?: $element['a'];
704
705
                                $offset += mb_strlen($matches[0]);
706
                            }
707
708
                            $reference = mb_strtolower($reference);
709
710
                            if (isset($this->reference_map[$reference])) {
711
                                $element['»'] = $this->reference_map[$reference]['»'];
712
713
                                if (isset($this->reference_map[$reference]['#'])) {
714
                                    $element['#'] = $this->reference_map[$reference]['#'];
715
                                }
716
                            } else {
717
                                unset($element);
718
                            }
719
                        } else {
720
                            unset($element);
721
                        }
722
                    }
723
724
                    if (isset($element)) {
725
                        $element['»'] = str_replace('&', '&amp;', $element['»']);
726
                        $element['»'] = str_replace('<', '&lt;', $element['»']);
727
728
                        if ($element['!']) {
729
                            $markup .= '<img alt="' . $element['a'] . '" src="' . $element['»'] . '" >';
730
                        } else {
731
                            $element['a'] = $this->parse_span_elements($element['a'], $markers);
732
733
                            $markup .= isset($element['#']) ? '<a href="' . $element['»'] . '" title="' . $element['#'] . '">' . $element['a'] . '</a>' : '<a href="' . $element['»'] . '">' . $element['a'] . '</a>';
734
                        }
735
736
                        unset($element);
737
                    } else {
738
                        $markup .= $closest_marker;
739
740
                        $offset = '![' === $closest_marker ? 2 : 1;
741
                    }
742
743
                    break;
744
                case '&':
745
746
                    if (preg_match('/^&#?\w+;/', $text, $matches)) {
747
                        $markup .= $matches[0];
748
749
                        $offset = mb_strlen($matches[0]);
750
                    } else {
751
                        $markup .= '&amp;';
752
753
                        $offset = 1;
754
                    }
755
756
                    break;
757
                case '*':
758
                case '_':
759
760
                    if ($text[1] === $closest_marker and preg_match($this->strong_regex[$closest_marker], $text, $matches)) {
761
                        $matches[1] = $this->parse_span_elements($matches[1], $markers);
762
763
                        $markup .= '<strong>' . $matches[1] . '</strong>';
764
                    } elseif (preg_match($this->em_regex[$closest_marker], $text, $matches)) {
765
                        $matches[1] = $this->parse_span_elements($matches[1], $markers);
766
767
                        $markup .= '<em>' . $matches[1] . '</em>';
768
                    } elseif ($text[1] === $closest_marker and preg_match($this->strong_em_regex[$closest_marker], $text, $matches)) {
769
                        $matches[2] = $this->parse_span_elements($matches[2], $markers);
770
771
                        $matches[1] and $matches[1] = $this->parse_span_elements($matches[1], $markers);
772
                        $matches[3] and $matches[3] = $this->parse_span_elements($matches[3], $markers);
773
774
                        $markup .= '<strong>' . $matches[1] . '<em>' . $matches[2] . '</em>' . $matches[3] . '</strong>';
775
                    } elseif (preg_match($this->em_strong_regex[$closest_marker], $text, $matches)) {
776
                        $matches[2] = $this->parse_span_elements($matches[2], $markers);
777
778
                        $matches[1] and $matches[1] = $this->parse_span_elements($matches[1], $markers);
779
                        $matches[3] and $matches[3] = $this->parse_span_elements($matches[3], $markers);
780
781
                        $markup .= '<em>' . $matches[1] . '<strong>' . $matches[2] . '</strong>' . $matches[3] . '</em>';
782
                    }
783
784
                    if (isset($matches) and $matches) {
785
                        $offset = mb_strlen($matches[0]);
786
                    } else {
787
                        $markup .= $closest_marker;
788
789
                        $offset = 1;
790
                    }
791
792
                    break;
793
                case '<':
794
795
                    if (false !== mb_strpos($text, '>')) {
796
                        if ('h' === $text[1] and preg_match('/^<(https?:[\/]{2}\S+?)>/i', $text, $matches)) {
797
                            $element_url = $matches[1];
798
                            $element_url = str_replace('&', '&amp;', $element_url);
799
                            $element_url = str_replace('<', '&lt;', $element_url);
800
801
                            $markup .= '<a href="' . $element_url . '">' . $element_url . '</a>';
802
803
                            $offset = mb_strlen($matches[0]);
804
                        } elseif (preg_match('/^<\/?\w.*?>/', $text, $matches)) {
805
                            $markup .= $matches[0];
806
807
                            $offset = mb_strlen($matches[0]);
808
                        } else {
809
                            $markup .= '&lt;';
810
811
                            $offset = 1;
812
                        }
813
                    } else {
814
                        $markup .= '&lt;';
815
816
                        $offset = 1;
817
                    }
818
819
                    break;
820
                case '`':
821
822
                    if (preg_match('/^`(.+?)`/', $text, $matches)) {
823
                        $element_text = $matches[1];
824
                        $element_text = htmlspecialchars($element_text, ENT_QUOTES | ENT_NOQUOTES, 'UTF-8');
825
826
                        if ($this->escape_sequence_map and false !== mb_strpos($element_text, "\x1A")) {
827
                            $element_text = strtr($element_text, $this->escape_sequence_map);
828
                        }
829
830
                        $markup .= '<code>' . $element_text . '</code>';
831
832
                        $offset = mb_strlen($matches[0]);
833
                    } else {
834
                        $markup .= '`';
835
836
                        $offset = 1;
837
                    }
838
839
                    break;
840
                case 'http':
841
842
                    if (preg_match('/^https?:[\/]{2}\S+\b/i', $text, $matches)) {
843
                        $element_url = $matches[0];
844
                        $element_url = str_replace('&', '&amp;', $element_url);
845
                        $element_url = str_replace('<', '&lt;', $element_url);
846
847
                        $markup .= '<a href="' . $element_url . '">' . $element_url . '</a>';
848
849
                        $offset = mb_strlen($matches[0]);
850
                    } else {
851
                        $markup .= 'http';
852
853
                        $offset = 4;
854
                    }
855
856
                    break;
857
                case '~~':
858
859
                    if (preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $text, $matches)) {
860
                        $matches[1] = $this->parse_span_elements($matches[1], $markers);
861
862
                        $markup .= '<del>' . $matches[1] . '</del>';
863
864
                        $offset = mb_strlen($matches[0]);
865
                    } else {
866
                        $markup .= '~~';
867
868
                        $offset = 2;
869
                    }
870
871
                    break;
872
            }
873
874
            if (isset($offset)) {
875
                $text = mb_substr($text, $offset);
876
            }
877
878
            $markers[$closest_marker_index] = $closest_marker;
879
        }
880
881
        $markup = str_replace($this->break_marker, '<br >' . "\n", $markup);
882
883
        return $markup;
884
    }
885
886
    #
887
    # Read-only
888
    #
889
890
    private $inline_tags = [
891
        'a',
892
        'abbr',
893
        'acronym',
894
        'b',
895
        'bdo',
896
        'big',
897
        'br',
898
        'button',
899
        'cite',
900
        'code',
901
        'dfn',
902
        'em',
903
        'i',
904
        'img',
905
        'input',
906
        'kbd',
907
        'label',
908
        'map',
909
        'object',
910
        'q',
911
        'samp',
912
        'script',
913
        'select',
914
        'small',
915
        'span',
916
        'strong',
917
        'sub',
918
        'sup',
919
        'textarea',
920
        'tt',
921
        'var',
922
    ];
923
924
    # ~
925
926
    private $strong_regex = [
927
        '*' => '/^[*]{2}([^*]+?)[*]{2}(?![*])/s',
928
        '_' => '/^__([^_]+?)__(?!_)/s',
929
    ];
930
931
    private $em_regex = [
932
        '*' => '/^[*]([^*]+?)[*](?![*])/s',
933
        '_' => '/^_([^_]+?)[_](?![_])\b/s',
934
    ];
935
936
    private $strong_em_regex = [
937
        '*' => '/^[*]{2}(.*?)[*](.+?)[*](.*?)[*]{2}/s',
938
        '_' => '/^__(.*?)_(.+?)_(.*?)__/s',
939
    ];
940
941
    private $em_strong_regex = [
942
        '*' => '/^[*](.*?)[*]{2}(.+?)[*]{2}(.*?)[*]/s',
943
        '_' => '/^_(.*?)__(.+?)__(.*?)_/s',
944
    ];
945
}
946