Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
75.32% |
1453 / 1929 |
|
45.93% |
62 / 135 |
CRAP | |
0.00% |
0 / 1 |
Markdown | |
75.32% |
1453 / 1929 |
|
45.93% |
62 / 135 |
5469.88 | |
0.00% |
0 / 1 |
__construct | |
46.77% |
29 / 62 |
|
0.00% |
0 / 1 |
29.25 | |||
textParent | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
body | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
text | |
27.27% |
3 / 11 |
|
0.00% |
0 / 1 |
10.15 | |||
contentsList | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
inlineCode | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
4.00 | |||
inlineEmailTag | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
6 | |||
inlineEmphasis | |
91.67% |
22 / 24 |
|
0.00% |
0 / 1 |
10.06 | |||
inlineImage | |
91.30% |
21 / 23 |
|
0.00% |
0 / 1 |
5.02 | |||
inlineLink | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
4.02 | |||
inlineMarkup | |
70.00% |
14 / 20 |
|
0.00% |
0 / 1 |
14.27 | |||
inlineStrikethrough | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
5.01 | |||
inlineUrl | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
7.01 | |||
inlineUrlTag | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
4.00 | |||
inlineEmojis | |
97.32% |
218 / 224 |
|
0.00% |
0 / 1 |
2 | |||
inlineMark | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
inlineKeystrokes | |
12.50% |
1 / 8 |
|
0.00% |
0 / 1 |
4.68 | |||
inlineSuperscript | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
inlineSubscript | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
inlineTypographer | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
inlineSmartypants | |
0.00% |
0 / 78 |
|
0.00% |
0 / 1 |
342 | |||
inlineMath | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
inlineEscapeSequence | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
7.23 | |||
blockFootnote | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockDefinitionList | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockCode | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
blockComment | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockHeader | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
7.10 | |||
blockList | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockQuote | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockRule | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockSetextHeader | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
7.10 | |||
blockMarkup | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockReference | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockTable | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockAbbreviation | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
5.93 | |||
blockMath | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
blockMathContinue | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
blockMathComplete | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
blockFencedCode | |
32.56% |
14 / 43 |
|
0.00% |
0 / 1 |
17.04 | |||
blockTableComplete | |
5.26% |
3 / 57 |
|
0.00% |
0 / 1 |
902.68 | |||
blockCheckbox | |
33.33% |
4 / 12 |
|
0.00% |
0 / 1 |
5.67 | |||
blockCheckboxContinue | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
blockCheckboxComplete | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
checkboxUnchecked | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
checkboxChecked | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
format | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
parseAttributeData | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
encodeTagToHash | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
2.15 | |||
decodeTagFromHash | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
getSalt | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getTagToC | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIdAttributeToC | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
createAnchorID | |
95.24% |
60 / 63 |
|
0.00% |
0 / 1 |
4 | |||
fetchText | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setContentsList | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setContentsListAsArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setContentsListAsString | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
incrementAnchorId | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
6.60 | |||
initBlacklist | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
12.72 | |||
lineElements | |
95.24% |
40 / 42 |
|
0.00% |
0 / 1 |
13 | |||
pregReplaceAssoc | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
blockAbbreviationBase | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
blockFootnoteBase | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
blockFootnoteContinue | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
blockFootnoteComplete | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
blockDefinitionListBase | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
4.00 | |||
blockDefinitionListContinue | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
blockHeaderBase | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
blockMarkupBase | |
72.00% |
18 / 25 |
|
0.00% |
0 / 1 |
13.66 | |||
blockMarkupContinue | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
blockMarkupComplete | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
blockSetextHeaderBase | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
inlineFootnoteMarker | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
4 | |||
insertAbreviation | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
inlineText | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
addDdElement | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
buildFootnoteElement | |
92.19% |
59 / 64 |
|
0.00% |
0 / 1 |
5.01 | |||
parseAttributeDataBase | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
processTag | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
6 | |||
sortFootnotes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
textElements | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
setBreaksEnabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setMarkupEscaped | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setUrlsLinked | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setSafeMode | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setStrictMode | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
lines | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
linesElements | |
100.00% |
58 / 58 |
|
100.00% |
1 / 1 |
24 | |||
extractElement | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
isBlockContinuable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isBlockCompletable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
blockCodeBase | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
blockCodeContinue | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
blockCodeComplete | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
blockCommentBase | |
16.67% |
2 / 12 |
|
0.00% |
0 / 1 |
19.47 | |||
blockCommentContinue | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
blockFencedCodeBase | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
4 | |||
blockFencedCodeContinue | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
blockFencedCodeComplete | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
blockHeaderParent | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
5.01 | |||
blockListBase | |
88.10% |
37 / 42 |
|
0.00% |
0 / 1 |
13.29 | |||
blockListContinue | |
95.45% |
42 / 44 |
|
0.00% |
0 / 1 |
17 | |||
blockListComplete | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
blockQuoteBase | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
blockQuoteContinue | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
blockRuleBase | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
blockSetextHeaderParent | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
7 | |||
blockReferenceBase | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
blockTableBase | |
97.01% |
65 / 67 |
|
0.00% |
0 / 1 |
17 | |||
blockTableContinue | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
7 | |||
paragraph | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
paragraphContinue | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
line | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
inlineTextParent | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
inlineLinkParent | |
97.44% |
38 / 39 |
|
0.00% |
0 / 1 |
7 | |||
inlineSpecialCharacter | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
8.74 | |||
unmarkedText | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
handle | |
68.75% |
11 / 16 |
|
0.00% |
0 / 1 |
5.76 | |||
handleElementRecursive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
handleElementsRecursive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
elementApplyRecursive | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
elementApplyRecursiveDepthFirst | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
elementsApplyRecursive | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
elementsApplyRecursiveDepthFirst | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
element | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
19 | |||
elements | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
7 | |||
li | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
pregReplaceElements | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
parse | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
sanitiseElement | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
7.08 | |||
filterUnsafeUrlInAttribute | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
escape | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
striAtStart | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
instance | |
0.00% |
0 / 5 |
|
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 | */ |
12 | declare(strict_types=1); |
13 | |
14 | namespace phpOMS\Utils\Parser\Markdown; |
15 | |
16 | use 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 | */ |
30 | class 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' => '©', |
837 | '/\(r\)/i' => '®', |
838 | '/\(tm\)/i' => '™', |
839 | '/\(p\)/i' => '¶', |
840 | '/\+-/i' => '±', |
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'] ?? '“'; |
862 | $backtickDoublequoteClose = $this->options['smarty']['substitutions']['right-double-quote'] ?? '”'; |
863 | |
864 | $smartDoublequoteOpen = $this->options['smarty']['substitutions']['left-double-quote'] ?? '“'; |
865 | $smartDoublequoteClose = $this->options['smarty']['substitutions']['right-double-quote'] ?? '”'; |
866 | $smartSinglequoteOpen = $this->options['smarty']['substitutions']['left-single-quote'] ?? '‘'; |
867 | $smartSinglequoteClose = $this->options['smarty']['substitutions']['right-single-quote'] ?? '’'; |
868 | |
869 | $leftAngleQuote = $this->options['smarty']['substitutions']['left-angle-quote'] ?? '«'; |
870 | $rightAngleQuote = $this->options['smarty']['substitutions']['right-angle-quote'] ?? '»'; |
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'] ?? '—', |
950 | ], |
951 | ]; |
952 | } |
953 | |
954 | if ($matches[1] === '--') { |
955 | return [ |
956 | 'extent' => \strlen($matches[0]), |
957 | 'element' => [ |
958 | 'rawHtml' => $this->options['smarty']['substitutions']['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'] ?? '…', |
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' => '↩', |
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' => ' ', |
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 | } |