PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.3
4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 All 201 releases
betterdocs / dependencies / vendor / erusev / parsedown / Parsedown.php

Parsedown.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.3, at dependencies/vendor/erusev/parsedown/Parsedown.php

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