PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.0.10
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.0.10
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / app / Services / Parsedown.php

Parsedown.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.0.10, at app/Services/Parsedown.php

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