├── .gitignore ├── LICENSE ├── Parser.php ├── README.md ├── cli.php ├── composer.json ├── composer.lock └── test └── test.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .*.swp 3 | .*.swo 4 | ._* 5 | .DS_Store 6 | /Debug/ 7 | /ImgCache/ 8 | /Backup_rar/ 9 | /Debug/ 10 | /debug/ 11 | /upload/ 12 | /avatar/ 13 | /.idea/ 14 | /.vagrant/ 15 | node_modules 16 | Vagrantfile 17 | *.orig 18 | *.aps 19 | *.APS 20 | *.chm 21 | *.exp 22 | *.pdb 23 | *.rar 24 | .smbdelete* 25 | *.sublime* 26 | .sass-cache 27 | config.rb 28 | config.codekit 29 | npm-debug.log 30 | *.log 31 | *.iml 32 | 33 | /vendor/ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | 3 | Copyright (c) 2015, SegmentFault 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, this 13 | list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | * Neither the name of schillmania.com nor the names of its contributors may be 17 | used to endorse or promote products derived from this software without 18 | specific prior written permission from schillmania.com. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Parser.php: -------------------------------------------------------------------------------- 1 | 10 | * @license BSD License 11 | */ 12 | class Parser 13 | { 14 | /** 15 | * _whiteList 16 | * 17 | * @var string 18 | */ 19 | private $_commonWhiteList = 'kbd|b|i|strong|em|sup|sub|br|code|del|a|hr|small'; 20 | 21 | /** 22 | * html tags 23 | * 24 | * @var string 25 | */ 26 | private $_blockHtmlTags = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|svg|script|noscript'; 27 | 28 | /** 29 | * _specialWhiteList 30 | * 31 | * @var mixed 32 | * @access private 33 | */ 34 | private $_specialWhiteList = array( 35 | 'table' => 'table|tbody|thead|tfoot|tr|td|th' 36 | ); 37 | 38 | /** 39 | * _footnotes 40 | * 41 | * @var array 42 | */ 43 | private $_footnotes; 44 | 45 | /** 46 | * @var bool 47 | */ 48 | private $_html = false; 49 | 50 | /** 51 | * @var bool 52 | */ 53 | private $_line = false; 54 | 55 | /** 56 | * @var array 57 | */ 58 | private $blockParsers = array( 59 | array('code', 10), 60 | array('shtml', 20), 61 | array('pre', 30), 62 | array('ahtml', 40), 63 | array('shr', 50), 64 | array('list', 60), 65 | array('math', 70), 66 | array('html', 80), 67 | array('footnote', 90), 68 | array('definition', 100), 69 | array('quote', 110), 70 | array('table', 120), 71 | array('sh', 130), 72 | array('mh', 140), 73 | array('dhr', 150), 74 | array('default', 9999) 75 | ); 76 | 77 | /** 78 | * _blocks 79 | * 80 | * @var array 81 | */ 82 | private $_blocks; 83 | 84 | /** 85 | * _current 86 | * 87 | * @var string 88 | */ 89 | private $_current; 90 | 91 | /** 92 | * _pos 93 | * 94 | * @var int 95 | */ 96 | private $_pos; 97 | 98 | /** 99 | * _definitions 100 | * 101 | * @var array 102 | */ 103 | private $_definitions; 104 | 105 | /** 106 | * @var array 107 | */ 108 | private $_hooks = array(); 109 | 110 | /** 111 | * @var array 112 | */ 113 | private $_holders; 114 | 115 | /** 116 | * @var string 117 | */ 118 | private $_uniqid; 119 | 120 | /** 121 | * @var int 122 | */ 123 | private $_id; 124 | 125 | /** 126 | * @var array 127 | */ 128 | private $_parsers = array(); 129 | 130 | /** 131 | * makeHtml 132 | * 133 | * @param mixed $text 134 | * @return string 135 | */ 136 | public function makeHtml($text) 137 | { 138 | $this->_footnotes = array(); 139 | $this->_definitions = array(); 140 | $this->_holders = array(); 141 | $this->_uniqid = md5(uniqid()); 142 | $this->_id = 0; 143 | 144 | usort($this->blockParsers, function ($a, $b) { 145 | return $a[1] < $b[1] ? -1 : 1; 146 | }); 147 | 148 | foreach ($this->blockParsers as $parser) { 149 | list($name) = $parser; 150 | 151 | if (isset($parser[2])) { 152 | $this->_parsers[$name] = $parser[2]; 153 | } else { 154 | $this->_parsers[$name] = array($this, 'parseBlock' . ucfirst($name)); 155 | } 156 | } 157 | 158 | $text = $this->initText($text); 159 | $html = $this->parse($text); 160 | $html = $this->makeFootnotes($html); 161 | $html = $this->optimizeLines($html); 162 | 163 | return $this->call('makeHtml', $html); 164 | } 165 | 166 | /** 167 | * @param $html 168 | */ 169 | public function enableHtml($html = true) 170 | { 171 | $this->_html = $html; 172 | } 173 | 174 | /** 175 | * @param bool $line 176 | */ 177 | public function enableLine($line = true) 178 | { 179 | $this->_line = $line; 180 | } 181 | 182 | /** 183 | * @param $type 184 | * @param $callback 185 | */ 186 | public function hook($type, $callback) 187 | { 188 | $this->_hooks[$type][] = $callback; 189 | } 190 | 191 | /** 192 | * @param $str 193 | * @return string 194 | */ 195 | public function makeHolder($str) 196 | { 197 | $key = "\r" . $this->_uniqid . $this->_id . "\r"; 198 | $this->_id ++; 199 | $this->_holders[$key] = $str; 200 | 201 | return $key; 202 | } 203 | 204 | /** 205 | * @param $text 206 | * @return mixed 207 | */ 208 | private function initText($text) 209 | { 210 | $text = str_replace(array("\t", "\r"), array(' ', ''), $text); 211 | return $text; 212 | } 213 | 214 | /** 215 | * @param $html 216 | * @return string 217 | */ 218 | private function makeFootnotes($html) 219 | { 220 | if (count($this->_footnotes) > 0) { 221 | $html .= '

    '; 222 | $index = 1; 223 | 224 | while ($val = array_shift($this->_footnotes)) { 225 | if (is_string($val)) { 226 | $val .= " "; 227 | } else { 228 | $val[count($val) - 1] .= " "; 229 | $val = count($val) > 1 ? $this->parse(implode("\n", $val)) : $this->parseInline($val[0]); 230 | } 231 | 232 | $html .= "
  1. {$val}
  2. "; 233 | $index ++; 234 | } 235 | 236 | $html .= '
'; 237 | } 238 | 239 | return $html; 240 | } 241 | 242 | /** 243 | * parse 244 | * 245 | * @param string $text 246 | * @param bool $inline 247 | * @param int $offset 248 | * @return string 249 | */ 250 | private function parse($text, $inline = false, $offset = 0) 251 | { 252 | $blocks = $this->parseBlock($text, $lines); 253 | $html = ''; 254 | 255 | // inline mode for single normal block 256 | if ($inline && count($blocks) == 1 && $blocks[0][0] == 'normal') { 257 | $blocks[0][3] = true; 258 | } 259 | 260 | foreach ($blocks as $block) { 261 | list($type, $start, $end, $value) = $block; 262 | $extract = array_slice($lines, $start, $end - $start + 1); 263 | $method = 'parse' . ucfirst($type); 264 | 265 | $extract = $this->call('before' . ucfirst($method), $extract, $value); 266 | $result = $this->{$method}($extract, $value, $start + $offset, $end + $offset); 267 | $result = $this->call('after' . ucfirst($method), $result, $value); 268 | 269 | $html .= $result; 270 | } 271 | 272 | return $html; 273 | } 274 | 275 | /** 276 | * @param $text 277 | * @param $clearHolders 278 | * @return string 279 | */ 280 | private function releaseHolder($text, $clearHolders = true) 281 | { 282 | $deep = 0; 283 | while (strpos($text, "\r") !== false && $deep < 10) { 284 | $text = str_replace(array_keys($this->_holders), array_values($this->_holders), $text); 285 | $deep ++; 286 | } 287 | 288 | if ($clearHolders) { 289 | $this->_holders = array(); 290 | } 291 | 292 | return $text; 293 | } 294 | 295 | /** 296 | * @param $start 297 | * @param int $end 298 | * @return string 299 | */ 300 | private function markLine($start, $end = -1) 301 | { 302 | if ($this->_line) { 303 | $end = $end < 0 ? $start : $end; 304 | return ''; 306 | } 307 | 308 | return ''; 309 | } 310 | 311 | /** 312 | * @param array $lines 313 | * @param $start 314 | * @return string[] 315 | */ 316 | private function markLines(array $lines, $start) 317 | { 318 | $i = -1; 319 | 320 | return $this->_line ? array_map(function ($line) use ($start, &$i) { 321 | $i ++; 322 | return $this->markLine($start + $i) . $line; 323 | }, $lines) : $lines; 324 | } 325 | 326 | /** 327 | * @param $html 328 | * @return string 329 | */ 330 | private function optimizeLines($html) 331 | { 332 | $last = 0; 333 | 334 | return $this->_line ? 335 | preg_replace_callback("/class=\"line\" data\-start=\"([0-9]+)\" data\-end=\"([0-9]+)\" (data\-id=\"{$this->_uniqid}\")/", 336 | function ($matches) use (&$last) { 337 | if ($matches[1] != $last) { 338 | $replace = 'class="line" data-start="' . $last . '" data-start-original="' . $matches[1] . '" data-end="' . $matches[2] . '" ' . $matches[3]; 339 | } else { 340 | $replace = $matches[0]; 341 | } 342 | 343 | $last = $matches[2] + 1; 344 | return $replace; 345 | }, $html) : $html; 346 | } 347 | 348 | /** 349 | * @param $type 350 | * @param $value 351 | * @return mixed 352 | */ 353 | private function call($type, $value) 354 | { 355 | if (empty($this->_hooks[$type])) { 356 | return $value; 357 | } 358 | 359 | $args = func_get_args(); 360 | $args = array_slice($args, 1); 361 | 362 | foreach ($this->_hooks[$type] as $callback) { 363 | $value = call_user_func_array($callback, $args); 364 | $args[0] = $value; 365 | } 366 | 367 | return $value; 368 | } 369 | 370 | /** 371 | * parseInline 372 | * 373 | * @param string $text 374 | * @param string $whiteList 375 | * @param bool $clearHolders 376 | * @param bool $enableAutoLink 377 | * @return string 378 | */ 379 | private function parseInline($text, $whiteList = '', $clearHolders = true, $enableAutoLink = true) 380 | { 381 | $text = $this->call('beforeParseInline', $text); 382 | 383 | // code 384 | $text = preg_replace_callback( 385 | "/(^|[^\\\])(`+)(.+?)\\2/", 386 | function ($matches) { 387 | return $matches[1] . $this->makeHolder( 388 | '' . htmlspecialchars($matches[3]) . '' 389 | ); 390 | }, 391 | $text 392 | ); 393 | 394 | // mathjax 395 | $text = preg_replace_callback( 396 | "/(^|[^\\\])(\\$+)(.+?)\\2/", 397 | function ($matches) { 398 | return $matches[1] . $this->makeHolder( 399 | $matches[2] . htmlspecialchars($matches[3]) . $matches[2] 400 | ); 401 | }, 402 | $text 403 | ); 404 | 405 | // escape 406 | $text = preg_replace_callback( 407 | "/\\\(.)/u", 408 | function ($matches) { 409 | $prefix = preg_match("/^[-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]$/", $matches[1]) ? '' : '\\'; 410 | $escaped = htmlspecialchars($matches[1]); 411 | $escaped = str_replace('$', '$', $escaped); 412 | return $this->makeHolder($prefix . $escaped); 413 | }, 414 | $text 415 | ); 416 | 417 | // link 418 | $text = preg_replace_callback( 419 | "/<(https?:\/\/.+|(?:mailto:)?[_a-z0-9-\.\+]+@[_\w-]+(?:\.[a-z]{2,})+)>/i", 420 | function ($matches) { 421 | $url = $this->cleanUrl($matches[1]); 422 | $link = $this->call('parseLink', $url); 423 | 424 | return $this->makeHolder( 425 | "{$link}" 426 | ); 427 | }, 428 | $text 429 | ); 430 | 431 | // encode unsafe tags 432 | $text = preg_replace_callback( 433 | "/<(\/?)([a-z0-9-]+)(\s+[^>]*)?>/i", 434 | function ($matches) use ($whiteList) { 435 | if ($this->_html || false !== stripos( 436 | '|' . $this->_commonWhiteList . '|' . $whiteList . '|', '|' . $matches[2] . '|' 437 | )) { 438 | return $this->makeHolder($matches[0]); 439 | } else { 440 | return $this->makeHolder(htmlspecialchars($matches[0])); 441 | } 442 | }, 443 | $text 444 | ); 445 | 446 | if ($this->_html) { 447 | $text = preg_replace_callback("//", function ($matches) { 448 | return $this->makeHolder($matches[0]); 449 | }, $text); 450 | } 451 | 452 | $text = str_replace(array('<', '>'), array('<', '>'), $text); 453 | 454 | // footnote 455 | $text = preg_replace_callback( 456 | "/\[\^((?:[^\]]|\\\\\]|\\\\\[)+?)\]/", 457 | function ($matches) { 458 | $id = array_search($matches[1], $this->_footnotes); 459 | 460 | if (false === $id) { 461 | $id = count($this->_footnotes) + 1; 462 | $this->_footnotes[$id] = $this->parseInline($matches[1], '', false); 463 | } 464 | 465 | return $this->makeHolder( 466 | "{$id}" 467 | ); 468 | }, 469 | $text 470 | ); 471 | 472 | // image 473 | $text = preg_replace_callback( 474 | "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/", 475 | function ($matches) { 476 | $escaped = htmlspecialchars($this->escapeBracket($matches[1])); 477 | $url = $this->escapeBracket($matches[2]); 478 | list ($url, $title) = $this->cleanUrl($url, true); 479 | $title = empty($title)? $escaped : " title=\"{$title}\""; 480 | 481 | return $this->makeHolder( 482 | "\"{$title}\"" 483 | ); 484 | }, 485 | $text 486 | ); 487 | 488 | $text = preg_replace_callback( 489 | "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/", 490 | function ($matches) { 491 | $escaped = htmlspecialchars($this->escapeBracket($matches[1])); 492 | 493 | $result = isset( $this->_definitions[$matches[2]] ) ? 494 | "_definitions[$matches[2]]}\" alt=\"{$escaped}\" title=\"{$escaped}\">" 495 | : $escaped; 496 | 497 | return $this->makeHolder($result); 498 | }, 499 | $text 500 | ); 501 | 502 | // link 503 | $text = preg_replace_callback( 504 | "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/", 505 | function ($matches) { 506 | $escaped = $this->parseInline( 507 | $this->escapeBracket($matches[1]), '', false, false 508 | ); 509 | $url = $this->escapeBracket($matches[2]); 510 | list ($url, $title) = $this->cleanUrl($url, true); 511 | $title = empty($title) ? '' : " title=\"{$title}\""; 512 | 513 | return $this->makeHolder("{$escaped}"); 514 | }, 515 | $text 516 | ); 517 | 518 | $text = preg_replace_callback( 519 | "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/", 520 | function ($matches) { 521 | $escaped = $this->parseInline( 522 | $this->escapeBracket($matches[1]), '', false 523 | ); 524 | $result = isset( $this->_definitions[$matches[2]] ) ? 525 | "_definitions[$matches[2]]}\">{$escaped}" 526 | : $escaped; 527 | 528 | return $this->makeHolder($result); 529 | }, 530 | $text 531 | ); 532 | 533 | // strong and em and some fuck 534 | $text = $this->parseInlineCallback($text); 535 | $text = preg_replace( 536 | "/<([_a-z0-9-\.\+]+@[^@]+\.[a-z]{2,})>/i", 537 | "\\1", 538 | $text 539 | ); 540 | 541 | // autolink url 542 | if ($enableAutoLink) { 543 | $text = preg_replace_callback( 544 | "/(^|[^\"])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\b([-a-zA-Z0-9@:%_\+.~#?&\/=]*)|(?:mailto:)?[_a-z0-9-\.\+]+@[_\w-]+(?:\.[a-z]{2,})+)($|[^\"])/", 545 | function ($matches) { 546 | $url = $this->cleanUrl($matches[2]); 547 | $link = $this->call('parseLink', $matches[2]); 548 | return "{$matches[1]}{$link}{$matches[5]}"; 549 | }, 550 | $text 551 | ); 552 | } 553 | 554 | $text = $this->call('afterParseInlineBeforeRelease', $text); 555 | $text = $this->releaseHolder($text, $clearHolders); 556 | 557 | $text = $this->call('afterParseInline', $text); 558 | 559 | return $text; 560 | } 561 | 562 | /** 563 | * @param $text 564 | * @return mixed 565 | */ 566 | private function parseInlineCallback($text) 567 | { 568 | $text = preg_replace_callback( 569 | "/(\*{3})(.+?)\\1/", 570 | function ($matches) { 571 | return '' . 572 | $this->parseInlineCallback($matches[2]) . 573 | ''; 574 | }, 575 | $text 576 | ); 577 | 578 | $text = preg_replace_callback( 579 | "/(\*{2})(.+?)\\1/", 580 | function ($matches) { 581 | return '' . 582 | $this->parseInlineCallback($matches[2]) . 583 | ''; 584 | }, 585 | $text 586 | ); 587 | 588 | $text = preg_replace_callback( 589 | "/(\*)(.+?)\\1/", 590 | function ($matches) { 591 | return '' . 592 | $this->parseInlineCallback($matches[2]) . 593 | ''; 594 | }, 595 | $text 596 | ); 597 | 598 | $text = preg_replace_callback( 599 | "/(\s+|^)(_{3})(.+?)\\2(\s+|$)/", 600 | function ($matches) { 601 | return $matches[1] . '' . 602 | $this->parseInlineCallback($matches[3]) . 603 | '' . $matches[4]; 604 | }, 605 | $text 606 | ); 607 | 608 | $text = preg_replace_callback( 609 | "/(\s+|^)(_{2})(.+?)\\2(\s+|$)/", 610 | function ($matches) { 611 | return $matches[1] . '' . 612 | $this->parseInlineCallback($matches[3]) . 613 | '' . $matches[4]; 614 | }, 615 | $text 616 | ); 617 | 618 | $text = preg_replace_callback( 619 | "/(\s+|^)(_)(.+?)\\2(\s+|$)/", 620 | function ($matches) { 621 | return $matches[1] . '' . 622 | $this->parseInlineCallback($matches[3]) . 623 | '' . $matches[4]; 624 | }, 625 | $text 626 | ); 627 | 628 | $text = preg_replace_callback( 629 | "/(~{2})(.+?)\\1/", 630 | function ($matches) { 631 | return '' . 632 | $this->parseInlineCallback($matches[2]) . 633 | ''; 634 | }, 635 | $text 636 | ); 637 | 638 | return $text; 639 | } 640 | 641 | /** 642 | * parseBlock 643 | * 644 | * @param string $text 645 | * @param array $lines 646 | * @return array 647 | */ 648 | private function parseBlock($text, &$lines) 649 | { 650 | $lines = explode("\n", $text); 651 | $this->_blocks = array(); 652 | $this->_current = 'normal'; 653 | $this->_pos = -1; 654 | 655 | $state = array( 656 | 'special' => implode("|", array_keys($this->_specialWhiteList)), 657 | 'empty' => 0, 658 | 'html' => false 659 | ); 660 | 661 | // analyze by line 662 | foreach ($lines as $key => $line) { 663 | $block = $this->getBlock(); 664 | $args = array($block, $key, $line, &$state, $lines); 665 | 666 | if ($this->_current != 'normal') { 667 | $pass = call_user_func_array($this->_parsers[$this->_current], $args); 668 | 669 | if (!$pass) { 670 | continue; 671 | } 672 | } 673 | 674 | foreach ($this->_parsers as $name => $parser) { 675 | if ($name != $this->_current) { 676 | $pass = call_user_func_array($parser, $args); 677 | 678 | if (!$pass) { 679 | break; 680 | } 681 | } 682 | } 683 | } 684 | 685 | return $this->optimizeBlocks($this->_blocks, $lines); 686 | } 687 | 688 | /** 689 | * @param $block 690 | * @param $key 691 | * @param $line 692 | * @param $state 693 | * @return bool 694 | */ 695 | private function parseBlockList($block, $key, $line, &$state) 696 | { 697 | if ($this->isBlock('list') && !preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line)) { 698 | if (preg_match("/^(\s*)(~{3,}|`{3,})([^`~]*)$/i", $line)) { 699 | // ignore code 700 | return true; 701 | } elseif ($state['empty'] <= 1 702 | && preg_match("/^(\s*)\S+/", $line, $matches) 703 | && strlen($matches[1]) >= ($block[3][0] + $state['empty'])) { 704 | 705 | $state['empty'] = 0; 706 | $this->setBlock($key); 707 | return false; 708 | } elseif (preg_match("/^(\s*)$/", $line) && $state['empty'] == 0) { 709 | $state['empty'] ++; 710 | $this->setBlock($key); 711 | return false; 712 | } 713 | } 714 | 715 | if (preg_match("/^(\s*)((?:[0-9]+\.)|\-|\+|\*)\s+/i", $line, $matches)) { 716 | $space = strlen($matches[1]); 717 | $tab = strlen($matches[0]) - $space; 718 | $state['empty'] = 0; 719 | $type = false !== strpos('+-*', $matches[2]) ? 'ul' : 'ol'; 720 | 721 | // opened 722 | if ($this->isBlock('list')) { 723 | if ($space < $block[3][0] || ($space == $block[3][0] && $type != $block[3][1])) { 724 | $this->startBlock('list', $key, [$space, $type, $tab]); 725 | } else { 726 | $this->setBlock($key); 727 | } 728 | } else { 729 | $this->startBlock('list', $key, [$space, $type, $tab]); 730 | } 731 | 732 | return false; 733 | } 734 | 735 | return true; 736 | } 737 | 738 | /** 739 | * @param $block 740 | * @param $key 741 | * @param $line 742 | * @param $state 743 | * @return bool 744 | */ 745 | private function parseBlockCode($block, $key, $line, &$state) 746 | { 747 | if (preg_match("/^(\s*)(~{3,}|`{3,})([^`~]*)$/i", $line, $matches)) { 748 | if ($this->isBlock('code')) { 749 | if ($state['code'] != $matches[2]) { 750 | $this->setBlock($key); 751 | return false; 752 | } 753 | 754 | $isAfterList = $block[3][2]; 755 | 756 | if ($isAfterList) { 757 | $state['empty'] = 0; 758 | $this->combineBlock() 759 | ->setBlock($key); 760 | } else { 761 | $this->setBlock($key) 762 | ->endBlock(); 763 | } 764 | } else { 765 | $isAfterList = false; 766 | 767 | if ($this->isBlock('list')) { 768 | $space = $block[3][0]; 769 | 770 | $isAfterList = strlen($matches[1]) >= $space + $state['empty']; 771 | } 772 | 773 | $state['code'] = $matches[2]; 774 | 775 | $this->startBlock('code', $key, array( 776 | $matches[1], $matches[3], $isAfterList 777 | )); 778 | } 779 | 780 | return false; 781 | } elseif ($this->isBlock('code')) { 782 | $this->setBlock($key); 783 | return false; 784 | } 785 | 786 | return true; 787 | } 788 | 789 | /** 790 | * @param $block 791 | * @param $key 792 | * @param $line 793 | * @param $state 794 | * @return bool 795 | */ 796 | private function parseBlockShtml($block, $key, $line, &$state) 797 | { 798 | if ($this->_html) { 799 | if (preg_match("/^(\s*)!!!(\s*)$/", $line, $matches)) { 800 | if ($this->isBlock('shtml')) { 801 | $this->setBlock($key)->endBlock(); 802 | } else { 803 | $this->startBlock('shtml', $key); 804 | } 805 | 806 | return false; 807 | } elseif ($this->isBlock('shtml')) { 808 | $this->setBlock($key); 809 | return false; 810 | } 811 | } 812 | 813 | return true; 814 | } 815 | 816 | /** 817 | * @param $block 818 | * @param $key 819 | * @param $line 820 | * @param $state 821 | * @return bool 822 | */ 823 | private function parseBlockAhtml($block, $key, $line, &$state) 824 | { 825 | if ($this->_html) { 826 | if (preg_match("/^\s*<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $matches)) { 827 | if ($this->isBlock('ahtml')) { 828 | $this->setBlock($key); 829 | return false; 830 | } elseif (empty($matches[2]) || $matches[2] != '/') { 831 | $this->startBlock('ahtml', $key); 832 | preg_match_all("/<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $allMatches); 833 | $lastMatch = $allMatches[1][count($allMatches[0]) - 1]; 834 | 835 | if (strpos($line, "") !== false) { 836 | $this->endBlock(); 837 | } else { 838 | $state['html'] = $lastMatch; 839 | } 840 | return false; 841 | } 842 | } elseif (!!$state['html'] && strpos($line, "") !== false) { 843 | $this->setBlock($key)->endBlock(); 844 | $state['html'] = false; 845 | return false; 846 | } elseif ($this->isBlock('ahtml')) { 847 | $this->setBlock($key); 848 | return false; 849 | } elseif (preg_match("/^\s*\s*$/", $line, $matches)) { 850 | $this->startBlock('ahtml', $key)->endBlock(); 851 | return false; 852 | } 853 | } 854 | 855 | return true; 856 | } 857 | 858 | /** 859 | * @param $block 860 | * @param $key 861 | * @param $line 862 | * @return bool 863 | */ 864 | private function parseBlockMath($block, $key, $line) 865 | { 866 | if (preg_match("/^(\s*)\\$\\$(\s*)$/", $line, $matches)) { 867 | if ($this->isBlock('math')) { 868 | $this->setBlock($key)->endBlock(); 869 | } else { 870 | $this->startBlock('math', $key); 871 | } 872 | 873 | return false; 874 | } elseif ($this->isBlock('math')) { 875 | $this->setBlock($key); 876 | return false; 877 | } 878 | 879 | return true; 880 | } 881 | 882 | /** 883 | * @param $block 884 | * @param $key 885 | * @param $line 886 | * @param $state 887 | * @return bool 888 | */ 889 | private function parseBlockPre($block, $key, $line, &$state) 890 | { 891 | if (preg_match("/^ {4}/", $line)) { 892 | if ($this->isBlock('pre')) { 893 | $this->setBlock($key); 894 | } else { 895 | $this->startBlock('pre', $key); 896 | } 897 | 898 | return false; 899 | } elseif ($this->isBlock('pre') && preg_match("/^\s*$/", $line)) { 900 | $this->setBlock($key); 901 | return false; 902 | } 903 | 904 | return true; 905 | } 906 | 907 | /** 908 | * @param $block 909 | * @param $key 910 | * @param $line 911 | * @param $state 912 | * @return bool 913 | */ 914 | private function parseBlockHtml($block, $key, $line, &$state) 915 | { 916 | if (preg_match("/^\s*<({$state['special']})(\s+[^>]*)?>/i", $line, $matches)) { 917 | $tag = strtolower($matches[1]); 918 | if (!$this->isBlock('html', $tag) && !$this->isBlock('pre')) { 919 | $this->startBlock('html', $key, $tag); 920 | } 921 | 922 | return false; 923 | } elseif (preg_match("/<\/({$state['special']})>\s*$/i", $line, $matches)) { 924 | $tag = strtolower($matches[1]); 925 | 926 | if ($this->isBlock('html', $tag)) { 927 | $this->setBlock($key) 928 | ->endBlock(); 929 | } 930 | 931 | return false; 932 | } elseif ($this->isBlock('html')) { 933 | $this->setBlock($key); 934 | return false; 935 | } 936 | 937 | return true; 938 | } 939 | 940 | /** 941 | * @param $block 942 | * @param $key 943 | * @param $line 944 | * @return bool 945 | */ 946 | private function parseBlockFootnote($block, $key, $line) 947 | { 948 | if (preg_match("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", $line, $matches)) { 949 | $space = strlen($matches[0]) - 1; 950 | $this->startBlock('footnote', $key, array( 951 | $space, $matches[1] 952 | )); 953 | 954 | return false; 955 | } 956 | 957 | return true; 958 | } 959 | 960 | /** 961 | * @param $block 962 | * @param $key 963 | * @param $line 964 | * @return bool 965 | */ 966 | private function parseBlockDefinition($block, $key, $line) 967 | { 968 | if (preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line, $matches)) { 969 | $this->_definitions[$matches[1]] = $this->cleanUrl($matches[2]); 970 | $this->startBlock('definition', $key) 971 | ->endBlock(); 972 | 973 | return false; 974 | } 975 | 976 | return true; 977 | } 978 | 979 | /** 980 | * @param $block 981 | * @param $key 982 | * @param $line 983 | * @return bool 984 | */ 985 | private function parseBlockQuote($block, $key, $line) 986 | { 987 | if (preg_match("/^(\s*)>/", $line, $matches)) { 988 | if ($this->isBlock('list') && strlen($matches[1]) > 0) { 989 | $this->setBlock($key); 990 | } elseif ($this->isBlock('quote')) { 991 | $this->setBlock($key); 992 | } else { 993 | $this->startBlock('quote', $key); 994 | } 995 | 996 | return false; 997 | } 998 | 999 | return true; 1000 | } 1001 | 1002 | /** 1003 | * @param $block 1004 | * @param $key 1005 | * @param $line 1006 | * @param $state 1007 | * @param $lines 1008 | * @return bool 1009 | */ 1010 | private function parseBlockTable($block, $key, $line, &$state, $lines) 1011 | { 1012 | if (preg_match("/^((?:(?:(?:\||\+)(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:[ :]*\-+[ :]*)(?:\||\+)(?:[ :]*\-+[ :]*))|(?:(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:\||\+)(?:[ :]*\-+[ :]*)))+)$/", $line, $matches)) { 1013 | if ($this->isBlock('table')) { 1014 | $block[3][0][] = $block[3][2]; 1015 | $block[3][2]++; 1016 | $this->setBlock($key, $block[3]); 1017 | } else { 1018 | $head = 0; 1019 | 1020 | if (empty($block) || 1021 | $block[0] != 'normal' || 1022 | preg_match("/^\s*$/", $lines[$block[2]])) { 1023 | $this->startBlock('table', $key); 1024 | } else { 1025 | $head = 1; 1026 | $this->backBlock(1, 'table'); 1027 | } 1028 | 1029 | if ($matches[1][0] == '|') { 1030 | $matches[1] = substr($matches[1], 1); 1031 | 1032 | if ($matches[1][strlen($matches[1]) - 1] == '|') { 1033 | $matches[1] = substr($matches[1], 0, -1); 1034 | } 1035 | } 1036 | 1037 | $rows = preg_split("/(\+|\|)/", $matches[1]); 1038 | $aligns = array(); 1039 | foreach ($rows as $row) { 1040 | $align = 'none'; 1041 | 1042 | if (preg_match("/^\s*(:?)\-+(:?)\s*$/", $row, $matches)) { 1043 | if (!empty($matches[1]) && !empty($matches[2])) { 1044 | $align = 'center'; 1045 | } elseif (!empty($matches[1])) { 1046 | $align = 'left'; 1047 | } elseif (!empty($matches[2])) { 1048 | $align = 'right'; 1049 | } 1050 | } 1051 | 1052 | $aligns[] = $align; 1053 | } 1054 | 1055 | $this->setBlock($key, array(array($head), $aligns, $head + 1)); 1056 | } 1057 | 1058 | return false; 1059 | } 1060 | 1061 | return true; 1062 | } 1063 | 1064 | /** 1065 | * @param $block 1066 | * @param $key 1067 | * @param $line 1068 | * @return bool 1069 | */ 1070 | private function parseBlockSh($block, $key, $line) 1071 | { 1072 | if (preg_match("/^(#+)(.*)$/", $line, $matches)) { 1073 | $num = min(strlen($matches[1]), 6); 1074 | $this->startBlock('sh', $key, $num) 1075 | ->endBlock(); 1076 | 1077 | return false; 1078 | } 1079 | 1080 | return true; 1081 | } 1082 | 1083 | /** 1084 | * @param $block 1085 | * @param $key 1086 | * @param $line 1087 | * @param $state 1088 | * @param $lines 1089 | * @return bool 1090 | */ 1091 | private function parseBlockMh($block, $key, $line, &$state, $lines) 1092 | { 1093 | if (preg_match("/^\s*((=|-){2,})\s*$/", $line, $matches) 1094 | && ($block && $block[0] == "normal" && !preg_match("/^\s*$/", $lines[$block[2]]))) { // check if last line isn't empty 1095 | if ($this->isBlock('normal')) { 1096 | $this->backBlock(1, 'mh', $matches[1][0] == '=' ? 1 : 2) 1097 | ->setBlock($key) 1098 | ->endBlock(); 1099 | } else { 1100 | $this->startBlock('normal', $key); 1101 | } 1102 | 1103 | return false; 1104 | } 1105 | 1106 | return true; 1107 | } 1108 | 1109 | /** 1110 | * @param $block 1111 | * @param $key 1112 | * @param $line 1113 | * @return bool 1114 | */ 1115 | private function parseBlockShr($block, $key, $line) 1116 | { 1117 | if (preg_match("/^(\* *){3,}\s*$/", $line)) { 1118 | $this->startBlock('hr', $key) 1119 | ->endBlock(); 1120 | 1121 | return false; 1122 | } 1123 | 1124 | return true; 1125 | } 1126 | 1127 | /** 1128 | * @param $block 1129 | * @param $key 1130 | * @param $line 1131 | * @return bool 1132 | */ 1133 | private function parseBlockDhr($block, $key, $line) 1134 | { 1135 | if (preg_match("/^(- *){3,}\s*$/", $line)) { 1136 | $this->startBlock('hr', $key) 1137 | ->endBlock(); 1138 | 1139 | return false; 1140 | } 1141 | 1142 | return true; 1143 | } 1144 | 1145 | /** 1146 | * @param $block 1147 | * @param $key 1148 | * @param $line 1149 | * @param $state 1150 | * @return bool 1151 | */ 1152 | private function parseBlockDefault($block, $key, $line, &$state) 1153 | { 1154 | if ($this->isBlock('footnote')) { 1155 | preg_match("/^(\s*)/", $line, $matches); 1156 | if (strlen($matches[1]) >= $block[3][0]) { 1157 | $this->setBlock($key); 1158 | } else { 1159 | $this->startBlock('normal', $key); 1160 | } 1161 | } elseif ($this->isBlock('table')) { 1162 | if (false !== strpos($line, '|')) { 1163 | $block[3][2] ++; 1164 | $this->setBlock($key, $block[3]); 1165 | } else { 1166 | $this->startBlock('normal', $key); 1167 | } 1168 | } elseif ($this->isBlock('quote')) { 1169 | if (!preg_match("/^(\s*)$/", $line)) { // empty line 1170 | $this->setBlock($key); 1171 | } else { 1172 | $this->startBlock('normal', $key); 1173 | } 1174 | } else { 1175 | if (empty($block) || $block[0] != 'normal') { 1176 | $this->startBlock('normal', $key); 1177 | } else { 1178 | $this->setBlock($key); 1179 | } 1180 | } 1181 | 1182 | return true; 1183 | } 1184 | 1185 | /** 1186 | * @param array $blocks 1187 | * @param array $lines 1188 | * @return array 1189 | */ 1190 | private function optimizeBlocks(array $blocks, array $lines) 1191 | { 1192 | $blocks = $this->call('beforeOptimizeBlocks', $blocks, $lines); 1193 | 1194 | $key = 0; 1195 | while (isset($blocks[$key])) { 1196 | $moved = false; 1197 | 1198 | $block = &$blocks[$key]; 1199 | $prevBlock = isset($blocks[$key - 1]) ? $blocks[$key - 1] : NULL; 1200 | $nextBlock = isset($blocks[$key + 1]) ? $blocks[$key + 1] : NULL; 1201 | 1202 | list($type, $from, $to) = $block; 1203 | 1204 | if ('pre' == $type) { 1205 | $isEmpty = array_reduce( 1206 | array_slice($lines, $block[1], $block[2] - $block[1] + 1), 1207 | function ($result, $line) { 1208 | return preg_match("/^\s*$/", $line) && $result; 1209 | }, 1210 | true 1211 | ); 1212 | 1213 | if ($isEmpty) { 1214 | $block[0] = $type = 'normal'; 1215 | } 1216 | } 1217 | 1218 | if ('normal' == $type) { 1219 | // combine two blocks 1220 | $types = array('list', 'quote'); 1221 | 1222 | if ($from == $to && preg_match("/^\s*$/", $lines[$from]) 1223 | && !empty($prevBlock) && !empty($nextBlock)) { 1224 | if ($prevBlock[0] == $nextBlock[0] && in_array($prevBlock[0], $types) 1225 | && ($prevBlock[0] != 'list' 1226 | || ($prevBlock[3][0] == $nextBlock[3][0] && $prevBlock[3][1] == $nextBlock[3][1]))) { 1227 | // combine 3 blocks 1228 | $blocks[$key - 1] = array( 1229 | $prevBlock[0], $prevBlock[1], $nextBlock[2], $prevBlock[3] ?? null 1230 | ); 1231 | array_splice($blocks, $key, 2); 1232 | 1233 | // do not move 1234 | $moved = true; 1235 | } 1236 | } 1237 | } 1238 | 1239 | if (!$moved) { 1240 | $key ++; 1241 | } 1242 | } 1243 | 1244 | return $this->call('afterOptimizeBlocks', $blocks, $lines); 1245 | } 1246 | 1247 | /** 1248 | * parseCode 1249 | * 1250 | * @param array $lines 1251 | * @param array $parts 1252 | * @param int $start 1253 | * @return string 1254 | */ 1255 | private function parseCode(array $lines, array $parts, $start) 1256 | { 1257 | list($blank, $lang) = $parts; 1258 | $lang = trim($lang); 1259 | $count = strlen($blank); 1260 | 1261 | if (!preg_match("/^[_a-z0-9-\+\#\:\.]+$/i", $lang)) { 1262 | $lang = NULL; 1263 | } else { 1264 | $parts = explode(':', $lang); 1265 | if (count($parts) > 1) { 1266 | list($lang, $rel) = $parts; 1267 | $lang = trim($lang); 1268 | $rel = trim($rel); 1269 | } 1270 | } 1271 | 1272 | $isEmpty = true; 1273 | 1274 | $lines = array_map(function ($line) use ($count, &$isEmpty) { 1275 | $line = preg_replace("/^[ ]{{$count}}/", '', $line); 1276 | if ($isEmpty && !preg_match("/^\s*$/", $line)) { 1277 | $isEmpty = false; 1278 | } 1279 | 1280 | return htmlspecialchars($line); 1281 | }, array_slice($lines, 1, -1)); 1282 | $str = implode("\n", $this->markLines($lines, $start + 1)); 1283 | 1284 | return $isEmpty ? '' : 1285 | '
'
1287 |             . $str . '
'; 1288 | } 1289 | 1290 | /** 1291 | * parsePre 1292 | * 1293 | * @param array $lines 1294 | * @param mixed $value 1295 | * @param int $start 1296 | * @return string 1297 | */ 1298 | private function parsePre(array $lines, $value, $start) 1299 | { 1300 | foreach ($lines as &$line) { 1301 | $line = htmlspecialchars(substr($line, 4)); 1302 | } 1303 | 1304 | $str = implode("\n", $this->markLines($lines, $start)); 1305 | return preg_match("/^\s*$/", $str) ? '' : '
' . $str . '
'; 1306 | } 1307 | 1308 | /** 1309 | * parseAhtml 1310 | * 1311 | * @param array $lines 1312 | * @param mixed $value 1313 | * @param int $start 1314 | * @return string 1315 | */ 1316 | private function parseAhtml(array $lines, $value, $start) 1317 | { 1318 | return trim(implode("\n", $this->markLines($lines, $start))); 1319 | } 1320 | 1321 | /** 1322 | * parseShtml 1323 | * 1324 | * @param array $lines 1325 | * @param mixed $value 1326 | * @param int $start 1327 | * @return string 1328 | */ 1329 | private function parseShtml(array $lines, $value, $start) 1330 | { 1331 | return trim(implode("\n", $this->markLines(array_slice($lines, 1, -1), $start + 1))); 1332 | } 1333 | 1334 | /** 1335 | * parseMath 1336 | * 1337 | * @param array $lines 1338 | * @param mixed $value 1339 | * @param int $start 1340 | * @param int $end 1341 | * @return string 1342 | */ 1343 | private function parseMath(array $lines, $value, $start, $end) 1344 | { 1345 | return '

' . $this->markLine($start, $end) . htmlspecialchars(implode("\n", $lines)) . '

'; 1346 | } 1347 | 1348 | /** 1349 | * parseSh 1350 | * 1351 | * @param array $lines 1352 | * @param int $num 1353 | * @param int $start 1354 | * @param int $end 1355 | * @return string 1356 | */ 1357 | private function parseSh(array $lines, $num, $start, $end) 1358 | { 1359 | $line = $this->markLine($start, $end) . $this->parseInline(trim($lines[0], '# ')); 1360 | return preg_match("/^\s*$/", $line) ? '' : "{$line}"; 1361 | } 1362 | 1363 | /** 1364 | * parseMh 1365 | * 1366 | * @param array $lines 1367 | * @param int $num 1368 | * @param int $start 1369 | * @param int $end 1370 | * @return string 1371 | */ 1372 | private function parseMh(array $lines, $num, $start, $end) 1373 | { 1374 | return $this->parseSh($lines, $num, $start, $end); 1375 | } 1376 | 1377 | /** 1378 | * parseQuote 1379 | * 1380 | * @param array $lines 1381 | * @param mixed $value 1382 | * @param int $start 1383 | * @return string 1384 | */ 1385 | private function parseQuote(array $lines, $value, $start) 1386 | { 1387 | foreach ($lines as &$line) { 1388 | $line = preg_replace("/^\s*> ?/", '', $line); 1389 | } 1390 | $str = implode("\n", $lines); 1391 | 1392 | return preg_match("/^\s*$/", $str) ? '' : '
' . $this->parse($str, true, $start) . '
'; 1393 | } 1394 | 1395 | /** 1396 | * parseList 1397 | * 1398 | * @param array $lines 1399 | * @param mixed $value 1400 | * @param int $start 1401 | * @return string 1402 | */ 1403 | private function parseList(array $lines, $value, $start) 1404 | { 1405 | $html = ''; 1406 | list($space, $type, $tab) = $value; 1407 | $rows = array(); 1408 | $suffix = ''; 1409 | $last = 0; 1410 | 1411 | foreach ($lines as $key => $line) { 1412 | if (preg_match("/^(\s{" . $space . "})((?:[0-9]+\.?)|\-|\+|\*)(\s+)(.*)$/i", $line, $matches)) { 1413 | if ($type == 'ol' && $key == 0) { 1414 | $start = intval($matches[2]); 1415 | 1416 | if ($start != 1) { 1417 | $suffix = ' start="' . $start . '"'; 1418 | } 1419 | } 1420 | 1421 | $rows[] = [$matches[4]]; 1422 | $last = count($rows) - 1; 1423 | } else { 1424 | $rows[$last][] = preg_replace("/^\s{" . ($tab + $space) . "}/", '', $line); 1425 | } 1426 | } 1427 | 1428 | foreach ($rows as $row) { 1429 | $html .= "
  • " . $this->parse(implode("\n", $row), true, $start) . "
  • "; 1430 | $start += count($row); 1431 | } 1432 | 1433 | return "<{$type}{$suffix}>{$html}"; 1434 | } 1435 | 1436 | /** 1437 | * @param array $lines 1438 | * @param array $value 1439 | * @param int $start 1440 | * @return string 1441 | */ 1442 | private function parseTable(array $lines, array $value, $start) 1443 | { 1444 | list($ignores, $aligns) = $value; 1445 | $head = count($ignores) > 0 && array_sum($ignores) > 0; 1446 | 1447 | $html = ''; 1448 | $body = $head ? NULL : true; 1449 | $output = false; 1450 | 1451 | foreach ($lines as $key => $line) { 1452 | if (in_array($key, $ignores)) { 1453 | if ($head && $output) { 1454 | $head = false; 1455 | $body = true; 1456 | } 1457 | 1458 | continue; 1459 | } 1460 | 1461 | $line = trim($line); 1462 | $output = true; 1463 | 1464 | if ($line[0] == '|') { 1465 | $line = substr($line, 1); 1466 | 1467 | if ($line[strlen($line) - 1] == '|') { 1468 | $line = substr($line, 0, -1); 1469 | } 1470 | } 1471 | 1472 | 1473 | $rows = array_map(function ($row) { 1474 | if (preg_match("/^\s*$/", $row)) { 1475 | return ' '; 1476 | } else { 1477 | return trim($row); 1478 | } 1479 | }, explode('|', $line)); 1480 | $columns = array(); 1481 | $last = -1; 1482 | 1483 | foreach ($rows as $row) { 1484 | if (strlen($row) > 0) { 1485 | $last ++; 1486 | $columns[$last] = array( 1487 | isset($columns[$last]) ? $columns[$last][0] + 1 : 1, $row 1488 | ); 1489 | } elseif (isset($columns[$last])) { 1490 | $columns[$last][0] ++; 1491 | } else { 1492 | $columns[0] = array(1, $row); 1493 | } 1494 | } 1495 | 1496 | if ($head) { 1497 | $html .= ''; 1498 | } elseif ($body) { 1499 | $html .= ''; 1500 | } 1501 | 1502 | $html .= '_line ? ' class="line" data-start="' 1503 | . ($start + $key) . '" data-end="' . ($start + $key) 1504 | . '" data-id="' . $this->_uniqid . '"' : '') . '>'; 1505 | 1506 | foreach ($columns as $key => $column) { 1507 | list($num, $text) = $column; 1508 | $tag = $head ? 'th' : 'td'; 1509 | 1510 | $html .= "<{$tag}"; 1511 | if ($num > 1) { 1512 | $html .= " colspan=\"{$num}\""; 1513 | } 1514 | 1515 | if (isset($aligns[$key]) && $aligns[$key] != 'none') { 1516 | $html .= " align=\"{$aligns[$key]}\""; 1517 | } 1518 | 1519 | $html .= '>' . $this->parseInline($text) . ""; 1520 | } 1521 | 1522 | $html .= ''; 1523 | 1524 | if ($head) { 1525 | $html .= ''; 1526 | } elseif ($body) { 1527 | $body = false; 1528 | } 1529 | } 1530 | 1531 | if ($body !== NULL) { 1532 | $html .= ''; 1533 | } 1534 | 1535 | $html .= '
    '; 1536 | return $html; 1537 | } 1538 | 1539 | /** 1540 | * parseHr 1541 | * 1542 | * @param array $lines 1543 | * @param array $value 1544 | * @param int $start 1545 | * @return string 1546 | */ 1547 | private function parseHr($lines, $value, $start) 1548 | { 1549 | return $this->_line ? '
    ' : '
    '; 1550 | } 1551 | 1552 | /** 1553 | * parseNormal 1554 | * 1555 | * @param array $lines 1556 | * @param bool $inline 1557 | * @param int $start 1558 | * @return string 1559 | */ 1560 | private function parseNormal(array $lines, $inline, $start) 1561 | { 1562 | foreach ($lines as $key => &$line) { 1563 | $line = $this->parseInline($line); 1564 | 1565 | if (!preg_match("/^\s*$/", $line)) { 1566 | $line = $this->markLine($start + $key) . $line; 1567 | } 1568 | } 1569 | 1570 | $str = trim(implode("\n", $lines)); 1571 | $str = preg_replace_callback("/(\n\s*){2,}/", function () use (&$inline) { 1572 | $inline = false; 1573 | return "

    "; 1574 | }, $str); 1575 | $str = preg_replace("/\n/", "
    ", $str); 1576 | 1577 | return preg_match("/^\s*$/", $str) ? '' : ($inline ? $str : "

    {$str}

    "); 1578 | } 1579 | 1580 | /** 1581 | * parseFootnote 1582 | * 1583 | * @param array $lines 1584 | * @param array $value 1585 | * @return string 1586 | */ 1587 | private function parseFootnote(array $lines, array $value) 1588 | { 1589 | list($space, $note) = $value; 1590 | $index = array_search($note, $this->_footnotes); 1591 | 1592 | if (false !== $index) { 1593 | $lines[0] = preg_replace("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", '', $lines[0]); 1594 | $this->_footnotes[$index] = $lines; 1595 | } 1596 | 1597 | return ''; 1598 | } 1599 | 1600 | /** 1601 | * parseDefine 1602 | * 1603 | * @return string 1604 | */ 1605 | private function parseDefinition() 1606 | { 1607 | return ''; 1608 | } 1609 | 1610 | /** 1611 | * parseHtml 1612 | * 1613 | * @param array $lines 1614 | * @param string $type 1615 | * @param int $start 1616 | * @return string 1617 | */ 1618 | private function parseHtml(array $lines, $type, $start) 1619 | { 1620 | foreach ($lines as &$line) { 1621 | $line = $this->parseInline($line, 1622 | isset($this->_specialWhiteList[$type]) ? $this->_specialWhiteList[$type] : ''); 1623 | } 1624 | 1625 | return implode("\n", $this->markLines($lines, $start)); 1626 | } 1627 | 1628 | /** 1629 | * @param $url 1630 | * @param bool $parseTitle 1631 | * 1632 | * @return mixed 1633 | */ 1634 | private function cleanUrl($url, $parseTitle = false) 1635 | { 1636 | $title = null; 1637 | $url = trim($url); 1638 | 1639 | if ($parseTitle) { 1640 | $pos = strpos($url, ' '); 1641 | 1642 | if ($pos !== false) { 1643 | $title = htmlspecialchars(trim(substr($url, $pos + 1), ' "\'')); 1644 | $url = substr($url, 0, $pos); 1645 | } 1646 | } 1647 | 1648 | $url = preg_replace("/[\"'<>\s]/", '', $url); 1649 | 1650 | if (preg_match("/^(mailto:)?[_a-z0-9-\.\+]+@[_\w-]+(?:\.[a-z]{2,})+$/i", $url, $matches)) { 1651 | if (empty($matches[1])) { 1652 | $url = 'mailto:' . $url; 1653 | } 1654 | } 1655 | 1656 | if (preg_match("/^\w+:/i", $url) && !preg_match("/^(https?|mailto):/i", $url)) { 1657 | return '#'; 1658 | } 1659 | 1660 | return $parseTitle ? [$url, $title] : $url; 1661 | } 1662 | 1663 | /** 1664 | * @param $str 1665 | * @return mixed 1666 | */ 1667 | private function escapeBracket($str) 1668 | { 1669 | return str_replace( 1670 | array('\[', '\]', '\(', '\)'), array('[', ']', '(', ')'), $str 1671 | ); 1672 | } 1673 | 1674 | /** 1675 | * startBlock 1676 | * 1677 | * @param mixed $type 1678 | * @param mixed $start 1679 | * @param mixed $value 1680 | * @return $this 1681 | */ 1682 | private function startBlock($type, $start, $value = NULL) 1683 | { 1684 | $this->_pos ++; 1685 | $this->_current = $type; 1686 | 1687 | $this->_blocks[$this->_pos] = array($type, $start, $start, $value); 1688 | 1689 | return $this; 1690 | } 1691 | 1692 | /** 1693 | * endBlock 1694 | * 1695 | * @return $this 1696 | */ 1697 | private function endBlock() 1698 | { 1699 | $this->_current = 'normal'; 1700 | return $this; 1701 | } 1702 | 1703 | /** 1704 | * isBlock 1705 | * 1706 | * @param mixed $type 1707 | * @param mixed $value 1708 | * @return bool 1709 | */ 1710 | private function isBlock($type, $value = NULL) 1711 | { 1712 | return $this->_current == $type 1713 | && (NULL === $value ? true : $this->_blocks[$this->_pos][3] == $value); 1714 | } 1715 | 1716 | /** 1717 | * getBlock 1718 | * 1719 | * @return array 1720 | */ 1721 | private function getBlock() 1722 | { 1723 | return isset($this->_blocks[$this->_pos]) ? $this->_blocks[$this->_pos] : NULL; 1724 | } 1725 | 1726 | /** 1727 | * setBlock 1728 | * 1729 | * @param mixed $to 1730 | * @param mixed $value 1731 | * @return $this 1732 | */ 1733 | private function setBlock($to = NULL, $value = NULL) 1734 | { 1735 | if (NULL !== $to) { 1736 | $this->_blocks[$this->_pos][2] = $to; 1737 | } 1738 | 1739 | if (NULL !== $value) { 1740 | $this->_blocks[$this->_pos][3] = $value; 1741 | } 1742 | 1743 | return $this; 1744 | } 1745 | 1746 | /** 1747 | * backBlock 1748 | * 1749 | * @param mixed $step 1750 | * @param mixed $type 1751 | * @param mixed $value 1752 | * @return $this 1753 | */ 1754 | private function backBlock($step, $type, $value = NULL) 1755 | { 1756 | if ($this->_pos < 0) { 1757 | return $this->startBlock($type, 0, $value); 1758 | } 1759 | 1760 | $last = $this->_blocks[$this->_pos][2]; 1761 | $this->_blocks[$this->_pos][2] = $last - $step; 1762 | 1763 | if ($this->_blocks[$this->_pos][1] <= $this->_blocks[$this->_pos][2]) { 1764 | $this->_pos ++; 1765 | } 1766 | 1767 | $this->_current = $type; 1768 | $this->_blocks[$this->_pos] = array( 1769 | $type, $last - $step + 1, $last, $value 1770 | ); 1771 | 1772 | return $this; 1773 | } 1774 | 1775 | /** 1776 | * @return $this 1777 | */ 1778 | private function combineBlock() 1779 | { 1780 | if ($this->_pos < 1) { 1781 | return $this; 1782 | } 1783 | 1784 | $prev = $this->_blocks[$this->_pos - 1]; 1785 | $current = $this->_blocks[$this->_pos]; 1786 | 1787 | $prev[2] = $current[2]; 1788 | $this->_blocks[$this->_pos - 1] = $prev; 1789 | $this->_current = $prev[0]; 1790 | unset($this->_blocks[$this->_pos]); 1791 | $this->_pos --; 1792 | 1793 | return $this; 1794 | } 1795 | } 1796 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 为何要写这样一个解析器 2 | ====================== 3 | 4 | Markdown已经面世许多年了,国内外许多大大小小的网站都在用它,但是它的解析器却依然混乱不堪。SegmentFault 是中国较大规模使用 Markdown 语法的网站,我们一直在使用一些开源类库,包括但不限于 5 | 6 | 1. [php-markdown](https://github.com/michelf/php-markdown) 7 | 2. [CommonMark for PHP](https://github.com/thephpleague/commonmark) 8 | 3. [Parsedown](https://github.com/erusev/parsedown) 9 | 10 | 他们都有或多或少的毛病,有的性能较差,有的代码比较业余,更多的情况是由于Markdown本身解析比较复杂,因此我们几乎无法去维护另外一个人写的代码。基于这个原因,我为 SegmentFault 专门编写了这么一个Markdown解析器。 11 | 12 | 使用方法 13 | -------- 14 | 15 | 与常规的解析类库没有任何区别 16 | 17 | ```php 18 | $parser = new HyperDown\Parser; 19 | $html = $parser->makeHtml($text); 20 | ``` 21 | 22 | 当前支持的语法 23 | -------------- 24 | 25 | - 标题 26 | - 列表(可递归) 27 | - 引用(可递归) 28 | - 缩进风格的代码块 29 | - Github风格的代码块 30 | - 各种行内文字加粗,斜体等效果 31 | - 链接,图片 32 | - 自动链接 33 | - 段内折行 34 | - 脚标 35 | - 分隔符 36 | - 表格 37 | - 图片和链接支持互相套用 38 | 39 | 浏览器中使用请参阅 [HyperDown.js](https://github.com/SegmentFault/HyperDown.js) 40 | -------------------------------------------------------------------------------- /cli.php: -------------------------------------------------------------------------------- 1 | enableHtml(true); 35 | } 36 | 37 | if ($line) { 38 | $parser->enableLine(true); 39 | } 40 | 41 | $buff = file_get_contents($file); 42 | echo $parser->makeHtml($buff); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joyqi/hyper-down", 3 | "description": "A light weight markdown parser library", 4 | "keywords": ["markdown", "hyperdown", "segmentfault"], 5 | "license": "BSD-4-Clause", 6 | "authors": [ 7 | { 8 | "name": "joyqi", 9 | "email": "joyqi@segmentfault.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.1.0", 14 | "ext-mbstring": "*" 15 | }, 16 | "autoload": { 17 | "psr-4": { "HyperDown\\": "./" } 18 | }, 19 | "scripts": { 20 | "test": [ 21 | "php test/test.php" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "666ba4403a5cfd7267a4b759137db9da", 8 | "content-hash": "04dfedcbd6db661d73db3733ed625e6e", 9 | "packages": [], 10 | "packages-dev": [ 11 | { 12 | "name": "doctrine/instantiator", 13 | "version": "1.0.5", 14 | "source": { 15 | "type": "git", 16 | "url": "https://github.com/doctrine/instantiator.git", 17 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" 18 | }, 19 | "dist": { 20 | "type": "zip", 21 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", 22 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", 23 | "shasum": "" 24 | }, 25 | "require": { 26 | "php": ">=5.3,<8.0-DEV" 27 | }, 28 | "require-dev": { 29 | "athletic/athletic": "~0.1.8", 30 | "ext-pdo": "*", 31 | "ext-phar": "*", 32 | "phpunit/phpunit": "~4.0", 33 | "squizlabs/php_codesniffer": "~2.0" 34 | }, 35 | "type": "library", 36 | "extra": { 37 | "branch-alias": { 38 | "dev-master": "1.0.x-dev" 39 | } 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 44 | } 45 | }, 46 | "notification-url": "https://packagist.org/downloads/", 47 | "license": [ 48 | "MIT" 49 | ], 50 | "authors": [ 51 | { 52 | "name": "Marco Pivetta", 53 | "email": "ocramius@gmail.com", 54 | "homepage": "http://ocramius.github.com/" 55 | } 56 | ], 57 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 58 | "homepage": "https://github.com/doctrine/instantiator", 59 | "keywords": [ 60 | "constructor", 61 | "instantiate" 62 | ], 63 | "time": "2015-06-14 21:17:01" 64 | }, 65 | { 66 | "name": "myclabs/deep-copy", 67 | "version": "1.5.0", 68 | "source": { 69 | "type": "git", 70 | "url": "https://github.com/myclabs/DeepCopy.git", 71 | "reference": "e3abefcd7f106677fd352cd7c187d6c969aa9ddc" 72 | }, 73 | "dist": { 74 | "type": "zip", 75 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e3abefcd7f106677fd352cd7c187d6c969aa9ddc", 76 | "reference": "e3abefcd7f106677fd352cd7c187d6c969aa9ddc", 77 | "shasum": "" 78 | }, 79 | "require": { 80 | "php": ">=5.4.0" 81 | }, 82 | "require-dev": { 83 | "doctrine/collections": "1.*", 84 | "phpunit/phpunit": "~4.1" 85 | }, 86 | "type": "library", 87 | "autoload": { 88 | "psr-4": { 89 | "DeepCopy\\": "src/DeepCopy/" 90 | } 91 | }, 92 | "notification-url": "https://packagist.org/downloads/", 93 | "license": [ 94 | "MIT" 95 | ], 96 | "description": "Create deep copies (clones) of your objects", 97 | "homepage": "https://github.com/myclabs/DeepCopy", 98 | "keywords": [ 99 | "clone", 100 | "copy", 101 | "duplicate", 102 | "object", 103 | "object graph" 104 | ], 105 | "time": "2015-11-07 22:20:37" 106 | }, 107 | { 108 | "name": "phpdocumentor/reflection-docblock", 109 | "version": "2.0.4", 110 | "source": { 111 | "type": "git", 112 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 113 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" 114 | }, 115 | "dist": { 116 | "type": "zip", 117 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", 118 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", 119 | "shasum": "" 120 | }, 121 | "require": { 122 | "php": ">=5.3.3" 123 | }, 124 | "require-dev": { 125 | "phpunit/phpunit": "~4.0" 126 | }, 127 | "suggest": { 128 | "dflydev/markdown": "~1.0", 129 | "erusev/parsedown": "~1.0" 130 | }, 131 | "type": "library", 132 | "extra": { 133 | "branch-alias": { 134 | "dev-master": "2.0.x-dev" 135 | } 136 | }, 137 | "autoload": { 138 | "psr-0": { 139 | "phpDocumentor": [ 140 | "src/" 141 | ] 142 | } 143 | }, 144 | "notification-url": "https://packagist.org/downloads/", 145 | "license": [ 146 | "MIT" 147 | ], 148 | "authors": [ 149 | { 150 | "name": "Mike van Riel", 151 | "email": "mike.vanriel@naenius.com" 152 | } 153 | ], 154 | "time": "2015-02-03 12:10:50" 155 | }, 156 | { 157 | "name": "phpspec/prophecy", 158 | "version": "v1.6.0", 159 | "source": { 160 | "type": "git", 161 | "url": "https://github.com/phpspec/prophecy.git", 162 | "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972" 163 | }, 164 | "dist": { 165 | "type": "zip", 166 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972", 167 | "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972", 168 | "shasum": "" 169 | }, 170 | "require": { 171 | "doctrine/instantiator": "^1.0.2", 172 | "php": "^5.3|^7.0", 173 | "phpdocumentor/reflection-docblock": "~2.0", 174 | "sebastian/comparator": "~1.1", 175 | "sebastian/recursion-context": "~1.0" 176 | }, 177 | "require-dev": { 178 | "phpspec/phpspec": "~2.0" 179 | }, 180 | "type": "library", 181 | "extra": { 182 | "branch-alias": { 183 | "dev-master": "1.5.x-dev" 184 | } 185 | }, 186 | "autoload": { 187 | "psr-0": { 188 | "Prophecy\\": "src/" 189 | } 190 | }, 191 | "notification-url": "https://packagist.org/downloads/", 192 | "license": [ 193 | "MIT" 194 | ], 195 | "authors": [ 196 | { 197 | "name": "Konstantin Kudryashov", 198 | "email": "ever.zet@gmail.com", 199 | "homepage": "http://everzet.com" 200 | }, 201 | { 202 | "name": "Marcello Duarte", 203 | "email": "marcello.duarte@gmail.com" 204 | } 205 | ], 206 | "description": "Highly opinionated mocking framework for PHP 5.3+", 207 | "homepage": "https://github.com/phpspec/prophecy", 208 | "keywords": [ 209 | "Double", 210 | "Dummy", 211 | "fake", 212 | "mock", 213 | "spy", 214 | "stub" 215 | ], 216 | "time": "2016-02-15 07:46:21" 217 | }, 218 | { 219 | "name": "phpunit/php-code-coverage", 220 | "version": "3.3.1", 221 | "source": { 222 | "type": "git", 223 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 224 | "reference": "2431befdd451fac43fbcde94d1a92fb3b8b68f86" 225 | }, 226 | "dist": { 227 | "type": "zip", 228 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2431befdd451fac43fbcde94d1a92fb3b8b68f86", 229 | "reference": "2431befdd451fac43fbcde94d1a92fb3b8b68f86", 230 | "shasum": "" 231 | }, 232 | "require": { 233 | "php": "^5.6 || ^7.0", 234 | "phpunit/php-file-iterator": "~1.3", 235 | "phpunit/php-text-template": "~1.2", 236 | "phpunit/php-token-stream": "^1.4.2", 237 | "sebastian/code-unit-reverse-lookup": "~1.0", 238 | "sebastian/environment": "^1.3.2", 239 | "sebastian/version": "~1.0|~2.0" 240 | }, 241 | "require-dev": { 242 | "ext-xdebug": ">=2.1.4", 243 | "phpunit/phpunit": "~5" 244 | }, 245 | "suggest": { 246 | "ext-dom": "*", 247 | "ext-xdebug": ">=2.4.0", 248 | "ext-xmlwriter": "*" 249 | }, 250 | "type": "library", 251 | "extra": { 252 | "branch-alias": { 253 | "dev-master": "3.3.x-dev" 254 | } 255 | }, 256 | "autoload": { 257 | "classmap": [ 258 | "src/" 259 | ] 260 | }, 261 | "notification-url": "https://packagist.org/downloads/", 262 | "license": [ 263 | "BSD-3-Clause" 264 | ], 265 | "authors": [ 266 | { 267 | "name": "Sebastian Bergmann", 268 | "email": "sb@sebastian-bergmann.de", 269 | "role": "lead" 270 | } 271 | ], 272 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 273 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 274 | "keywords": [ 275 | "coverage", 276 | "testing", 277 | "xunit" 278 | ], 279 | "time": "2016-04-08 08:14:53" 280 | }, 281 | { 282 | "name": "phpunit/php-file-iterator", 283 | "version": "1.4.1", 284 | "source": { 285 | "type": "git", 286 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 287 | "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" 288 | }, 289 | "dist": { 290 | "type": "zip", 291 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", 292 | "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", 293 | "shasum": "" 294 | }, 295 | "require": { 296 | "php": ">=5.3.3" 297 | }, 298 | "type": "library", 299 | "extra": { 300 | "branch-alias": { 301 | "dev-master": "1.4.x-dev" 302 | } 303 | }, 304 | "autoload": { 305 | "classmap": [ 306 | "src/" 307 | ] 308 | }, 309 | "notification-url": "https://packagist.org/downloads/", 310 | "license": [ 311 | "BSD-3-Clause" 312 | ], 313 | "authors": [ 314 | { 315 | "name": "Sebastian Bergmann", 316 | "email": "sb@sebastian-bergmann.de", 317 | "role": "lead" 318 | } 319 | ], 320 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 321 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 322 | "keywords": [ 323 | "filesystem", 324 | "iterator" 325 | ], 326 | "time": "2015-06-21 13:08:43" 327 | }, 328 | { 329 | "name": "phpunit/php-text-template", 330 | "version": "1.2.1", 331 | "source": { 332 | "type": "git", 333 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 334 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 335 | }, 336 | "dist": { 337 | "type": "zip", 338 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 339 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 340 | "shasum": "" 341 | }, 342 | "require": { 343 | "php": ">=5.3.3" 344 | }, 345 | "type": "library", 346 | "autoload": { 347 | "classmap": [ 348 | "src/" 349 | ] 350 | }, 351 | "notification-url": "https://packagist.org/downloads/", 352 | "license": [ 353 | "BSD-3-Clause" 354 | ], 355 | "authors": [ 356 | { 357 | "name": "Sebastian Bergmann", 358 | "email": "sebastian@phpunit.de", 359 | "role": "lead" 360 | } 361 | ], 362 | "description": "Simple template engine.", 363 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 364 | "keywords": [ 365 | "template" 366 | ], 367 | "time": "2015-06-21 13:50:34" 368 | }, 369 | { 370 | "name": "phpunit/php-timer", 371 | "version": "1.0.7", 372 | "source": { 373 | "type": "git", 374 | "url": "https://github.com/sebastianbergmann/php-timer.git", 375 | "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" 376 | }, 377 | "dist": { 378 | "type": "zip", 379 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", 380 | "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", 381 | "shasum": "" 382 | }, 383 | "require": { 384 | "php": ">=5.3.3" 385 | }, 386 | "type": "library", 387 | "autoload": { 388 | "classmap": [ 389 | "src/" 390 | ] 391 | }, 392 | "notification-url": "https://packagist.org/downloads/", 393 | "license": [ 394 | "BSD-3-Clause" 395 | ], 396 | "authors": [ 397 | { 398 | "name": "Sebastian Bergmann", 399 | "email": "sb@sebastian-bergmann.de", 400 | "role": "lead" 401 | } 402 | ], 403 | "description": "Utility class for timing", 404 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 405 | "keywords": [ 406 | "timer" 407 | ], 408 | "time": "2015-06-21 08:01:12" 409 | }, 410 | { 411 | "name": "phpunit/php-token-stream", 412 | "version": "1.4.8", 413 | "source": { 414 | "type": "git", 415 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 416 | "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" 417 | }, 418 | "dist": { 419 | "type": "zip", 420 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", 421 | "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", 422 | "shasum": "" 423 | }, 424 | "require": { 425 | "ext-tokenizer": "*", 426 | "php": ">=5.3.3" 427 | }, 428 | "require-dev": { 429 | "phpunit/phpunit": "~4.2" 430 | }, 431 | "type": "library", 432 | "extra": { 433 | "branch-alias": { 434 | "dev-master": "1.4-dev" 435 | } 436 | }, 437 | "autoload": { 438 | "classmap": [ 439 | "src/" 440 | ] 441 | }, 442 | "notification-url": "https://packagist.org/downloads/", 443 | "license": [ 444 | "BSD-3-Clause" 445 | ], 446 | "authors": [ 447 | { 448 | "name": "Sebastian Bergmann", 449 | "email": "sebastian@phpunit.de" 450 | } 451 | ], 452 | "description": "Wrapper around PHP's tokenizer extension.", 453 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 454 | "keywords": [ 455 | "tokenizer" 456 | ], 457 | "time": "2015-09-15 10:49:45" 458 | }, 459 | { 460 | "name": "phpunit/phpunit", 461 | "version": "5.3.2", 462 | "source": { 463 | "type": "git", 464 | "url": "https://github.com/sebastianbergmann/phpunit.git", 465 | "reference": "2c6da3536035617bae3fe3db37283c9e0eb63ab3" 466 | }, 467 | "dist": { 468 | "type": "zip", 469 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2c6da3536035617bae3fe3db37283c9e0eb63ab3", 470 | "reference": "2c6da3536035617bae3fe3db37283c9e0eb63ab3", 471 | "shasum": "" 472 | }, 473 | "require": { 474 | "ext-dom": "*", 475 | "ext-json": "*", 476 | "ext-pcre": "*", 477 | "ext-reflection": "*", 478 | "ext-spl": "*", 479 | "myclabs/deep-copy": "~1.3", 480 | "php": "^5.6 || ^7.0", 481 | "phpspec/prophecy": "^1.3.1", 482 | "phpunit/php-code-coverage": "^3.3.0", 483 | "phpunit/php-file-iterator": "~1.4", 484 | "phpunit/php-text-template": "~1.2", 485 | "phpunit/php-timer": "^1.0.6", 486 | "phpunit/phpunit-mock-objects": "^3.1", 487 | "sebastian/comparator": "~1.1", 488 | "sebastian/diff": "~1.2", 489 | "sebastian/environment": "~1.3", 490 | "sebastian/exporter": "~1.2", 491 | "sebastian/global-state": "~1.0", 492 | "sebastian/object-enumerator": "~1.0", 493 | "sebastian/resource-operations": "~1.0", 494 | "sebastian/version": "~1.0|~2.0", 495 | "symfony/yaml": "~2.1|~3.0" 496 | }, 497 | "suggest": { 498 | "phpunit/php-invoker": "~1.1" 499 | }, 500 | "bin": [ 501 | "phpunit" 502 | ], 503 | "type": "library", 504 | "extra": { 505 | "branch-alias": { 506 | "dev-master": "5.3.x-dev" 507 | } 508 | }, 509 | "autoload": { 510 | "classmap": [ 511 | "src/" 512 | ] 513 | }, 514 | "notification-url": "https://packagist.org/downloads/", 515 | "license": [ 516 | "BSD-3-Clause" 517 | ], 518 | "authors": [ 519 | { 520 | "name": "Sebastian Bergmann", 521 | "email": "sebastian@phpunit.de", 522 | "role": "lead" 523 | } 524 | ], 525 | "description": "The PHP Unit Testing framework.", 526 | "homepage": "https://phpunit.de/", 527 | "keywords": [ 528 | "phpunit", 529 | "testing", 530 | "xunit" 531 | ], 532 | "time": "2016-04-12 16:20:08" 533 | }, 534 | { 535 | "name": "phpunit/phpunit-mock-objects", 536 | "version": "3.1.2", 537 | "source": { 538 | "type": "git", 539 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 540 | "reference": "7c34c9bdde4131b824086457a3145e27dba10ca1" 541 | }, 542 | "dist": { 543 | "type": "zip", 544 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/7c34c9bdde4131b824086457a3145e27dba10ca1", 545 | "reference": "7c34c9bdde4131b824086457a3145e27dba10ca1", 546 | "shasum": "" 547 | }, 548 | "require": { 549 | "doctrine/instantiator": "^1.0.2", 550 | "php": ">=5.6", 551 | "phpunit/php-text-template": "~1.2", 552 | "sebastian/exporter": "~1.2" 553 | }, 554 | "require-dev": { 555 | "phpunit/phpunit": "~5" 556 | }, 557 | "suggest": { 558 | "ext-soap": "*" 559 | }, 560 | "type": "library", 561 | "extra": { 562 | "branch-alias": { 563 | "dev-master": "3.1.x-dev" 564 | } 565 | }, 566 | "autoload": { 567 | "classmap": [ 568 | "src/" 569 | ] 570 | }, 571 | "notification-url": "https://packagist.org/downloads/", 572 | "license": [ 573 | "BSD-3-Clause" 574 | ], 575 | "authors": [ 576 | { 577 | "name": "Sebastian Bergmann", 578 | "email": "sb@sebastian-bergmann.de", 579 | "role": "lead" 580 | } 581 | ], 582 | "description": "Mock Object library for PHPUnit", 583 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 584 | "keywords": [ 585 | "mock", 586 | "xunit" 587 | ], 588 | "time": "2016-03-24 05:58:25" 589 | }, 590 | { 591 | "name": "sebastian/code-unit-reverse-lookup", 592 | "version": "1.0.0", 593 | "source": { 594 | "type": "git", 595 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 596 | "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" 597 | }, 598 | "dist": { 599 | "type": "zip", 600 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", 601 | "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", 602 | "shasum": "" 603 | }, 604 | "require": { 605 | "php": ">=5.6" 606 | }, 607 | "require-dev": { 608 | "phpunit/phpunit": "~5" 609 | }, 610 | "type": "library", 611 | "extra": { 612 | "branch-alias": { 613 | "dev-master": "1.0.x-dev" 614 | } 615 | }, 616 | "autoload": { 617 | "classmap": [ 618 | "src/" 619 | ] 620 | }, 621 | "notification-url": "https://packagist.org/downloads/", 622 | "license": [ 623 | "BSD-3-Clause" 624 | ], 625 | "authors": [ 626 | { 627 | "name": "Sebastian Bergmann", 628 | "email": "sebastian@phpunit.de" 629 | } 630 | ], 631 | "description": "Looks up which function or method a line of code belongs to", 632 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 633 | "time": "2016-02-13 06:45:14" 634 | }, 635 | { 636 | "name": "sebastian/comparator", 637 | "version": "1.2.0", 638 | "source": { 639 | "type": "git", 640 | "url": "https://github.com/sebastianbergmann/comparator.git", 641 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" 642 | }, 643 | "dist": { 644 | "type": "zip", 645 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", 646 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", 647 | "shasum": "" 648 | }, 649 | "require": { 650 | "php": ">=5.3.3", 651 | "sebastian/diff": "~1.2", 652 | "sebastian/exporter": "~1.2" 653 | }, 654 | "require-dev": { 655 | "phpunit/phpunit": "~4.4" 656 | }, 657 | "type": "library", 658 | "extra": { 659 | "branch-alias": { 660 | "dev-master": "1.2.x-dev" 661 | } 662 | }, 663 | "autoload": { 664 | "classmap": [ 665 | "src/" 666 | ] 667 | }, 668 | "notification-url": "https://packagist.org/downloads/", 669 | "license": [ 670 | "BSD-3-Clause" 671 | ], 672 | "authors": [ 673 | { 674 | "name": "Jeff Welch", 675 | "email": "whatthejeff@gmail.com" 676 | }, 677 | { 678 | "name": "Volker Dusch", 679 | "email": "github@wallbash.com" 680 | }, 681 | { 682 | "name": "Bernhard Schussek", 683 | "email": "bschussek@2bepublished.at" 684 | }, 685 | { 686 | "name": "Sebastian Bergmann", 687 | "email": "sebastian@phpunit.de" 688 | } 689 | ], 690 | "description": "Provides the functionality to compare PHP values for equality", 691 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 692 | "keywords": [ 693 | "comparator", 694 | "compare", 695 | "equality" 696 | ], 697 | "time": "2015-07-26 15:48:44" 698 | }, 699 | { 700 | "name": "sebastian/diff", 701 | "version": "1.4.1", 702 | "source": { 703 | "type": "git", 704 | "url": "https://github.com/sebastianbergmann/diff.git", 705 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" 706 | }, 707 | "dist": { 708 | "type": "zip", 709 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", 710 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", 711 | "shasum": "" 712 | }, 713 | "require": { 714 | "php": ">=5.3.3" 715 | }, 716 | "require-dev": { 717 | "phpunit/phpunit": "~4.8" 718 | }, 719 | "type": "library", 720 | "extra": { 721 | "branch-alias": { 722 | "dev-master": "1.4-dev" 723 | } 724 | }, 725 | "autoload": { 726 | "classmap": [ 727 | "src/" 728 | ] 729 | }, 730 | "notification-url": "https://packagist.org/downloads/", 731 | "license": [ 732 | "BSD-3-Clause" 733 | ], 734 | "authors": [ 735 | { 736 | "name": "Kore Nordmann", 737 | "email": "mail@kore-nordmann.de" 738 | }, 739 | { 740 | "name": "Sebastian Bergmann", 741 | "email": "sebastian@phpunit.de" 742 | } 743 | ], 744 | "description": "Diff implementation", 745 | "homepage": "https://github.com/sebastianbergmann/diff", 746 | "keywords": [ 747 | "diff" 748 | ], 749 | "time": "2015-12-08 07:14:41" 750 | }, 751 | { 752 | "name": "sebastian/environment", 753 | "version": "1.3.5", 754 | "source": { 755 | "type": "git", 756 | "url": "https://github.com/sebastianbergmann/environment.git", 757 | "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf" 758 | }, 759 | "dist": { 760 | "type": "zip", 761 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", 762 | "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", 763 | "shasum": "" 764 | }, 765 | "require": { 766 | "php": ">=5.3.3" 767 | }, 768 | "require-dev": { 769 | "phpunit/phpunit": "~4.4" 770 | }, 771 | "type": "library", 772 | "extra": { 773 | "branch-alias": { 774 | "dev-master": "1.3.x-dev" 775 | } 776 | }, 777 | "autoload": { 778 | "classmap": [ 779 | "src/" 780 | ] 781 | }, 782 | "notification-url": "https://packagist.org/downloads/", 783 | "license": [ 784 | "BSD-3-Clause" 785 | ], 786 | "authors": [ 787 | { 788 | "name": "Sebastian Bergmann", 789 | "email": "sebastian@phpunit.de" 790 | } 791 | ], 792 | "description": "Provides functionality to handle HHVM/PHP environments", 793 | "homepage": "http://www.github.com/sebastianbergmann/environment", 794 | "keywords": [ 795 | "Xdebug", 796 | "environment", 797 | "hhvm" 798 | ], 799 | "time": "2016-02-26 18:40:46" 800 | }, 801 | { 802 | "name": "sebastian/exporter", 803 | "version": "1.2.1", 804 | "source": { 805 | "type": "git", 806 | "url": "https://github.com/sebastianbergmann/exporter.git", 807 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e" 808 | }, 809 | "dist": { 810 | "type": "zip", 811 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", 812 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e", 813 | "shasum": "" 814 | }, 815 | "require": { 816 | "php": ">=5.3.3", 817 | "sebastian/recursion-context": "~1.0" 818 | }, 819 | "require-dev": { 820 | "phpunit/phpunit": "~4.4" 821 | }, 822 | "type": "library", 823 | "extra": { 824 | "branch-alias": { 825 | "dev-master": "1.2.x-dev" 826 | } 827 | }, 828 | "autoload": { 829 | "classmap": [ 830 | "src/" 831 | ] 832 | }, 833 | "notification-url": "https://packagist.org/downloads/", 834 | "license": [ 835 | "BSD-3-Clause" 836 | ], 837 | "authors": [ 838 | { 839 | "name": "Jeff Welch", 840 | "email": "whatthejeff@gmail.com" 841 | }, 842 | { 843 | "name": "Volker Dusch", 844 | "email": "github@wallbash.com" 845 | }, 846 | { 847 | "name": "Bernhard Schussek", 848 | "email": "bschussek@2bepublished.at" 849 | }, 850 | { 851 | "name": "Sebastian Bergmann", 852 | "email": "sebastian@phpunit.de" 853 | }, 854 | { 855 | "name": "Adam Harvey", 856 | "email": "aharvey@php.net" 857 | } 858 | ], 859 | "description": "Provides the functionality to export PHP variables for visualization", 860 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 861 | "keywords": [ 862 | "export", 863 | "exporter" 864 | ], 865 | "time": "2015-06-21 07:55:53" 866 | }, 867 | { 868 | "name": "sebastian/global-state", 869 | "version": "1.1.1", 870 | "source": { 871 | "type": "git", 872 | "url": "https://github.com/sebastianbergmann/global-state.git", 873 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" 874 | }, 875 | "dist": { 876 | "type": "zip", 877 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", 878 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", 879 | "shasum": "" 880 | }, 881 | "require": { 882 | "php": ">=5.3.3" 883 | }, 884 | "require-dev": { 885 | "phpunit/phpunit": "~4.2" 886 | }, 887 | "suggest": { 888 | "ext-uopz": "*" 889 | }, 890 | "type": "library", 891 | "extra": { 892 | "branch-alias": { 893 | "dev-master": "1.0-dev" 894 | } 895 | }, 896 | "autoload": { 897 | "classmap": [ 898 | "src/" 899 | ] 900 | }, 901 | "notification-url": "https://packagist.org/downloads/", 902 | "license": [ 903 | "BSD-3-Clause" 904 | ], 905 | "authors": [ 906 | { 907 | "name": "Sebastian Bergmann", 908 | "email": "sebastian@phpunit.de" 909 | } 910 | ], 911 | "description": "Snapshotting of global state", 912 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 913 | "keywords": [ 914 | "global state" 915 | ], 916 | "time": "2015-10-12 03:26:01" 917 | }, 918 | { 919 | "name": "sebastian/object-enumerator", 920 | "version": "1.0.0", 921 | "source": { 922 | "type": "git", 923 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 924 | "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" 925 | }, 926 | "dist": { 927 | "type": "zip", 928 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", 929 | "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", 930 | "shasum": "" 931 | }, 932 | "require": { 933 | "php": ">=5.6", 934 | "sebastian/recursion-context": "~1.0" 935 | }, 936 | "require-dev": { 937 | "phpunit/phpunit": "~5" 938 | }, 939 | "type": "library", 940 | "extra": { 941 | "branch-alias": { 942 | "dev-master": "1.0.x-dev" 943 | } 944 | }, 945 | "autoload": { 946 | "classmap": [ 947 | "src/" 948 | ] 949 | }, 950 | "notification-url": "https://packagist.org/downloads/", 951 | "license": [ 952 | "BSD-3-Clause" 953 | ], 954 | "authors": [ 955 | { 956 | "name": "Sebastian Bergmann", 957 | "email": "sebastian@phpunit.de" 958 | } 959 | ], 960 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 961 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 962 | "time": "2016-01-28 13:25:10" 963 | }, 964 | { 965 | "name": "sebastian/recursion-context", 966 | "version": "1.0.2", 967 | "source": { 968 | "type": "git", 969 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 970 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791" 971 | }, 972 | "dist": { 973 | "type": "zip", 974 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", 975 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791", 976 | "shasum": "" 977 | }, 978 | "require": { 979 | "php": ">=5.3.3" 980 | }, 981 | "require-dev": { 982 | "phpunit/phpunit": "~4.4" 983 | }, 984 | "type": "library", 985 | "extra": { 986 | "branch-alias": { 987 | "dev-master": "1.0.x-dev" 988 | } 989 | }, 990 | "autoload": { 991 | "classmap": [ 992 | "src/" 993 | ] 994 | }, 995 | "notification-url": "https://packagist.org/downloads/", 996 | "license": [ 997 | "BSD-3-Clause" 998 | ], 999 | "authors": [ 1000 | { 1001 | "name": "Jeff Welch", 1002 | "email": "whatthejeff@gmail.com" 1003 | }, 1004 | { 1005 | "name": "Sebastian Bergmann", 1006 | "email": "sebastian@phpunit.de" 1007 | }, 1008 | { 1009 | "name": "Adam Harvey", 1010 | "email": "aharvey@php.net" 1011 | } 1012 | ], 1013 | "description": "Provides functionality to recursively process PHP variables", 1014 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1015 | "time": "2015-11-11 19:50:13" 1016 | }, 1017 | { 1018 | "name": "sebastian/resource-operations", 1019 | "version": "1.0.0", 1020 | "source": { 1021 | "type": "git", 1022 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1023 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" 1024 | }, 1025 | "dist": { 1026 | "type": "zip", 1027 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1028 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1029 | "shasum": "" 1030 | }, 1031 | "require": { 1032 | "php": ">=5.6.0" 1033 | }, 1034 | "type": "library", 1035 | "extra": { 1036 | "branch-alias": { 1037 | "dev-master": "1.0.x-dev" 1038 | } 1039 | }, 1040 | "autoload": { 1041 | "classmap": [ 1042 | "src/" 1043 | ] 1044 | }, 1045 | "notification-url": "https://packagist.org/downloads/", 1046 | "license": [ 1047 | "BSD-3-Clause" 1048 | ], 1049 | "authors": [ 1050 | { 1051 | "name": "Sebastian Bergmann", 1052 | "email": "sebastian@phpunit.de" 1053 | } 1054 | ], 1055 | "description": "Provides a list of PHP built-in functions that operate on resources", 1056 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1057 | "time": "2015-07-28 20:34:47" 1058 | }, 1059 | { 1060 | "name": "sebastian/version", 1061 | "version": "2.0.0", 1062 | "source": { 1063 | "type": "git", 1064 | "url": "https://github.com/sebastianbergmann/version.git", 1065 | "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5" 1066 | }, 1067 | "dist": { 1068 | "type": "zip", 1069 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", 1070 | "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", 1071 | "shasum": "" 1072 | }, 1073 | "require": { 1074 | "php": ">=5.6" 1075 | }, 1076 | "type": "library", 1077 | "extra": { 1078 | "branch-alias": { 1079 | "dev-master": "2.0.x-dev" 1080 | } 1081 | }, 1082 | "autoload": { 1083 | "classmap": [ 1084 | "src/" 1085 | ] 1086 | }, 1087 | "notification-url": "https://packagist.org/downloads/", 1088 | "license": [ 1089 | "BSD-3-Clause" 1090 | ], 1091 | "authors": [ 1092 | { 1093 | "name": "Sebastian Bergmann", 1094 | "email": "sebastian@phpunit.de", 1095 | "role": "lead" 1096 | } 1097 | ], 1098 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1099 | "homepage": "https://github.com/sebastianbergmann/version", 1100 | "time": "2016-02-04 12:56:52" 1101 | }, 1102 | { 1103 | "name": "symfony/yaml", 1104 | "version": "v3.0.4", 1105 | "source": { 1106 | "type": "git", 1107 | "url": "https://github.com/symfony/yaml.git", 1108 | "reference": "0047c8366744a16de7516622c5b7355336afae96" 1109 | }, 1110 | "dist": { 1111 | "type": "zip", 1112 | "url": "https://api.github.com/repos/symfony/yaml/zipball/0047c8366744a16de7516622c5b7355336afae96", 1113 | "reference": "0047c8366744a16de7516622c5b7355336afae96", 1114 | "shasum": "" 1115 | }, 1116 | "require": { 1117 | "php": ">=5.5.9" 1118 | }, 1119 | "type": "library", 1120 | "extra": { 1121 | "branch-alias": { 1122 | "dev-master": "3.0-dev" 1123 | } 1124 | }, 1125 | "autoload": { 1126 | "psr-4": { 1127 | "Symfony\\Component\\Yaml\\": "" 1128 | }, 1129 | "exclude-from-classmap": [ 1130 | "/Tests/" 1131 | ] 1132 | }, 1133 | "notification-url": "https://packagist.org/downloads/", 1134 | "license": [ 1135 | "MIT" 1136 | ], 1137 | "authors": [ 1138 | { 1139 | "name": "Fabien Potencier", 1140 | "email": "fabien@symfony.com" 1141 | }, 1142 | { 1143 | "name": "Symfony Community", 1144 | "homepage": "https://symfony.com/contributors" 1145 | } 1146 | ], 1147 | "description": "Symfony Yaml Component", 1148 | "homepage": "https://symfony.com", 1149 | "time": "2016-03-04 07:55:57" 1150 | } 1151 | ], 1152 | "aliases": [], 1153 | "minimum-stability": "stable", 1154 | "stability-flags": [], 1155 | "prefer-stable": false, 1156 | "prefer-lowest": false, 1157 | "platform": { 1158 | "php": ">=5.4.0" 1159 | }, 1160 | "platform-dev": [] 1161 | } 1162 | -------------------------------------------------------------------------------- /test/test.php: -------------------------------------------------------------------------------- 1 | $rows) { 13 | echo " ${key}\n"; 14 | 15 | foreach ($rows as $k => $v) { 16 | list ($text, $html) = $v; 17 | 18 | $markdown = $parser->makeHtml($text); 19 | 20 | if ($html == $markdown) { 21 | echo " ✓ ${k}\n"; 22 | } else { 23 | $num = count($errors) + 1; 24 | echo " ${num}) ${k}\n"; 25 | $errors[] = [$key . ' ' . $k, $markdown, $html]; 26 | } 27 | 28 | $count ++; 29 | } 30 | } 31 | 32 | echo "\n " . ($count - count($errors)) . " passing\n"; 33 | echo " " . count($errors) . " falling\n\n"; 34 | 35 | foreach ($errors as $k => $error) { 36 | list ($key, $markdown, $html) = $error; 37 | 38 | echo ' ' . ($k + 1) . ') ' . $title . " {$key}:\n"; 39 | echo ' ' . $html; 40 | echo "\n ==============\n"; 41 | echo ' ' . $markdown; 42 | echo "\n"; 43 | } 44 | } 45 | 46 | test('HyperDown', [ 47 | 'footnote' => [ 48 | '脚注' => [ 49 | "Never write \"[click here][^2]\".\n [^2]: http://www.w3.org/QA/Tips/noClickHere", 50 | '

    Never write "[click here]1".


    1. 2
    ' 51 | ] 52 | ], 53 | 'heading type1' => [ 54 | '#heading#' => [ 55 | '#heading#', 56 | '

    heading

    ' 57 | ], 58 | '######heading######' => [ 59 | '######heading######', 60 | '
    heading
    ' 61 | ] 62 | ], 63 | 'heading type2' => [ 64 | 'heading ======' => [ 65 | "heading\n======", 66 | '

    heading

    ' 67 | ] 68 | ], 69 | 'bold' => [ 70 | '**bold**' => [ 71 | '123**bold**123', 72 | '

    123bold123

    ' 73 | ] 74 | ], 75 | 'italy' => [ 76 | '*italy*' => [ 77 | '123 *italy* 123', 78 | '

    123 italy 123

    ' 79 | ] 80 | ], 81 | 'list' => [ 82 | 'ul' => [ 83 | "\n\n - list", 84 | '' 85 | ], 86 | 'ol' => [ 87 | '1. list', 88 | '
    1. list
    ' 89 | ], 90 | 'mix' => [ 91 | "1. list 1\n2. list 2\n * aaa\n * bbb\n3. list 3\n- dddd\n- cccc", 92 | '' 93 | ] 94 | ], 95 | 'bugfix' => [ 96 | 'escape' => [ 97 | "\\[系统盘]:\\Documents and Settings\\\\[用户名]\\\\Cookies$\\lambda$", 98 | "

    [系统盘]:\\Documents and Settings\\[用户名]\\Cookies$\\lambda$

    " 99 | ], 100 | 101 | 'table' => [ 102 | "|---------------|-------|\n| Variable_name | Value |\n| ------------- | ----- |\n| sql_mode | ONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO, NO_AUTO_CREATE_USER, NO_ENGINE_SUBSTITUTION |\n|---------------|-------|", 103 | '
    Variable_nameValue
    sql_modeONLY_FULL_GROUP_BY, STRICT_TRANS_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO, NO_AUTO_CREATE_USER, NO_ENGINE_SUBSTITUTION
    ' 104 | ] 105 | ], 106 | 'url'=>[ 107 | 'exclamatory'=>[ // 感叹号 108 | 'http://sqlfiddle.com/#!9/ca126b/1中文 break', 109 | '

    http://sqlfiddle.com/#!9/ca126b/1中文 break

    ' 110 | ] 111 | ] 112 | ]); 113 | 114 | --------------------------------------------------------------------------------