PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 1.10.2
Fluent Support – Helpdesk & Customer Support Ticket System v1.10.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.10.2, at app/Services/Parser/Parsedown.php

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