Parsedown.php (37940B)
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 class Parsedown 17 { 18 # ~ 19 20 const version = '1.6.0'; 21 22 # ~ 23 24 function text($text) 25 { 26 # make sure no definitions are set 27 $this->DefinitionData = array(); 28 29 # standardize line breaks 30 $text = str_replace(array("\r\n", "\r"), "\n", $text); 31 32 # remove surrounding line breaks 33 $text = trim($text, "\n"); 34 35 # split text into lines 36 $lines = explode("\n", $text); 37 38 # iterate through lines to identify blocks 39 $markup = $this->lines($lines); 40 41 # trim line breaks 42 $markup = trim($markup, "\n"); 43 44 return $markup; 45 } 46 47 # 48 # Setters 49 # 50 51 function setBreaksEnabled($breaksEnabled) 52 { 53 $this->breaksEnabled = $breaksEnabled; 54 55 return $this; 56 } 57 58 protected $breaksEnabled; 59 60 function setMarkupEscaped($markupEscaped) 61 { 62 $this->markupEscaped = $markupEscaped; 63 64 return $this; 65 } 66 67 protected $markupEscaped; 68 69 function setUrlsLinked($urlsLinked) 70 { 71 $this->urlsLinked = $urlsLinked; 72 73 return $this; 74 } 75 76 protected $urlsLinked = true; 77 78 # 79 # Lines 80 # 81 82 protected $BlockTypes = array( 83 '#' => array('Header'), 84 '*' => array('Rule', 'List'), 85 '+' => array('List'), 86 '-' => array('SetextHeader', 'Table', 'Rule', 'List'), 87 '0' => array('List'), 88 '1' => array('List'), 89 '2' => array('List'), 90 '3' => array('List'), 91 '4' => array('List'), 92 '5' => array('List'), 93 '6' => array('List'), 94 '7' => array('List'), 95 '8' => array('List'), 96 '9' => array('List'), 97 ':' => array('Table'), 98 '<' => array('Comment', 'Markup'), 99 '=' => array('SetextHeader'), 100 '>' => array('Quote'), 101 '[' => array('Reference'), 102 '_' => array('Rule'), 103 '`' => array('FencedCode'), 104 '|' => array('Table'), 105 '~' => array('FencedCode'), 106 ); 107 108 # ~ 109 110 protected $unmarkedBlockTypes = array( 111 'Code', 112 ); 113 114 # 115 # Blocks 116 # 117 118 protected function lines(array $lines) 119 { 120 $CurrentBlock = null; 121 122 foreach ($lines as $line) 123 { 124 if (chop($line) === '') 125 { 126 if (isset($CurrentBlock)) 127 { 128 $CurrentBlock['interrupted'] = true; 129 } 130 131 continue; 132 } 133 134 if (strpos($line, "\t") !== false) 135 { 136 $parts = explode("\t", $line); 137 138 $line = $parts[0]; 139 140 unset($parts[0]); 141 142 foreach ($parts as $part) 143 { 144 $shortage = 4 - mb_strlen($line, 'utf-8') % 4; 145 146 $line .= str_repeat(' ', $shortage); 147 $line .= $part; 148 } 149 } 150 151 $indent = 0; 152 153 while (isset($line[$indent]) and $line[$indent] === ' ') 154 { 155 $indent ++; 156 } 157 158 $text = $indent > 0 ? substr($line, $indent) : $line; 159 160 # ~ 161 162 $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); 163 164 # ~ 165 166 if (isset($CurrentBlock['continuable'])) 167 { 168 $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); 169 170 if (isset($Block)) 171 { 172 $CurrentBlock = $Block; 173 174 continue; 175 } 176 else 177 { 178 if ($this->isBlockCompletable($CurrentBlock['type'])) 179 { 180 $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); 181 } 182 } 183 } 184 185 # ~ 186 187 $marker = $text[0]; 188 189 # ~ 190 191 $blockTypes = $this->unmarkedBlockTypes; 192 193 if (isset($this->BlockTypes[$marker])) 194 { 195 foreach ($this->BlockTypes[$marker] as $blockType) 196 { 197 $blockTypes []= $blockType; 198 } 199 } 200 201 # 202 # ~ 203 204 foreach ($blockTypes as $blockType) 205 { 206 $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); 207 208 if (isset($Block)) 209 { 210 $Block['type'] = $blockType; 211 212 if ( ! isset($Block['identified'])) 213 { 214 $Blocks []= $CurrentBlock; 215 216 $Block['identified'] = true; 217 } 218 219 if ($this->isBlockContinuable($blockType)) 220 { 221 $Block['continuable'] = true; 222 } 223 224 $CurrentBlock = $Block; 225 226 continue 2; 227 } 228 } 229 230 # ~ 231 232 if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) 233 { 234 $CurrentBlock['element']['text'] .= "\n".$text; 235 } 236 else 237 { 238 $Blocks []= $CurrentBlock; 239 240 $CurrentBlock = $this->paragraph($Line); 241 242 $CurrentBlock['identified'] = true; 243 } 244 } 245 246 # ~ 247 248 if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) 249 { 250 $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); 251 } 252 253 # ~ 254 255 $Blocks []= $CurrentBlock; 256 257 unset($Blocks[0]); 258 259 # ~ 260 261 $markup = ''; 262 263 foreach ($Blocks as $Block) 264 { 265 if (isset($Block['hidden'])) 266 { 267 continue; 268 } 269 270 $markup .= "\n"; 271 $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); 272 } 273 274 $markup .= "\n"; 275 276 # ~ 277 278 return $markup; 279 } 280 281 protected function isBlockContinuable($Type) 282 { 283 return method_exists($this, 'block'.$Type.'Continue'); 284 } 285 286 protected function isBlockCompletable($Type) 287 { 288 return method_exists($this, 'block'.$Type.'Complete'); 289 } 290 291 # 292 # Code 293 294 protected function blockCode($Line, $Block = null) 295 { 296 if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) 297 { 298 return; 299 } 300 301 if ($Line['indent'] >= 4) 302 { 303 $text = substr($Line['body'], 4); 304 305 $Block = array( 306 'element' => array( 307 'name' => 'pre', 308 'handler' => 'element', 309 'text' => array( 310 'name' => 'code', 311 'text' => $text, 312 ), 313 ), 314 ); 315 316 return $Block; 317 } 318 } 319 320 protected function blockCodeContinue($Line, $Block) 321 { 322 if ($Line['indent'] >= 4) 323 { 324 if (isset($Block['interrupted'])) 325 { 326 $Block['element']['text']['text'] .= "\n"; 327 328 unset($Block['interrupted']); 329 } 330 331 $Block['element']['text']['text'] .= "\n"; 332 333 $text = substr($Line['body'], 4); 334 335 $Block['element']['text']['text'] .= $text; 336 337 return $Block; 338 } 339 } 340 341 protected function blockCodeComplete($Block) 342 { 343 $text = $Block['element']['text']['text']; 344 345 $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); 346 347 $Block['element']['text']['text'] = $text; 348 349 return $Block; 350 } 351 352 # 353 # Comment 354 355 protected function blockComment($Line) 356 { 357 if ($this->markupEscaped) 358 { 359 return; 360 } 361 362 if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') 363 { 364 $Block = array( 365 'markup' => $Line['body'], 366 ); 367 368 if (preg_match('/-->$/', $Line['text'])) 369 { 370 $Block['closed'] = true; 371 } 372 373 return $Block; 374 } 375 } 376 377 protected function blockCommentContinue($Line, array $Block) 378 { 379 if (isset($Block['closed'])) 380 { 381 return; 382 } 383 384 $Block['markup'] .= "\n" . $Line['body']; 385 386 if (preg_match('/-->$/', $Line['text'])) 387 { 388 $Block['closed'] = true; 389 } 390 391 return $Block; 392 } 393 394 # 395 # Fenced Code 396 397 protected function blockFencedCode($Line) 398 { 399 if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) 400 { 401 $Element = array( 402 'name' => 'code', 403 'text' => '', 404 ); 405 406 if (isset($matches[1])) 407 { 408 $class = 'language-'.$matches[1]; 409 410 $Element['attributes'] = array( 411 'class' => $class, 412 ); 413 } 414 415 $Block = array( 416 'char' => $Line['text'][0], 417 'element' => array( 418 'name' => 'pre', 419 'handler' => 'element', 420 'text' => $Element, 421 ), 422 ); 423 424 return $Block; 425 } 426 } 427 428 protected function blockFencedCodeContinue($Line, $Block) 429 { 430 if (isset($Block['complete'])) 431 { 432 return; 433 } 434 435 if (isset($Block['interrupted'])) 436 { 437 $Block['element']['text']['text'] .= "\n"; 438 439 unset($Block['interrupted']); 440 } 441 442 if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) 443 { 444 $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); 445 446 $Block['complete'] = true; 447 448 return $Block; 449 } 450 451 $Block['element']['text']['text'] .= "\n".$Line['body']; 452 453 return $Block; 454 } 455 456 protected function blockFencedCodeComplete($Block) 457 { 458 $text = $Block['element']['text']['text']; 459 460 $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); 461 462 $Block['element']['text']['text'] = $text; 463 464 return $Block; 465 } 466 467 # 468 # Header 469 470 protected function blockHeader($Line) 471 { 472 if (isset($Line['text'][1])) 473 { 474 $level = 1; 475 476 while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') 477 { 478 $level ++; 479 } 480 481 if ($level > 6) 482 { 483 return; 484 } 485 486 $text = trim($Line['text'], '# '); 487 488 $Block = array( 489 'element' => array( 490 'name' => 'h' . min(6, $level), 491 'text' => $text, 492 'handler' => 'line', 493 ), 494 ); 495 496 return $Block; 497 } 498 } 499 500 # 501 # List 502 503 protected function blockList($Line) 504 { 505 list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); 506 507 if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) 508 { 509 $Block = array( 510 'indent' => $Line['indent'], 511 'pattern' => $pattern, 512 'element' => array( 513 'name' => $name, 514 'handler' => 'elements', 515 ), 516 ); 517 518 if($name === 'ol') 519 { 520 $listStart = stristr($matches[0], '.', true); 521 522 if($listStart !== '1') 523 { 524 $Block['element']['attributes'] = array('start' => $listStart); 525 } 526 } 527 528 $Block['li'] = array( 529 'name' => 'li', 530 'handler' => 'li', 531 'text' => array( 532 $matches[2], 533 ), 534 ); 535 536 $Block['element']['text'] []= & $Block['li']; 537 538 return $Block; 539 } 540 } 541 542 protected function blockListContinue($Line, array $Block) 543 { 544 if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) 545 { 546 if (isset($Block['interrupted'])) 547 { 548 $Block['li']['text'] []= ''; 549 550 unset($Block['interrupted']); 551 } 552 553 unset($Block['li']); 554 555 $text = isset($matches[1]) ? $matches[1] : ''; 556 557 $Block['li'] = array( 558 'name' => 'li', 559 'handler' => 'li', 560 'text' => array( 561 $text, 562 ), 563 ); 564 565 $Block['element']['text'] []= & $Block['li']; 566 567 return $Block; 568 } 569 570 if ($Line['text'][0] === '[' and $this->blockReference($Line)) 571 { 572 return $Block; 573 } 574 575 if ( ! isset($Block['interrupted'])) 576 { 577 $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); 578 579 $Block['li']['text'] []= $text; 580 581 return $Block; 582 } 583 584 if ($Line['indent'] > 0) 585 { 586 $Block['li']['text'] []= ''; 587 588 $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); 589 590 $Block['li']['text'] []= $text; 591 592 unset($Block['interrupted']); 593 594 return $Block; 595 } 596 } 597 598 # 599 # Quote 600 601 protected function blockQuote($Line) 602 { 603 if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) 604 { 605 $Block = array( 606 'element' => array( 607 'name' => 'blockquote', 608 'handler' => 'lines', 609 'text' => (array) $matches[1], 610 ), 611 ); 612 613 return $Block; 614 } 615 } 616 617 protected function blockQuoteContinue($Line, array $Block) 618 { 619 if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) 620 { 621 if (isset($Block['interrupted'])) 622 { 623 $Block['element']['text'] []= ''; 624 625 unset($Block['interrupted']); 626 } 627 628 $Block['element']['text'] []= $matches[1]; 629 630 return $Block; 631 } 632 633 if ( ! isset($Block['interrupted'])) 634 { 635 $Block['element']['text'] []= $Line['text']; 636 637 return $Block; 638 } 639 } 640 641 # 642 # Rule 643 644 protected function blockRule($Line) 645 { 646 if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) 647 { 648 $Block = array( 649 'element' => array( 650 'name' => 'hr' 651 ), 652 ); 653 654 return $Block; 655 } 656 } 657 658 # 659 # Setext 660 661 protected function blockSetextHeader($Line, array $Block = null) 662 { 663 if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) 664 { 665 return; 666 } 667 668 if (chop($Line['text'], $Line['text'][0]) === '') 669 { 670 $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; 671 672 return $Block; 673 } 674 } 675 676 # 677 # Markup 678 679 protected function blockMarkup($Line) 680 { 681 if ($this->markupEscaped) 682 { 683 return; 684 } 685 686 if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) 687 { 688 $element = strtolower($matches[1]); 689 690 if (in_array($element, $this->textLevelElements)) 691 { 692 return; 693 } 694 695 $Block = array( 696 'name' => $matches[1], 697 'depth' => 0, 698 'markup' => $Line['text'], 699 ); 700 701 $length = strlen($matches[0]); 702 703 $remainder = substr($Line['text'], $length); 704 705 if (trim($remainder) === '') 706 { 707 if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) 708 { 709 $Block['closed'] = true; 710 711 $Block['void'] = true; 712 } 713 } 714 else 715 { 716 if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) 717 { 718 return; 719 } 720 721 if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) 722 { 723 $Block['closed'] = true; 724 } 725 } 726 727 return $Block; 728 } 729 } 730 731 protected function blockMarkupContinue($Line, array $Block) 732 { 733 if (isset($Block['closed'])) 734 { 735 return; 736 } 737 738 if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open 739 { 740 $Block['depth'] ++; 741 } 742 743 if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close 744 { 745 if ($Block['depth'] > 0) 746 { 747 $Block['depth'] --; 748 } 749 else 750 { 751 $Block['closed'] = true; 752 } 753 } 754 755 if (isset($Block['interrupted'])) 756 { 757 $Block['markup'] .= "\n"; 758 759 unset($Block['interrupted']); 760 } 761 762 $Block['markup'] .= "\n".$Line['body']; 763 764 return $Block; 765 } 766 767 # 768 # Reference 769 770 protected function blockReference($Line) 771 { 772 if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) 773 { 774 $id = strtolower($matches[1]); 775 776 $Data = array( 777 'url' => $matches[2], 778 'title' => null, 779 ); 780 781 if (isset($matches[3])) 782 { 783 $Data['title'] = $matches[3]; 784 } 785 786 $this->DefinitionData['Reference'][$id] = $Data; 787 788 $Block = array( 789 'hidden' => true, 790 ); 791 792 return $Block; 793 } 794 } 795 796 # 797 # Table 798 799 protected function blockTable($Line, array $Block = null) 800 { 801 if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) 802 { 803 return; 804 } 805 806 if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') 807 { 808 $alignments = array(); 809 810 $divider = $Line['text']; 811 812 $divider = trim($divider); 813 $divider = trim($divider, '|'); 814 815 $dividerCells = explode('|', $divider); 816 817 foreach ($dividerCells as $dividerCell) 818 { 819 $dividerCell = trim($dividerCell); 820 821 if ($dividerCell === '') 822 { 823 continue; 824 } 825 826 $alignment = null; 827 828 if ($dividerCell[0] === ':') 829 { 830 $alignment = 'left'; 831 } 832 833 if (substr($dividerCell, - 1) === ':') 834 { 835 $alignment = $alignment === 'left' ? 'center' : 'right'; 836 } 837 838 $alignments []= $alignment; 839 } 840 841 # ~ 842 843 $HeaderElements = array(); 844 845 $header = $Block['element']['text']; 846 847 $header = trim($header); 848 $header = trim($header, '|'); 849 850 $headerCells = explode('|', $header); 851 852 foreach ($headerCells as $index => $headerCell) 853 { 854 $headerCell = trim($headerCell); 855 856 $HeaderElement = array( 857 'name' => 'th', 858 'text' => $headerCell, 859 'handler' => 'line', 860 ); 861 862 if (isset($alignments[$index])) 863 { 864 $alignment = $alignments[$index]; 865 866 $HeaderElement['attributes'] = array( 867 'style' => 'text-align: '.$alignment.';', 868 ); 869 } 870 871 $HeaderElements []= $HeaderElement; 872 } 873 874 # ~ 875 876 $Block = array( 877 'alignments' => $alignments, 878 'identified' => true, 879 'element' => array( 880 'name' => 'table', 881 'handler' => 'elements', 882 ), 883 ); 884 885 $Block['element']['text'] []= array( 886 'name' => 'thead', 887 'handler' => 'elements', 888 ); 889 890 $Block['element']['text'] []= array( 891 'name' => 'tbody', 892 'handler' => 'elements', 893 'text' => array(), 894 ); 895 896 $Block['element']['text'][0]['text'] []= array( 897 'name' => 'tr', 898 'handler' => 'elements', 899 'text' => $HeaderElements, 900 ); 901 902 return $Block; 903 } 904 } 905 906 protected function blockTableContinue($Line, array $Block) 907 { 908 if (isset($Block['interrupted'])) 909 { 910 return; 911 } 912 913 if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) 914 { 915 $Elements = array(); 916 917 $row = $Line['text']; 918 919 $row = trim($row); 920 $row = trim($row, '|'); 921 922 preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); 923 924 foreach ($matches[0] as $index => $cell) 925 { 926 $cell = trim($cell); 927 928 $Element = array( 929 'name' => 'td', 930 'handler' => 'line', 931 'text' => $cell, 932 ); 933 934 if (isset($Block['alignments'][$index])) 935 { 936 $Element['attributes'] = array( 937 'style' => 'text-align: '.$Block['alignments'][$index].';', 938 ); 939 } 940 941 $Elements []= $Element; 942 } 943 944 $Element = array( 945 'name' => 'tr', 946 'handler' => 'elements', 947 'text' => $Elements, 948 ); 949 950 $Block['element']['text'][1]['text'] []= $Element; 951 952 return $Block; 953 } 954 } 955 956 # 957 # ~ 958 # 959 960 protected function paragraph($Line) 961 { 962 $Block = array( 963 'element' => array( 964 'name' => 'p', 965 'text' => $Line['text'], 966 'handler' => 'line', 967 ), 968 ); 969 970 return $Block; 971 } 972 973 # 974 # Inline Elements 975 # 976 977 protected $InlineTypes = array( 978 '"' => array('SpecialCharacter'), 979 '!' => array('Image'), 980 '&' => array('SpecialCharacter'), 981 '*' => array('Emphasis'), 982 ':' => array('Url'), 983 '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), 984 '>' => array('SpecialCharacter'), 985 '[' => array('Link'), 986 '_' => array('Emphasis'), 987 '`' => array('Code'), 988 '~' => array('Strikethrough'), 989 '\\' => array('EscapeSequence'), 990 ); 991 992 # ~ 993 994 protected $inlineMarkerList = '!"*_&[:<>`~\\'; 995 996 # 997 # ~ 998 # 999 1000 public function line($text) 1001 { 1002 $markup = ''; 1003 1004 # $excerpt is based on the first occurrence of a marker 1005 1006 while ($excerpt = strpbrk($text, $this->inlineMarkerList)) 1007 { 1008 $marker = $excerpt[0]; 1009 1010 $markerPosition = strpos($text, $marker); 1011 1012 $Excerpt = array('text' => $excerpt, 'context' => $text); 1013 1014 foreach ($this->InlineTypes[$marker] as $inlineType) 1015 { 1016 $Inline = $this->{'inline'.$inlineType}($Excerpt); 1017 1018 if ( ! isset($Inline)) 1019 { 1020 continue; 1021 } 1022 1023 # makes sure that the inline belongs to "our" marker 1024 1025 if (isset($Inline['position']) and $Inline['position'] > $markerPosition) 1026 { 1027 continue; 1028 } 1029 1030 # sets a default inline position 1031 1032 if ( ! isset($Inline['position'])) 1033 { 1034 $Inline['position'] = $markerPosition; 1035 } 1036 1037 # the text that comes before the inline 1038 $unmarkedText = substr($text, 0, $Inline['position']); 1039 1040 # compile the unmarked text 1041 $markup .= $this->unmarkedText($unmarkedText); 1042 1043 # compile the inline 1044 $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); 1045 1046 # remove the examined text 1047 $text = substr($text, $Inline['position'] + $Inline['extent']); 1048 1049 continue 2; 1050 } 1051 1052 # the marker does not belong to an inline 1053 1054 $unmarkedText = substr($text, 0, $markerPosition + 1); 1055 1056 $markup .= $this->unmarkedText($unmarkedText); 1057 1058 $text = substr($text, $markerPosition + 1); 1059 } 1060 1061 $markup .= $this->unmarkedText($text); 1062 1063 return $markup; 1064 } 1065 1066 # 1067 # ~ 1068 # 1069 1070 protected function inlineCode($Excerpt) 1071 { 1072 $marker = $Excerpt['text'][0]; 1073 1074 if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches)) 1075 { 1076 $text = $matches[2]; 1077 $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); 1078 $text = preg_replace("/[ ]*\n/", ' ', $text); 1079 1080 return array( 1081 'extent' => strlen($matches[0]), 1082 'element' => array( 1083 'name' => 'code', 1084 'text' => $text, 1085 ), 1086 ); 1087 } 1088 } 1089 1090 protected function inlineEmailTag($Excerpt) 1091 { 1092 if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) 1093 { 1094 $url = $matches[1]; 1095 1096 if ( ! isset($matches[2])) 1097 { 1098 $url = 'mailto:' . $url; 1099 } 1100 1101 return array( 1102 'extent' => strlen($matches[0]), 1103 'element' => array( 1104 'name' => 'a', 1105 'text' => $matches[1], 1106 'attributes' => array( 1107 'href' => $url, 1108 ), 1109 ), 1110 ); 1111 } 1112 } 1113 1114 protected function inlineEmphasis($Excerpt) 1115 { 1116 if ( ! isset($Excerpt['text'][1])) 1117 { 1118 return; 1119 } 1120 1121 $marker = $Excerpt['text'][0]; 1122 1123 if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) 1124 { 1125 $emphasis = 'strong'; 1126 } 1127 elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) 1128 { 1129 $emphasis = 'em'; 1130 } 1131 else 1132 { 1133 return; 1134 } 1135 1136 return array( 1137 'extent' => strlen($matches[0]), 1138 'element' => array( 1139 'name' => $emphasis, 1140 'handler' => 'line', 1141 'text' => $matches[1], 1142 ), 1143 ); 1144 } 1145 1146 protected function inlineEscapeSequence($Excerpt) 1147 { 1148 if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) 1149 { 1150 return array( 1151 'markup' => $Excerpt['text'][1], 1152 'extent' => 2, 1153 ); 1154 } 1155 } 1156 1157 protected function inlineImage($Excerpt) 1158 { 1159 if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') 1160 { 1161 return; 1162 } 1163 1164 $Excerpt['text']= substr($Excerpt['text'], 1); 1165 1166 $Link = $this->inlineLink($Excerpt); 1167 1168 if ($Link === null) 1169 { 1170 return; 1171 } 1172 1173 $Inline = array( 1174 'extent' => $Link['extent'] + 1, 1175 'element' => array( 1176 'name' => 'img', 1177 'attributes' => array( 1178 'src' => $Link['element']['attributes']['href'], 1179 'alt' => $Link['element']['text'], 1180 ), 1181 ), 1182 ); 1183 1184 $Inline['element']['attributes'] += $Link['element']['attributes']; 1185 1186 unset($Inline['element']['attributes']['href']); 1187 1188 return $Inline; 1189 } 1190 1191 protected function inlineLink($Excerpt) 1192 { 1193 $Element = array( 1194 'name' => 'a', 1195 'handler' => 'line', 1196 'text' => null, 1197 'attributes' => array( 1198 'href' => null, 1199 'title' => null, 1200 ), 1201 ); 1202 1203 $extent = 0; 1204 1205 $remainder = $Excerpt['text']; 1206 1207 if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) 1208 { 1209 $Element['text'] = $matches[1]; 1210 1211 $extent += strlen($matches[0]); 1212 1213 $remainder = substr($remainder, $extent); 1214 } 1215 else 1216 { 1217 return; 1218 } 1219 1220 if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) 1221 { 1222 $Element['attributes']['href'] = $matches[1]; 1223 1224 if (isset($matches[2])) 1225 { 1226 $Element['attributes']['title'] = substr($matches[2], 1, - 1); 1227 } 1228 1229 $extent += strlen($matches[0]); 1230 } 1231 else 1232 { 1233 if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) 1234 { 1235 $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; 1236 $definition = strtolower($definition); 1237 1238 $extent += strlen($matches[0]); 1239 } 1240 else 1241 { 1242 $definition = strtolower($Element['text']); 1243 } 1244 1245 if ( ! isset($this->DefinitionData['Reference'][$definition])) 1246 { 1247 return; 1248 } 1249 1250 $Definition = $this->DefinitionData['Reference'][$definition]; 1251 1252 $Element['attributes']['href'] = $Definition['url']; 1253 $Element['attributes']['title'] = $Definition['title']; 1254 } 1255 1256 $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']); 1257 1258 return array( 1259 'extent' => $extent, 1260 'element' => $Element, 1261 ); 1262 } 1263 1264 protected function inlineMarkup($Excerpt) 1265 { 1266 if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) 1267 { 1268 return; 1269 } 1270 1271 if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) 1272 { 1273 return array( 1274 'markup' => $matches[0], 1275 'extent' => strlen($matches[0]), 1276 ); 1277 } 1278 1279 if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches)) 1280 { 1281 return array( 1282 'markup' => $matches[0], 1283 'extent' => strlen($matches[0]), 1284 ); 1285 } 1286 1287 if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) 1288 { 1289 return array( 1290 'markup' => $matches[0], 1291 'extent' => strlen($matches[0]), 1292 ); 1293 } 1294 } 1295 1296 protected function inlineSpecialCharacter($Excerpt) 1297 { 1298 if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) 1299 { 1300 return array( 1301 'markup' => '&', 1302 'extent' => 1, 1303 ); 1304 } 1305 1306 $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); 1307 1308 if (isset($SpecialCharacter[$Excerpt['text'][0]])) 1309 { 1310 return array( 1311 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', 1312 'extent' => 1, 1313 ); 1314 } 1315 } 1316 1317 protected function inlineStrikethrough($Excerpt) 1318 { 1319 if ( ! isset($Excerpt['text'][1])) 1320 { 1321 return; 1322 } 1323 1324 if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) 1325 { 1326 return array( 1327 'extent' => strlen($matches[0]), 1328 'element' => array( 1329 'name' => 'del', 1330 'text' => $matches[1], 1331 'handler' => 'line', 1332 ), 1333 ); 1334 } 1335 } 1336 1337 protected function inlineUrl($Excerpt) 1338 { 1339 if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') 1340 { 1341 return; 1342 } 1343 1344 if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) 1345 { 1346 $Inline = array( 1347 'extent' => strlen($matches[0][0]), 1348 'position' => $matches[0][1], 1349 'element' => array( 1350 'name' => 'a', 1351 'text' => $matches[0][0], 1352 'attributes' => array( 1353 'href' => $matches[0][0], 1354 ), 1355 ), 1356 ); 1357 1358 return $Inline; 1359 } 1360 } 1361 1362 protected function inlineUrlTag($Excerpt) 1363 { 1364 if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) 1365 { 1366 $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]); 1367 1368 return array( 1369 'extent' => strlen($matches[0]), 1370 'element' => array( 1371 'name' => 'a', 1372 'text' => $url, 1373 'attributes' => array( 1374 'href' => $url, 1375 ), 1376 ), 1377 ); 1378 } 1379 } 1380 1381 # ~ 1382 1383 protected function unmarkedText($text) 1384 { 1385 if ($this->breaksEnabled) 1386 { 1387 $text = preg_replace('/[ ]*\n/', "<br />\n", $text); 1388 } 1389 else 1390 { 1391 $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text); 1392 $text = str_replace(" \n", "\n", $text); 1393 } 1394 1395 return $text; 1396 } 1397 1398 # 1399 # Handlers 1400 # 1401 1402 protected function element(array $Element) 1403 { 1404 $markup = '<'.$Element['name']; 1405 1406 if (isset($Element['attributes'])) 1407 { 1408 foreach ($Element['attributes'] as $name => $value) 1409 { 1410 if ($value === null) 1411 { 1412 continue; 1413 } 1414 1415 $markup .= ' '.$name.'="'.$value.'"'; 1416 } 1417 } 1418 1419 if (isset($Element['text'])) 1420 { 1421 $markup .= '>'; 1422 1423 if (isset($Element['handler'])) 1424 { 1425 $markup .= $this->{$Element['handler']}($Element['text']); 1426 } 1427 else 1428 { 1429 $markup .= $Element['text']; 1430 } 1431 1432 $markup .= '</'.$Element['name'].'>'; 1433 } 1434 else 1435 { 1436 $markup .= ' />'; 1437 } 1438 1439 return $markup; 1440 } 1441 1442 protected function elements(array $Elements) 1443 { 1444 $markup = ''; 1445 1446 foreach ($Elements as $Element) 1447 { 1448 $markup .= "\n" . $this->element($Element); 1449 } 1450 1451 $markup .= "\n"; 1452 1453 return $markup; 1454 } 1455 1456 # ~ 1457 1458 protected function li($lines) 1459 { 1460 $markup = $this->lines($lines); 1461 1462 $trimmedMarkup = trim($markup); 1463 1464 if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>') 1465 { 1466 $markup = $trimmedMarkup; 1467 $markup = substr($markup, 3); 1468 1469 $position = strpos($markup, "</p>"); 1470 1471 $markup = substr_replace($markup, '', $position, 4); 1472 } 1473 1474 return $markup; 1475 } 1476 1477 # 1478 # Deprecated Methods 1479 # 1480 1481 function parse($text) 1482 { 1483 $markup = $this->text($text); 1484 1485 return $markup; 1486 } 1487 1488 # 1489 # Static Methods 1490 # 1491 1492 static function instance($name = 'default') 1493 { 1494 if (isset(self::$instances[$name])) 1495 { 1496 return self::$instances[$name]; 1497 } 1498 1499 $instance = new static(); 1500 1501 self::$instances[$name] = $instance; 1502 1503 return $instance; 1504 } 1505 1506 private static $instances = array(); 1507 1508 # 1509 # Fields 1510 # 1511 1512 protected $DefinitionData; 1513 1514 # 1515 # Read-Only 1516 1517 protected $specialCharacters = array( 1518 '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', 1519 ); 1520 1521 protected $StrongRegex = array( 1522 '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', 1523 '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', 1524 ); 1525 1526 protected $EmRegex = array( 1527 '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', 1528 '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', 1529 ); 1530 1531 protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; 1532 1533 protected $voidElements = array( 1534 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 1535 ); 1536 1537 protected $textLevelElements = array( 1538 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', 1539 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', 1540 'i', 'rp', 'del', 'code', 'strike', 'marquee', 1541 'q', 'rt', 'ins', 'font', 'strong', 1542 's', 'tt', 'kbd', 'mark', 1543 'u', 'xm', 'sub', 'nobr', 1544 'sup', 'ruby', 1545 'var', 'span', 1546 'wbr', 'time', 1547 ); 1548 }