PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.7.5
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.7.5
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
fluent-community / app / Services / Parsedown.php

Parsedown.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.7.5, at app/Services/Parsedown.php

1,846 lines 49.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable -- Third-party library (Parsedown by Emanuil Rusev). Not subject to project coding standards.
3
4 namespace FluentCommunity\App\Services;
5 #
6 #
7 # Parsedown
8 # http://parsedown.org
9 #
10 # (c) Emanuil Rusev
11 # http://erusev.com
12 #
13 # For the full license information, view the LICENSE file that was distributed
14 # with this source code.
15 #
16 #
17
18 class Parsedown
19 {
20 # ~
21
22 const version = '1.8.0-beta-7';
23
24
25 protected $breaksEnabled;
26
27
28 protected $markupEscaped;
29
30
31 protected $urlsLinked = true;
32
33
34 protected $safeMode;
35
36
37 protected $strictMode;
38
39 protected $safeLinksWhitelist = array(
40 'http://',
41 'https://',
42 'ftp://',
43 'ftps://',
44 'mailto:',
45 'tel:',
46 'data:image/png;base64,',
47 'data:image/gif;base64,',
48 'data:image/jpeg;base64,',
49 'irc:',
50 'ircs:',
51 'git:',
52 'ssh:',
53 'news:',
54 'steam:',
55 );
56
57 #
58 # Lines
59 #
60
61 protected $BlockTypes = array(
62 '#' => array( 'Header' ),
63 '*' => array( 'Rule', 'List' ),
64 '+' => array( 'List' ),
65 '-' => array( 'SetextHeader', 'Table', 'Rule', 'List' ),
66 '0' => array( 'List' ),
67 '1' => array( 'List' ),
68 '2' => array( 'List' ),
69 '3' => array( 'List' ),
70 '4' => array( 'List' ),
71 '5' => array( 'List' ),
72 '6' => array( 'List' ),
73 '7' => array( 'List' ),
74 '8' => array( 'List' ),
75 '9' => array( 'List' ),
76 ':' => array( 'Table' ),
77 '<' => array( 'Comment', 'Markup' ),
78 '=' => array( 'SetextHeader' ),
79 '>' => array( 'Quote' ),
80 '[' => array( 'Reference' ),
81 '_' => array( 'Rule' ),
82 '`' => array( 'FencedCode' ),
83 '|' => array( 'Table' ),
84 '~' => array( 'FencedCode' ),
85 );
86
87 # ~
88
89 protected $unmarkedBlockTypes = array(
90 'Code',
91 );
92
93 public $minHeaderLevel = 0;
94
95 public function __construct($config = [])
96 {
97
98 $config = wp_parse_args($config, [
99 'minHeaderLevel' => 0,
100 ]);
101
102 $this->minHeaderLevel = $config['minHeaderLevel'];
103 }
104
105 # ~
106
107 function text($text)
108 {
109 $Elements = $this->textElements($text);
110
111 # convert to markup
112 $markup = $this->elements($Elements);
113
114 # trim line breaks
115 $markup = trim($markup, "\n");
116
117 return $markup;
118 }
119
120 protected function textElements($text)
121 {
122 # make sure no definitions are set
123 $this->DefinitionData = array();
124
125 # standardize line breaks
126 $text = str_replace(array( "\r\n", "\r" ), "\n", $text);
127
128 # remove surrounding line breaks
129 $text = trim($text, "\n");
130
131 # split text into lines
132 $lines = explode("\n", $text);
133
134 # iterate through lines to identify blocks
135 return $this->linesElements($lines);
136 }
137
138 #
139 # Setters
140 #
141
142 function setBreaksEnabled($breaksEnabled)
143 {
144 $this->breaksEnabled = $breaksEnabled;
145
146 return $this;
147 }
148
149 function setMarkupEscaped($markupEscaped)
150 {
151 $this->markupEscaped = $markupEscaped;
152
153 return $this;
154 }
155
156
157 function setUrlsLinked($urlsLinked)
158 {
159 $this->urlsLinked = $urlsLinked;
160
161 return $this;
162 }
163
164 function setSafeMode($safeMode)
165 {
166 $this->safeMode = (bool)$safeMode;
167
168 return $this;
169 }
170
171 function setStrictMode($strictMode)
172 {
173 $this->strictMode = (bool)$strictMode;
174
175 return $this;
176 }
177
178 #
179 # Blocks
180 #
181
182 protected function lines(array $lines)
183 {
184 return $this->elements($this->linesElements($lines));
185 }
186
187 protected function linesElements(array $lines)
188 {
189 $Elements = array();
190 $CurrentBlock = null;
191
192 foreach ($lines as $line) {
193 if (chop($line) === '') {
194 if (isset($CurrentBlock)) {
195 $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
196 ? $CurrentBlock['interrupted'] + 1 : 1
197 );
198 }
199
200 continue;
201 }
202
203 while (($beforeTab = strstr($line, "\t", true)) !== false) {
204 $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;
205
206 $line = $beforeTab
207 . str_repeat(' ', $shortage)
208 . substr($line, strlen($beforeTab) + 1);
209 }
210
211 $indent = strspn($line, ' ');
212
213 $text = $indent > 0 ? substr($line, $indent) : $line;
214
215 # ~
216
217 $Line = array( 'body' => $line, 'indent' => $indent, 'text' => $text );
218
219 # ~
220
221 if (isset($CurrentBlock['continuable'])) {
222 $methodName = 'block' . $CurrentBlock['type'] . 'Continue';
223 $Block = $this->$methodName($Line, $CurrentBlock);
224
225 if (isset($Block)) {
226 $CurrentBlock = $Block;
227
228 continue;
229 } elseif ($this->isBlockCompletable($CurrentBlock['type'])) {
230 $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
231 $CurrentBlock = $this->$methodName($CurrentBlock);
232 }
233 }
234
235 # ~
236
237 $marker = $text[0];
238
239 # ~
240
241 $blockTypes = $this->unmarkedBlockTypes;
242
243 if (isset($this->BlockTypes[$marker])) {
244 foreach ($this->BlockTypes[$marker] as $blockType) {
245 $blockTypes [] = $blockType;
246 }
247 }
248
249 #
250 # ~
251
252 foreach ($blockTypes as $blockType) {
253 $Block = $this->{"block$blockType"}($Line, $CurrentBlock);
254
255 if (isset($Block)) {
256 $Block['type'] = $blockType;
257
258 if (!isset($Block['identified'])) {
259 if (isset($CurrentBlock)) {
260 $Elements[] = $this->extractElement($CurrentBlock);
261 }
262
263 $Block['identified'] = true;
264 }
265
266 if ($this->isBlockContinuable($blockType)) {
267 $Block['continuable'] = true;
268 }
269
270 $CurrentBlock = $Block;
271
272 continue 2;
273 }
274 }
275
276 # ~
277
278 if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') {
279 $Block = $this->paragraphContinue($Line, $CurrentBlock);
280 }
281
282 if (isset($Block)) {
283 $CurrentBlock = $Block;
284 } else {
285 if (isset($CurrentBlock)) {
286 $Elements[] = $this->extractElement($CurrentBlock);
287 }
288
289 $CurrentBlock = $this->paragraph($Line);
290
291 $CurrentBlock['identified'] = true;
292 }
293 }
294
295 # ~
296
297 if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) {
298 $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
299 $CurrentBlock = $this->$methodName($CurrentBlock);
300 }
301
302 # ~
303
304 if (isset($CurrentBlock)) {
305 $Elements[] = $this->extractElement($CurrentBlock);
306 }
307
308 # ~
309
310 return $Elements;
311 }
312
313 protected function extractElement(array $Component)
314 {
315 if (!isset($Component['element'])) {
316 if (isset($Component['markup'])) {
317 $Component['element'] = array( 'rawHtml' => $Component['markup'] );
318 } elseif (isset($Component['hidden'])) {
319 $Component['element'] = array();
320 }
321 }
322
323 return $Component['element'];
324 }
325
326 protected function isBlockContinuable($Type)
327 {
328 return method_exists($this, 'block' . $Type . 'Continue');
329 }
330
331 protected function isBlockCompletable($Type)
332 {
333 return method_exists($this, 'block' . $Type . 'Complete');
334 }
335
336 #
337 # Code
338
339 protected function blockCode($Line, $Block = null)
340 {
341 if (isset($Block) and $Block['type'] === 'Paragraph' and !isset($Block['interrupted'])) {
342 return;
343 }
344
345 if ($Line['indent'] >= 4) {
346 $text = substr($Line['body'], 4);
347
348 $Block = array(
349 'element' => array(
350 'name' => 'pre',
351 'element' => array(
352 'name' => 'code',
353 'text' => $text,
354 ),
355 ),
356 );
357
358 return $Block;
359 }
360 }
361
362 protected function blockCodeContinue($Line, $Block)
363 {
364 if ($Line['indent'] >= 4) {
365 if (isset($Block['interrupted'])) {
366 $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
367
368 unset($Block['interrupted']);
369 }
370
371 $Block['element']['element']['text'] .= "\n";
372
373 $text = substr($Line['body'], 4);
374
375 $Block['element']['element']['text'] .= $text;
376
377 return $Block;
378 }
379 }
380
381 protected function blockCodeComplete($Block)
382 {
383 return $Block;
384 }
385
386 #
387 # Comment
388
389 protected function blockComment($Line)
390 {
391 if ($this->markupEscaped or $this->safeMode) {
392 return;
393 }
394
395 if (strpos($Line['text'], '<!--') === 0) {
396 $Block = array(
397 'element' => array(
398 'rawHtml' => $Line['body'],
399 'autobreak' => true,
400 ),
401 );
402
403 if (strpos($Line['text'], '-->') !== false) {
404 $Block['closed'] = true;
405 }
406
407 return $Block;
408 }
409 }
410
411 protected function blockCommentContinue($Line, array $Block)
412 {
413 if (isset($Block['closed'])) {
414 return;
415 }
416
417 $Block['element']['rawHtml'] .= "\n" . $Line['body'];
418
419 if (strpos($Line['text'], '-->') !== false) {
420 $Block['closed'] = true;
421 }
422
423 return $Block;
424 }
425
426 #
427 # Fenced Code
428
429 protected function blockFencedCode($Line)
430 {
431 $marker = $Line['text'][0];
432
433 $openerLength = strspn($Line['text'], $marker);
434
435 if ($openerLength < 3) {
436 return;
437 }
438
439 $infostring = trim(substr($Line['text'], $openerLength), "\t ");
440
441 if (strpos($infostring, '`') !== false) {
442 return;
443 }
444
445 $Element = array(
446 'name' => 'code',
447 'text' => '',
448 );
449
450 if ($infostring !== '') {
451 /**
452 * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
453 * Every HTML element may have a class attribute specified.
454 * The attribute, if specified, must have a value that is a set
455 * of space-separated tokens representing the various classes
456 * that the element belongs to.
457 * [...]
458 * The space characters, for the purposes of this specification,
459 * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
460 * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
461 * U+000D CARRIAGE RETURN (CR).
462 */
463 $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r"));
464
465 $Element['attributes'] = array( 'class' => "language-$language" );
466 }
467
468 $Block = array(
469 'char' => $marker,
470 'openerLength' => $openerLength,
471 'element' => array(
472 'name' => 'pre',
473 'element' => $Element,
474 ),
475 );
476
477 return $Block;
478 }
479
480 protected function blockFencedCodeContinue($Line, $Block)
481 {
482 if (isset($Block['complete'])) {
483 return;
484 }
485
486 if (isset($Block['interrupted'])) {
487 $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
488
489 unset($Block['interrupted']);
490 }
491
492 if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength']
493 and chop(substr($Line['text'], $len), ' ') === ''
494 ) {
495 $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1);
496
497 $Block['complete'] = true;
498
499 return $Block;
500 }
501
502 $Block['element']['element']['text'] .= "\n" . $Line['body'];
503
504 return $Block;
505 }
506
507 protected function blockFencedCodeComplete($Block)
508 {
509 return $Block;
510 }
511
512 #
513 # Header
514
515 protected function blockHeader($Line)
516 {
517 $level = strspn($Line['text'], '#');
518
519 if ($level > 6) {
520 return;
521 }
522
523 if($level <= $this->minHeaderLevel) {
524 return;
525 }
526
527 // Add this line to check for a space after the "#" symbols
528 if (substr($Line['text'], $level, 1) !== ' ') {
529 return;
530 }
531
532 $text = trim($Line['text'], '#');
533
534 if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') {
535 return;
536 }
537
538 $text = trim($text, ' ');
539
540 $Block = array(
541 'element' => array(
542 'name' => 'h' . $level,
543 'handler' => array(
544 'function' => 'lineElements',
545 'argument' => $text,
546 'destination' => 'elements',
547 ),
548 ),
549 );
550
551 return $Block;
552 }
553
554 #
555 # List
556
557 protected function blockList($Line, array $CurrentBlock = null)
558 {
559 list($name, $pattern) = $Line['text'][0] <= '-' ? array( 'ul', '[*+-]' ) : array( 'ol', '[0-9]{1,9}+[.\)]' );
560
561 if (preg_match('/^(' . $pattern . '([ ]++|$))(.*+)/', $Line['text'], $matches)) {
562 $contentIndent = strlen($matches[2]);
563
564 if ($contentIndent >= 5) {
565 $contentIndent -= 1;
566 $matches[1] = substr($matches[1], 0, -$contentIndent);
567 $matches[3] = str_repeat(' ', $contentIndent) . $matches[3];
568 } elseif ($contentIndent === 0) {
569 $matches[1] .= ' ';
570 }
571
572 $markerWithoutWhitespace = strstr($matches[1], ' ', true);
573
574 $Block = array(
575 'indent' => $Line['indent'],
576 'pattern' => $pattern,
577 'data' => array(
578 'type' => $name,
579 'marker' => $matches[1],
580 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)),
581 ),
582 'element' => array(
583 'name' => $name,
584 'elements' => array(),
585 ),
586 );
587 $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/');
588
589 if ($name === 'ol') {
590 $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
591
592 if ($listStart !== '1') {
593 if (
594 isset($CurrentBlock)
595 and $CurrentBlock['type'] === 'Paragraph'
596 and !isset($CurrentBlock['interrupted'])
597 ) {
598 return;
599 }
600
601 $Block['element']['attributes'] = array( 'start' => $listStart );
602 }
603 }
604
605 $Block['li'] = array(
606 'name' => 'li',
607 'handler' => array(
608 'function' => 'li',
609 'argument' => !empty($matches[3]) ? array( $matches[3] ) : array(),
610 'destination' => 'elements',
611 ),
612 );
613
614 $Block['element']['elements'] [] = &$Block['li'];
615
616 return $Block;
617 }
618 }
619
620 protected function blockListContinue($Line, array $Block)
621 {
622 if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) {
623 return null;
624 }
625
626 $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker']));
627
628 if ($Line['indent'] < $requiredIndent
629 and (
630 (
631 $Block['data']['type'] === 'ol'
632 and preg_match('/^[0-9]++' . $Block['data']['markerTypeRegex'] . '(?:[ ]++(.*)|$)/', $Line['text'], $matches)
633 ) or (
634 $Block['data']['type'] === 'ul'
635 and preg_match('/^' . $Block['data']['markerTypeRegex'] . '(?:[ ]++(.*)|$)/', $Line['text'], $matches)
636 )
637 )
638 ) {
639 if (isset($Block['interrupted'])) {
640 $Block['li']['handler']['argument'] [] = '';
641
642 $Block['loose'] = true;
643
644 unset($Block['interrupted']);
645 }
646
647 unset($Block['li']);
648
649 $text = isset($matches[1]) ? $matches[1] : '';
650
651 $Block['indent'] = $Line['indent'];
652
653 $Block['li'] = array(
654 'name' => 'li',
655 'handler' => array(
656 'function' => 'li',
657 'argument' => array( $text ),
658 'destination' => 'elements',
659 ),
660 );
661
662 $Block['element']['elements'] [] = &$Block['li'];
663
664 return $Block;
665 } elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) {
666 return null;
667 }
668
669 if ($Line['text'][0] === '[' and $this->blockReference($Line)) {
670 return $Block;
671 }
672
673 if ($Line['indent'] >= $requiredIndent) {
674 if (isset($Block['interrupted'])) {
675 $Block['li']['handler']['argument'] [] = '';
676
677 $Block['loose'] = true;
678
679 unset($Block['interrupted']);
680 }
681
682 $text = substr($Line['body'], $requiredIndent);
683
684 $Block['li']['handler']['argument'] [] = $text;
685
686 return $Block;
687 }
688
689 if (!isset($Block['interrupted'])) {
690 $text = preg_replace('/^[ ]{0,' . $requiredIndent . '}+/', '', $Line['body']);
691
692 $Block['li']['handler']['argument'] [] = $text;
693
694 return $Block;
695 }
696 }
697
698 protected function blockListComplete(array $Block)
699 {
700 if (isset($Block['loose'])) {
701 foreach ($Block['element']['elements'] as &$li) {
702 if (end($li['handler']['argument']) !== '') {
703 $li['handler']['argument'] [] = '';
704 }
705 }
706 }
707
708 return $Block;
709 }
710
711 #
712 # Quote
713
714 protected function blockQuote($Line)
715 {
716 if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) {
717 $Block = array(
718 'element' => array(
719 'name' => 'blockquote',
720 'handler' => array(
721 'function' => 'linesElements',
722 'argument' => (array)$matches[1],
723 'destination' => 'elements',
724 ),
725 ),
726 );
727
728 return $Block;
729 }
730 }
731
732 protected function blockQuoteContinue($Line, array $Block)
733 {
734 if (isset($Block['interrupted'])) {
735 return;
736 }
737
738 if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) {
739 $Block['element']['handler']['argument'] [] = $matches[1];
740
741 return $Block;
742 }
743
744 if (!isset($Block['interrupted'])) {
745 $Block['element']['handler']['argument'] [] = $Line['text'];
746
747 return $Block;
748 }
749 }
750
751 #
752 # Rule
753
754 protected function blockRule($Line)
755 {
756 $marker = $Line['text'][0];
757
758 if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') {
759 $Block = array(
760 'element' => array(
761 'name' => 'hr',
762 ),
763 );
764
765 return $Block;
766 }
767 }
768
769 #
770 # Setext
771
772 protected function blockSetextHeader($Line, array $Block = null)
773 {
774 if (!isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) {
775 return;
776 }
777
778 if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') {
779 $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
780
781 return $Block;
782 }
783 }
784
785 #
786 # Markup
787
788 protected function blockMarkup($Line)
789 {
790 if ($this->markupEscaped or $this->safeMode) {
791 return;
792 }
793
794 if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+' . $this->regexHtmlAttribute . ')*+[ ]*+(\/)?>/', $Line['text'], $matches)) {
795 $element = strtolower($matches[1]);
796
797 if (in_array($element, $this->textLevelElements)) {
798 return;
799 }
800
801 $Block = array(
802 'name' => $matches[1],
803 'element' => array(
804 'rawHtml' => $Line['text'],
805 'autobreak' => true,
806 ),
807 );
808
809 return $Block;
810 }
811 }
812
813 protected function blockMarkupContinue($Line, array $Block)
814 {
815 if (isset($Block['closed']) or isset($Block['interrupted'])) {
816 return;
817 }
818
819 $Block['element']['rawHtml'] .= "\n" . $Line['body'];
820
821 return $Block;
822 }
823
824 #
825 # Reference
826
827 protected function blockReference($Line)
828 {
829 if (strpos($Line['text'], ']') !== false
830 and preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
831 ) {
832 $id = strtolower($matches[1]);
833
834 $Data = array(
835 'url' => $matches[2],
836 'title' => isset($matches[3]) ? $matches[3] : null,
837 );
838
839 $this->DefinitionData['Reference'][$id] = $Data;
840
841 $Block = array(
842 'element' => array(),
843 );
844
845 return $Block;
846 }
847 }
848
849 #
850 # Table
851
852 protected function blockTable($Line, array $Block = null)
853 {
854 if (!isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) {
855 return;
856 }
857
858 if (
859 strpos($Block['element']['handler']['argument'], '|') === false
860 and strpos($Line['text'], '|') === false
861 and strpos($Line['text'], ':') === false
862 or strpos($Block['element']['handler']['argument'], "\n") !== false
863 ) {
864 return;
865 }
866
867 if (chop($Line['text'], ' -:|') !== '') {
868 return;
869 }
870
871 $alignments = array();
872
873 $divider = $Line['text'];
874
875 $divider = trim($divider);
876 $divider = trim($divider, '|');
877
878 $dividerCells = explode('|', $divider);
879
880 foreach ($dividerCells as $dividerCell) {
881 $dividerCell = trim($dividerCell);
882
883 if ($dividerCell === '') {
884 return;
885 }
886
887 $alignment = null;
888
889 if ($dividerCell[0] === ':') {
890 $alignment = 'left';
891 }
892
893 if (substr($dividerCell, -1) === ':') {
894 $alignment = $alignment === 'left' ? 'center' : 'right';
895 }
896
897 $alignments [] = $alignment;
898 }
899
900 # ~
901
902 $HeaderElements = array();
903
904 $header = $Block['element']['handler']['argument'];
905
906 $header = trim($header);
907 $header = trim($header, '|');
908
909 $headerCells = explode('|', $header);
910
911 if (count($headerCells) !== count($alignments)) {
912 return;
913 }
914
915 foreach ($headerCells as $index => $headerCell) {
916 $headerCell = trim($headerCell);
917
918 $HeaderElement = array(
919 'name' => 'th',
920 'handler' => array(
921 'function' => 'lineElements',
922 'argument' => $headerCell,
923 'destination' => 'elements',
924 ),
925 );
926
927 if (isset($alignments[$index])) {
928 $alignment = $alignments[$index];
929
930 $HeaderElement['attributes'] = array(
931 'style' => "text-align: $alignment;",
932 );
933 }
934
935 $HeaderElements [] = $HeaderElement;
936 }
937
938 # ~
939
940 $Block = array(
941 'alignments' => $alignments,
942 'identified' => true,
943 'element' => array(
944 'name' => 'table',
945 'elements' => array(),
946 ),
947 );
948
949 $Block['element']['elements'] [] = array(
950 'name' => 'thead',
951 );
952
953 $Block['element']['elements'] [] = array(
954 'name' => 'tbody',
955 'elements' => array(),
956 );
957
958 $Block['element']['elements'][0]['elements'] [] = array(
959 'name' => 'tr',
960 'elements' => $HeaderElements,
961 );
962
963 return $Block;
964 }
965
966 protected function blockTableContinue($Line, array $Block)
967 {
968 if (isset($Block['interrupted'])) {
969 return;
970 }
971
972 if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) {
973 $Elements = array();
974
975 $row = $Line['text'];
976
977 $row = trim($row);
978 $row = trim($row, '|');
979
980 preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
981
982 $cells = array_slice($matches[0], 0, count($Block['alignments']));
983
984 foreach ($cells as $index => $cell) {
985 $cell = trim($cell);
986
987 $Element = array(
988 'name' => 'td',
989 'handler' => array(
990 'function' => 'lineElements',
991 'argument' => $cell,
992 'destination' => 'elements',
993 ),
994 );
995
996 if (isset($Block['alignments'][$index])) {
997 $Element['attributes'] = array(
998 'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
999 );
1000 }
1001
1002 $Elements [] = $Element;
1003 }
1004
1005 $Element = array(
1006 'name' => 'tr',
1007 'elements' => $Elements,
1008 );
1009
1010 $Block['element']['elements'][1]['elements'] [] = $Element;
1011
1012 return $Block;
1013 }
1014 }
1015
1016 #
1017 # ~
1018 #
1019
1020 protected function paragraph($Line)
1021 {
1022 return array(
1023 'type' => 'Paragraph',
1024 'element' => array(
1025 'name' => 'p',
1026 'handler' => array(
1027 'function' => 'lineElements',
1028 'argument' => $Line['text'],
1029 'destination' => 'elements',
1030 ),
1031 ),
1032 );
1033 }
1034
1035 protected function paragraphContinue($Line, array $Block)
1036 {
1037 if (isset($Block['interrupted'])) {
1038 return;
1039 }
1040
1041 $Block['element']['handler']['argument'] .= "\n" . $Line['text'];
1042
1043 return $Block;
1044 }
1045
1046 #
1047 # Inline Elements
1048 #
1049
1050 protected $InlineTypes = array(
1051 '!' => array( 'Image' ),
1052 '&' => array( 'SpecialCharacter' ),
1053 '*' => array( 'Emphasis' ),
1054 ':' => array( 'Url' ),
1055 '<' => array( 'UrlTag', 'EmailTag', 'Markup' ),
1056 '[' => array( 'Link' ),
1057 '_' => array( 'Emphasis' ),
1058 '`' => array( 'Code' ),
1059 '~' => array( 'Strikethrough' ),
1060 '\\' => array( 'EscapeSequence' ),
1061 );
1062
1063 # ~
1064
1065 protected $inlineMarkerList = '!*_&[:<`~\\';
1066
1067 #
1068 # ~
1069 #
1070
1071 public function line($text, $nonNestables = array())
1072 {
1073 return $this->elements($this->lineElements($text, $nonNestables));
1074 }
1075
1076 protected function lineElements($text, $nonNestables = array())
1077 {
1078 # standardize line breaks
1079 $text = str_replace(array( "\r\n", "\r" ), "\n", $text);
1080
1081 $Elements = array();
1082
1083 $nonNestables = (empty($nonNestables)
1084 ? array()
1085 : array_combine($nonNestables, $nonNestables)
1086 );
1087
1088 # $excerpt is based on the first occurrence of a marker
1089
1090 while ($excerpt = strpbrk($text, $this->inlineMarkerList)) {
1091 $marker = $excerpt[0];
1092
1093 $markerPosition = strlen($text) - strlen($excerpt);
1094
1095 $Excerpt = array( 'text' => $excerpt, 'context' => $text );
1096
1097 foreach ($this->InlineTypes[$marker] as $inlineType) {
1098 # check to see if the current inline type is nestable in the current context
1099
1100 if (isset($nonNestables[$inlineType])) {
1101 continue;
1102 }
1103
1104 $Inline = $this->{"inline$inlineType"}($Excerpt);
1105
1106 if (!isset($Inline)) {
1107 continue;
1108 }
1109
1110 # makes sure that the inline belongs to "our" marker
1111
1112 if (isset($Inline['position']) and $Inline['position'] > $markerPosition) {
1113 continue;
1114 }
1115
1116 # sets a default inline position
1117
1118 if (!isset($Inline['position'])) {
1119 $Inline['position'] = $markerPosition;
1120 }
1121
1122 # cause the new element to 'inherit' our non nestables
1123
1124
1125 $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
1126 ? array_merge($Inline['element']['nonNestables'], $nonNestables)
1127 : $nonNestables;
1128
1129 # the text that comes before the inline
1130 $unmarkedText = substr($text, 0, $Inline['position']);
1131
1132 # compile the unmarked text
1133 $InlineText = $this->inlineText($unmarkedText);
1134 $Elements[] = $InlineText['element'];
1135
1136 # compile the inline
1137 $Elements[] = $this->extractElement($Inline);
1138
1139 # remove the examined text
1140 $text = substr($text, $Inline['position'] + $Inline['extent']);
1141
1142 continue 2;
1143 }
1144
1145 # the marker does not belong to an inline
1146
1147 $unmarkedText = substr($text, 0, $markerPosition + 1);
1148
1149 $InlineText = $this->inlineText($unmarkedText);
1150 $Elements[] = $InlineText['element'];
1151
1152 $text = substr($text, $markerPosition + 1);
1153 }
1154
1155 $InlineText = $this->inlineText($text);
1156 $Elements[] = $InlineText['element'];
1157
1158 foreach ($Elements as &$Element) {
1159 if (!isset($Element['autobreak'])) {
1160 $Element['autobreak'] = false;
1161 }
1162 }
1163
1164 return $Elements;
1165 }
1166
1167 #
1168 # ~
1169 #
1170
1171 protected function inlineText($text)
1172 {
1173 $Inline = array(
1174 'extent' => strlen($text),
1175 'element' => array(),
1176 );
1177
1178 $Inline['element']['elements'] = self::pregReplaceElements(
1179 $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
1180 array(
1181 array( 'name' => 'br' ),
1182 array( 'text' => "\n" ),
1183 ),
1184 $text
1185 );
1186
1187 return $Inline;
1188 }
1189
1190 protected function inlineCode($Excerpt)
1191 {
1192 $marker = $Excerpt['text'][0];
1193
1194 if (preg_match('/^([' . $marker . ']++)[ ]*+(.+?)[ ]*+(?<![' . $marker . '])\1(?!' . $marker . ')/s', $Excerpt['text'], $matches)) {
1195 $text = $matches[2];
1196 $text = preg_replace('/[ ]*+\n/', ' ', $text);
1197
1198 return array(
1199 'extent' => strlen($matches[0]),
1200 'element' => array(
1201 'name' => 'code',
1202 'text' => $text,
1203 ),
1204 );
1205 }
1206 }
1207
1208 protected function inlineEmailTag($Excerpt)
1209 {
1210 $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
1211
1212 $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
1213 . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
1214
1215 if (strpos($Excerpt['text'], '>') !== false
1216 and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches)
1217 ) {
1218 $url = $matches[1];
1219
1220 if (!isset($matches[2])) {
1221 $url = "mailto:$url";
1222 }
1223
1224 return array(
1225 'extent' => strlen($matches[0]),
1226 'element' => array(
1227 'name' => 'a',
1228 'text' => $matches[1],
1229 'attributes' => array(
1230 'href' => $url,
1231 ),
1232 ),
1233 );
1234 }
1235 }
1236
1237 protected function inlineEmphasis($Excerpt)
1238 {
1239 if (!isset($Excerpt['text'][1])) {
1240 return;
1241 }
1242
1243 $marker = $Excerpt['text'][0];
1244
1245 if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) {
1246 $emphasis = 'strong';
1247 } elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) {
1248 $emphasis = 'em';
1249 } else {
1250 return;
1251 }
1252
1253 return array(
1254 'extent' => strlen($matches[0]),
1255 'element' => array(
1256 'name' => $emphasis,
1257 'handler' => array(
1258 'function' => 'lineElements',
1259 'argument' => $matches[1],
1260 'destination' => 'elements',
1261 ),
1262 ),
1263 );
1264 }
1265
1266 protected function inlineEscapeSequence($Excerpt)
1267 {
1268 if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) {
1269 return array(
1270 'element' => array( 'rawHtml' => $Excerpt['text'][1] ),
1271 'extent' => 2,
1272 );
1273 }
1274 }
1275
1276 protected function inlineImage($Excerpt)
1277 {
1278 if (!isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') {
1279 return;
1280 }
1281
1282 $Excerpt['text'] = substr($Excerpt['text'], 1);
1283
1284 $Link = $this->inlineLink($Excerpt);
1285
1286 if ($Link === null) {
1287 return;
1288 }
1289
1290 $Inline = array(
1291 'extent' => $Link['extent'] + 1,
1292 'element' => array(
1293 'name' => 'img',
1294 'attributes' => array(
1295 'src' => $Link['element']['attributes']['href'],
1296 'alt' => $Link['element']['handler']['argument'],
1297 ),
1298 'autobreak' => true,
1299 ),
1300 );
1301
1302 $Inline['element']['attributes'] += $Link['element']['attributes'];
1303
1304 unset($Inline['element']['attributes']['href']);
1305
1306 return $Inline;
1307 }
1308
1309 protected function inlineLink($Excerpt)
1310 {
1311 $Element = array(
1312 'name' => 'a',
1313 'handler' => array(
1314 'function' => 'lineElements',
1315 'argument' => null,
1316 'destination' => 'elements',
1317 ),
1318 'nonNestables' => array( 'Url', 'Link' ),
1319 'attributes' => array(
1320 'href' => null,
1321 'title' => null,
1322 ),
1323 );
1324
1325 $extent = 0;
1326
1327 $remainder = $Excerpt['text'];
1328
1329 if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
1330 $Element['handler']['argument'] = $matches[1];
1331
1332 $extent += strlen($matches[0]);
1333
1334 $remainder = substr($remainder, $extent);
1335 } else {
1336 return;
1337 }
1338
1339 if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) {
1340 $Element['attributes']['href'] = $matches[1];
1341
1342 if (isset($matches[2])) {
1343 $Element['attributes']['title'] = substr($matches[2], 1, -1);
1344 }
1345
1346 $extent += strlen($matches[0]);
1347 } else {
1348 if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
1349 $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument'];
1350 $definition = strtolower($definition);
1351
1352 $extent += strlen($matches[0]);
1353 } else {
1354 $definition = strtolower($Element['handler']['argument']);
1355 }
1356
1357 if (!isset($this->DefinitionData['Reference'][$definition])) {
1358 return;
1359 }
1360
1361 $Definition = $this->DefinitionData['Reference'][$definition];
1362
1363 $Element['attributes']['href'] = $Definition['url'];
1364 $Element['attributes']['title'] = $Definition['title'];
1365 }
1366
1367 return array(
1368 'extent' => $extent,
1369 'element' => $Element,
1370 );
1371 }
1372
1373 protected function inlineMarkup($Excerpt)
1374 {
1375 if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) {
1376 return;
1377 }
1378
1379 if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) {
1380 return array(
1381 'element' => array( 'rawHtml' => $matches[0] ),
1382 'extent' => strlen($matches[0]),
1383 );
1384 }
1385
1386 if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches)) {
1387 return array(
1388 'element' => array( 'rawHtml' => $matches[0] ),
1389 'extent' => strlen($matches[0]),
1390 );
1391 }
1392
1393 if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+' . $this->regexHtmlAttribute . ')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) {
1394 return array(
1395 'element' => array( 'rawHtml' => $matches[0] ),
1396 'extent' => strlen($matches[0]),
1397 );
1398 }
1399 }
1400
1401 protected function inlineSpecialCharacter($Excerpt)
1402 {
1403 if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false
1404 and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
1405 ) {
1406 return array(
1407 'element' => array( 'rawHtml' => '&' . $matches[1] . ';' ),
1408 'extent' => strlen($matches[0]),
1409 );
1410 }
1411
1412 return;
1413 }
1414
1415 protected function inlineStrikethrough($Excerpt)
1416 {
1417 if (!isset($Excerpt['text'][1])) {
1418 return;
1419 }
1420
1421 if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) {
1422 return array(
1423 'extent' => strlen($matches[0]),
1424 'element' => array(
1425 'name' => 'del',
1426 'handler' => array(
1427 'function' => 'lineElements',
1428 'argument' => $matches[1],
1429 'destination' => 'elements',
1430 ),
1431 ),
1432 );
1433 }
1434 }
1435
1436 protected function inlineUrl($Excerpt)
1437 {
1438 if ($this->urlsLinked !== true or !isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') {
1439 return;
1440 }
1441
1442 if (strpos($Excerpt['context'], 'http') !== false
1443 and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)
1444 ) {
1445 $url = $matches[0][0];
1446
1447 $Inline = array(
1448 'extent' => strlen($matches[0][0]),
1449 'position' => $matches[0][1],
1450 'element' => array(
1451 'name' => 'a',
1452 'text' => $url,
1453 'attributes' => array(
1454 'href' => $url,
1455 ),
1456 ),
1457 );
1458
1459 return $Inline;
1460 }
1461 }
1462
1463 protected function inlineUrlTag($Excerpt)
1464 {
1465 if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) {
1466 $url = $matches[1];
1467
1468 return array(
1469 'extent' => strlen($matches[0]),
1470 'element' => array(
1471 'name' => 'a',
1472 'text' => $url,
1473 'attributes' => array(
1474 'href' => $url,
1475 ),
1476 ),
1477 );
1478 }
1479 }
1480
1481 # ~
1482
1483 protected function unmarkedText($text)
1484 {
1485 $Inline = $this->inlineText($text);
1486 return $this->element($Inline['element']);
1487 }
1488
1489 #
1490 # Handlers
1491 #
1492
1493 protected function handle(array $Element)
1494 {
1495 if (isset($Element['handler'])) {
1496 if (!isset($Element['nonNestables'])) {
1497 $Element['nonNestables'] = array();
1498 }
1499
1500 if (is_string($Element['handler'])) {
1501 $function = $Element['handler'];
1502 $argument = $Element['text'];
1503 unset($Element['text']);
1504 $destination = 'rawHtml';
1505 } else {
1506 $function = $Element['handler']['function'];
1507 $argument = $Element['handler']['argument'];
1508 $destination = $Element['handler']['destination'];
1509 }
1510
1511 $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
1512
1513 if ($destination === 'handler') {
1514 $Element = $this->handle($Element);
1515 }
1516
1517 unset($Element['handler']);
1518 }
1519
1520 return $Element;
1521 }
1522
1523 protected function handleElementRecursive(array $Element)
1524 {
1525 return $this->elementApplyRecursive(array( $this, 'handle' ), $Element);
1526 }
1527
1528 protected function handleElementsRecursive(array $Elements)
1529 {
1530 return $this->elementsApplyRecursive(array( $this, 'handle' ), $Elements);
1531 }
1532
1533 protected function elementApplyRecursive($closure, array $Element)
1534 {
1535 $Element = call_user_func($closure, $Element);
1536
1537 if (isset($Element['elements'])) {
1538 $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
1539 } elseif (isset($Element['element'])) {
1540 $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
1541 }
1542
1543 return $Element;
1544 }
1545
1546 protected function elementApplyRecursiveDepthFirst($closure, array $Element)
1547 {
1548 if (isset($Element['elements'])) {
1549 $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
1550 } elseif (isset($Element['element'])) {
1551 $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
1552 }
1553
1554 $Element = call_user_func($closure, $Element);
1555
1556 return $Element;
1557 }
1558
1559 protected function elementsApplyRecursive($closure, array $Elements)
1560 {
1561 foreach ($Elements as &$Element) {
1562 $Element = $this->elementApplyRecursive($closure, $Element);
1563 }
1564
1565 return $Elements;
1566 }
1567
1568 protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
1569 {
1570 foreach ($Elements as &$Element) {
1571 $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
1572 }
1573
1574 return $Elements;
1575 }
1576
1577 protected function element(array $Element)
1578 {
1579 if ($this->safeMode) {
1580 $Element = $this->sanitiseElement($Element);
1581 }
1582
1583 # identity map if element has no handler
1584 $Element = $this->handle($Element);
1585
1586 $hasName = isset($Element['name']);
1587
1588 $markup = '';
1589
1590 if ($hasName) {
1591 $markup .= '<' . $Element['name'];
1592
1593 if (isset($Element['attributes'])) {
1594 foreach ($Element['attributes'] as $name => $value) {
1595 if ($value === null) {
1596 continue;
1597 }
1598
1599 $markup .= " $name=\"" . self::escape($value) . '"';
1600 }
1601 }
1602 }
1603
1604 $permitRawHtml = false;
1605
1606 if (isset($Element['text'])) {
1607 $text = $Element['text'];
1608 }
1609 // very strongly consider an alternative if you're writing an
1610 // extension
1611 elseif (isset($Element['rawHtml'])) {
1612 $text = $Element['rawHtml'];
1613
1614 $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
1615 $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
1616 }
1617
1618 $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
1619
1620 if ($hasContent) {
1621 $markup .= $hasName ? '>' : '';
1622
1623 if (isset($Element['elements'])) {
1624 $markup .= $this->elements($Element['elements']);
1625 } elseif (isset($Element['element'])) {
1626 $markup .= $this->element($Element['element']);
1627 } elseif (!$permitRawHtml) {
1628 $markup .= self::escape($text ?? '', true);
1629 } else {
1630 $markup .= $text ?? '';
1631 }
1632
1633 $markup .= $hasName ? '</' . $Element['name'] . '>' : '';
1634 } elseif ($hasName) {
1635 $markup .= ' />';
1636 }
1637
1638 return $markup;
1639 }
1640
1641 protected function elements(array $Elements)
1642 {
1643 $markup = '';
1644
1645 $autoBreak = true;
1646
1647 foreach ($Elements as $Element) {
1648 if (empty($Element)) {
1649 continue;
1650 }
1651
1652 $autoBreakNext = (isset($Element['autobreak'])
1653 ? $Element['autobreak'] : isset($Element['name'])
1654 );
1655 // (autobreak === false) covers both sides of an element
1656 $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;
1657
1658 $markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
1659 $autoBreak = $autoBreakNext;
1660 }
1661
1662 $markup .= $autoBreak ? "\n" : '';
1663
1664 return $markup;
1665 }
1666
1667 # ~
1668
1669 protected function li($lines)
1670 {
1671 $Elements = $this->linesElements($lines);
1672
1673 if (!in_array('', $lines)
1674 and isset($Elements[0]) and isset($Elements[0]['name'])
1675 and $Elements[0]['name'] === 'p'
1676 ) {
1677 unset($Elements[0]['name']);
1678 }
1679
1680 return $Elements;
1681 }
1682
1683 #
1684 # AST Convenience
1685 #
1686
1687 /**
1688 * Replace occurrences $regexp with $Elements in $text. Return an array of
1689 * elements representing the replacement.
1690 */
1691 protected static function pregReplaceElements($regexp, $Elements, $text)
1692 {
1693 $newElements = array();
1694
1695 while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) {
1696 $offset = $matches[0][1];
1697 $before = substr($text, 0, $offset);
1698 $after = substr($text, $offset + strlen($matches[0][0]));
1699
1700 $newElements[] = array( 'text' => $before );
1701
1702 foreach ($Elements as $Element) {
1703 $newElements[] = $Element;
1704 }
1705
1706 $text = $after;
1707 }
1708
1709 $newElements[] = array( 'text' => $text );
1710
1711 return $newElements;
1712 }
1713
1714 #
1715 # Deprecated Methods
1716 #
1717
1718 function parse($text)
1719 {
1720 $markup = $this->text($text);
1721
1722 return $markup;
1723 }
1724
1725 protected function sanitiseElement(array $Element)
1726 {
1727 static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
1728 static $safeUrlNameToAtt = array(
1729 'a' => 'href',
1730 'img' => 'src',
1731 );
1732
1733 if (!isset($Element['name'])) {
1734 unset($Element['attributes']);
1735 return $Element;
1736 }
1737
1738 if (isset($safeUrlNameToAtt[$Element['name']])) {
1739 $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
1740 }
1741
1742 if (!empty($Element['attributes'])) {
1743 foreach ($Element['attributes'] as $att => $val) {
1744 # filter out badly parsed attribute
1745 if (!preg_match($goodAttribute, $att)) {
1746 unset($Element['attributes'][$att]);
1747 } # dump onevent attribute
1748 elseif (self::striAtStart($att, 'on')) {
1749 unset($Element['attributes'][$att]);
1750 }
1751 }
1752 }
1753
1754 return $Element;
1755 }
1756
1757 protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
1758 {
1759 foreach ($this->safeLinksWhitelist as $scheme) {
1760 if (self::striAtStart($Element['attributes'][$attribute], $scheme)) {
1761 return $Element;
1762 }
1763 }
1764
1765 $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
1766
1767 return $Element;
1768 }
1769
1770 #
1771 # Static Methods
1772 #
1773
1774 protected static function escape($text, $allowQuotes = false)
1775 {
1776 return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
1777 }
1778
1779 protected static function striAtStart($string, $needle)
1780 {
1781 $len = strlen($needle);
1782
1783 if ($len > strlen($string)) {
1784 return false;
1785 } else {
1786 return strtolower(substr($string, 0, $len)) === strtolower($needle);
1787 }
1788 }
1789
1790 static function instance($name = 'default')
1791 {
1792 if (isset(self::$instances[$name])) {
1793 return self::$instances[$name];
1794 }
1795
1796 $instance = new static();
1797
1798 self::$instances[$name] = $instance;
1799
1800 return $instance;
1801 }
1802
1803 private static $instances = array();
1804
1805 #
1806 # Fields
1807 #
1808
1809 protected $DefinitionData;
1810
1811 #
1812 # Read-Only
1813
1814 protected $specialCharacters = array(
1815 '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~',
1816 );
1817
1818 protected $StrongRegex = array(
1819 '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
1820 '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
1821 );
1822
1823 protected $EmRegex = array(
1824 '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1825 '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1826 );
1827
1828 protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
1829
1830 protected $voidElements = array(
1831 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1832 );
1833
1834 protected $textLevelElements = array(
1835 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1836 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1837 'i', 'rp', 'del', 'code', 'strike', 'marquee',
1838 'q', 'rt', 'ins', 'font', 'strong',
1839 's', 'tt', 'kbd', 'mark',
1840 'u', 'xm', 'sub', 'nobr',
1841 'sup', 'ruby',
1842 'var', 'span',
1843 'wbr', 'time',
1844 );
1845 }
1846