PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 1.4.2
Fluent Support – Helpdesk & Customer Support Ticket System v1.4.2
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
fluent-support / app / Services / Parser / Parsedown.php

Parsedown.php in Fluent Support – Helpdesk & Customer Support Ticket System 1.4.2, at app/Services/Parser/Parsedown.php

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