PluginProbe
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services / 8.7.1
WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services v8.7.1
8.7.7 8.7.6 8.7.5 8.7.4 8.7.3 8.7.2 8.7.1 8.7.0 8.6.9 8.6.8 8.6.7 8.6.6 8.6.5 8.6.4 8.6.2 8.6.1 8.6.0 8.5.9 8.5.8 8.5.7 8.5.6 8.5.5 8.5.4 8.5.3 8.5.2 All 533 releases
chatbot / includes / openai / Parsedown.php

Parsedown.php in WPBot – AI ChatBot for Live Support, Lead Generation, WordPress Automation, AI Services 8.7.1, at includes/openai/Parsedown.php

1,995 lines 57.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) exit; // Exit if accessed directly
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 if(!class_exists('Qcld_Parsedown')){
16 class Qcld_Parsedown
17 {
18 # ~
19
20 const version = '1.8.0-beta-7';
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 return;
1507 }
1508
1509 protected function inlineStrikethrough($Excerpt)
1510 {
1511 if ( ! isset($Excerpt['text'][1]))
1512 {
1513 return;
1514 }
1515
1516 if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1517 {
1518 return array(
1519 'extent' => strlen($matches[0]),
1520 'element' => array(
1521 'name' => 'del',
1522 'handler' => array(
1523 'function' => 'lineElements',
1524 'argument' => $matches[1],
1525 'destination' => 'elements',
1526 )
1527 ),
1528 );
1529 }
1530 }
1531
1532 protected function inlineUrl($Excerpt)
1533 {
1534 if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1535 {
1536 return;
1537 }
1538
1539 if (strpos($Excerpt['context'], 'http') !== false
1540 and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)
1541 ) {
1542 $url = $matches[0][0];
1543
1544 $Inline = array(
1545 'extent' => strlen($matches[0][0]),
1546 'position' => $matches[0][1],
1547 'element' => array(
1548 'name' => 'a',
1549 'text' => $url,
1550 'attributes' => array(
1551 'href' => $url,
1552 ),
1553 ),
1554 );
1555
1556 return $Inline;
1557 }
1558 }
1559
1560 protected function inlineUrlTag($Excerpt)
1561 {
1562 if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches))
1563 {
1564 $url = $matches[1];
1565
1566 return array(
1567 'extent' => strlen($matches[0]),
1568 'element' => array(
1569 'name' => 'a',
1570 'text' => $url,
1571 'attributes' => array(
1572 'href' => $url,
1573 ),
1574 ),
1575 );
1576 }
1577 }
1578
1579 # ~
1580
1581 protected function unmarkedText($text)
1582 {
1583 $Inline = $this->inlineText($text);
1584 return $this->element($Inline['element']);
1585 }
1586
1587 #
1588 # Handlers
1589 #
1590
1591 protected function handle(array $Element)
1592 {
1593 if (isset($Element['handler']))
1594 {
1595 if (!isset($Element['nonNestables']))
1596 {
1597 $Element['nonNestables'] = array();
1598 }
1599
1600 if (is_string($Element['handler']))
1601 {
1602 $function = $Element['handler'];
1603 $argument = $Element['text'];
1604 unset($Element['text']);
1605 $destination = 'rawHtml';
1606 }
1607 else
1608 {
1609 $function = $Element['handler']['function'];
1610 $argument = $Element['handler']['argument'];
1611 $destination = $Element['handler']['destination'];
1612 }
1613
1614 $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
1615
1616 if ($destination === 'handler')
1617 {
1618 $Element = $this->handle($Element);
1619 }
1620
1621 unset($Element['handler']);
1622 }
1623
1624 return $Element;
1625 }
1626
1627 protected function handleElementRecursive(array $Element)
1628 {
1629 return $this->elementApplyRecursive(array($this, 'handle'), $Element);
1630 }
1631
1632 protected function handleElementsRecursive(array $Elements)
1633 {
1634 return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);
1635 }
1636
1637 protected function elementApplyRecursive($closure, array $Element)
1638 {
1639 $Element = call_user_func($closure, $Element);
1640
1641 if (isset($Element['elements']))
1642 {
1643 $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
1644 }
1645 elseif (isset($Element['element']))
1646 {
1647 $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
1648 }
1649
1650 return $Element;
1651 }
1652
1653 protected function elementApplyRecursiveDepthFirst($closure, array $Element)
1654 {
1655 if (isset($Element['elements']))
1656 {
1657 $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
1658 }
1659 elseif (isset($Element['element']))
1660 {
1661 $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
1662 }
1663
1664 $Element = call_user_func($closure, $Element);
1665
1666 return $Element;
1667 }
1668
1669 protected function elementsApplyRecursive($closure, array $Elements)
1670 {
1671 foreach ($Elements as &$Element)
1672 {
1673 $Element = $this->elementApplyRecursive($closure, $Element);
1674 }
1675
1676 return $Elements;
1677 }
1678
1679 protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
1680 {
1681 foreach ($Elements as &$Element)
1682 {
1683 $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
1684 }
1685
1686 return $Elements;
1687 }
1688
1689 protected function element(array $Element)
1690 {
1691 if ($this->safeMode)
1692 {
1693 $Element = $this->sanitiseElement($Element);
1694 }
1695
1696 # identity map if element has no handler
1697 $Element = $this->handle($Element);
1698
1699 $hasName = isset($Element['name']);
1700
1701 $markup = '';
1702
1703 if ($hasName)
1704 {
1705 $markup .= '<' . $Element['name'];
1706
1707 if (isset($Element['attributes']))
1708 {
1709 foreach ($Element['attributes'] as $name => $value)
1710 {
1711 if ($value === null)
1712 {
1713 continue;
1714 }
1715
1716 $markup .= " $name=\"".self::escape($value).'"';
1717 }
1718 }
1719 }
1720
1721 $permitRawHtml = false;
1722
1723 if (isset($Element['text']))
1724 {
1725 $text = $Element['text'];
1726 }
1727 // very strongly consider an alternative if you're writing an
1728 // extension
1729 elseif (isset($Element['rawHtml']))
1730 {
1731 $text = $Element['rawHtml'];
1732
1733 $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
1734 $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
1735 }
1736
1737 $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
1738
1739 if ($hasContent)
1740 {
1741 $markup .= $hasName ? '>' : '';
1742
1743 if (isset($Element['elements']))
1744 {
1745 $markup .= $this->elements($Element['elements']);
1746 }
1747 elseif (isset($Element['element']))
1748 {
1749 $markup .= $this->element($Element['element']);
1750 }
1751 else
1752 {
1753 if (!$permitRawHtml)
1754 {
1755 $markup .= self::escape($text, true);
1756 }
1757 else
1758 {
1759 $markup .= $text;
1760 }
1761 }
1762
1763 $markup .= $hasName ? '</' . $Element['name'] . '>' : '';
1764 }
1765 elseif ($hasName)
1766 {
1767 $markup .= ' />';
1768 }
1769
1770 return $markup;
1771 }
1772
1773 protected function elements(array $Elements)
1774 {
1775 $markup = '';
1776
1777 $autoBreak = true;
1778
1779 foreach ($Elements as $Element)
1780 {
1781 if (empty($Element))
1782 {
1783 continue;
1784 }
1785
1786 $autoBreakNext = (isset($Element['autobreak'])
1787 ? $Element['autobreak'] : isset($Element['name'])
1788 );
1789 // (autobreak === false) covers both sides of an element
1790 $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;
1791
1792 $markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
1793 $autoBreak = $autoBreakNext;
1794 }
1795
1796 $markup .= $autoBreak ? "\n" : '';
1797
1798 return $markup;
1799 }
1800
1801 # ~
1802
1803 protected function li($lines)
1804 {
1805 $Elements = $this->linesElements($lines);
1806
1807 if ( ! in_array('', $lines)
1808 and isset($Elements[0]) and isset($Elements[0]['name'])
1809 and $Elements[0]['name'] === 'p'
1810 ) {
1811 unset($Elements[0]['name']);
1812 }
1813
1814 return $Elements;
1815 }
1816
1817 #
1818 # AST Convenience
1819 #
1820
1821 /**
1822 * Replace occurrences $regexp with $Elements in $text. Return an array of
1823 * elements representing the replacement.
1824 */
1825 protected static function pregReplaceElements($regexp, $Elements, $text)
1826 {
1827 $newElements = array();
1828
1829 while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE))
1830 {
1831 $offset = $matches[0][1];
1832 $before = substr($text, 0, $offset);
1833 $after = substr($text, $offset + strlen($matches[0][0]));
1834
1835 $newElements[] = array('text' => $before);
1836
1837 foreach ($Elements as $Element)
1838 {
1839 $newElements[] = $Element;
1840 }
1841
1842 $text = $after;
1843 }
1844
1845 $newElements[] = array('text' => $text);
1846
1847 return $newElements;
1848 }
1849
1850 #
1851 # Deprecated Methods
1852 #
1853
1854 function parse($text)
1855 {
1856 $markup = $this->text($text);
1857
1858 return $markup;
1859 }
1860
1861 protected function sanitiseElement(array $Element)
1862 {
1863 static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
1864 static $safeUrlNameToAtt = array(
1865 'a' => 'href',
1866 'img' => 'src',
1867 );
1868
1869 if ( ! isset($Element['name']))
1870 {
1871 unset($Element['attributes']);
1872 return $Element;
1873 }
1874
1875 if (isset($safeUrlNameToAtt[$Element['name']]))
1876 {
1877 $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
1878 }
1879
1880 if ( ! empty($Element['attributes']))
1881 {
1882 foreach ($Element['attributes'] as $att => $val)
1883 {
1884 # filter out badly parsed attribute
1885 if ( ! preg_match($goodAttribute, $att))
1886 {
1887 unset($Element['attributes'][$att]);
1888 }
1889 # dump onevent attribute
1890 elseif (self::striAtStart($att, 'on'))
1891 {
1892 unset($Element['attributes'][$att]);
1893 }
1894 }
1895 }
1896
1897 return $Element;
1898 }
1899
1900 protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
1901 {
1902 foreach ($this->safeLinksWhitelist as $scheme)
1903 {
1904 if (self::striAtStart($Element['attributes'][$attribute], $scheme))
1905 {
1906 return $Element;
1907 }
1908 }
1909
1910 $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
1911
1912 return $Element;
1913 }
1914
1915 #
1916 # Static Methods
1917 #
1918
1919 protected static function escape($text, $allowQuotes = false)
1920 {
1921 return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
1922 }
1923
1924 protected static function striAtStart($string, $needle)
1925 {
1926 $len = strlen($needle);
1927
1928 if ($len > strlen($string))
1929 {
1930 return false;
1931 }
1932 else
1933 {
1934 return strtolower(substr($string, 0, $len)) === strtolower($needle);
1935 }
1936 }
1937
1938 static function instance($name = 'default')
1939 {
1940 if (isset(self::$instances[$name]))
1941 {
1942 return self::$instances[$name];
1943 }
1944
1945 $instance = new static();
1946
1947 self::$instances[$name] = $instance;
1948
1949 return $instance;
1950 }
1951
1952 private static $instances = array();
1953
1954 #
1955 # Fields
1956 #
1957
1958 protected $DefinitionData;
1959
1960 #
1961 # Read-Only
1962
1963 protected $specialCharacters = array(
1964 '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~'
1965 );
1966
1967 protected $StrongRegex = array(
1968 '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
1969 '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
1970 );
1971
1972 protected $EmRegex = array(
1973 '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1974 '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1975 );
1976
1977 protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
1978
1979 protected $voidElements = array(
1980 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1981 );
1982
1983 protected $textLevelElements = array(
1984 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1985 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1986 'i', 'rp', 'del', 'code', 'strike', 'marquee',
1987 'q', 'rt', 'ins', 'font', 'strong',
1988 's', 'tt', 'kbd', 'mark',
1989 'u', 'xm', 'sub', 'nobr',
1990 'sup', 'ruby',
1991 'var', 'span',
1992 'wbr', 'time',
1993 );
1994 }
1995 }