Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.32% covered (warning)
75.32%
1453 / 1929
45.93% covered (danger)
45.93%
62 / 135
CRAP
0.00% covered (danger)
0.00%
0 / 1
Markdown
75.32% covered (warning)
75.32%
1453 / 1929
45.93% covered (danger)
45.93%
62 / 135
5469.88
0.00% covered (danger)
0.00%
0 / 1
 __construct
46.77% covered (danger)
46.77%
29 / 62
0.00% covered (danger)
0.00%
0 / 1
29.25
 textParent
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 body
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 text
27.27% covered (danger)
27.27%
3 / 11
0.00% covered (danger)
0.00%
0 / 1
10.15
 contentsList
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 inlineCode
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 inlineEmailTag
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 inlineEmphasis
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
10.06
 inlineImage
91.30% covered (success)
91.30%
21 / 23
0.00% covered (danger)
0.00%
0 / 1
5.02
 inlineLink
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 inlineMarkup
70.00% covered (warning)
70.00%
14 / 20
0.00% covered (danger)
0.00%
0 / 1
14.27
 inlineStrikethrough
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
5.01
 inlineUrl
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
7.01
 inlineUrlTag
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 inlineEmojis
97.32% covered (success)
97.32%
218 / 224
0.00% covered (danger)
0.00%
0 / 1
2
 inlineMark
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 inlineKeystrokes
12.50% covered (danger)
12.50%
1 / 8
0.00% covered (danger)
0.00%
0 / 1
4.68
 inlineSuperscript
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 inlineSubscript
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 inlineTypographer
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 inlineSmartypants
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 1
342
 inlineMath
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 inlineEscapeSequence
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
7.23
 blockFootnote
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockDefinitionList
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockCode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 blockComment
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockHeader
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
7.10
 blockList
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockQuote
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockRule
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockSetextHeader
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
7.10
 blockMarkup
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockReference
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockTable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockAbbreviation
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
5.93
 blockMath
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 blockMathContinue
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 blockMathComplete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 blockFencedCode
32.56% covered (danger)
32.56%
14 / 43
0.00% covered (danger)
0.00%
0 / 1
17.04
 blockTableComplete
5.26% covered (danger)
5.26%
3 / 57
0.00% covered (danger)
0.00%
0 / 1
902.68
 blockCheckbox
33.33% covered (danger)
33.33%
4 / 12
0.00% covered (danger)
0.00%
0 / 1
5.67
 blockCheckboxContinue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 blockCheckboxComplete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 checkboxUnchecked
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 checkboxChecked
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 format
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 parseAttributeData
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 encodeTagToHash
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 decodeTagFromHash
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 getSalt
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getTagToC
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIdAttributeToC
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 createAnchorID
95.24% covered (success)
95.24%
60 / 63
0.00% covered (danger)
0.00%
0 / 1
4
 fetchText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setContentsList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setContentsListAsArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setContentsListAsString
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 incrementAnchorId
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
6.60
 initBlacklist
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
12.72
 lineElements
95.24% covered (success)
95.24%
40 / 42
0.00% covered (danger)
0.00%
0 / 1
13
 pregReplaceAssoc
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 blockAbbreviationBase
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 blockFootnoteBase
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 blockFootnoteContinue
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 blockFootnoteComplete
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 blockDefinitionListBase
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
4.00
 blockDefinitionListContinue
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 blockHeaderBase
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 blockMarkupBase
72.00% covered (warning)
72.00%
18 / 25
0.00% covered (danger)
0.00%
0 / 1
13.66
 blockMarkupContinue
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 blockMarkupComplete
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 blockSetextHeaderBase
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 inlineFootnoteMarker
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
4
 insertAbreviation
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 inlineText
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 addDdElement
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 buildFootnoteElement
92.19% covered (success)
92.19%
59 / 64
0.00% covered (danger)
0.00%
0 / 1
5.01
 parseAttributeDataBase
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 processTag
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 sortFootnotes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 textElements
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 setBreaksEnabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setMarkupEscaped
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setUrlsLinked
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setSafeMode
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setStrictMode
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lines
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 linesElements
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
1 / 1
24
 extractElement
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 isBlockContinuable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isBlockCompletable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 blockCodeBase
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 blockCodeContinue
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 blockCodeComplete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 blockCommentBase
16.67% covered (danger)
16.67%
2 / 12
0.00% covered (danger)
0.00%
0 / 1
19.47
 blockCommentContinue
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 blockFencedCodeBase
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
4
 blockFencedCodeContinue
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 blockFencedCodeComplete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 blockHeaderParent
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
5.01
 blockListBase
88.10% covered (warning)
88.10%
37 / 42
0.00% covered (danger)
0.00%
0 / 1
13.29
 blockListContinue
95.45% covered (success)
95.45%
42 / 44
0.00% covered (danger)
0.00%
0 / 1
17
 blockListComplete
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 blockQuoteBase
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 blockQuoteContinue
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 blockRuleBase
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 blockSetextHeaderParent
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
7
 blockReferenceBase
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 blockTableBase
97.01% covered (success)
97.01%
65 / 67
0.00% covered (danger)
0.00%
0 / 1
17
 blockTableContinue
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
7
 paragraph
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 paragraphContinue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 line
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 inlineTextParent
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 inlineLinkParent
97.44% covered (success)
97.44%
38 / 39
0.00% covered (danger)
0.00%
0 / 1
7
 inlineSpecialCharacter
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
8.74
 unmarkedText
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 handle
68.75% covered (warning)
68.75%
11 / 16
0.00% covered (danger)
0.00%
0 / 1
5.76
 handleElementRecursive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handleElementsRecursive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 elementApplyRecursive
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 elementApplyRecursiveDepthFirst
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 elementsApplyRecursive
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 elementsApplyRecursiveDepthFirst
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 element
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
19
 elements
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 li
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 pregReplaceElements
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 parse
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 sanitiseElement
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
7.08
 filterUnsafeUrlInAttribute
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 escape
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 striAtStart
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 instance
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package    phpOMS\Utils\Parser\Markdown
8 * @license    Original license Emanuil Rusev, erusev.com (MIT)
9 * @version    1.0.0
10 * @link       https://jingga.app
11 */
12declare(strict_types=1);
13
14namespace phpOMS\Utils\Parser\Markdown;
15
16use phpOMS\Uri\UriFactory;
17
18/**
19 * Markdown parser class.
20 *
21 * @package    phpOMS\Utils\Parser\Markdown
22 * @license    Original & extra license Emanuil Rusev, erusev.com (MIT)
23 * @license    Extended license Benjamin Hoegh (MIT)
24 * @link       https://jingga.app
25 * @see        https://github.com/erusev/parsedown
26 * @see        https://github.com/erusev/parsedown-extra
27 * @see        https://github.com/BenjaminHoegh/ParsedownExtended
28 * @since      1.0.0
29 */
30class Markdown
31{
32    # ~
33
34    public const version = '1.8.0-beta-7';
35
36    private array $options = [];
37
38    # ~
39
40    private string $idToc = '';
41
42    public function __construct(array $params = [])
43    {
44        $this->options = $params;
45
46        $this->options['toc'] = $this->options['toc'] ?? false;
47
48        // Marks
49        $state = $this->options['mark'] ?? true;
50        if ($state !== false) {
51            $this->InlineTypes['='][] = 'mark';
52            $this->inlineMarkerList .= '=';
53        }
54
55        // Keystrokes
56        $state = $this->options['keystrokes'] ?? true;
57        if ($state !== false) {
58            $this->InlineTypes['['][] = 'Keystrokes';
59            $this->inlineMarkerList .= '[';
60        }
61
62        // Inline Math
63        $state = $this->options['math'] ?? false;
64        if ($state !== false) {
65            $this->InlineTypes['\\'][] = 'Math';
66            $this->inlineMarkerList .= '\\';
67            $this->InlineTypes['$'][] = 'Math';
68            $this->inlineMarkerList .= '$';
69        }
70
71        // Superscript
72        $state = $this->options['sup'] ?? false;
73        if ($state !== false) {
74            $this->InlineTypes['^'][] = 'Superscript';
75            $this->inlineMarkerList .= '^';
76        }
77
78        // Subscript
79        $state = $this->options['sub'] ?? false;
80        if ($state !== false) {
81            $this->InlineTypes['~'][] = 'Subscript';
82        }
83
84        // Emojis
85        $state = $this->options['emojis'] ?? true;
86        if ($state !== false) {
87            $this->InlineTypes[':'][] = 'Emojis';
88            $this->inlineMarkerList .= ':';
89        }
90
91        // Typographer
92        $state = $this->options['typographer'] ?? false;
93        if ($state !== false) {
94            $this->InlineTypes['('][] = 'Typographer';
95            $this->inlineMarkerList .= '(';
96            $this->InlineTypes['.'][] = 'Typographer';
97            $this->inlineMarkerList .= '.';
98            $this->InlineTypes['+'][] = 'Typographer';
99            $this->inlineMarkerList .= '+';
100            $this->InlineTypes['!'][] = 'Typographer';
101            $this->inlineMarkerList .= '!';
102            $this->InlineTypes['?'][] = 'Typographer';
103            $this->inlineMarkerList .= '?';
104        }
105
106        // Smartypants
107        $state = $this->options['smarty'] ?? false;
108        if ($state !== false) {
109            $this->InlineTypes['<'][] = 'Smartypants';
110            $this->inlineMarkerList .= '<';
111            $this->InlineTypes['>'][] = 'Smartypants';
112            $this->inlineMarkerList .= '>';
113            $this->InlineTypes['-'][] = 'Smartypants';
114            $this->inlineMarkerList .= '-';
115            $this->InlineTypes['.'][] = 'Smartypants';
116            $this->inlineMarkerList .= '.';
117            $this->InlineTypes["'"][] = 'Smartypants';
118            $this->inlineMarkerList .= "'";
119            $this->InlineTypes['"'][] = 'Smartypants';
120            $this->inlineMarkerList .= '"';
121            $this->InlineTypes['`'][] = 'Smartypants';
122            $this->inlineMarkerList .= '`';
123        }
124
125        /*
126         * Blocks
127         * ------------------------------------------------------------------------
128         */
129
130        // Block Math
131        $state = $this->options['math'] ?? false;
132        if ($state !== false) {
133            $this->BlockTypes['\\'][] = 'Math';
134            $this->BlockTypes['$'][]  = 'Math';
135        }
136
137        // Task
138        $state = $this->options['lists']['tasks'] ?? true;
139        if ($state !== false) {
140            $this->BlockTypes['['][] = 'Checkbox';
141        }
142    }
143
144    public function textParent($text)
145    {
146        $Elements = $this->textElements($text);
147
148        # convert to markup
149        $markup = $this->elements($Elements);
150
151        # trim line breaks
152        $markup = \trim($markup, "\n");
153
154        # merge consecutive dl elements
155
156        $markup = \preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
157
158        # add footnotes
159
160        if (isset($this->DefinitionData['Footnote']))
161        {
162            $Element = $this->buildFootnoteElement();
163
164            $markup .= "\n" . $this->element($Element);
165        }
166
167        return $markup;
168    }
169
170    /**
171     * Parses the given markdown string to an HTML string but it leaves the ToC
172     * tag as is. It's an alias of the parent method "\DynamicParent::text()".
173     */
174    public function body($text) : string
175    {
176        $text = $this->encodeTagToHash($text);  // Escapes ToC tag temporary
177        $html = $this->textParent($text);     // Parses the markdown text
178
179        return $this->decodeTagFromHash($html); // Unescape the ToC tag
180    }
181
182    /**
183     * Parses markdown string to HTML and also the "[toc]" tag as well.
184     * It overrides the parent method: \Parsedown::text().
185     */
186    public function text($text)
187    {
188        // Parses the markdown text except the ToC tag. This also searches
189        // the list of contents and available to get from "contentsList()"
190        // method.
191        $html = $this->body($text);
192
193        if (isset($this->options['toc']) && $this->options['toc'] == false) {
194            return $html;
195        }
196
197        $tagOrigin = $this->getTagToC();
198
199        if (\strpos($text, $tagOrigin) === false) {
200            return $html;
201        }
202
203        $tocData = $this->contentsList();
204        $tocId   = $this->getIdAttributeToC();
205        $needle  = '<p>'.$tagOrigin.'</p>';
206        $replace = "<div id=\"{$tocId}\">{$tocData}</div>";
207
208        return \str_replace($needle, $replace, $html);
209    }
210
211    /**
212     * Returns the parsed ToC.
213     *
214     * @param string $typeReturn Type of the return format. "html" or "json".
215     *
216     * @return string HTML/JSON string of ToC
217     */
218    public function contentsList($typeReturn = 'html')
219    {
220        if (\strtolower($typeReturn) === 'html') {
221            $result = '';
222            if (!empty($this->contentsListString)) {
223                // Parses the ToC list in markdown to HTML
224                $result = $this->body($this->contentsListString);
225            }
226
227            return $result;
228        }
229
230        if (\strtolower($typeReturn) === 'json') {
231            return \json_encode($this->contentsListArray);
232        }
233
234        // Forces to return ToC as "html"
235        \error_log(
236            'Unknown return type given while parsing ToC.'
237            .' At: '.__FUNCTION__.'() '
238            .' in Line:'.__LINE__.' (Using default type)'
239        );
240
241        return $this->contentsList('html');
242    }
243
244    /**
245     * ------------------------------------------------------------------------
246     * Inline
247     * ------------------------------------------------------------------------.
248     */
249
250    // inlineCode
251    protected function inlineCode($Excerpt)
252    {
253        $codeSnippets = $this->options['code']['inline'] ?? true;
254        $codeMain     = $this->options['code'] ?? true;
255
256        if ($codeSnippets !== true || $codeMain !== true) {
257            return;
258        }
259
260        $marker = $Excerpt['text'][0];
261
262        if (\preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
263        {
264            $text = $matches[2];
265            $text = \preg_replace('/[ ]*+\n/', ' ', $text);
266
267            return [
268                'extent'  => \strlen($matches[0]),
269                'element' => [
270                    'name' => 'code',
271                    'text' => $text,
272                ],
273            ];
274        }
275    }
276
277    protected function inlineEmailTag($Excerpt)
278    {
279        $mainState = $this->options['links'] ?? true;
280        $state     = $this->options['links']['email_links'] ?? true;
281
282        if (!$mainState || !$state) {
283            return;
284        }
285
286        $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
287
288        $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
289            . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
290
291        if (\strpos($Excerpt['text'], '>') !== false
292            && \preg_match("/^<((mailto:)?{$commonMarkEmail})>/i", $Excerpt['text'], $matches)
293        ){
294            $url = $matches[1];
295
296            if (!isset($matches[2]))
297            {
298                $url = "mailto:{$url}";
299            }
300
301            return [
302                'extent'  => \strlen($matches[0]),
303                'element' => [
304                    'name'       => 'a',
305                    'text'       => $matches[1],
306                    'attributes' => [
307                        'href' => $url,
308                    ],
309                ],
310            ];
311        }
312    }
313
314    protected function inlineEmphasis($Excerpt)
315    {
316        $state = $this->options['emphasis'] ?? true;
317        if (!$state) {
318            return;
319        }
320
321        if (!isset($Excerpt['text'][1]))
322        {
323            return;
324        }
325
326        $marker = $Excerpt['text'][0];
327
328        if ($Excerpt['text'][1] === $marker && isset($this->StrongRegex[$marker]) && \preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
329        {
330            $emphasis = 'strong';
331        }
332        elseif ($Excerpt['text'][1] === $marker && isset($this->UnderlineRegex[$marker]) && \preg_match($this->UnderlineRegex[$marker], $Excerpt['text'], $matches))
333        {
334            $emphasis = 'u';
335        }
336        elseif (\preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
337        {
338            $emphasis = 'em';
339        }
340        else
341        {
342            return;
343        }
344
345        return [
346            'extent'  => \strlen($matches[0]),
347            'element' => [
348                'name'    => $emphasis,
349                'handler' => [
350                    'function'    => 'lineElements',
351                    'argument'    => $matches[1],
352                    'destination' => 'elements',
353                ],
354            ],
355        ];
356    }
357
358    protected function inlineImage($Excerpt)
359    {
360        $state = $this->options['images'] ?? true;
361        if (!$state) {
362            return;
363        }
364
365        if (!isset($Excerpt['text'][1]) || $Excerpt['text'][1] !== '[')
366        {
367            return;
368        }
369
370        $Excerpt['text']= \substr($Excerpt['text'], 1);
371
372        $Link = $this->inlineLink($Excerpt);
373
374        if ($Link === null)
375        {
376            return;
377        }
378
379        $Inline = [
380            'extent'  => $Link['extent'] + 1,
381            'element' => [
382                'name'       => 'img',
383                'attributes' => [
384                    'src' => $Link['element']['attributes']['href'],
385                    'alt' => $Link['element']['handler']['argument'],
386                ],
387                'autobreak' => true,
388            ],
389        ];
390
391        $Inline['element']['attributes'] += $Link['element']['attributes'];
392
393        unset($Inline['element']['attributes']['href']);
394
395        return $Inline;
396    }
397
398    protected function inlineLink($Excerpt)
399    {
400        $state = $this->options['links'] ?? true;
401        if (!$state) {
402            return;
403        }
404
405        $Link = $this->inlineLinkParent($Excerpt);
406
407        $remainder = $Link !== null ? \substr($Excerpt['text'], $Link['extent']) : '';
408
409        if (\preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
410        {
411            $Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
412
413            $Link['extent'] += \strlen($matches[0]);
414        }
415
416        return $Link;
417    }
418
419    protected function inlineMarkup($Excerpt)
420    {
421        $state = $this->options['markup'] ?? true;
422        if (!$state) {
423            return;
424        }
425
426        if ($this->markupEscaped || $this->safeMode || \strpos($Excerpt['text'], '>') === false)
427        {
428            return;
429        }
430
431        if ($Excerpt['text'][1] === '/' && \preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches))
432        {
433            return [
434                'element' => ['rawHtml' => $matches[0]],
435                'extent'  => \strlen($matches[0]),
436            ];
437        }
438
439        if ($Excerpt['text'][1] === '!' && \preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches))
440        {
441            return [
442                'element' => ['rawHtml' => $matches[0]],
443                'extent'  => \strlen($matches[0]),
444            ];
445        }
446
447        if ($Excerpt['text'][1] !== ' ' && \preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches))
448        {
449            return [
450                'element' => ['rawHtml' => $matches[0]],
451                'extent'  => \strlen($matches[0]),
452            ];
453        }
454    }
455
456    protected function inlineStrikethrough($Excerpt)
457    {
458        $state = $this->options['strikethroughs'] ?? true;
459        if (!$state) {
460            return;
461        }
462
463        if (!isset($Excerpt['text'][1]))
464        {
465            return;
466        }
467
468        if ($Excerpt['text'][1] === '~' && \preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
469        {
470            return [
471                'extent'  => \strlen($matches[0]),
472                'element' => [
473                    'name'    => 'del',
474                    'handler' => [
475                        'function'    => 'lineElements',
476                        'argument'    => $matches[1],
477                        'destination' => 'elements',
478                    ],
479                ],
480            ];
481        }
482    }
483
484    protected function inlineUrl($Excerpt)
485    {
486        $state = $this->options['links'] ?? true;
487        if (!$state) {
488            return;
489        }
490
491        if ($this->urlsLinked !== true || !isset($Excerpt['text'][2]) || $Excerpt['text'][2] !== '/')
492        {
493            return;
494        }
495
496        if (\strpos($Excerpt['context'], 'http') !== false
497            && \preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, \PREG_OFFSET_CAPTURE)
498        ) {
499            $url = $matches[0][0];
500
501            return [
502                'extent'   => \strlen($matches[0][0]),
503                'position' => $matches[0][1],
504                'element'  => [
505                    'name'       => 'a',
506                    'text'       => $url,
507                    'attributes' => [
508                        'href' => $url,
509                    ],
510                ],
511            ];
512        }
513    }
514
515    protected function inlineUrlTag($Excerpt)
516    {
517        $state = $this->options['links'] ?? true;
518        if (!$state) {
519            return;
520        }
521
522        if (\strpos($Excerpt['text'], '>') !== false && \preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches))
523        {
524            $url = $matches[1];
525
526            return [
527                'extent'  => \strlen($matches[0]),
528                'element' => [
529                    'name'       => 'a',
530                    'text'       => $url,
531                    'attributes' => [
532                        'href' => $url,
533                    ],
534                ],
535            ];
536        }
537    }
538
539    protected function inlineEmojis($excerpt)
540    {
541        $emojiMap = [
542            ':smile:'                           => '😄', ':laughing:' => '😆', ':blush:' => '😊', ':smiley:' => '😃',
543            ':relaxed:'                         => '☺️', ':smirk:' => '😏', ':heart_eyes:' => '😍', ':kissing_heart:' => '😘',
544            ':kissing_closed_eyes:'             => '😚', ':flushed:' => '😳', ':relieved:' => '😌', ':satisfied:' => '😆',
545            ':grin:'                            => '😁', ':wink:' => '😉', ':stuck_out_tongue_winking_eye:' => '😜', ':stuck_out_tongue_closed_eyes:' => '😝',
546            ':grinning:'                        => '😀', ':kissing:' => '😗', ':kissing_smiling_eyes:' => '😙', ':stuck_out_tongue:' => '😛',
547            ':sleeping:'                        => '😴', ':worried:' => '😟', ':frowning:' => '😦', ':anguished:' => '😧',
548            ':open_mouth:'                      => '😮', ':grimacing:' => '😬', ':confused:' => '😕', ':hushed:' => '😯',
549            ':expressionless:'                  => '😑', ':unamused:' => '😒', ':sweat_smile:' => '😅', ':sweat:' => '😓',
550            ':disappointed_relieved:'           => '😥', ':weary:' => '😩', ':pensive:' => '😔', ':disappointed:' => '😞',
551            ':confounded:'                      => '😖', ':fearful:' => '😨', ':cold_sweat:' => '😰', ':persevere:' => '😣',
552            ':cry:'                             => '😢', ':sob:' => '😭', ':joy:' => '😂', ':astonished:' => '😲',
553            ':scream:'                          => '😱', ':tired_face:' => '😫', ':angry:' => '😠', ':rage:' => '😡',
554            ':triumph:'                         => '😤', ':sleepy:' => '😪', ':yum:' => '😋', ':mask:' => '😷',
555            ':sunglasses:'                      => '😎', ':dizzy_face:' => '😵', ':imp:' => '👿', ':smiling_imp:' => '😈',
556            ':neutral_face:'                    => '😐', ':no_mouth:' => '😶', ':innocent:' => '😇', ':alien:' => '👽',
557            ':yellow_heart:'                    => '💛', ':blue_heart:' => '💙', ':purple_heart:' => '💜', ':heart:' => '❤️',
558            ':green_heart:'                     => '💚', ':broken_heart:' => '💔', ':heartbeat:' => '💓', ':heartpulse:' => '💗',
559            ':two_hearts:'                      => '💕', ':revolving_hearts:' => '💞', ':cupid:' => '💘', ':sparkling_heart:' => '💖',
560            ':sparkles:'                        => '✨', ':star:' => '⭐️', ':star2:' => '🌟', ':dizzy:' => '💫',
561            ':boom:'                            => '💥', ':collision:' => '💥', ':anger:' => '💢', ':exclamation:' => '❗️',
562            ':question:'                        => '❓', ':grey_exclamation:' => '❕', ':grey_question:' => '❔', ':zzz:' => '💤',
563            ':dash:'                            => '💨', ':sweat_drops:' => '💦', ':notes:' => '🎶', ':musical_note:' => '🎵',
564            ':fire:'                            => '🔥', ':hankey:' => '💩', ':poop:' => '💩', ':shit:' => '💩',
565            ':+1:'                              => '👍', ':thumbsup:' => '👍', ':-1:' => '👎', ':thumbsdown:' => '👎',
566            ':ok_hand:'                         => '👌', ':punch:' => '👊', ':facepunch:' => '👊', ':fist:' => '✊',
567            ':v:'                               => '✌️', ':wave:' => '👋', ':hand:' => '✋', ':raised_hand:' => '✋',
568            ':open_hands:'                      => '👐', ':point_up:' => '☝️', ':point_down:' => '👇', ':point_left:' => '👈',
569            ':point_right:'                     => '👉', ':raised_hands:' => '🙌', ':pray:' => '🙏', ':point_up_2:' => '👆',
570            ':clap:'                            => '👏', ':muscle:' => '💪', ':metal:' => '🤘', ':fu:' => '🖕',
571            ':walking:'                         => '🚶', ':runner:' => '🏃', ':running:' => '🏃', ':couple:' => '👫',
572            ':family:'                          => '👪', ':two_men_holding_hands:' => '👬', ':two_women_holding_hands:' => '👭', ':dancer:' => '💃',
573            ':dancers:'                         => '👯', ':ok_woman:' => '🙆', ':no_good:' => '🙅', ':information_desk_person:' => '💁',
574            ':raising_hand:'                    => '🙋', ':bride_with_veil:' => '👰', ':person_with_pouting_face:' => '🙎', ':person_frowning:' => '🙍',
575            ':bow:'                             => '🙇', ':couple_with_heart:' => '💑', ':massage:' => '💆', ':haircut:' => '💇',
576            ':nail_care:'                       => '💅', ':boy:' => '👦', ':girl:' => '👧', ':woman:' => '👩',
577            ':man:'                             => '👨', ':baby:' => '👶', ':older_woman:' => '👵', ':older_man:' => '👴',
578            ':person_with_blond_hair:'          => '👱', ':man_with_gua_pi_mao:' => '👲', ':man_with_turban:' => '👳', ':construction_worker:' => '👷',
579            ':cop:'                             => '👮', ':angel:' => '👼', ':princess:' => '👸', ':smiley_cat:' => '😺',
580            ':smile_cat:'                       => '😸', ':heart_eyes_cat:' => '😻', ':kissing_cat:' => '😽', ':smirk_cat:' => '😼',
581            ':scream_cat:'                      => '🙀', ':crying_cat_face:' => '😿', ':joy_cat:' => '😹', ':pouting_cat:' => '😾',
582            ':japanese_ogre:'                   => '👹', ':japanese_goblin:' => '👺', ':see_no_evil:' => '🙈', ':hear_no_evil:' => '🙉',
583            ':speak_no_evil:'                   => '🙊', ':guardsman:' => '💂', ':skull:' => '💀', ':feet:' => '🐾',
584            ':lips:'                            => '👄', ':kiss:' => '💋', ':droplet:' => '💧', ':ear:' => '👂',
585            ':eyes:'                            => '👀', ':nose:' => '👃', ':tongue:' => '👅', ':love_letter:' => '💌',
586            ':bust_in_silhouette:'              => '👤', ':busts_in_silhouette:' => '👥', ':speech_balloon:' => '💬', ':thought_balloon:' => '💭',
587            ':sunny:'                           => '☀️', ':umbrella:' => '☔️', ':cloud:' => '☁️', ':snowflake:' => '❄️',
588            ':snowman:'                         => '⛄️', ':zap:' => '⚡️', ':cyclone:' => '🌀', ':foggy:' => '🌁',
589            ':ocean:'                           => '🌊', ':cat:' => '🐱', ':dog:' => '🐶', ':mouse:' => '🐭',
590            ':hamster:'                         => '🐹', ':rabbit:' => '🐰', ':wolf:' => '🐺', ':frog:' => '🐸',
591            ':tiger:'                           => '🐯', ':koala:' => '🐨', ':bear:' => '🐻', ':pig:' => '🐷',
592            ':pig_nose:'                        => '🐽', ':cow:' => '🐮', ':boar:' => '🐗', ':monkey_face:' => '🐵',
593            ':monkey:'                          => '🐒', ':horse:' => '🐴', ':racehorse:' => '🐎', ':camel:' => '🐫',
594            ':sheep:'                           => '🐑', ':elephant:' => '🐘', ':panda_face:' => '🐼', ':snake:' => '🐍',
595            ':bird:'                            => '🐦', ':baby_chick:' => '🐤', ':hatched_chick:' => '🐥', ':hatching_chick:' => '🐣',
596            ':chicken:'                         => '🐔', ':penguin:' => '🐧', ':turtle:' => '🐢', ':bug:' => '🐛',
597            ':honeybee:'                        => '🐝', ':ant:' => '🐜', ':beetle:' => '🐞', ':snail:' => '🐌',
598            ':octopus:'                         => '🐙', ':tropical_fish:' => '🐠', ':fish:' => '🐟', ':whale:' => '🐳',
599            ':whale2:'                          => '🐋', ':dolphin:' => '🐬', ':cow2:' => '🐄', ':ram:' => '🐏',
600            ':rat:'                             => '🐀', ':water_buffalo:' => '🐃', ':tiger2:' => '🐅', ':rabbit2:' => '🐇',
601            ':dragon:'                          => '🐉', ':goat:' => '🐐', ':rooster:' => '🐓', ':dog2:' => '🐕',
602            ':pig2:'                            => '🐖', ':mouse2:' => '🐁', ':ox:' => '🐂', ':dragon_face:' => '🐲',
603            ':blowfish:'                        => '🐡', ':crocodile:' => '🐊', ':dromedary_camel:' => '🐪', ':leopard:' => '🐆',
604            ':cat2:'                            => '🐈', ':poodle:' => '🐩', ':crab' => '🦀', ':paw_prints:' => '🐾', ':bouquet:' => '💐',
605            ':cherry_blossom:'                  => '🌸', ':tulip:' => '🌷', ':four_leaf_clover:' => '🍀', ':rose:' => '🌹',
606            ':sunflower:'                       => '🌻', ':hibiscus:' => '🌺', ':maple_leaf:' => '🍁', ':leaves:' => '🍃',
607            ':fallen_leaf:'                     => '🍂', ':herb:' => '🌿', ':mushroom:' => '🍄', ':cactus:' => '🌵',
608            ':palm_tree:'                       => '🌴', ':evergreen_tree:' => '🌲', ':deciduous_tree:' => '🌳', ':chestnut:' => '🌰',
609            ':seedling:'                        => '🌱', ':blossom:' => '🌼', ':ear_of_rice:' => '🌾', ':shell:' => '🐚',
610            ':globe_with_meridians:'            => '🌐', ':sun_with_face:' => '🌞', ':full_moon_with_face:' => '🌝', ':new_moon_with_face:' => '🌚',
611            ':new_moon:'                        => '🌑', ':waxing_crescent_moon:' => '🌒', ':first_quarter_moon:' => '🌓', ':waxing_gibbous_moon:' => '🌔',
612            ':full_moon:'                       => '🌕', ':waning_gibbous_moon:' => '🌖', ':last_quarter_moon:' => '🌗', ':waning_crescent_moon:' => '🌘',
613            ':last_quarter_moon_with_face:'     => '🌜', ':first_quarter_moon_with_face:' => '🌛', ':moon:' => '🌔', ':earth_africa:' => '🌍',
614            ':earth_americas:'                  => '🌎', ':earth_asia:' => '🌏', ':volcano:' => '🌋', ':milky_way:' => '🌌',
615            ':partly_sunny:'                    => '⛅️', ':bamboo:' => '🎍', ':gift_heart:' => '💝', ':dolls:' => '🎎',
616            ':school_satchel:'                  => '🎒', ':mortar_board:' => '🎓', ':flags:' => '🎏', ':fireworks:' => '🎆',
617            ':sparkler:'                        => '🎇', ':wind_chime:' => '🎐', ':rice_scene:' => '🎑', ':jack_o_lantern:' => '🎃',
618            ':ghost:'                           => '👻', ':santa:' => '🎅', ':christmas_tree:' => '🎄', ':gift:' => '🎁',
619            ':bell:'                            => '🔔', ':no_bell:' => '🔕', ':tanabata_tree:' => '🎋', ':tada:' => '🎉',
620            ':confetti_ball:'                   => '🎊', ':balloon:' => '🎈', ':crystal_ball:' => '🔮', ':cd:' => '💿',
621            ':dvd:'                             => '📀', ':floppy_disk:' => '💾', ':camera:' => '📷', ':video_camera:' => '📹',
622            ':movie_camera:'                    => '🎥', ':computer:' => '💻', ':tv:' => '📺', ':iphone:' => '📱',
623            ':phone:'                           => '☎️', ':telephone:' => '☎️', ':telephone_receiver:' => '📞', ':pager:' => '📟',
624            ':fax:'                             => '📠', ':minidisc:' => '💽', ':vhs:' => '📼', ':sound:' => '🔉',
625            ':speaker:'                         => '🔈', ':mute:' => '🔇', ':loudspeaker:' => '📢', ':mega:' => '📣',
626            ':hourglass:'                       => '⌛️', ':hourglass_flowing_sand:' => '⏳', ':alarm_clock:' => '⏰', ':watch:' => '⌚️',
627            ':radio:'                           => '📻', ':satellite:' => '📡', ':loop:' => '➿', ':mag:' => '🔍',
628            ':mag_right:'                       => '🔎', ':unlock:' => '🔓', ':lock:' => '🔒', ':lock_with_ink_pen:' => '🔏',
629            ':closed_lock_with_key:'            => '🔐', ':key:' => '🔑', ':bulb:' => '💡', ':flashlight:' => '🔦',
630            ':high_brightness:'                 => '🔆', ':low_brightness:' => '🔅', ':electric_plug:' => '🔌', ':battery:' => '🔋',
631            ':calling:'                         => '📲', ':email:' => '✉️', ':mailbox:' => '📫', ':postbox:' => '📮',
632            ':bath:'                            => '🛀', ':bathtub:' => '🛁', ':shower:' => '🚿', ':toilet:' => '🚽',
633            ':wrench:'                          => '🔧', ':nut_and_bolt:' => '🔩', ':hammer:' => '🔨', ':seat:' => '💺',
634            ':moneybag:'                        => '💰', ':yen:' => '💴', ':dollar:' => '💵', ':pound:' => '💷',
635            ':euro:'                            => '💶', ':credit_card:' => '💳', ':money_with_wings:' => '💸', ':e-mail:' => '📧',
636            ':inbox_tray:'                      => '📥', ':outbox_tray:' => '📤', ':envelope:' => '✉️', ':incoming_envelope:' => '📨',
637            ':postal_horn:'                     => '📯', ':mailbox_closed:' => '📪', ':mailbox_with_mail:' => '📬', ':mailbox_with_no_mail:' => '📭',
638            ':door:'                            => '🚪', ':smoking:' => '🚬', ':bomb:' => '💣', ':gun:' => '🔫',
639            ':hocho:'                           => '🔪', ':pill:' => '💊', ':syringe:' => '💉', ':page_facing_up:' => '📄',
640            ':page_with_curl:'                  => '📃', ':bookmark_tabs:' => '📑', ':bar_chart:' => '📊', ':chart_with_upwards_trend:' => '📈',
641            ':chart_with_downwards_trend:'      => '📉', ':scroll:' => '📜', ':clipboard:' => '📋', ':calendar:' => '📆',
642            ':date:'                            => '📅', ':card_index:' => '📇', ':file_folder:' => '📁', ':open_file_folder:' => '📂',
643            ':scissors:'                        => '✂️', ':pushpin:' => '📌', ':paperclip:' => '📎', ':black_nib:' => '✒️',
644            ':pencil2:'                         => '✏️', ':straight_ruler:' => '📏', ':triangular_ruler:' => '📐', ':closed_book:' => '📕',
645            ':green_book:'                      => '📗', ':blue_book:' => '📘', ':orange_book:' => '📙', ':notebook:' => '📓',
646            ':notebook_with_decorative_cover:'  => '📔', ':ledger:' => '📒', ':books:' => '📚', ':bookmark:' => '🔖',
647            ':name_badge:'                      => '📛', ':microscope:' => '🔬', ':telescope:' => '🔭', ':newspaper:' => '📰',
648            ':football:'                        => '🏈', ':basketball:' => '🏀', ':soccer:' => '⚽️', ':baseball:' => '⚾️',
649            ':tennis:'                          => '🎾', ':8ball:' => '🎱', ':rugby_football:' => '🏉', ':bowling:' => '🎳',
650            ':golf:'                            => '⛳️', ':mountain_bicyclist:' => '🚵', ':bicyclist:' => '🚴', ':horse_racing:' => '🏇',
651            ':snowboarder:'                     => '🏂', ':swimmer:' => '🏊', ':surfer:' => '🏄', ':ski:' => '🎿',
652            ':spades:'                          => '♠️', ':hearts:' => '♥️', ':clubs:' => '♣️', ':diamonds:' => '♦️',
653            ':gem:'                             => '💎', ':ring:' => '💍', ':trophy:' => '🏆', ':musical_score:' => '🎼',
654            ':musical_keyboard:'                => '🎹', ':violin:' => '🎻', ':space_invader:' => '👾', ':video_game:' => '🎮',
655            ':black_joker:'                     => '🃏', ':flower_playing_cards:' => '🎴', ':game_die:' => '🎲', ':dart:' => '🎯',
656            ':mahjong:'                         => '🀄️', ':clapper:' => '🎬', ':memo:' => '📝', ':pencil:' => '📝',
657            ':book:'                            => '📖', ':art:' => '🎨', ':microphone:' => '🎤', ':headphones:' => '🎧',
658            ':trumpet:'                         => '🎺', ':saxophone:' => '🎷', ':guitar:' => '🎸', ':shoe:' => '👞',
659            ':sandal:'                          => '👡', ':high_heel:' => '👠', ':lipstick:' => '💄', ':boot:' => '👢',
660            ':shirt:'                           => '👕', ':tshirt:' => '👕', ':necktie:' => '👔', ':womans_clothes:' => '👚',
661            ':dress:'                           => '👗', ':running_shirt_with_sash:' => '🎽', ':jeans:' => '👖', ':kimono:' => '👘',
662            ':bikini:'                          => '👙', ':ribbon:' => '🎀', ':tophat:' => '🎩', ':crown:' => '👑',
663            ':womans_hat:'                      => '👒', ':mans_shoe:' => '👞', ':closed_umbrella:' => '🌂', ':briefcase:' => '💼',
664            ':handbag:'                         => '👜', ':pouch:' => '👝', ':purse:' => '👛', ':eyeglasses:' => '👓',
665            ':fishing_pole_and_fish:'           => '🎣', ':coffee:' => '☕️', ':tea:' => '🍵', ':sake:' => '🍶',
666            ':baby_bottle:'                     => '🍼', ':beer:' => '🍺', ':beers:' => '🍻', ':cocktail:' => '🍸',
667            ':tropical_drink:'                  => '🍹', ':wine_glass:' => '🍷', ':fork_and_knife:' => '🍴', ':pizza:' => '🍕',
668            ':hamburger:'                       => '🍔', ':fries:' => '🍟', ':poultry_leg:' => '🍗', ':meat_on_bone:' => '🍖',
669            ':spaghetti:'                       => '🍝', ':curry:' => '🍛', ':fried_shrimp:' => '🍤', ':bento:' => '🍱',
670            ':sushi:'                           => '🍣', ':fish_cake:' => '🍥', ':rice_ball:' => '🍙', ':rice_cracker:' => '🍘',
671            ':rice:'                            => '🍚', ':ramen:' => '🍜', ':stew:' => '🍲', ':oden:' => '🍢',
672            ':dango:'                           => '🍡', ':egg:' => '🥚', ':bread:' => '🍞', ':doughnut:' => '🍩',
673            ':custard:'                         => '🍮', ':icecream:' => '🍦', ':ice_cream:' => '🍨', ':shaved_ice:' => '🍧',
674            ':birthday:'                        => '🎂', ':cake:' => '🍰', ':cookie:' => '🍪', ':chocolate_bar:' => '🍫',
675            ':candy:'                           => '🍬', ':lollipop:' => '🍭', ':honey_pot:' => '🍯', ':apple:' => '🍎',
676            ':green_apple:'                     => '🍏', ':tangerine:' => '🍊', ':lemon:' => '🍋', ':cherries:' => '🍒',
677            ':grapes:'                          => '🍇', ':watermelon:' => '🍉', ':strawberry:' => '🍓', ':peach:' => '🍑',
678            ':melon:'                           => '🍈', ':banana:' => '🍌', ':pear:' => '🍐', ':pineapple:' => '🍍',
679            ':sweet_potato:'                    => '🍠', ':eggplant:' => '🍆', ':tomato:' => '🍅', ':corn:' => '🌽',
680            ':house:'                           => '🏠', ':house_with_garden:' => '🏡', ':school:' => '🏫', ':office:' => '🏢',
681            ':post_office:'                     => '🏣', ':hospital:' => '🏥', ':bank:' => '🏦', ':convenience_store:' => '🏪',
682            ':love_hotel:'                      => '🏩', ':hotel:' => '🏨', ':wedding:' => '💒', ':church:' => '⛪️',
683            ':department_store:'                => '🏬', ':european_post_office:' => '🏤', ':city_sunrise:' => '🌇', ':city_sunset:' => '🌆',
684            ':japanese_castle:'                 => '🏯', ':european_castle:' => '🏰', ':tent:' => '⛺️', ':factory:' => '🏭',
685            ':tokyo_tower:'                     => '🗼', ':japan:' => '🗾', ':mount_fuji:' => '🗻', ':sunrise_over_mountains:' => '🌄',
686            ':sunrise:'                         => '🌅', ':stars:' => '🌠', ':statue_of_liberty:' => '🗽', ':bridge_at_night:' => '🌉',
687            ':carousel_horse:'                  => '🎠', ':rainbow:' => '🌈', ':ferris_wheel:' => '🎡', ':fountain:' => '⛲️',
688            ':roller_coaster:'                  => '🎢', ':ship:' => '🚢', ':speedboat:' => '🚤', ':boat:' => '⛵️',
689            ':sailboat:'                        => '⛵️', ':rowboat:' => '🚣', ':anchor:' => '⚓️', ':rocket:' => '🚀',
690            ':airplane:'                        => '✈️', ':helicopter:' => '🚁', ':steam_locomotive:' => '🚂', ':tram:' => '🚊',
691            ':mountain_railway:'                => '🚞', ':bike:' => '🚲', ':aerial_tramway:' => '🚡', ':suspension_railway:' => '🚟',
692            ':mountain_cableway:'               => '🚠', ':tractor:' => '🚜', ':blue_car:' => '🚙', ':oncoming_automobile:' => '🚘',
693            ':car:'                             => '🚗', ':red_car:' => '🚗', ':taxi:' => '🚕', ':oncoming_taxi:' => '🚖',
694            ':articulated_lorry:'               => '🚛', ':bus:' => '🚌', ':oncoming_bus:' => '🚍', ':rotating_light:' => '🚨',
695            ':police_car:'                      => '🚓', ':oncoming_police_car:' => '🚔', ':fire_engine:' => '🚒', ':ambulance:' => '🚑',
696            ':minibus:'                         => '🚐', ':truck:' => '🚚', ':train:' => '🚋', ':station:' => '🚉',
697            ':train2:'                          => '🚆', ':bullettrain_front:' => '🚅', ':bullettrain_side:' => '🚄', ':light_rail:' => '🚈',
698            ':monorail:'                        => '🚝', ':railway_car:' => '🚃', ':trolleybus:' => '🚎', ':ticket:' => '🎫',
699            ':fuelpump:'                        => '⛽️', ':vertical_traffic_light:' => '🚦', ':traffic_light:' => '🚥', ':warning:' => '⚠️',
700            ':construction:'                    => '🚧', ':beginner:' => '🔰', ':atm:' => '🏧', ':slot_machine:' => '🎰',
701            ':busstop:'                         => '🚏', ':barber:' => '💈', ':hotsprings:' => '♨️', ':checkered_flag:' => '🏁',
702            ':crossed_flags:'                   => '🎌', ':izakaya_lantern:' => '🏮', ':moyai:' => '🗿', ':circus_tent:' => '🎪',
703            ':performing_arts:'                 => '🎭', ':round_pushpin:' => '📍', ':triangular_flag_on_post:' => '🚩', ':jp:' => '🇯🇵',
704            ':kr:'                              => '🇰🇷', ':cn:' => '🇨🇳', ':us:' => '🇺🇸', ':fr:' => '🇫🇷',
705            ':es:'                              => '🇪🇸', ':it:' => '🇮🇹', ':ru:' => '🇷🇺', ':gb:' => '🇬🇧',
706            ':uk:'                              => '🇬🇧', ':de:' => '🇩🇪', ':one:' => '1️⃣', ':two:' => '2️⃣',
707            ':three:'                           => '3️⃣', ':four:' => '4️⃣', ':five:' => '5️⃣', ':six:' => '6️⃣',
708            ':seven:'                           => '7️⃣', ':eight:' => '8️⃣', ':nine:' => '9️⃣', ':keycap_ten:' => '🔟',
709            ':1234:'                            => '🔢', ':zero:' => '0️⃣', ':hash:' => '#️⃣', ':symbols:' => '🔣',
710            ':arrow_backward:'                  => '◀️', ':arrow_down:' => '⬇️', ':arrow_forward:' => '▶️', ':arrow_left:' => '⬅️',
711            ':capital_abcd:'                    => '🔠', ':abcd:' => '🔡', ':abc:' => '🔤', ':arrow_lower_left:' => '↙️',
712            ':arrow_lower_right:'               => '↘️', ':arrow_right:' => '➡️', ':arrow_up:' => '⬆️', ':arrow_upper_left:' => '↖️',
713            ':arrow_upper_right:'               => '↗️', ':arrow_double_down:' => '⏬', ':arrow_double_up:' => '⏫', ':arrow_down_small:' => '🔽',
714            ':arrow_heading_down:'              => '⤵️', ':arrow_heading_up:' => '⤴️', ':leftwards_arrow_with_hook:' => '↩️', ':arrow_right_hook:' => '↪️',
715            ':left_right_arrow:'                => '↔️', ':arrow_up_down:' => '↕️', ':arrow_up_small:' => '🔼', ':arrows_clockwise:' => '🔃',
716            ':arrows_counterclockwise:'         => '🔄', ':rewind:' => '⏪', ':fast_forward:' => '⏩', ':information_source:' => 'ℹ️',
717            ':ok:'                              => '🆗', ':twisted_rightwards_arrows:' => '🔀', ':repeat:' => '🔁', ':repeat_one:' => '🔂',
718            ':new:'                             => '🆕', ':top:' => '🔝', ':up:' => '🆙', ':cool:' => '🆒',
719            ':free:'                            => '🆓', ':ng:' => '🆖', ':cinema:' => '🎦', ':koko:' => '🈁',
720            ':signal_strength:'                 => '📶', ':u5272:' => '🈹', ':u5408:' => '🈴', ':u55b6:' => '🈺',
721            ':u6307:'                           => '🈯️', ':u6708:' => '🈷️', ':u6709:' => '🈶', ':u6e80:' => '🈵',
722            ':u7121:'                           => '🈚️', ':u7533:' => '🈸', ':u7a7a:' => '🈳', ':u7981:' => '🈲',
723            ':sa:'                              => '🈂️', ':restroom:' => '🚻', ':mens:' => '🚹', ':womens:' => '🚺',
724            ':baby_symbol:'                     => '🚼', ':no_smoking:' => '🚭', ':parking:' => '🅿️', ':wheelchair:' => '♿️',
725            ':metro:'                           => '🚇', ':baggage_claim:' => '🛄', ':accept:' => '🉑', ':wc:' => '🚾',
726            ':potable_water:'                   => '🚰', ':put_litter_in_its_place:' => '🚮', ':secret:' => '㊙️', ':congratulations:' => '㊗️',
727            ':m:'                               => 'Ⓜ️', ':passport_control:' => '🛂', ':left_luggage:' => '🛅', ':customs:' => '🛃',
728            ':ideograph_advantage:'             => '🉐', ':cl:' => '🆑', ':sos:' => '🆘', ':id:' => '🆔',
729            ':no_entry_sign:'                   => '🚫', ':underage:' => '🔞', ':no_mobile_phones:' => '📵', ':do_not_litter:' => '🚯',
730            ':non-potable_water:'               => '🚱', ':no_bicycles:' => '🚳', ':no_pedestrians:' => '🚷', ':children_crossing:' => '🚸',
731            ':no_entry:'                        => '⛔️', ':eight_spoked_asterisk:' => '✳️', ':eight_pointed_black_star:' => '✴️', ':heart_decoration:' => '💟',
732            ':vs:'                              => '🆚', ':vibration_mode:' => '📳', ':mobile_phone_off:' => '📴', ':chart:' => '💹',
733            ':currency_exchange:'               => '💱', ':aries:' => '♈️', ':taurus:' => '♉️', ':gemini:' => '♊️',
734            ':cancer:'                          => '♋️', ':leo:' => '♌️', ':virgo:' => '♍️', ':libra:' => '♎️',
735            ':scorpius:'                        => '♏️', ':sagittarius:' => '♐️', ':capricorn:' => '♑️', ':aquarius:' => '♒️',
736            ':pisces:'                          => '♓️', ':ophiuchus:' => '⛎', ':six_pointed_star:' => '🔯', ':negative_squared_cross_mark:' => '❎',
737            ':a:'                               => '🅰️', ':b:' => '🅱️', ':ab:' => '🆎', ':o2:' => '🅾️',
738            ':diamond_shape_with_a_dot_inside:' => '💠', ':recycle:' => '♻️', ':end:' => '🔚', ':on:' => '🔛',
739            ':soon:'                            => '🔜', ':clock1:' => '🕐', ':clock130:' => '🕜', ':clock10:' => '🕙',
740            ':clock1030:'                       => '🕥', ':clock11:' => '🕚', ':clock1130:' => '🕦', ':clock12:' => '🕛',
741            ':clock1230:'                       => '🕧', ':clock2:' => '🕑', ':clock230:' => '🕝', ':clock3:' => '🕒',
742            ':clock330:'                        => '🕞', ':clock4:' => '🕓', ':clock430:' => '🕟', ':clock5:' => '🕔',
743            ':clock530:'                        => '🕠', ':clock6:' => '🕕', ':clock630:' => '🕡', ':clock7:' => '🕖',
744            ':clock730:'                        => '🕢', ':clock8:' => '🕗', ':clock830:' => '🕣', ':clock9:' => '🕘',
745            ':clock930:'                        => '🕤', ':heavy_dollar_sign:' => '💲', ':copyright:' => '©️', ':registered:' => '®️',
746            ':tm:'                              => '™️', ':x:' => '❌', ':heavy_exclamation_mark:' => '❗️', ':bangbang:' => '‼️',
747            ':interrobang:'                     => '⁉️', ':o:' => '⭕️', ':heavy_multiplication_x:' => '✖️', ':heavy_plus_sign:' => '➕',
748            ':heavy_minus_sign:'                => '➖', ':heavy_division_sign:' => '➗', ':white_flower:' => '💮', ':100:' => '💯',
749            ':heavy_check_mark:'                => '✔️', ':ballot_box_with_check:' => '☑️', ':radio_button:' => '🔘', ':link:' => '🔗',
750            ':curly_loop:'                      => '➰', ':wavy_dash:' => '〰️', ':part_alternation_mark:' => '〽️', ':trident:' => '🔱',
751            ':white_check_mark:'                => '✅', ':black_square_button:' => '🔲', ':white_square_button:' => '🔳', ':black_circle:' => '⚫️',
752            ':white_circle:'                    => '⚪️', ':red_circle:' => '🔴', ':large_blue_circle:' => '🔵', ':large_blue_diamond:' => '🔷',
753            ':large_orange_diamond:'            => '🔶', ':small_blue_diamond:' => '🔹', ':small_orange_diamond:' => '🔸', ':small_red_triangle:' => '🔺',
754            ':small_red_triangle_down:'         => '🔻', ':black_small_square:' => '▪️', ':black_medium_small_square:' => '◾', ':black_medium_square:' => '◼️',
755            ':black_large_square:'              => '⬛', ':white_small_square:' => '▫️', ':white_medium_small_square:' => '◽', ':white_medium_square:' => '◻️',
756            ':white_large_square:'              => '⬜',
757        ];
758
759        if (\preg_match('/^(:)([^: ]*?)(:)/', $excerpt['text'], $matches)) {
760            return [
761                'extent'  => \strlen($matches[0]),
762                'element' => [
763                    'text' => \str_replace(\array_keys($emojiMap), $emojiMap, $matches[0]),
764                ],
765            ];
766        }
767    }
768
769    // Inline Marks
770
771    protected function inlineMark($excerpt)
772    {
773        if (\preg_match('/^(==)([^=]*?)(==)/', $excerpt['text'], $matches)) {
774            return [
775                'extent'  => \strlen($matches[0]),
776                'element' => [
777                    'name' => 'mark',
778                    'text' => $matches[2],
779                ],
780            ];
781        }
782    }
783
784    // Inline Keystrokes
785
786    protected function inlineKeystrokes($excerpt)
787    {
788        if (\preg_match('/^(?<!\[)(?:\[\[([^\[\]]*|[\[\]])\]\])(?!\])/s', $excerpt['text'], $matches)) {
789            return [
790                'extent'  => \strlen($matches[0]),
791                'element' => [
792                    'name' => 'kbd',
793                    'text' => $matches[1],
794                ],
795            ];
796        }
797    }
798
799    // Inline Superscript
800
801    protected function inlineSuperscript($excerpt)
802    {
803        if (\preg_match('/(?:\^(?!\^)([^\^ ]*)\^(?!\^))/', $excerpt['text'], $matches)) {
804            return [
805                'extent'  => \strlen($matches[0]),
806                'element' => [
807                    'name'     => 'sup',
808                    'text'     => $matches[1],
809                    'function' => 'lineElements',
810                ],
811            ];
812        }
813    }
814
815    // Inline Subscript
816
817    protected function inlineSubscript($excerpt)
818    {
819        if (\preg_match('/(?:~(?!~)([^~ ]*)~(?!~))/', $excerpt['text'], $matches)) {
820            return [
821                'extent'  => \strlen($matches[0]),
822                'element' => [
823                    'name'     => 'sub',
824                    'text'     => $matches[1],
825                    'function' => 'lineElements',
826                ],
827            ];
828        }
829    }
830
831    // Inline typographer
832
833    protected function inlineTypographer($excerpt)
834    {
835        $substitutions = [
836            '/\(c\)/i'        => '&copy;',
837            '/\(r\)/i'        => '&reg;',
838            '/\(tm\)/i'       => '&trade;',
839            '/\(p\)/i'        => '&para;',
840            '/\+-/i'          => '&plusmn;',
841            '/\.{4,}|\.{2}/i' => '...',
842            '/\!\.{3,}/i'     => '!..',
843            '/\?\.{3,}/i'     => '?..',
844        ];
845
846        if (\preg_match('/\+-|\(p\)|\(tm\)|\(r\)|\(c\)|\.{2,}|\!\.{3,}|\?\.{3,}/i', $excerpt['text'], $matches)) {
847            return [
848                'extent'  => \strlen($matches[0]),
849                'element' => [
850                    'rawHtml' => \preg_replace(\array_keys($substitutions), \array_values($substitutions), $matches[0]),
851                ],
852            ];
853        }
854    }
855
856    // Inline Smartypants
857
858    protected function inlineSmartypants($excerpt)
859    {
860        // Substitutions
861        $backtickDoublequoteOpen  = $this->options['smarty']['substitutions']['left-double-quote'] ?? '&ldquo;';
862        $backtickDoublequoteClose = $this->options['smarty']['substitutions']['right-double-quote'] ?? '&rdquo;';
863
864        $smartDoublequoteOpen  = $this->options['smarty']['substitutions']['left-double-quote'] ?? '&ldquo;';
865        $smartDoublequoteClose = $this->options['smarty']['substitutions']['right-double-quote'] ?? '&rdquo;';
866        $smartSinglequoteOpen  = $this->options['smarty']['substitutions']['left-single-quote'] ?? '&lsquo;';
867        $smartSinglequoteClose = $this->options['smarty']['substitutions']['right-single-quote'] ?? '&rsquo;';
868
869        $leftAngleQuote  = $this->options['smarty']['substitutions']['left-angle-quote'] ?? '&laquo;';
870        $rightAngleQuote = $this->options['smarty']['substitutions']['right-angle-quote'] ?? '&raquo;';
871
872        if (\preg_match('/(``)(?!\s)([^"\'`]{1,})(\'\')|(\")(?!\s)([^\"]{1,})(\")|(\')(?!\s)([^\']{1,})(\')|(<{2})(?!\s)([^<>]{1,})(>{2})|(\.{3})|(-{3})|(-{2})/i', $excerpt['text'], $matches)) {
873            $matches = \array_values(\array_filter($matches));
874
875            // Smart backticks
876            $smartBackticks = $this->options['smarty']['smart_backticks'] ?? false;
877
878            if ($smartBackticks && $matches[1] === '``') {
879                $length = \strlen(\trim($excerpt['before']));
880                if ($length > 0) {
881                    return;
882                }
883                return [
884                    'extent'  => \strlen($matches[0]),
885                    'element' => [
886                        'text' => \html_entity_decode($backtickDoublequoteOpen).$matches[2].\html_entity_decode($backtickDoublequoteClose),
887                    ],
888                ];
889            }
890
891            // Smart quotes
892            $smartQuotes = $this->options['smarty']['smart_quotes'] ?? true;
893
894            if ($smartQuotes) {
895                if ($matches[1] === "'") {
896                    $length = \strlen(\trim($excerpt['before']));
897                    if ($length > 0) {
898                        return;
899                    }
900
901                    return [
902                        'extent'  => \strlen($matches[0]),
903                        'element' => [
904                            'text' => \html_entity_decode($smartSinglequoteOpen).$matches[2].\html_entity_decode($smartSinglequoteClose),
905                        ],
906                    ];
907                }
908
909                if ($matches[1] === '"') {
910                    $length = \strlen(\trim($excerpt['before']));
911                    if ($length > 0) {
912                        return;
913                    }
914
915                    return [
916                        'extent'  => \strlen($matches[0]),
917                        'element' => [
918                            'text' => \html_entity_decode($smartDoublequoteOpen).$matches[2].\html_entity_decode($smartDoublequoteClose),
919                        ],
920                    ];
921                }
922            }
923
924            // Smart angled quotes
925            $smartAngledQuotes = $this->options['smarty']['smart_angled_quotes'] ?? true;
926
927            if ($smartAngledQuotes && $matches[1] === '<<') {
928                $length = \strlen(\trim($excerpt['before']));
929                if ($length > 0) {
930                    return;
931                }
932
933                return [
934                    'extent'  => \strlen($matches[0]),
935                    'element' => [
936                        'text' => \html_entity_decode($leftAngleQuote).$matches[2].\html_entity_decode($rightAngleQuote),
937                    ],
938                ];
939            }
940
941            // Smart dashes
942            $smartDashes = $this->options['smarty']['smart_dashes'] ?? true;
943
944            if ($smartDashes) {
945                if ($matches[1] === '---') {
946                    return [
947                        'extent'  => \strlen($matches[0]),
948                        'element' => [
949                            'rawHtml' => $this->options['smarty']['substitutions']['mdash'] ?? '&mdash;',
950                        ],
951                    ];
952                }
953
954                if ($matches[1] === '--') {
955                    return [
956                        'extent'  => \strlen($matches[0]),
957                        'element' => [
958                            'rawHtml' => $this->options['smarty']['substitutions']['ndash'] ?? '&ndash;',
959                        ],
960                    ];
961                }
962            }
963
964            // Smart ellipses
965            $smartEllipses = $this->options['smarty']['smart_ellipses'] ?? true;
966
967            if ($smartEllipses && $matches[1] === '...') {
968                return [
969                    'extent'  => \strlen($matches[0]),
970                    'element' => [
971                        'rawHtml' => $this->options['smarty']['substitutions']['ellipses'] ?? '&hellip;',
972                    ],
973                ];
974            }
975        }
976    }
977
978    // Inline Math
979
980    protected function inlineMath($excerpt)
981    {
982        $matchSingleDollar = $this->options['math']['single_dollar'] ?? false;
983        // Inline Matches
984        if ($matchSingleDollar) {
985            // Match single dollar - experimental
986            if (\preg_match('/^(?<!\\\\)((?<!\$)\$(?!\$)(.*?)(?<!\$)\$(?!\$)|(?<!\\\\\()\\\\\((.*?)(?<!\\\\\()\\\\\)(?!\\\\\)))/s', $excerpt['text'], $matches)) {
987                $mathMatch = $matches[0];
988            }
989        } elseif (\preg_match('/^(?<!\\\\\()\\\\\((.*?)(?<!\\\\\()\\\\\)(?!\\\\\))/s', $excerpt['text'], $matches)) {
990            $mathMatch = $matches[0];
991        }
992
993        if (isset($mathMatch)) {
994            return [
995                'extent'  => \strlen($mathMatch),
996                'element' => [
997                    'text' => $mathMatch,
998                ],
999            ];
1000        }
1001    }
1002
1003    protected function inlineEscapeSequence($excerpt)
1004    {
1005        $element = [
1006            'element' => [
1007                'rawHtml' => $excerpt['text'][1],
1008            ],
1009            'extent' => 2,
1010        ];
1011
1012        $state = $this->options['math'] ?? false;
1013
1014        if ($state) {
1015            if (isset($excerpt['text'][1]) && \in_array($excerpt['text'][1], $this->specialCharacters) && !\preg_match('/^(?<!\\\\)(?<!\\\\\()\\\\\((.{2,}?)(?<!\\\\\()\\\\\)(?!\\\\\))/s', $excerpt['text'])) {
1016                return $element;
1017            }
1018        } elseif (isset($excerpt['text'][1]) && \in_array($excerpt['text'][1], $this->specialCharacters)) {
1019            return $element;
1020        }
1021    }
1022
1023    /**
1024     * ------------------------------------------------------------------------
1025     *  Blocks.
1026     * ------------------------------------------------------------------------
1027     */
1028    protected function blockFootnote($line, array $_ = null)
1029    {
1030        $state = $this->options['footnotes'] ?? true;
1031        if ($state) {
1032            return $this->blockFootnoteBase($line);
1033        }
1034    }
1035
1036    protected function blockDefinitionList($line, $block)
1037    {
1038        $state = $this->options['definition_lists'] ?? true;
1039        if ($state) {
1040            return $this->blockDefinitionListBase($line, $block);
1041        }
1042    }
1043
1044    protected function blockCode($line, $block = null)
1045    {
1046        $codeBlock = $this->options['code']['blocks'] ?? true;
1047        $codeMain  = $this->options['code'] ?? true;
1048        if ($codeBlock === true && $codeMain === true) {
1049            return $this->blockCodeBase($line, $block);
1050        }
1051    }
1052
1053    protected function blockComment($line, array $_ = null)
1054    {
1055        $state = $this->options['comments'] ?? true;
1056        if ($state) {
1057            return $this->blockCommentBase($line);
1058        }
1059    }
1060
1061    protected function blockHeader($line, array $_ = null)
1062    {
1063        $state = $this->options['headings'] ?? true;
1064        if (!$state) {
1065            return;
1066        }
1067
1068        $block = $this->blockHeaderBase($line);
1069        if (!empty($block)) {
1070            // Get the text of the heading
1071            if (isset($block['element']['handler']['argument'])) {
1072                $text = $block['element']['handler']['argument'];
1073            }
1074
1075            // Get the heading level. Levels are h1, h2, ..., h6
1076            $level = $block['element']['name'];
1077
1078            $headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
1079            if (!\in_array($level, $headersAllowed)) {
1080                return;
1081            }
1082
1083            // Checks if auto generated anchors is allowed
1084            $autoAnchors = $this->options['headings']['auto_anchors'] ?? true;
1085
1086            if ($autoAnchors) {
1087                // Get the anchor of the heading to link from the ToC list
1088                $id = $block['element']['attributes']['id'] ?? $this->createAnchorID($text);
1089            } else {
1090                // Get the anchor of the heading to link from the ToC list
1091                $id = $block['element']['attributes']['id'] ?? null;
1092            }
1093
1094            // Set attributes to head tags
1095            $block['element']['attributes']['id'] = $id;
1096
1097            $tocHeaders = $this->options['toc']['headings'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
1098            // Check if level are defined as a heading
1099            if (\in_array($level, $tocHeaders)) {
1100                // Add/stores the heading element info to the ToC list
1101                $this->setContentsList([
1102                    'text'  => $text,
1103                    'id'    => $id,
1104                    'level' => $level,
1105                ]);
1106            }
1107
1108            return $block;
1109        }
1110    }
1111
1112    protected function blockList($line, array $CurrentBlock = null)
1113    {
1114        $state = $this->options['lists'] ?? true;
1115        if ($state) {
1116            return $this->blockListBase($line, $CurrentBlock);
1117        }
1118    }
1119
1120    protected function blockQuote($line, array $_ = null)
1121    {
1122        $state = $this->options['qoutes'] ?? true;
1123        if ($state) {
1124            return $this->blockQuoteBase($line);
1125        }
1126    }
1127
1128    protected function blockRule($line, array $_ = null)
1129    {
1130        $state = $this->options['thematic_breaks'] ?? true;
1131        if ($state) {
1132            return $this->blockRuleBase($line);
1133        }
1134    }
1135
1136    protected function blockSetextHeader($line, $block = null)
1137    {
1138        $state = $this->options['headings'] ?? true;
1139        if (!$state) {
1140            return;
1141        }
1142        $block = $this->blockSetextHeaderBase($line, $block);
1143        if (!empty($block)) {
1144            // Get the text of the heading
1145            if (isset($block['element']['handler']['argument'])) {
1146                $text = $block['element']['handler']['argument'];
1147            }
1148
1149            // Get the heading level. Levels are h1, h2, ..., h6
1150            $level = $block['element']['name'];
1151
1152            $headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
1153            if (!\in_array($level, $headersAllowed)) {
1154                return;
1155            }
1156
1157            // Checks if auto generated anchors is allowed
1158            $autoAnchors = $this->options['headings']['auto_anchors'] ?? true;
1159
1160            if ($autoAnchors) {
1161                // Get the anchor of the heading to link from the ToC list
1162                $id = $block['element']['attributes']['id'] ?? $this->createAnchorID($text);
1163            } else {
1164                // Get the anchor of the heading to link from the ToC list
1165                $id = $block['element']['attributes']['id'] ?? null;
1166            }
1167
1168            // Set attributes to head tags
1169            $block['element']['attributes']['id'] = $id;
1170
1171            $headersAllowed = $this->options['headings']['allowed'] ?? ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
1172
1173            // Check if level are defined as a heading
1174            if (\in_array($level, $headersAllowed)) {
1175                // Add/stores the heading element info to the ToC list
1176                $this->setContentsList([
1177                    'text'  => $text,
1178                    'id'    => $id,
1179                    'level' => $level,
1180                ]);
1181            }
1182
1183            return $block;
1184        }
1185    }
1186
1187    protected function blockMarkup($line, array $_ = null)
1188    {
1189        $state = $this->options['markup'] ?? true;
1190        if ($state) {
1191            return $this->blockMarkupBase($line);
1192        }
1193    }
1194
1195    protected function blockReference($line, array $_ = null)
1196    {
1197        $state = $this->options['references'] ?? true;
1198        if ($state) {
1199            return $this->blockReferenceBase($line);
1200        }
1201    }
1202
1203    protected function blockTable($line, $block = null)
1204    {
1205        $state = $this->options['tables'] ?? true;
1206        if ($state) {
1207            return $this->blockTableBase($line, $block);
1208        }
1209    }
1210
1211    protected function blockAbbreviation($line, array $_ = null)
1212    {
1213        $allowCustomAbbr = $this->options['abbreviations']['allow_custom_abbr'] ?? true;
1214
1215        $state = $this->options['abbreviations'] ?? true;
1216        if ($state) {
1217            if (isset($this->options['abbreviations']['predefine'])) {
1218                foreach ($this->options['abbreviations']['predefine'] as $abbreviations => $description) {
1219                    $this->DefinitionData['Abbreviation'][$abbreviations] = $description;
1220                }
1221            }
1222
1223            if ($allowCustomAbbr == true) {
1224                return $this->blockAbbreviationBase($line);
1225            }
1226
1227            return;
1228        }
1229    }
1230
1231    // Block Math
1232
1233    protected function blockMath($line, array $_ = null)
1234    {
1235        $block = [
1236            'element' => [
1237                'text' => '',
1238            ],
1239        ];
1240
1241        if (\preg_match('/^(?<!\\\\)(\\\\\[)(?!.)$/', $line['text'])) {
1242            $block['end'] = '\]';
1243
1244            return $block;
1245        }
1246        if (\preg_match('/^(?<!\\\\)(\$\$)(?!.)$/', $line['text'])) {
1247            $block['end'] = '$$';
1248
1249            return $block;
1250        }
1251    }
1252
1253    // ~
1254
1255    protected function blockMathContinue($line, $block)
1256    {
1257        if (isset($block['complete'])) {
1258            return;
1259        }
1260
1261        if (isset($block['interrupted'])) {
1262            $block['element']['text'] .= \str_repeat(
1263                "\n",
1264                $block['interrupted']
1265            );
1266
1267            unset($block['interrupted']);
1268        }
1269
1270        if (\preg_match('/^(?<!\\\\)(\\\\\])$/', $line['text']) && $block['end'] === '\]') {
1271            $block['complete']        = true;
1272            $block['math']            = true;
1273            $block['element']['text'] =
1274             '\\['.$block['element']['text'].'\\]';
1275
1276            return $block;
1277        }
1278        if (\preg_match('/^(?<!\\\\)(\$\$)$/', $line['text']) && $block['end'] === '$$') {
1279            $block['complete']        = true;
1280            $block['math']            = true;
1281            $block['element']['text'] = '$$'.$block['element']['text'].'$$';
1282
1283            return $block;
1284        }
1285
1286        $block['element']['text'] .= "\n".$line['body'];
1287
1288        // ~
1289
1290        return $block;
1291    }
1292
1293    // ~
1294
1295    protected function blockMathComplete($block)
1296    {
1297        return $block;
1298    }
1299
1300    // Block Fenced Code
1301
1302    protected function blockFencedCode($line, array $_ = null)
1303    {
1304        $codeBlock = $this->options['code']['blocks'] ?? true;
1305        $codeMain  = $this->options['code'] ?? true;
1306        if ($codeBlock === false || $codeMain === false) {
1307            return;
1308        }
1309        $block = $this->blockFencedCodeBase($line);
1310
1311        $marker       = $line['text'][0];
1312        $openerLength = \strspn($line['text'], $marker);
1313        $language     = \trim(
1314            \preg_replace('/^`{3}([^\s]+)(.+)?/s', '$1', $line['text'])
1315        );
1316
1317        $state = $this->options['diagrams'] ?? true;
1318        if ($state) {
1319            // Mermaid.js https://mermaidjs.github.io
1320            if (\strtolower($language) == 'mermaid') {
1321                $element = [
1322                    'text' => '',
1323                ];
1324
1325                return [
1326                    'char'         => $marker,
1327                    'openerLength' => $openerLength,
1328                    'element'      => [
1329                        'element'    => $element,
1330                        'name'       => 'div',
1331                        'attributes' => [
1332                            'class' => 'mermaid',
1333                        ],
1334                    ],
1335                ];
1336            }
1337
1338            // Chart.js https://www.chartjs.org/
1339            if (\strtolower($language) == 'chart') {
1340                $element = [
1341                    'text' => '',
1342                ];
1343
1344                return [
1345                    'char'         => $marker,
1346                    'openerLength' => $openerLength,
1347                    'element'      => [
1348                        'element'    => $element,
1349                        'name'       => 'canvas',
1350                        'attributes' => [
1351                            'class' => 'chartjs',
1352                        ],
1353                    ],
1354                ];
1355            }
1356        }
1357
1358        return $block;
1359    }
1360
1361    // Parsedown Tablespan from @KENNYSOFT
1362    protected function blockTableComplete(array $block)
1363    {
1364        $state = $this->options['tables']['tablespan'] ?? false;
1365        if ($state === false) {
1366            return $block;
1367        }
1368
1369        if (!isset($block)) {
1370            return null;
1371        }
1372
1373        $HeaderElements = &$block['element']['elements'][0]['elements'][0]['elements'];
1374
1375        for ($index = \count($HeaderElements) - 1; $index >= 0; --$index) {
1376            $colspan       = 1;
1377            $HeaderElement = &$HeaderElements[$index];
1378
1379            while ($index && $HeaderElements[$index - 1]['handler']['argument'] === '>') {
1380                ++$colspan;
1381                $PreviousHeaderElement           = &$HeaderElements[--$index];
1382                $PreviousHeaderElement['merged'] = true;
1383                if (isset($PreviousHeaderElement['attributes'])) {
1384                    $HeaderElement['attributes'] = $PreviousHeaderElement['attributes'];
1385                }
1386            }
1387
1388            if ($colspan > 1) {
1389                if (!isset($HeaderElement['attributes'])) {
1390                    $HeaderElement['attributes'] = [];
1391                }
1392                $HeaderElement['attributes']['colspan'] = $colspan;
1393            }
1394        }
1395
1396        for ($index = \count($HeaderElements) - 1; $index >= 0; --$index) {
1397            if (isset($HeaderElements[$index]['merged'])) {
1398                \array_splice($HeaderElements, $index, 1);
1399            }
1400        }
1401
1402        $rows = &$block['element']['elements'][1]['elements'];
1403
1404        foreach ($rows as $rowNo => &$row) {
1405            $elements = &$row['elements'];
1406
1407            for ($index = \count($elements) - 1; $index >= 0; --$index) {
1408                $colspan = 1;
1409                $element = &$elements[$index];
1410
1411                while ($index && $elements[$index - 1]['handler']['argument'] === '>') {
1412                    ++$colspan;
1413                    $PreviousElement           = &$elements[--$index];
1414                    $PreviousElement['merged'] = true;
1415                    if (isset($PreviousElement['attributes'])) {
1416                        $element['attributes'] = $PreviousElement['attributes'];
1417                    }
1418                }
1419
1420                if ($colspan > 1) {
1421                    if (!isset($element['attributes'])) {
1422                        $element['attributes'] = [];
1423                    }
1424                    $element['attributes']['colspan'] = $colspan;
1425                }
1426            }
1427        }
1428
1429        foreach ($rows as $rowNo => &$row) {
1430            $elements = &$row['elements'];
1431
1432            foreach ($elements as $index => &$element) {
1433                $rowspan = 1;
1434
1435                if (isset($element['merged'])) {
1436                    continue;
1437                }
1438
1439                while ($rowNo + $rowspan < \count($rows) && $index < \count($rows[$rowNo + $rowspan]['elements']) && $rows[$rowNo + $rowspan]['elements'][$index]['handler']['argument'] === '^' && (@$element['attributes']['colspan'] ?: null) === (@$rows[$rowNo + $rowspan]['elements'][$index]['attributes']['colspan'] ?: null)) {
1440                    $rows[$rowNo + $rowspan]['elements'][$index]['merged'] = true;
1441                    ++$rowspan;
1442                }
1443
1444                if ($rowspan > 1) {
1445                    if (!isset($element['attributes'])) {
1446                        $element['attributes'] = [];
1447                    }
1448                    $element['attributes']['rowspan'] = $rowspan;
1449                }
1450            }
1451        }
1452
1453        foreach ($rows as $rowNo => &$row) {
1454            $elements = &$row['elements'];
1455
1456            for ($index = \count($elements) - 1; $index >= 0; --$index) {
1457                if (isset($elements[$index]['merged'])) {
1458                    \array_splice($elements, $index, 1);
1459                }
1460            }
1461        }
1462
1463        return $block;
1464    }
1465
1466    /*
1467    * Checkbox
1468    * -------------------------------------------------------------------------
1469    */
1470    protected function blockCheckbox($line)
1471    {
1472        $text      = \trim($line['text']);
1473        $beginLine = \substr($text, 0, 4);
1474        if ($beginLine === '[ ] ') {
1475            return [
1476                'handler' => 'checkboxUnchecked',
1477                'text'    => \substr(\trim($text), 4),
1478            ];
1479        }
1480
1481        if ($beginLine === '[x] ') {
1482            return [
1483                'handler' => 'checkboxChecked',
1484                'text'    => \substr(\trim($text), 4),
1485            ];
1486        }
1487    }
1488
1489    protected function blockCheckboxContinue(array $block) : void
1490    {
1491        // This is here because Parsedown require it.
1492    }
1493
1494    protected function blockCheckboxComplete(array $block)
1495    {
1496        $block['element'] = [
1497            'rawHtml'                => $this->{$block['handler']}($block['text']),
1498            'allowRawHtmlInSafeMode' => true,
1499        ];
1500
1501        return $block;
1502    }
1503
1504    protected function checkboxUnchecked($text) : string
1505    {
1506        if ($this->markupEscaped || $this->safeMode) {
1507            $text = self::escape($text);
1508        }
1509
1510        return '<input type="checkbox" disabled /> '.$this->format($text);
1511    }
1512
1513    protected function checkboxChecked($text) : string
1514    {
1515        if ($this->markupEscaped || $this->safeMode) {
1516            $text = self::escape($text);
1517        }
1518
1519        return '<input type="checkbox" checked disabled /> '.$this->format($text);
1520    }
1521
1522    /**
1523     * ------------------------------------------------------------------------
1524     *  Helpers.
1525     * ------------------------------------------------------------------------.
1526     */
1527
1528    /**
1529     * Formats the checkbox label without double escaping.
1530     */
1531    protected function format($text)
1532    {
1533        // backup settings
1534        $markupEscaped = $this->markupEscaped;
1535        $safeMode      = $this->safeMode;
1536
1537        // disable rules to prevent double escaping.
1538        $this->setMarkupEscaped(false);
1539        $this->setSafeMode(false);
1540
1541        // format line
1542        $text = $this->line($text);
1543
1544        // reset old values
1545        $this->setMarkupEscaped($markupEscaped);
1546        $this->setSafeMode($safeMode);
1547
1548        return $text;
1549    }
1550
1551    protected function parseAttributeData($attributeString)
1552    {
1553        $state = $this->options['special_attributes'] ?? true;
1554        if ($state) {
1555            return $this->parseAttributeDataBase($attributeString);
1556        }
1557
1558        return [];
1559    }
1560
1561    /**
1562     * Encodes the ToC tag to a hashed tag and replace.
1563     *
1564     * This is used to avoid parsing user defined ToC tag which includes "_" in
1565     * their tag such as "[[_toc_]]". Unless it will be parsed as:
1566     *   "<p>[[<em>TOC</em>]]</p>"
1567     */
1568    protected function encodeTagToHash($text)
1569    {
1570        $salt      = $this->getSalt();
1571        $tagOrigin = $this->getTagToC();
1572
1573        if (\strpos($text, $tagOrigin) === false) {
1574            return $text;
1575        }
1576
1577        $tagHashed = \hash('sha256', $salt.$tagOrigin);
1578
1579        return \str_replace($tagOrigin, $tagHashed, $text);
1580    }
1581
1582    /**
1583     * Decodes the hashed ToC tag to an original tag and replaces.
1584     *
1585     * This is used to avoid parsing user defined ToC tag which includes "_" in
1586     * their tag such as "[[_toc_]]". Unless it will be parsed as:
1587     *   "<p>[[<em>TOC</em>]]</p>"
1588     */
1589    protected function decodeTagFromHash($text)
1590    {
1591        $salt      = $this->getSalt();
1592        $tagOrigin = $this->getTagToC();
1593        $tagHashed = \hash('sha256', $salt.$tagOrigin);
1594
1595        if (\strpos($text, $tagHashed) === false) {
1596            return $text;
1597        }
1598
1599        return \str_replace($tagHashed, $tagOrigin, $text);
1600    }
1601
1602    /**
1603     * Unique string to use as a salt value.
1604     */
1605    protected function getSalt()
1606    {
1607        static $salt;
1608        if (isset($salt)) {
1609            return $salt;
1610        }
1611
1612        $salt = \hash('md5', (string) \time());
1613
1614        return $salt;
1615    }
1616
1617    /**
1618     * Gets the markdown tag for ToC.
1619     */
1620    protected function getTagToC()
1621    {
1622        return $this->options['toc']['set_toc_tag'] ?? '[toc]';
1623    }
1624
1625    /**
1626     * Gets the ID attribute of the ToC for HTML tags.
1627     */
1628    protected function getIdAttributeToC()
1629    {
1630        if (!empty($this->idToc)) {
1631            return $this->idToc;
1632        }
1633
1634        return self::ID_ATTRIBUTE_DEFAULT;
1635    }
1636
1637    /**
1638     * Generates an anchor text that are link-able even if the heading is not in
1639     * ASCII.
1640     */
1641    protected function createAnchorID($str) : string
1642    {
1643        $optionUrlEncode = $this->options['toc']['urlencode'] ?? false;
1644        if ($optionUrlEncode) {
1645            // Check AnchorID is unique
1646            $str = $this->incrementAnchorId($str);
1647
1648            return \urlencode($str);
1649        }
1650
1651        $charMap = [
1652            // Latin
1653            'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'AA', 'Æ' => 'AE', 'Ç' => 'C',
1654            'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
1655            'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ő' => 'O',
1656            'Ø' => 'OE', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ű' => 'U', 'Ý' => 'Y', 'Þ' => 'TH',
1657            'ß' => 'ss',
1658            'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'aa', 'æ' => 'ae', 'ç' => 'c',
1659            'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
1660            'ð' => 'd', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ő' => 'o',
1661            'ø' => 'oe', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ű' => 'u', 'ý' => 'y', 'þ' => 'th',
1662            'ÿ' => 'y',
1663
1664            // Latin symbols
1665            '©' => '(c)', '®' => '(r)', '™' => '(tm)',
1666
1667            // Greek
1668            'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'H', 'Θ' => '8',
1669            'Ι' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => '3', 'Ο' => 'O', 'Π' => 'P',
1670            'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'PS', 'Ω' => 'W',
1671            'Ά' => 'A', 'Έ' => 'E', 'Ί' => 'I', 'Ό' => 'O', 'Ύ' => 'Y', 'Ή' => 'H', 'Ώ' => 'W', 'Ϊ' => 'I',
1672            'Ϋ' => 'Y',
1673            'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 'ζ' => 'z', 'η' => 'h', 'θ' => '8',
1674            'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => '3', 'ο' => 'o', 'π' => 'p',
1675            'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'ps', 'ω' => 'w',
1676            'ά' => 'a', 'έ' => 'e', 'ί' => 'i', 'ό' => 'o', 'ύ' => 'y', 'ή' => 'h', 'ώ' => 'w', 'ς' => 's',
1677            'ϊ' => 'i', 'ΰ' => 'y', 'ϋ' => 'y', 'ΐ' => 'i',
1678
1679            // Turkish
1680            'Ş' => 'S', 'İ' => 'I', 'Ğ' => 'G',
1681            'ş' => 's', 'ı' => 'i', 'ğ' => 'g',
1682
1683            // Russian
1684            'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh',
1685            'З' => 'Z', 'И' => 'I', 'Й' => 'J', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O',
1686            'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'C',
1687            'Ч' => 'Ch', 'Ш' => 'Sh', 'Щ' => 'Sh', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '', 'Э' => 'E', 'Ю' => 'Yu',
1688            'Я' => 'Ya',
1689            'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'yo', 'ж' => 'zh',
1690            'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o',
1691            'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c',
1692            'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sh', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu',
1693            'я' => 'ya',
1694
1695            // Ukrainian
1696            'Є' => 'Ye', 'І' => 'I', 'Ї' => 'Yi', 'Ґ' => 'G',
1697            'є' => 'ye', 'і' => 'i', 'ї' => 'yi', 'ґ' => 'g',
1698
1699            // Czech
1700            'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U',
1701            'Ž' => 'Z',
1702            'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u',
1703            'ž' => 'z',
1704
1705            // Polish
1706            'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'e', 'Ł' => 'L', 'Ń' => 'N', 'Ś' => 'S', 'Ź' => 'Z',
1707            'Ż' => 'Z',
1708            'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ś' => 's', 'ź' => 'z',
1709            'ż' => 'z',
1710
1711            // Latvian
1712            'Ā' => 'A', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'i', 'Ķ' => 'k', 'Ļ' => 'L', 'Ņ' => 'N', 'Ū' => 'u',
1713            'ā' => 'a', 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n', 'ū' => 'u',
1714        ];
1715
1716        // Transliterate characters to ASCII
1717        $optionTransliterate = $this->options['toc']['transliterate'] ?? false;
1718        if ($optionTransliterate) {
1719            $str = \str_replace(\array_keys($charMap), $charMap, $str);
1720        }
1721
1722        // Replace non-alphanumeric characters with our delimiter
1723        $optionDelimiter = $this->options['toc']['delimiter'] ?? '-';
1724        $str             = \preg_replace('/[^\p{L}\p{Nd}]+/u', $optionDelimiter, $str);
1725
1726        // Remove duplicate delimiters
1727        $str = \preg_replace('/('.\preg_quote($optionDelimiter, '/').'){2,}/', '$1', $str);
1728
1729        // Truncate slug to max. characters
1730        $optionLimit = $this->options['toc']['limit'] ?? \mb_strlen($str, 'UTF-8');
1731        $str         = \mb_substr($str, 0, $optionLimit, 'UTF-8');
1732
1733        // Remove delimiter from ends
1734        $str = \trim($str, $optionDelimiter);
1735
1736        $urlLowercase = $this->options['toc']['lowercase'] ?? true;
1737        $str          = $urlLowercase ? \mb_strtolower($str, 'UTF-8') : $str;
1738
1739        return $this->incrementAnchorId($str);
1740    }
1741
1742    /**
1743     * Get only the text from a markdown string.
1744     * It parses to HTML once then trims the tags to get the text.
1745     */
1746    protected function fetchText($text) : string
1747    {
1748        return \trim(\strip_tags($this->line($text)));
1749    }
1750
1751    /**
1752     * Set/stores the heading block to ToC list in a string and array format.
1753     */
1754    protected function setContentsList(array $Content) : void
1755    {
1756        // Stores as an array
1757        $this->setContentsListAsArray($Content);
1758        // Stores as string in markdown list format.
1759        $this->setContentsListAsString($Content);
1760    }
1761
1762    /**
1763     * Sets/stores the heading block info as an array.
1764     */
1765    protected function setContentsListAsArray(array $Content) : void
1766    {
1767        $this->contentsListArray[] = $Content;
1768    }
1769
1770    /**
1771     * Sets/stores the heading block info as a list in markdown format.
1772     */
1773    protected function setContentsListAsString(array $Content) : void
1774    {
1775        $text  = $this->fetchText($Content['text']);
1776        $id    = $Content['id'];
1777        $level = (int) \trim($Content['level'], 'h');
1778        $link  = "[{$text}](#{$id})";
1779
1780        if ($this->firstHeadLevel === 0) {
1781            $this->firstHeadLevel = $level;
1782        }
1783        $cutIndent = $this->firstHeadLevel - 1;
1784        $level     = $cutIndent > $level ? 1 : $level - $cutIndent;
1785
1786        $indent = \str_repeat('  ', $level);
1787
1788        // Stores in markdown list format as below:
1789        // - [Header1](#Header1)
1790        //   - [Header2-1](#Header2-1)
1791        //     - [Header3](#Header3)
1792        //   - [Header2-2](#Header2-2)
1793        // ...
1794        $this->contentsListString .= "{$indent}{$link}".\PHP_EOL;
1795    }
1796
1797    /**
1798     * Collect and count anchors in use to prevent duplicated ids. Return string
1799     * with incremental, numeric suffix. Also init optional blacklist of ids.
1800     */
1801    protected function incrementAnchorId($str)
1802    {
1803        // add blacklist to list of used anchors
1804        if (!$this->isBlacklistInitialized) {
1805            $this->initBlacklist();
1806        }
1807
1808        $this->anchorDuplicates[$str] = isset($this->anchorDuplicates[$str]) ? ++$this->anchorDuplicates[$str] : 0;
1809
1810        $newStr = $str;
1811
1812        if ($count = $this->anchorDuplicates[$str]) {
1813            $newStr .= "-{$count}";
1814
1815            // increment until conversion doesn't produce new duplicates anymore
1816            if (isset($this->anchorDuplicates[$newStr])) {
1817                $newStr = $this->incrementAnchorId($str);
1818            } else {
1819                $this->anchorDuplicates[$newStr] = 0;
1820            }
1821        }
1822
1823        return $newStr;
1824    }
1825
1826    /**
1827     * Add blacklisted ids to anchor list.
1828     */
1829    protected function initBlacklist() : void
1830    {
1831        if ($this->isBlacklistInitialized) {
1832            return;
1833        }
1834
1835        if (!empty($this->options['headings']['blacklist']) && \is_array($this->options['headings']['blacklist'])) {
1836            foreach ($this->options['headings']['blacklist'] as $v) {
1837                if (\is_string($v)) {
1838                    $this->anchorDuplicates[$v] = 0;
1839                }
1840            }
1841        }
1842
1843        $this->isBlacklistInitialized = true;
1844    }
1845
1846    protected function lineElements($text, $nonNestables = [])
1847    {
1848        $Elements = [];
1849
1850        $nonNestables = (
1851            empty($nonNestables)
1852            ? []
1853            : \array_combine($nonNestables, $nonNestables)
1854        );
1855
1856        // $excerpt is based on the first occurrence of a marker
1857
1858        while ($excerpt = \strpbrk($text, $this->inlineMarkerList)) {
1859            $marker = $excerpt[0];
1860
1861            $markerPosition = \strlen($text) - \strlen($excerpt);
1862
1863            // Get the first char before the marker
1864            $beforeMarkerPosition = $markerPosition - 1;
1865            $charBeforeMarker     = $beforeMarkerPosition >= 0 ? $text[$markerPosition - 1] : '';
1866
1867            $Excerpt = ['text' => $excerpt, 'context' => $text, 'before' => $charBeforeMarker];
1868
1869            foreach ($this->InlineTypes[$marker] as $inlineType) {
1870                // check to see if the current inline type is nestable in the current context
1871
1872                if (isset($nonNestables[$inlineType])) {
1873                    continue;
1874                }
1875
1876                $Inline = $this->{"inline{$inlineType}"}($Excerpt);
1877
1878                if (!isset($Inline)) {
1879                    continue;
1880                }
1881
1882                // makes sure that the inline belongs to "our" marker
1883
1884                if (isset($Inline['position']) && $Inline['position'] > $markerPosition) {
1885                    continue;
1886                }
1887
1888                // sets a default inline position
1889
1890                if (!isset($Inline['position'])) {
1891                    $Inline['position'] = $markerPosition;
1892                }
1893
1894                // cause the new element to 'inherit' our non nestables
1895
1896                $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
1897                    ? \array_merge($Inline['element']['nonNestables'], $nonNestables)
1898                    : $nonNestables
1899                ;
1900
1901                // the text that comes before the inline
1902                $unmarkedText = \substr($text, 0, $Inline['position']);
1903
1904                // compile the unmarked text
1905                $InlineText = $this->inlineText($unmarkedText);
1906                $Elements[] = $InlineText['element'];
1907
1908                // compile the inline
1909                $Elements[] = $this->extractElement($Inline);
1910
1911                // remove the examined text
1912                $text = \substr($text, $Inline['position'] + $Inline['extent']);
1913
1914                continue 2;
1915            }
1916
1917            // the marker does not belong to an inline
1918
1919            $unmarkedText = \substr($text, 0, $markerPosition + 1);
1920
1921            $InlineText = $this->inlineText($unmarkedText);
1922            $Elements[] = $InlineText['element'];
1923
1924            $text = \substr($text, $markerPosition + 1);
1925        }
1926
1927        $InlineText = $this->inlineText($text);
1928        $Elements[] = $InlineText['element'];
1929
1930        foreach ($Elements as &$Element) {
1931            if (!isset($Element['autobreak'])) {
1932                $Element['autobreak'] = false;
1933            }
1934        }
1935
1936        return $Elements;
1937    }
1938
1939    private function pregReplaceAssoc(array $replace, $subject)
1940    {
1941        return \preg_replace(\array_keys($replace), \array_values($replace), $subject);
1942    }
1943
1944    #
1945    # Blocks
1946    #
1947
1948    #
1949    # Abbreviation
1950
1951    protected function blockAbbreviationBase($Line)
1952    {
1953        if (\preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
1954        {
1955            $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
1956
1957            return [
1958                'hidden' => true,
1959            ];
1960        }
1961    }
1962
1963    #
1964    # Footnote
1965
1966    protected function blockFootnoteBase($Line)
1967    {
1968        if (\preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
1969        {
1970            return [
1971                'label'  => $matches[1],
1972                'text'   => $matches[2],
1973                'hidden' => true,
1974            ];
1975        }
1976    }
1977
1978    protected function blockFootnoteContinue($Line, $Block)
1979    {
1980        if ($Line['text'][0] === '[' && \preg_match('/^\[\^(.+?)\]:/', $Line['text']))
1981        {
1982            return;
1983        }
1984
1985        if (isset($Block['interrupted']))
1986        {
1987            if ($Line['indent'] >= 4)
1988            {
1989                $Block['text'] .= "\n\n" . $Line['text'];
1990
1991                return $Block;
1992            }
1993        }
1994        else
1995        {
1996            $Block['text'] .= "\n" . $Line['text'];
1997
1998            return $Block;
1999        }
2000    }
2001
2002    protected function blockFootnoteComplete($Block)
2003    {
2004        $this->DefinitionData['Footnote'][$Block['label']] = [
2005            'text'   => $Block['text'],
2006            'count'  => null,
2007            'number' => null,
2008        ];
2009
2010        return $Block;
2011    }
2012
2013    #
2014    # Definition List
2015
2016    protected function blockDefinitionListBase($Line, $Block)
2017    {
2018        if (!isset($Block) || $Block['type'] !== 'Paragraph')
2019        {
2020            return;
2021        }
2022
2023        $Element = [
2024            'name'     => 'dl',
2025            'elements' => [],
2026        ];
2027
2028        $terms = \explode("\n", $Block['element']['handler']['argument']);
2029
2030        foreach ($terms as $term)
2031        {
2032            $Element['elements'] []= [
2033                'name'    => 'dt',
2034                'handler' => [
2035                    'function'    => 'lineElements',
2036                    'argument'    => $term,
2037                    'destination' => 'elements',
2038                ],
2039            ];
2040        }
2041
2042        $Block['element'] = $Element;
2043
2044        return $this->addDdElement($Line, $Block);
2045    }
2046
2047    protected function blockDefinitionListContinue($Line, array $Block)
2048    {
2049        if ($Line['text'][0] === ':')
2050        {
2051            return $this->addDdElement($Line, $Block);
2052        }
2053        else
2054        {
2055            if (isset($Block['interrupted']) && $Line['indent'] === 0)
2056            {
2057                return;
2058            }
2059
2060            if (isset($Block['interrupted']))
2061            {
2062                $Block['dd']['handler']['function'] = 'textElements';
2063                $Block['dd']['handler']['argument'] .= "\n\n";
2064
2065                $Block['dd']['handler']['destination'] = 'elements';
2066
2067                unset($Block['interrupted']);
2068            }
2069
2070            $text = \substr($Line['body'], \min($Line['indent'], 4));
2071
2072            $Block['dd']['handler']['argument'] .= "\n" . $text;
2073
2074            return $Block;
2075        }
2076    }
2077
2078    #
2079    # Header
2080
2081    protected function blockHeaderBase($Line)
2082    {
2083        $Block = $this->blockHeaderParent($Line);
2084
2085        if ($Block !== null && \preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, \PREG_OFFSET_CAPTURE))
2086        {
2087            $attributeString = $matches[1][0];
2088
2089            $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
2090
2091            $Block['element']['handler']['argument'] = \substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
2092        }
2093
2094        return $Block;
2095    }
2096
2097    #
2098    # Markup
2099
2100    protected function blockMarkupBase($Line)
2101    {
2102        if ($this->markupEscaped || $this->safeMode)
2103        {
2104            return;
2105        }
2106
2107        if (\preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
2108        {
2109            $element = \strtolower($matches[1]);
2110
2111            if (\in_array($element, $this->textLevelElements))
2112            {
2113                return;
2114            }
2115
2116            $Block = [
2117                'name'    => $matches[1],
2118                'depth'   => 0,
2119                'element' => [
2120                    'rawHtml'   => $Line['text'],
2121                    'autobreak' => true,
2122                ],
2123            ];
2124
2125            $length    = \strlen($matches[0]);
2126            $remainder = \substr($Line['text'], $length);
2127
2128            if (\trim($remainder) === '')
2129            {
2130                if (isset($matches[2]) || \in_array($matches[1], $this->voidElements))
2131                {
2132                    $Block['closed'] = true;
2133                    $Block['void']   = true;
2134                }
2135            }
2136            else
2137            {
2138                if (isset($matches[2]) || \in_array($matches[1], $this->voidElements))
2139                {
2140                    return;
2141                }
2142                if (\preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
2143                {
2144                    $Block['closed'] = true;
2145                }
2146            }
2147
2148            return $Block;
2149        }
2150    }
2151
2152    protected function blockMarkupContinue($Line, array $Block)
2153    {
2154        if (isset($Block['closed']))
2155        {
2156            return;
2157        }
2158
2159        if (\preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
2160        {
2161            ++$Block['depth'];
2162        }
2163
2164        if (\preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
2165        {
2166            if ($Block['depth'] > 0)
2167            {
2168                --$Block['depth'];
2169            }
2170            else
2171            {
2172                $Block['closed'] = true;
2173            }
2174        }
2175
2176        if (isset($Block['interrupted']))
2177        {
2178            $Block['element']['rawHtml'] .= "\n";
2179            unset($Block['interrupted']);
2180        }
2181
2182        $Block['element']['rawHtml'] .= "\n".$Line['body'];
2183
2184        return $Block;
2185    }
2186
2187    protected function blockMarkupComplete($Block)
2188    {
2189        if (!isset($Block['void']))
2190        {
2191            $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']);
2192        }
2193
2194        return $Block;
2195    }
2196
2197    #
2198    # Setext
2199
2200    protected function blockSetextHeaderBase($Line, array $Block = null)
2201    {
2202        $Block = $this->blockSetextHeaderParent($Line, $Block);
2203
2204        if ($Block !== null && \preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, \PREG_OFFSET_CAPTURE))
2205        {
2206            $attributeString = $matches[1][0];
2207
2208            $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
2209
2210            $Block['element']['handler']['argument'] = \substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
2211        }
2212
2213        return $Block;
2214    }
2215
2216    #
2217    # Inline Elements
2218    #
2219
2220    #
2221    # Footnote Marker
2222
2223    protected function inlineFootnoteMarker($Excerpt)
2224    {
2225        if (\preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches))
2226        {
2227            $name = $matches[1];
2228
2229            if (!isset($this->DefinitionData['Footnote'][$name]))
2230            {
2231                return;
2232            }
2233
2234            ++$this->DefinitionData['Footnote'][$name]['count'];
2235
2236            if (!isset($this->DefinitionData['Footnote'][$name]['number']))
2237            {
2238                $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » &
2239            }
2240
2241            $Element = [
2242                'name'       => 'sup',
2243                'attributes' => ['id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name],
2244                'element'    => [
2245                    'name'       => 'a',
2246                    'attributes' => ['href' => '#fn:'.$name, 'class' => 'footnote-ref'],
2247                    'text'       => $this->DefinitionData['Footnote'][$name]['number'],
2248                ],
2249            ];
2250
2251            return [
2252                'extent'  => \strlen($matches[0]),
2253                'element' => $Element,
2254            ];
2255        }
2256    }
2257
2258    private $footnoteCount = 0;
2259
2260    #
2261    # ~
2262    #
2263
2264    private $currentAbreviation;
2265
2266    private $currentMeaning;
2267
2268    protected function insertAbreviation(array $Element)
2269    {
2270        if (isset($Element['text']))
2271        {
2272            $Element['elements'] = self::pregReplaceElements(
2273                '/\b'.\preg_quote($this->currentAbreviation, '/').'\b/',
2274                [
2275                    [
2276                        'name'       => 'abbr',
2277                        'attributes' => [
2278                            'title' => $this->currentMeaning,
2279                        ],
2280                        'text' => $this->currentAbreviation,
2281                    ],
2282                ],
2283                $Element['text']
2284            );
2285
2286            unset($Element['text']);
2287        }
2288
2289        return $Element;
2290    }
2291
2292    protected function inlineText($text)
2293    {
2294        $Inline = $this->inlineTextParent($text);
2295
2296        if (isset($this->DefinitionData['Abbreviation']))
2297        {
2298            foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
2299            {
2300                $this->currentAbreviation = $abbreviation;
2301                $this->currentMeaning     = $meaning;
2302
2303                $Inline['element'] = $this->elementApplyRecursiveDepthFirst(
2304                    [$this, 'insertAbreviation'],
2305                    $Inline['element']
2306                );
2307            }
2308        }
2309
2310        return $Inline;
2311    }
2312
2313    #
2314    # Util Methods
2315    #
2316
2317    protected function addDdElement(array $Line, array $Block)
2318    {
2319        $text = \substr($Line['text'], 1);
2320        $text = \trim($text);
2321
2322        unset($Block['dd']);
2323
2324        $Block['dd'] = [
2325            'name'    => 'dd',
2326            'handler' => [
2327                'function'    => 'lineElements',
2328                'argument'    => $text,
2329                'destination' => 'elements',
2330            ],
2331        ];
2332
2333        if (isset($Block['interrupted']))
2334        {
2335            $Block['dd']['handler']['function'] = 'textElements';
2336
2337            unset($Block['interrupted']);
2338        }
2339
2340        $Block['element']['elements'] []= & $Block['dd'];
2341
2342        return $Block;
2343    }
2344
2345    protected function buildFootnoteElement()
2346    {
2347        $Element = [
2348            'name'       => 'div',
2349            'attributes' => ['class' => 'footnotes'],
2350            'elements'   => [
2351                ['name' => 'hr'],
2352                [
2353                    'name'     => 'ol',
2354                    'elements' => [],
2355                ],
2356            ],
2357        ];
2358
2359        \uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
2360
2361        foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
2362        {
2363            if (!isset($DefinitionData['number']))
2364            {
2365                continue;
2366            }
2367
2368            $text = $DefinitionData['text'];
2369
2370            $textElements = $this->textElements($text);
2371
2372            $numbers = \range(1, $DefinitionData['count']);
2373
2374            $backLinkElements = [];
2375
2376            foreach ($numbers as $number)
2377            {
2378                $backLinkElements[] = ['text' => ' '];
2379                $backLinkElements[] = [
2380                    'name'       => 'a',
2381                    'attributes' => [
2382                        'href'  => "#fnref{$number}:{$definitionId}",
2383                        'rev'   => 'footnote',
2384                        'class' => 'footnote-backref',
2385                    ],
2386                    'rawHtml'                => '&#8617;',
2387                    'allowRawHtmlInSafeMode' => true,
2388                    'autobreak'              => false,
2389                ];
2390            }
2391
2392            unset($backLinkElements[0]);
2393
2394            $n = \count($textElements) - 1;
2395
2396            if ($textElements[$n]['name'] === 'p')
2397            {
2398                $backLinkElements = \array_merge(
2399                    [
2400                        [
2401                            'rawHtml'                => '&#160;',
2402                            'allowRawHtmlInSafeMode' => true,
2403                        ],
2404                    ],
2405                    $backLinkElements
2406                );
2407
2408                unset($textElements[$n]['name']);
2409
2410                $textElements[$n] = [
2411                    'name'     => 'p',
2412                    'elements' => \array_merge(
2413                        [$textElements[$n]],
2414                        $backLinkElements
2415                    ),
2416                ];
2417            }
2418            else
2419            {
2420                $textElements[] = [
2421                    'name'     => 'p',
2422                    'elements' => $backLinkElements,
2423                ];
2424            }
2425
2426            $Element['elements'][1]['elements'] []= [
2427                'name'       => 'li',
2428                'attributes' => ['id' => 'fn:'.$definitionId],
2429                'elements'   => \array_merge(
2430                    $textElements
2431                ),
2432            ];
2433        }
2434
2435        return $Element;
2436    }
2437
2438    # ~
2439
2440    protected function parseAttributeDataBase($attributeString)
2441    {
2442        $Data = [];
2443
2444        $attributes = \preg_split('/[ ]+/', $attributeString, - 1, \PREG_SPLIT_NO_EMPTY);
2445
2446        foreach ($attributes as $attribute)
2447        {
2448            if ($attribute[0] === '#')
2449            {
2450                $Data['id'] = \substr($attribute, 1);
2451            }
2452            else # "."
2453            {
2454                $classes []= \substr($attribute, 1);
2455            }
2456        }
2457
2458        if (isset($classes))
2459        {
2460            $Data['class'] = \implode(' ', $classes);
2461        }
2462
2463        return $Data;
2464    }
2465
2466    # ~
2467
2468    protected function processTag($elementMarkup) # recursive
2469    {
2470        # http://stackoverflow.com/q/1148928/200145
2471        \libxml_use_internal_errors(true);
2472
2473        $DOMDocument = new \DOMDocument();
2474
2475        # http://stackoverflow.com/q/11309194/200145
2476        $elementMarkup = \mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
2477
2478        # http://stackoverflow.com/q/4879946/200145
2479        $DOMDocument->loadHTML($elementMarkup);
2480        $DOMDocument->removeChild($DOMDocument->doctype);
2481        $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
2482
2483        $elementText = '';
2484
2485        if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
2486        {
2487            foreach ($DOMDocument->documentElement->childNodes as $Node)
2488            {
2489                $elementText .= $DOMDocument->saveHTML($Node);
2490            }
2491
2492            $DOMDocument->documentElement->removeAttribute('markdown');
2493
2494            $elementText = "\n".$this->text($elementText)."\n";
2495        }
2496        else
2497        {
2498            foreach ($DOMDocument->documentElement->childNodes as $Node)
2499            {
2500                $nodeMarkup = $DOMDocument->saveHTML($Node);
2501
2502                if ($Node instanceof \DOMElement && ! \in_array($Node->nodeName, $this->textLevelElements))
2503                {
2504                    $elementText .= $this->processTag($nodeMarkup);
2505                }
2506                else
2507                {
2508                    $elementText .= $nodeMarkup;
2509                }
2510            }
2511        }
2512
2513        # because we don't want for markup to get encoded
2514        $DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
2515
2516        $markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
2517
2518        return \str_replace('placeholder\x1A', $elementText, $markup);
2519    }
2520
2521    # ~
2522
2523    protected function sortFootnotes($A, $B) # callback
2524    {
2525        return $A['number'] - $B['number'];
2526    }
2527
2528    #
2529    # Fields
2530    #
2531
2532    protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
2533
2534    protected function textElements($text)
2535    {
2536        # make sure no definitions are set
2537        $this->DefinitionData = [];
2538
2539        # standardize line breaks
2540        $text = \str_replace(["\r\n", "\r"], "\n", $text);
2541
2542        # remove surrounding line breaks
2543        $text = \trim($text, "\n");
2544
2545        # split text into lines
2546        $lines = \explode("\n", $text);
2547
2548        # iterate through lines to identify blocks
2549        return $this->linesElements($lines);
2550    }
2551
2552    #
2553    # Setters
2554    #
2555
2556    public function setBreaksEnabled($breaksEnabled)
2557    {
2558        $this->breaksEnabled = $breaksEnabled;
2559
2560        return $this;
2561    }
2562
2563    protected $breaksEnabled;
2564
2565    public function setMarkupEscaped($markupEscaped)
2566    {
2567        $this->markupEscaped = $markupEscaped;
2568
2569        return $this;
2570    }
2571
2572    protected $markupEscaped;
2573
2574    public function setUrlsLinked($urlsLinked)
2575    {
2576        $this->urlsLinked = $urlsLinked;
2577
2578        return $this;
2579    }
2580
2581    protected $urlsLinked = true;
2582
2583    public function setSafeMode($safeMode)
2584    {
2585        $this->safeMode = (bool) $safeMode;
2586
2587        return $this;
2588    }
2589
2590    protected $safeMode;
2591
2592    public function setStrictMode($strictMode)
2593    {
2594        $this->strictMode = (bool) $strictMode;
2595
2596        return $this;
2597    }
2598
2599    protected $strictMode;
2600
2601    protected $safeLinksWhitelist = [
2602        'http://',
2603        'https://',
2604        'ftp://',
2605        'ftps://',
2606        'mailto:',
2607        'tel:',
2608        'data:image/png;base64,',
2609        'data:image/gif;base64,',
2610        'data:image/jpeg;base64,',
2611        'irc:',
2612        'ircs:',
2613        'git:',
2614        'ssh:',
2615        'news:',
2616        'steam:',
2617    ];
2618
2619    #
2620    # Lines
2621    #
2622
2623    protected $BlockTypes = [
2624        '#' => ['Header'],
2625        '*' => ['Rule', 'List', 'Abbreviation'],
2626        '+' => ['List'],
2627        '-' => ['SetextHeader', 'Table', 'Rule', 'List'],
2628        '0' => ['List'],
2629        '1' => ['List'],
2630        '2' => ['List'],
2631        '3' => ['List'],
2632        '4' => ['List'],
2633        '5' => ['List'],
2634        '6' => ['List'],
2635        '7' => ['List'],
2636        '8' => ['List'],
2637        '9' => ['List'],
2638        ':' => ['Table', 'DefinitionList'],
2639        '<' => ['Comment', 'Markup'],
2640        '=' => ['SetextHeader'],
2641        '>' => ['Quote'],
2642        '[' => ['Footnote', 'Reference'],
2643        '_' => ['Rule'],
2644        '`' => ['FencedCode'],
2645        '|' => ['Table'],
2646        '~' => ['FencedCode'],
2647    ];
2648
2649    # ~
2650
2651    protected $unmarkedBlockTypes = [
2652        'Code',
2653    ];
2654
2655    #
2656    # Blocks
2657    #
2658
2659    protected function lines(array $lines)
2660    {
2661        return $this->elements($this->linesElements($lines));
2662    }
2663
2664    protected function linesElements(array $lines)
2665    {
2666        $Elements     = [];
2667        $CurrentBlock = null;
2668
2669        foreach ($lines as $line)
2670        {
2671            if (\rtrim($line) === '')
2672            {
2673                if (isset($CurrentBlock))
2674                {
2675                    $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
2676                        ? $CurrentBlock['interrupted'] + 1 : 1
2677                    );
2678                }
2679
2680                continue;
2681            }
2682
2683            while (($beforeTab = \strstr($line, "\t", true)) !== false)
2684            {
2685                $shortage = 4 - \mb_strlen($beforeTab, 'utf-8') % 4;
2686
2687                $line = $beforeTab
2688                    . \str_repeat(' ', $shortage)
2689                    . \substr($line, \strlen($beforeTab) + 1)
2690                ;
2691            }
2692
2693            $indent = \strspn($line, ' ');
2694
2695            $text = $indent > 0 ? \substr($line, $indent) : $line;
2696
2697            # ~
2698
2699            $Line = ['body' => $line, 'indent' => $indent, 'text' => $text];
2700
2701            # ~
2702
2703            if (isset($CurrentBlock['continuable']))
2704            {
2705                $methodName = 'block' . $CurrentBlock['type'] . 'Continue';
2706                $Block      = $this->{$methodName}($Line, $CurrentBlock);
2707
2708                if (isset($Block)) {
2709                    $CurrentBlock = $Block;
2710                    continue;
2711                } elseif ($this->isBlockCompletable($CurrentBlock['type'])) {
2712                    $methodName   = 'block' . $CurrentBlock['type'] . 'Complete';
2713                    $CurrentBlock = $this->{$methodName}($CurrentBlock);
2714                }
2715            }
2716
2717            # ~
2718
2719            $marker = $text[0];
2720
2721            # ~
2722
2723            $blockTypes = $this->unmarkedBlockTypes;
2724
2725            if (isset($this->BlockTypes[$marker]))
2726            {
2727                foreach ($this->BlockTypes[$marker] as $blockType)
2728                {
2729                    $blockTypes []= $blockType;
2730                }
2731            }
2732
2733            #
2734            # ~
2735
2736            foreach ($blockTypes as $blockType)
2737            {
2738                $Block = $this->{"block{$blockType}"}($Line, $CurrentBlock);
2739
2740                if (isset($Block))
2741                {
2742                    $Block['type'] = $blockType;
2743
2744                    if (!isset($Block['identified']))
2745                    {
2746                        if (isset($CurrentBlock))
2747                        {
2748                            $Elements[] = $this->extractElement($CurrentBlock);
2749                        }
2750
2751                        $Block['identified'] = true;
2752                    }
2753
2754                    if ($this->isBlockContinuable($blockType))
2755                    {
2756                        $Block['continuable'] = true;
2757                    }
2758
2759                    $CurrentBlock = $Block;
2760
2761                    continue 2;
2762                }
2763            }
2764
2765            # ~
2766
2767            if (isset($CurrentBlock) && $CurrentBlock['type'] === 'Paragraph')
2768            {
2769                $Block = $this->paragraphContinue($Line, $CurrentBlock);
2770            }
2771
2772            if (isset($Block))
2773            {
2774                $CurrentBlock = $Block;
2775            }
2776            else
2777            {
2778                if (isset($CurrentBlock))
2779                {
2780                    $Elements[] = $this->extractElement($CurrentBlock);
2781                }
2782
2783                $CurrentBlock = $this->paragraph($Line);
2784
2785                $CurrentBlock['identified'] = true;
2786            }
2787        }
2788
2789        # ~
2790
2791        if (isset($CurrentBlock['continuable']) && $this->isBlockCompletable($CurrentBlock['type']))
2792        {
2793            $methodName   = 'block' . $CurrentBlock['type'] . 'Complete';
2794            $CurrentBlock = $this->{$methodName}($CurrentBlock);
2795        }
2796
2797        # ~
2798
2799        if (isset($CurrentBlock))
2800        {
2801            $Elements[] = $this->extractElement($CurrentBlock);
2802        }
2803
2804        # ~
2805
2806        return $Elements;
2807    }
2808
2809    protected function extractElement(array $Component)
2810    {
2811        if (!isset($Component['element']))
2812        {
2813            if (isset($Component['markup']))
2814            {
2815                $Component['element'] = ['rawHtml' => $Component['markup']];
2816            }
2817            elseif (isset($Component['hidden']))
2818            {
2819                $Component['element'] = [];
2820            }
2821        }
2822
2823        return $Component['element'];
2824    }
2825
2826    protected function isBlockContinuable($Type) : bool
2827    {
2828        return \method_exists($this, 'block' . $Type . 'Continue');
2829    }
2830
2831    protected function isBlockCompletable($Type) : bool
2832    {
2833        return \method_exists($this, 'block' . $Type . 'Complete');
2834    }
2835
2836    #
2837    # Code
2838
2839    protected function blockCodeBase($Line, $Block = null)
2840    {
2841        if (isset($Block) && $Block['type'] === 'Paragraph' && !isset($Block['interrupted']))
2842        {
2843            return;
2844        }
2845
2846        if ($Line['indent'] >= 4)
2847        {
2848            $text = \substr($Line['body'], 4);
2849
2850            return [
2851                'element' => [
2852                    'name'    => 'pre',
2853                    'element' => [
2854                        'name' => 'code',
2855                        'text' => $text,
2856                    ],
2857                ],
2858            ];
2859        }
2860    }
2861
2862    protected function blockCodeContinue($Line, $Block)
2863    {
2864        if ($Line['indent'] >= 4)
2865        {
2866            if (isset($Block['interrupted']))
2867            {
2868                $Block['element']['element']['text'] .= \str_repeat("\n", $Block['interrupted']);
2869
2870                unset($Block['interrupted']);
2871            }
2872
2873            $Block['element']['element']['text'] .= "\n";
2874
2875            $text = \substr($Line['body'], 4);
2876
2877            $Block['element']['element']['text'] .= $text;
2878
2879            return $Block;
2880        }
2881    }
2882
2883    protected function blockCodeComplete($Block)
2884    {
2885        return $Block;
2886    }
2887
2888    #
2889    # Comment
2890
2891    protected function blockCommentBase($Line)
2892    {
2893        if ($this->markupEscaped || $this->safeMode)
2894        {
2895            return;
2896        }
2897
2898        if (\str_starts_with($Line['text'], '<!--'))
2899        {
2900            $Block = [
2901                'element' => [
2902                    'rawHtml'   => $Line['body'],
2903                    'autobreak' => true,
2904                ],
2905            ];
2906
2907            if (\strpos($Line['text'], '-->') !== false)
2908            {
2909                $Block['closed'] = true;
2910            }
2911
2912            return $Block;
2913        }
2914    }
2915
2916    protected function blockCommentContinue($Line, array $Block)
2917    {
2918        if (isset($Block['closed']))
2919        {
2920            return;
2921        }
2922
2923        $Block['element']['rawHtml'] .= "\n" . $Line['body'];
2924
2925        if (\strpos($Line['text'], '-->') !== false)
2926        {
2927            $Block['closed'] = true;
2928        }
2929
2930        return $Block;
2931    }
2932
2933    #
2934    # Fenced Code
2935
2936    protected function blockFencedCodeBase($Line)
2937    {
2938        $marker = $Line['text'][0];
2939
2940        $openerLength = \strspn($Line['text'], $marker);
2941
2942        if ($openerLength < 3)
2943        {
2944            return;
2945        }
2946
2947        $infostring = \trim(\substr($Line['text'], $openerLength), "\t ");
2948
2949        if (\strpos($infostring, '`') !== false)
2950        {
2951            return;
2952        }
2953
2954        $Element = [
2955            'name' => 'code',
2956            'text' => '',
2957        ];
2958
2959        if ($infostring !== '')
2960        {
2961            /**
2962             * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
2963             * Every HTML element may have a class attribute specified.
2964             * The attribute, if specified, must have a value that is a set
2965             * of space-separated tokens representing the various classes
2966             * that the element belongs to.
2967             * [...]
2968             * The space characters, for the purposes of this specification,
2969             * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
2970             * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
2971             * U+000D CARRIAGE RETURN (CR).
2972             */
2973            $language = \substr($infostring, 0, \strcspn($infostring, " \t\n\f\r"));
2974
2975            $Element['attributes'] = ['class' => "language-{$language}"];
2976        }
2977
2978        return [
2979            'char'         => $marker,
2980            'openerLength' => $openerLength,
2981            'element'      => [
2982                'name'    => 'pre',
2983                'element' => $Element,
2984            ],
2985        ];
2986    }
2987
2988    protected function blockFencedCodeContinue($Line, $Block)
2989    {
2990        if (isset($Block['complete']))
2991        {
2992            return;
2993        }
2994
2995        if (isset($Block['interrupted']))
2996        {
2997            $Block['element']['element']['text'] .= \str_repeat("\n", $Block['interrupted']);
2998
2999            unset($Block['interrupted']);
3000        }
3001
3002        if (($len = \strspn($Line['text'], $Block['char'])) >= $Block['openerLength']
3003            && \rtrim(\substr($Line['text'], $len), ' ') === ''
3004        ) {
3005            $Block['element']['element']['text'] = \substr($Block['element']['element']['text'], 1);
3006
3007            $Block['complete'] = true;
3008
3009            return $Block;
3010        }
3011
3012        $Block['element']['element']['text'] .= "\n" . $Line['body'];
3013
3014        return $Block;
3015    }
3016
3017    protected function blockFencedCodeComplete($Block)
3018    {
3019        return $Block;
3020    }
3021
3022    #
3023    # Header
3024
3025    protected function blockHeaderParent($Line)
3026    {
3027        $level = \strspn($Line['text'], '#');
3028
3029        if ($level > 6)
3030        {
3031            return;
3032        }
3033
3034        $text = \trim($Line['text'], '#');
3035
3036        if ($this->strictMode && isset($text[0]) && $text[0] !== ' ')
3037        {
3038            return;
3039        }
3040
3041        $text = \trim($text, ' ');
3042
3043        return [
3044            'element' => [
3045                'name'    => 'h' . $level,
3046                'handler' => [
3047                    'function'    => 'lineElements',
3048                    'argument'    => $text,
3049                    'destination' => 'elements',
3050                ],
3051            ],
3052        ];
3053    }
3054
3055    #
3056    # List
3057
3058    protected function blockListBase($Line, array $CurrentBlock = null)
3059    {
3060        list($name, $pattern) = $Line['text'][0] <= '-' ? ['ul', '[*+-]'] : ['ol', '[0-9]{1,9}+[.\)]'];
3061
3062        if (\preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches))
3063        {
3064            $contentIndent = \strlen($matches[2]);
3065
3066            if ($contentIndent >= 5)
3067            {
3068                --$contentIndent;
3069                $matches[1] = \substr($matches[1], 0, -$contentIndent);
3070                $matches[3] = \str_repeat(' ', $contentIndent) . $matches[3];
3071            }
3072            elseif ($contentIndent === 0)
3073            {
3074                $matches[1] .= ' ';
3075            }
3076
3077            $markerWithoutWhitespace = \strstr($matches[1], ' ', true);
3078
3079            $Block = [
3080                'indent'  => $Line['indent'],
3081                'pattern' => $pattern,
3082                'data'    => [
3083                    'type'       => $name,
3084                    'marker'     => $matches[1],
3085                    'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : \substr($markerWithoutWhitespace, -1)),
3086                ],
3087                'element' => [
3088                    'name'     => $name,
3089                    'elements' => [],
3090                ],
3091            ];
3092            $Block['data']['markerTypeRegex'] = \preg_quote($Block['data']['markerType'], '/');
3093
3094            if ($name === 'ol')
3095            {
3096                $listStart = \ltrim(\strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
3097
3098                if ($listStart !== '1')
3099                {
3100                    if (
3101                        isset($CurrentBlock)
3102                        && $CurrentBlock['type'] === 'Paragraph'
3103                        && !isset($CurrentBlock['interrupted'])
3104                    ) {
3105                        return;
3106                    }
3107
3108                    $Block['element']['attributes'] = ['start' => $listStart];
3109                }
3110            }
3111
3112            $Block['li'] = [
3113                'name'    => 'li',
3114                'handler' => [
3115                    'function'    => 'li',
3116                    'argument'    => empty($matches[3]) ? [] : [$matches[3]],
3117                    'destination' => 'elements',
3118                ],
3119            ];
3120
3121            $Block['element']['elements'] []= & $Block['li'];
3122
3123            return $Block;
3124        }
3125    }
3126
3127    protected function blockListContinue($Line, array $Block)
3128    {
3129        if (isset($Block['interrupted']) && empty($Block['li']['handler']['argument']))
3130        {
3131            return null;
3132        }
3133
3134        $requiredIndent = ($Block['indent'] + \strlen($Block['data']['marker']));
3135
3136        if ($Line['indent'] < $requiredIndent
3137            && (
3138                (
3139                    $Block['data']['type'] === 'ol'
3140                    && \preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
3141                ) || (
3142                    $Block['data']['type'] === 'ul'
3143                    && \preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
3144                )
3145            )
3146        ) {
3147            if (isset($Block['interrupted']))
3148            {
3149                $Block['li']['handler']['argument'] []= '';
3150
3151                $Block['loose'] = true;
3152
3153                unset($Block['interrupted']);
3154            }
3155
3156            unset($Block['li']);
3157
3158            $text = isset($matches[1]) ? $matches[1] : '';
3159
3160            $Block['indent'] = $Line['indent'];
3161
3162            $Block['li'] = [
3163                'name'    => 'li',
3164                'handler' => [
3165                    'function'    => 'li',
3166                    'argument'    => [$text],
3167                    'destination' => 'elements',
3168                ],
3169            ];
3170
3171            $Block['element']['elements'] []= & $Block['li'];
3172
3173            return $Block;
3174        }
3175        elseif ($Line['indent'] < $requiredIndent && $this->blockList($Line))
3176        {
3177            return null;
3178        }
3179
3180        if ($Line['text'][0] === '[' && $this->blockReference($Line))
3181        {
3182            return $Block;
3183        }
3184
3185        if ($Line['indent'] >= $requiredIndent)
3186        {
3187            if (isset($Block['interrupted']))
3188            {
3189                $Block['li']['handler']['argument'] []= '';
3190
3191                $Block['loose'] = true;
3192
3193                unset($Block['interrupted']);
3194            }
3195
3196            $text = \substr($Line['body'], $requiredIndent);
3197
3198            $Block['li']['handler']['argument'] []= $text;
3199
3200            return $Block;
3201        }
3202
3203        if (!isset($Block['interrupted']))
3204        {
3205            $text = \preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']);
3206
3207            $Block['li']['handler']['argument'] []= $text;
3208
3209            return $Block;
3210        }
3211    }
3212
3213    protected function blockListComplete(array $Block)
3214    {
3215        if (isset($Block['loose']))
3216        {
3217            foreach ($Block['element']['elements'] as &$li)
3218            {
3219                if (\end($li['handler']['argument']) !== '')
3220                {
3221                    $li['handler']['argument'] []= '';
3222                }
3223            }
3224        }
3225
3226        return $Block;
3227    }
3228
3229    #
3230    # Quote
3231
3232    protected function blockQuoteBase($Line)
3233    {
3234        if (\preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
3235        {
3236            return [
3237                'element' => [
3238                    'name'    => 'blockquote',
3239                    'handler' => [
3240                        'function'    => 'linesElements',
3241                        'argument'    => (array) $matches[1],
3242                        'destination' => 'elements',
3243                    ],
3244                ],
3245            ];
3246        }
3247    }
3248
3249    protected function blockQuoteContinue($Line, array $Block)
3250    {
3251        if (isset($Block['interrupted']))
3252        {
3253            return;
3254        }
3255
3256        if ($Line['text'][0] === '>' && \preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
3257        {
3258            $Block['element']['handler']['argument'] []= $matches[1];
3259
3260            return $Block;
3261        }
3262
3263        if (!isset($Block['interrupted']))
3264        {
3265            $Block['element']['handler']['argument'] []= $Line['text'];
3266
3267            return $Block;
3268        }
3269    }
3270
3271    #
3272    # Rule
3273
3274    protected function blockRuleBase($Line)
3275    {
3276        $marker = $Line['text'][0];
3277
3278        if (\substr_count($Line['text'], $marker) >= 3 && \rtrim($Line['text'], " {$marker}") === '')
3279        {
3280            return [
3281                'element' => [
3282                    'name' => 'hr',
3283                ],
3284            ];
3285        }
3286    }
3287
3288    #
3289    # Setext
3290
3291    protected function blockSetextHeaderParent($Line, array $Block = null)
3292    {
3293        if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted']))
3294        {
3295            return;
3296        }
3297
3298        if ($Line['indent'] < 4 && \rtrim(\rtrim($Line['text'], ' '), $Line['text'][0]) === '')
3299        {
3300            $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
3301
3302            return $Block;
3303        }
3304    }
3305
3306    #
3307    # Reference
3308
3309    protected function blockReferenceBase($Line)
3310    {
3311        if (\strpos($Line['text'], ']') !== false
3312            && \preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
3313        ) {
3314            $id = \strtolower($matches[1]);
3315
3316            $Data = [
3317                'url'   => UriFactory::build($matches[2]),
3318                'title' => isset($matches[3]) ? $matches[3] : null,
3319            ];
3320
3321            $this->DefinitionData['Reference'][$id] = $Data;
3322
3323            return [
3324                'element' => [],
3325            ];
3326        }
3327    }
3328
3329    #
3330    # Table
3331
3332    protected function blockTableBase($Line, array $Block = null)
3333    {
3334        if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted']))
3335        {
3336            return;
3337        }
3338
3339        if (
3340            \strpos($Block['element']['handler']['argument'], '|') === false
3341            && \strpos($Line['text'], '|') === false
3342            && \strpos($Line['text'], ':') === false
3343            || \strpos($Block['element']['handler']['argument'], "\n") !== false
3344        ) {
3345            return;
3346        }
3347
3348        if (\rtrim($Line['text'], ' -:|') !== '')
3349        {
3350            return;
3351        }
3352
3353        $alignments = [];
3354
3355        $divider = $Line['text'];
3356
3357        $divider = \trim($divider);
3358        $divider = \trim($divider, '|');
3359
3360        $dividerCells = \explode('|', $divider);
3361
3362        foreach ($dividerCells as $dividerCell)
3363        {
3364            $dividerCell = \trim($dividerCell);
3365
3366            if ($dividerCell === '')
3367            {
3368                return;
3369            }
3370
3371            $alignment = null;
3372
3373            if ($dividerCell[0] === ':')
3374            {
3375                $alignment = 'left';
3376            }
3377
3378            if (\substr($dividerCell, - 1) === ':')
3379            {
3380                $alignment = $alignment === 'left' ? 'center' : 'right';
3381            }
3382
3383            $alignments []= $alignment;
3384        }
3385
3386        # ~
3387
3388        $HeaderElements = [];
3389
3390        $header = $Block['element']['handler']['argument'];
3391
3392        $header = \trim($header);
3393        $header = \trim($header, '|');
3394
3395        $headerCells = \explode('|', $header);
3396
3397        if (\count($headerCells) !== \count($alignments))
3398        {
3399            return;
3400        }
3401
3402        foreach ($headerCells as $index => $headerCell)
3403        {
3404            $headerCell = \trim($headerCell);
3405
3406            $HeaderElement = [
3407                'name'    => 'th',
3408                'handler' => [
3409                    'function'    => 'lineElements',
3410                    'argument'    => $headerCell,
3411                    'destination' => 'elements',
3412                ],
3413            ];
3414
3415            if (isset($alignments[$index]))
3416            {
3417                $alignment = $alignments[$index];
3418
3419                $HeaderElement['attributes'] = [
3420                    'style' => "text-align: {$alignment};",
3421                ];
3422            }
3423
3424            $HeaderElements []= $HeaderElement;
3425        }
3426
3427        # ~
3428
3429        $Block = [
3430            'alignments' => $alignments,
3431            'identified' => true,
3432            'element'    => [
3433                'name'     => 'table',
3434                'elements' => [],
3435            ],
3436        ];
3437
3438        $Block['element']['elements'] []= [
3439            'name' => 'thead',
3440        ];
3441
3442        $Block['element']['elements'] []= [
3443            'name'     => 'tbody',
3444            'elements' => [],
3445        ];
3446
3447        $Block['element']['elements'][0]['elements'] []= [
3448            'name'     => 'tr',
3449            'elements' => $HeaderElements,
3450        ];
3451
3452        return $Block;
3453    }
3454
3455    protected function blockTableContinue($Line, array $Block)
3456    {
3457        if (isset($Block['interrupted']))
3458        {
3459            return;
3460        }
3461
3462        if (\count($Block['alignments']) === 1 || $Line['text'][0] === '|' || \strpos($Line['text'], '|'))
3463        {
3464            $Elements = [];
3465
3466            $row = $Line['text'];
3467
3468            $row = \trim($row);
3469            $row = \trim($row, '|');
3470
3471            \preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
3472
3473            $cells = \array_slice($matches[0], 0, \count($Block['alignments']));
3474
3475            foreach ($cells as $index => $cell)
3476            {
3477                $cell = \trim($cell);
3478
3479                $Element = [
3480                    'name'    => 'td',
3481                    'handler' => [
3482                        'function'    => 'lineElements',
3483                        'argument'    => $cell,
3484                        'destination' => 'elements',
3485                    ],
3486                ];
3487
3488                if (isset($Block['alignments'][$index]))
3489                {
3490                    $Element['attributes'] = [
3491                        'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
3492                    ];
3493                }
3494
3495                $Elements []= $Element;
3496            }
3497
3498            $Element = [
3499                'name'     => 'tr',
3500                'elements' => $Elements,
3501            ];
3502
3503            $Block['element']['elements'][1]['elements'] []= $Element;
3504
3505            return $Block;
3506        }
3507    }
3508
3509    #
3510    # ~
3511    #
3512
3513    protected function paragraph($Line)
3514    {
3515        return [
3516            'type'    => 'Paragraph',
3517            'element' => [
3518                'name'    => 'p',
3519                'handler' => [
3520                    'function'    => 'lineElements',
3521                    'argument'    => $Line['text'],
3522                    'destination' => 'elements',
3523                ],
3524            ],
3525        ];
3526    }
3527
3528    protected function paragraphContinue($Line, array $Block)
3529    {
3530        if (isset($Block['interrupted']))
3531        {
3532            return;
3533        }
3534
3535        $Block['element']['handler']['argument'] .= "\n".$Line['text'];
3536
3537        return $Block;
3538    }
3539
3540    #
3541    # Inline Elements
3542    #
3543
3544    protected $InlineTypes = [
3545        '!'  => ['Image'],
3546        '&'  => ['SpecialCharacter'],
3547        '*'  => ['Emphasis'],
3548        ':'  => ['Url'],
3549        '<'  => ['UrlTag', 'EmailTag', 'Markup'],
3550        '['  => ['FootnoteMarker', 'Link'],
3551        '_'  => ['Emphasis'],
3552        '`'  => ['Code'],
3553        '~'  => ['Strikethrough'],
3554        '\\' => ['EscapeSequence'],
3555    ];
3556
3557    # ~
3558
3559    protected $inlineMarkerList = '!*_&[:<`~\\';
3560
3561    #
3562    # ~
3563    #
3564
3565    public function line($text, $nonNestables = [])
3566    {
3567        return $this->elements($this->lineElements($text, $nonNestables));
3568    }
3569
3570    /*
3571    protected function lineElements($text, $nonNestables = array())
3572    {
3573        # standardize line breaks
3574        $text = str_replace(array("\r\n", "\r"), "\n", $text);
3575
3576        $Elements = array();
3577
3578        $nonNestables = (empty($nonNestables)
3579            ? array()
3580            : array_combine($nonNestables, $nonNestables)
3581        );
3582
3583        # $excerpt is based on the first occurrence of a marker
3584
3585        while ($excerpt = strpbrk($text, $this->inlineMarkerList))
3586        {
3587            $marker = $excerpt[0];
3588
3589            $markerPosition = strlen($text) - strlen($excerpt);
3590
3591            $Excerpt = array('text' => $excerpt, 'context' => $text);
3592
3593            foreach ($this->InlineTypes[$marker] as $inlineType)
3594            {
3595                # check to see if the current inline type is nestable in the current context
3596
3597                if (isset($nonNestables[$inlineType]))
3598                {
3599                    continue;
3600                }
3601
3602                $Inline = $this->{"inline$inlineType"}($Excerpt);
3603
3604                if ( !isset($Inline))
3605                {
3606                    continue;
3607                }
3608
3609                # makes sure that the inline belongs to "our" marker
3610
3611                if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
3612                {
3613                    continue;
3614                }
3615
3616                # sets a default inline position
3617
3618                if ( !isset($Inline['position']))
3619                {
3620                    $Inline['position'] = $markerPosition;
3621                }
3622
3623                # cause the new element to 'inherit' our non nestables
3624
3625
3626                $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
3627                    ? array_merge($Inline['element']['nonNestables'], $nonNestables)
3628                    : $nonNestables
3629                ;
3630
3631                # the text that comes before the inline
3632                $unmarkedText = substr($text, 0, $Inline['position']);
3633
3634                # compile the unmarked text
3635                $InlineText = $this->inlineText($unmarkedText);
3636                $Elements[] = $InlineText['element'];
3637
3638                # compile the inline
3639                $Elements[] = $this->extractElement($Inline);
3640
3641                # remove the examined text
3642                $text = substr($text, $Inline['position'] + $Inline['extent']);
3643
3644                continue 2;
3645            }
3646
3647            # the marker does not belong to an inline
3648
3649            $unmarkedText = substr($text, 0, $markerPosition + 1);
3650
3651            $InlineText = $this->inlineText($unmarkedText);
3652            $Elements[] = $InlineText['element'];
3653
3654            $text = substr($text, $markerPosition + 1);
3655        }
3656
3657        $InlineText = $this->inlineText($text);
3658        $Elements[] = $InlineText['element'];
3659
3660        foreach ($Elements as &$Element)
3661        {
3662            if ( !isset($Element['autobreak']))
3663            {
3664                $Element['autobreak'] = false;
3665            }
3666        }
3667
3668        return $Elements;
3669    }
3670    */
3671
3672    #
3673    # ~
3674    #
3675
3676    protected function inlineTextParent($text)
3677    {
3678        $Inline = [
3679            'extent'  => \strlen($text),
3680            'element' => [],
3681        ];
3682
3683        $Inline['element']['elements'] = self::pregReplaceElements(
3684            $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
3685            [
3686                ['name' => 'br'],
3687                ['text' => "\n"],
3688            ],
3689            $text
3690        );
3691
3692        return $Inline;
3693    }
3694
3695    protected function inlineLinkParent($Excerpt)
3696    {
3697        $Element = [
3698            'name'    => 'a',
3699            'handler' => [
3700                'function'    => 'lineElements',
3701                'argument'    => null,
3702                'destination' => 'elements',
3703            ],
3704            'nonNestables' => ['Url', 'Link'],
3705            'attributes'   => [
3706                'href'  => null,
3707                'title' => null,
3708            ],
3709        ];
3710
3711        $extent = 0;
3712
3713        $remainder = $Excerpt['text'];
3714
3715        if (\preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
3716        {
3717            $Element['handler']['argument'] = $matches[1];
3718
3719            $extent += \strlen($matches[0]);
3720
3721            $remainder = \substr($remainder, $extent);
3722        }
3723        else
3724        {
3725            return;
3726        }
3727
3728        if (\preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches))
3729        {
3730            $Element['attributes']['href'] = $matches[1];
3731
3732            if (isset($matches[2]))
3733            {
3734                $Element['attributes']['title'] = \substr($matches[2], 1, - 1);
3735            }
3736
3737            $extent += \strlen($matches[0]);
3738        }
3739        else
3740        {
3741            if (\preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
3742            {
3743                $definition = \strlen($matches[1]) !== 0 ? $matches[1] : $Element['handler']['argument'];
3744                $definition = \strtolower($definition);
3745
3746                $extent += \strlen($matches[0]);
3747            }
3748            else
3749            {
3750                $definition = \strtolower($Element['handler']['argument']);
3751            }
3752
3753            if (!isset($this->DefinitionData['Reference'][$definition]))
3754            {
3755                return;
3756            }
3757
3758            $Definition = $this->DefinitionData['Reference'][$definition];
3759
3760            $Element['attributes']['href']  = $Definition['url'];
3761            $Element['attributes']['title'] = $Definition['title'];
3762        }
3763
3764        return [
3765            'extent'  => $extent,
3766            'element' => $Element,
3767        ];
3768    }
3769
3770    protected function inlineSpecialCharacter($Excerpt)
3771    {
3772        if (\substr($Excerpt['text'], 1, 1) !== ' ' && \strpos($Excerpt['text'], ';') !== false
3773            && \preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
3774        ) {
3775            return [
3776                'element' => ['rawHtml' => '&' . $matches[1] . ';'],
3777                'extent'  => \strlen($matches[0]),
3778            ];
3779        }
3780    }
3781
3782    # ~
3783
3784    protected function unmarkedText($text)
3785    {
3786        $Inline = $this->inlineText($text);
3787        return $this->element($Inline['element']);
3788    }
3789
3790    #
3791    # Handlers
3792    #
3793
3794    protected function handle(array $Element)
3795    {
3796        if (isset($Element['handler']))
3797        {
3798            if (!isset($Element['nonNestables']))
3799            {
3800                $Element['nonNestables'] = [];
3801            }
3802
3803            if (\is_string($Element['handler']))
3804            {
3805                $function = $Element['handler'];
3806                $argument = $Element['text'];
3807                unset($Element['text']);
3808                $destination = 'rawHtml';
3809            }
3810            else
3811            {
3812                $function    = $Element['handler']['function'];
3813                $argument    = $Element['handler']['argument'];
3814                $destination = $Element['handler']['destination'];
3815            }
3816
3817            $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
3818
3819            if ($destination === 'handler')
3820            {
3821                $Element = $this->handle($Element);
3822            }
3823
3824            unset($Element['handler']);
3825        }
3826
3827        return $Element;
3828    }
3829
3830    protected function handleElementRecursive(array $Element)
3831    {
3832        return $this->elementApplyRecursive([$this, 'handle'], $Element);
3833    }
3834
3835    protected function handleElementsRecursive(array $Elements)
3836    {
3837        return $this->elementsApplyRecursive([$this, 'handle'], $Elements);
3838    }
3839
3840    protected function elementApplyRecursive($closure, array $Element)
3841    {
3842        $Element = $closure($Element);
3843
3844        if (isset($Element['elements']))
3845        {
3846            $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
3847        }
3848        elseif (isset($Element['element']))
3849        {
3850            $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
3851        }
3852
3853        return $Element;
3854    }
3855
3856    protected function elementApplyRecursiveDepthFirst($closure, array $Element)
3857    {
3858        if (isset($Element['elements']))
3859        {
3860            $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
3861        }
3862        elseif (isset($Element['element']))
3863        {
3864            $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
3865        }
3866
3867        return $closure($Element);
3868    }
3869
3870    protected function elementsApplyRecursive($closure, array $Elements)
3871    {
3872        foreach ($Elements as &$Element)
3873        {
3874            $Element = $this->elementApplyRecursive($closure, $Element);
3875        }
3876
3877        return $Elements;
3878    }
3879
3880    protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
3881    {
3882        foreach ($Elements as &$Element)
3883        {
3884            $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
3885        }
3886
3887        return $Elements;
3888    }
3889
3890    protected function element(array $Element)
3891    {
3892        if ($this->safeMode)
3893        {
3894            $Element = $this->sanitiseElement($Element);
3895        }
3896
3897        # identity map if element has no handler
3898        $Element = $this->handle($Element);
3899
3900        $hasName = isset($Element['name']);
3901
3902        $markup = '';
3903
3904        if ($hasName)
3905        {
3906            $markup .= '<' . $Element['name'];
3907
3908            if (isset($Element['attributes']))
3909            {
3910                foreach ($Element['attributes'] as $name => $value)
3911                {
3912                    if ($value === null)
3913                    {
3914                        continue;
3915                    }
3916
3917                    $markup .= " {$name}=\"".self::escape($value).'"';
3918                }
3919            }
3920        }
3921
3922        $permitRawHtml = false;
3923
3924        if (isset($Element['text']))
3925        {
3926            $text = $Element['text'];
3927        }
3928        // very strongly consider an alternative if you're writing an
3929        // extension
3930        elseif (isset($Element['rawHtml']))
3931        {
3932            $text = $Element['rawHtml'];
3933
3934            $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
3935            $permitRawHtml          = !$this->safeMode || $allowRawHtmlInSafeMode;
3936        }
3937
3938        $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
3939
3940        if ($hasContent)
3941        {
3942            $markup .= $hasName ? '>' : '';
3943
3944            if (isset($Element['elements']))
3945            {
3946                $markup .= $this->elements($Element['elements']);
3947            }
3948            elseif (isset($Element['element']))
3949            {
3950                $markup .= $this->element($Element['element']);
3951            } elseif (!$permitRawHtml) {
3952                $markup .= self::escape((string) $text, true);
3953            }
3954            else
3955            {
3956                $markup .= $text;
3957            }
3958
3959            $markup .= $hasName ? '</' . $Element['name'] . '>' : '';
3960        }
3961        elseif ($hasName)
3962        {
3963            $markup .= ' />';
3964        }
3965
3966        return $markup;
3967    }
3968
3969    protected function elements(array $Elements) : string
3970    {
3971        $markup = '';
3972
3973        $autoBreak = true;
3974
3975        foreach ($Elements as $Element)
3976        {
3977            if (empty($Element))
3978            {
3979                continue;
3980            }
3981
3982            $autoBreakNext = (isset($Element['autobreak'])
3983                ? $Element['autobreak'] : isset($Element['name'])
3984            );
3985            // (autobreak === false) covers both sides of an element
3986            $autoBreak = $autoBreak ? $autoBreakNext : $autoBreak;
3987
3988            $markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
3989            $autoBreak = $autoBreakNext;
3990        }
3991
3992        return $markup . ($autoBreak ? "\n" : '');
3993    }
3994
3995    # ~
3996
3997    protected function li($lines)
3998    {
3999        $Elements = $this->linesElements($lines);
4000
4001        if (! \in_array('', $lines)
4002            && isset($Elements[0], $Elements[0]['name'])
4003            && $Elements[0]['name'] === 'p'
4004        ) {
4005            unset($Elements[0]['name']);
4006        }
4007
4008        return $Elements;
4009    }
4010
4011    #
4012    # AST Convenience
4013    #
4014
4015    /**
4016     * Replace occurrences $regexp with $Elements in $text. Return an array of
4017     * elements representing the replacement.
4018     */
4019    protected static function pregReplaceElements($regexp, $Elements, $text)
4020    {
4021        $newElements = [];
4022
4023        while (\preg_match($regexp, $text, $matches, \PREG_OFFSET_CAPTURE))
4024        {
4025            $offset = $matches[0][1];
4026            $before = \substr($text, 0, $offset);
4027            $after  = \substr($text, $offset + \strlen($matches[0][0]));
4028
4029            $newElements[] = ['text' => $before];
4030
4031            foreach ($Elements as $Element)
4032            {
4033                $newElements[] = $Element;
4034            }
4035
4036            $text = $after;
4037        }
4038
4039        $newElements[] = ['text' => $text];
4040
4041        return $newElements;
4042    }
4043
4044    #
4045    # Deprecated Methods
4046    #
4047
4048    public static function parse($text)
4049    {
4050        $parsedown = new self();
4051
4052        return $parsedown->text($text);
4053    }
4054
4055    protected function sanitiseElement(array $Element)
4056    {
4057        static $goodAttribute     = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
4058        static $safeUrlNameToAtt  = [
4059            'a'   => 'href',
4060            'img' => 'src',
4061        ];
4062
4063        if (!isset($Element['name']))
4064        {
4065            unset($Element['attributes']);
4066            return $Element;
4067        }
4068
4069        if (isset($safeUrlNameToAtt[$Element['name']]))
4070        {
4071            $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
4072        }
4073
4074        if (! empty($Element['attributes']))
4075        {
4076            foreach ($Element['attributes'] as $att => $val)
4077            {
4078                # filter out badly parsed attribute
4079                if (! \preg_match($goodAttribute, $att))
4080                {
4081                    unset($Element['attributes'][$att]);
4082                }
4083                # dump onevent attribute
4084                elseif (self::striAtStart($att, 'on'))
4085                {
4086                    unset($Element['attributes'][$att]);
4087                }
4088            }
4089        }
4090
4091        return $Element;
4092    }
4093
4094    protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
4095    {
4096        foreach ($this->safeLinksWhitelist as $scheme)
4097        {
4098            if (self::striAtStart($Element['attributes'][$attribute], $scheme))
4099            {
4100                return $Element;
4101            }
4102        }
4103
4104        $Element['attributes'][$attribute] = \str_replace(':', '%3A', $Element['attributes'][$attribute]);
4105
4106        return $Element;
4107    }
4108
4109    #
4110    # Static Methods
4111    #
4112
4113    protected static function escape(string $text, bool $allowQuotes = false) : string
4114    {
4115        return \htmlspecialchars($text, $allowQuotes ? \ENT_NOQUOTES : \ENT_QUOTES, 'UTF-8');
4116    }
4117
4118    protected static function striAtStart($string, $needle)
4119    {
4120        $len = \strlen($needle);
4121
4122        if ($len > \strlen($string))
4123        {
4124            return false;
4125        }
4126        else
4127        {
4128            return \strtolower(\substr($string, 0, $len)) === \strtolower($needle);
4129        }
4130    }
4131
4132    public static function instance($name = 'default')
4133    {
4134        if (isset(self::$instances[$name]))
4135        {
4136            return self::$instances[$name];
4137        }
4138
4139        $instance = new static();
4140
4141        self::$instances[$name] = $instance;
4142
4143        return $instance;
4144    }
4145
4146    private static $instances = [];
4147
4148    #
4149    # Fields
4150    #
4151
4152    protected $DefinitionData;
4153
4154    public const ID_ATTRIBUTE_DEFAULT = 'toc';
4155
4156    protected $tagToc = '[toc]';
4157
4158    protected $contentsListArray = [];
4159
4160    protected $contentsListString = '';
4161
4162    protected $firstHeadLevel = 0;
4163
4164    protected $isBlacklistInitialized = false;
4165
4166    protected $anchorDuplicates = [];
4167
4168    #
4169    # Read-Only
4170
4171    protected $specialCharacters = [
4172        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '?', '"', "'", '<',
4173    ];
4174
4175    protected $StrongRegex = [
4176        '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
4177    ];
4178
4179    protected $UnderlineRegex = [
4180        '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
4181    ];
4182
4183    protected $EmRegex = [
4184        '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
4185        '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
4186    ];
4187
4188    protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
4189
4190    protected $voidElements = [
4191        'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
4192    ];
4193
4194    protected $textLevelElements = [
4195        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
4196        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
4197        'i', 'rp', 'del', 'code',          'strike', 'marquee',
4198        'q', 'rt', 'ins', 'font',          'strong',
4199        's', 'tt', 'kbd', 'mark',
4200        'u', 'xm', 'sub', 'nobr',
4201                   'sup', 'ruby',
4202                   'var', 'span',
4203                   'wbr', 'time',
4204    ];
4205}