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