PluginProbe
WP Coder – Insert & Manage Code Snippets / 4.5
WP Coder – Insert & Manage Code Snippets v4.5
4.5.1 1.1 2.3.1 2.3.2 2.4.1 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.1 3.1.1 3.2 3.2.1 3.3 3.4 3.5 3.5.1 All 39 releases
wp-coder / classes / Parsedown / Parsedown.php

Parsedown.php in WP Coder – Insert & Manage Code Snippets 4.5, at classes/Parsedown/Parsedown.php

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