PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.6.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.6.0
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
fluent-community / app / Services / Parsedown.php

Parsedown.php in FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses 2.6.0, at app/Services/Parsedown.php

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