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 | } |