diff --git a/ParserToc b/ParserToc
new file mode 100644
index 0000000..443f93e
--- /dev/null
+++ b/ParserToc
@@ -0,0 +1,1360 @@
+
+ * @license BSD License
+ */
+class Parser
+{
+ /**
+ * _whiteList
+ *
+ * @var string
+ */
+ public $_commonWhiteList = 'kbd|b|i|strong|em|sup|sub|br|code|del|a|hr|small';
+
+ /**
+ * _specialWhiteList
+ *
+ * @var mixed
+ * @access private
+ */
+ private $_specialWhiteList = array(
+ 'table' => 'table|tbody|thead|tfoot|tr|td|th'
+ );
+
+ /**
+ * _footnotes
+ *
+ * @var array
+ */
+ public $_footnotes;
+
+ /**
+ * _blocks
+ *
+ * @var array
+ */
+ private $_blocks;
+
+ /**
+ * _current
+ *
+ * @var string
+ */
+ private $_current;
+
+ /**
+ * _pos
+ *
+ * @var int
+ */
+ private $_pos;
+
+ /**
+ * _definitions
+ *
+ * @var array
+ */
+ public $_definitions;
+
+ /**
+ * @var array
+ */
+ private $_hooks = array();
+
+ /**
+ * @var array
+ */
+ private $_holders;
+
+ /**
+ * @var string
+ */
+ private $_uniqid;
+
+ /**
+ * @var int
+ */
+ private $_id;
+
+ /**
+ * @var sh
+ */
+ private $_sh;
+
+ /**
+ * makeHtml
+ *
+ * @param mixed $text
+ * @return string
+ */
+ public function makeHtml($text)
+ {
+ $this->_footnotes = array();
+ $this->_definitions = array();
+ $this->_holders = array();
+ $this->_uniqid = md5(uniqid());
+ $this->_id = 0;
+
+ $text = $this->initText($text);
+ $html = $this->parse($text);
+ $html = $this->makeFootnotes($html);
+
+ return $this->call('makeHtml', $html);
+ }
+
+ /**
+ * @param $type
+ * @param $callback
+ */
+ public function hook($type, $callback)
+ {
+ $this->_hooks[$type][] = $callback;
+ }
+
+ /**
+ * @param $str
+ * @return string
+ */
+ public function makeHolder($str)
+ {
+ $key = "\r" . $this->_uniqid . $this->_id . "\r";
+ $this->_id ++;
+ $this->_holders[$key] = $str;
+
+ return $key;
+ }
+
+ /**
+ * @param $text
+ * @return mixed
+ */
+ private function initText($text)
+ {
+ $text = str_replace(array("\t", "\r"), array(' ', ''), $text);
+ return $text;
+ }
+
+ /**
+ * @param $html
+ * @return string
+ */
+ private function makeFootnotes($html)
+ {
+ if (count($this->_footnotes) > 0) {
+ $html .= '
';
+ }
+
+ return $html;
+ }
+
+ /**
+ * parse
+ *
+ * @param string $text
+ * @return string
+ */
+ private function parse($text)
+ {
+ $blocks = $this->parseBlock($text, $lines);
+ $html = '';
+
+ foreach ($blocks as $block) {
+ list ($type, $start, $end, $value) = $block;
+ $extract = array_slice($lines, $start, $end - $start + 1);
+ $method = 'parse' . ucfirst($type);
+
+ $extract = $this->call('before' . ucfirst($method), $extract, $value);
+ $result = $this->{$method}($extract, $value);
+ $result = $this->call('after' . ucfirst($method), $result, $value);
+
+ $html .= $result;
+ }
+
+ return $html;
+ }
+
+ /**
+ * @param $type
+ * @param $value
+ * @return mixed
+ */
+ private function call($type, $value)
+ {
+ if (empty($this->_hooks[$type])) {
+ return $value;
+ }
+
+ $args = func_get_args();
+ $args = array_slice($args, 1);
+
+ foreach ($this->_hooks[$type] as $callback) {
+ $value = call_user_func_array($callback, $args);
+ $args[0] = $value;
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param $text
+ * @param $clearHolders
+ * @return string
+ */
+ private function releaseHolder($text, $clearHolders = true)
+ {
+ $deep = 0;
+ while (strpos($text, "\r") !== false && $deep < 10) {
+ $text = str_replace(array_keys($this->_holders), array_values($this->_holders), $text);
+ $deep ++;
+ }
+
+ if ($clearHolders) {
+ $this->_holders = array();
+ }
+
+ return $text;
+ }
+
+ /**
+ * parseInline
+ *
+ * @param string $text
+ * @param string $whiteList
+ * @param bool $clearHolders
+ * @param bool $enableAutoLink
+ * @return string
+ */
+ public function parseInline($text, $whiteList = '', $clearHolders = true, $enableAutoLink = true)
+ {
+ $self = $this;
+ $text = $this->call('beforeParseInline', $text);
+
+ // code
+ $text = preg_replace_callback(
+ "/(^|[^\\\])(`+)(.+?)\\2/",
+ function ($matches) use ($self) {
+ return $matches[1] . $self->makeHolder(
+ '' . htmlspecialchars($matches[3]) . '
'
+ );
+ },
+ $text
+ );
+
+ // escape
+ $text = preg_replace_callback(
+ "/\\\(.)/u",
+ function ($matches) use ($self) {
+ $escaped = htmlspecialchars($matches[1]);
+ $escaped = str_replace('$', '$', $escaped);
+ return $self->makeHolder($escaped);
+ },
+ $text
+ );
+
+ // link
+ $text = preg_replace_callback(
+ "/<(https?:\/\/.+)>/i",
+ function ($matches) use ($self) {
+ $url = $self->cleanUrl($matches[1]);
+ $link = $self->call('parseLink', $matches[1]);
+
+ return $self->makeHolder(
+ "{$link}"
+ );
+ },
+ $text
+ );
+
+ // encode unsafe tags
+ $text = preg_replace_callback(
+ "/<(\/?)([a-z0-9-]+)(\s+[^>]*)?>/i",
+ function ($matches) use ($self, $whiteList) {
+ if (false !== stripos(
+ '|' . $self->_commonWhiteList . '|' . $whiteList . '|', '|' . $matches[2] . '|'
+ )) {
+ return $self->makeHolder($matches[0]);
+ } else {
+ return htmlspecialchars($matches[0]);
+ }
+ },
+ $text
+ );
+
+ $text = str_replace(array('<', '>'), array('<', '>'), $text);
+
+ // footnote
+ $text = preg_replace_callback(
+ "/\[\^((?:[^\]]|\\\\\]|\\\\\[)+?)\]/",
+ function ($matches) use ($self) {
+ $id = array_search($matches[1], $self->_footnotes);
+
+ if (false === $id) {
+ $id = count($self->_footnotes) + 1;
+ $self->_footnotes[$id] = $self->parseInline($matches[1], '', false);
+ }
+
+ return $self->makeHolder(
+ ""
+ );
+ },
+ $text
+ );
+
+ // image
+ $text = preg_replace_callback(
+ "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/",
+ function ($matches) use ($self) {
+ $escaped = $self->escapeBracket($matches[1]);
+ $url = $self->escapeBracket($matches[2]);
+ $url = $self->cleanUrl($url);
+ return $self->makeHolder(
+ "
"
+ );
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/",
+ function ($matches) use ($self) {
+ $escaped = $self->escapeBracket($matches[1]);
+
+ $result = isset( $self->_definitions[$matches[2]] ) ?
+ "
_definitions[$matches[2]]}\" alt=\"{$escaped}\" title=\"{$escaped}\">"
+ : $escaped;
+
+ return $self->makeHolder($result);
+ },
+ $text
+ );
+
+ // link
+ $text = preg_replace_callback(
+ "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/",
+ function ($matches) use ($self) {
+ $escaped = $self->parseInline(
+ $self->escapeBracket($matches[1]), '', false, false
+ );
+ $url = $self->escapeBracket($matches[2]);
+ $url = $self->cleanUrl($url);
+ return $self->makeHolder("{$escaped}");
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/",
+ function ($matches) use ($self) {
+ $escaped = $self->parseInline(
+ $self->escapeBracket($matches[1]), '', false
+ );
+ $result = isset( $self->_definitions[$matches[2]] ) ?
+ "_definitions[$matches[2]]}\">{$escaped}"
+ : $escaped;
+
+ return $self->makeHolder($result);
+ },
+ $text
+ );
+
+ // strong and em and some fuck
+ $text = $this->parseInlineCallback($text);
+ $text = preg_replace(
+ "/<([_a-z0-9-\.\+]+@[^@]+\.[a-z]{2,})>/i",
+ "\\1",
+ $text
+ );
+
+ // autolink url
+ if ($enableAutoLink) {
+ $text = preg_replace_callback(
+ "/(^|[^\"])((https?):[x80-xff_a-z0-9-\.\/%#@\?\+=~\|\,&\(\)]+)($|[^\"])/i",
+ function ($matches) use ($self) {
+ $link = $self->call('parseLink', $matches[2]);
+ return "{$matches[1]}{$link}{$matches[4]}";
+ },
+ $text
+ );
+ }
+
+ $text = $this->call('afterParseInlineBeforeRelease', $text);
+ $text = $this->releaseHolder($text, $clearHolders);
+
+ $text = $this->call('afterParseInline', $text);
+
+ return $text;
+ }
+
+ /**
+ * @param $text
+ * @return mixed
+ */
+ public function parseInlineCallback($text)
+ {
+ $self = $this;
+
+ $text = preg_replace_callback(
+ "/(\*{3})(.+?)\\1/",
+ function ($matches) use ($self) {
+ return '' .
+ $self->parseInlineCallback($matches[2]) .
+ '';
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ "/(\*{2})(.+?)\\1/",
+ function ($matches) use ($self) {
+ return '' .
+ $self->parseInlineCallback($matches[2]) .
+ '';
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ "/(\*)(.+?)\\1/",
+ function ($matches) use ($self) {
+ return '' .
+ $self->parseInlineCallback($matches[2]) .
+ '';
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ "/(\s+|^)(_{3})(.+?)\\2(\s+|$)/",
+ function ($matches) use ($self) {
+ return $matches[1] . '' .
+ $self->parseInlineCallback($matches[3]) .
+ '' . $matches[4];
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ "/(\s+|^)(_{2})(.+?)\\2(\s+|$)/",
+ function ($matches) use ($self) {
+ return $matches[1] . '' .
+ $self->parseInlineCallback($matches[3]) .
+ '' . $matches[4];
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ "/(\s+|^)(_)(.+?)\\2(\s+|$)/",
+ function ($matches) use ($self) {
+ return $matches[1] . '' .
+ $self->parseInlineCallback($matches[3]) .
+ '' . $matches[4];
+ },
+ $text
+ );
+
+ $text = preg_replace_callback(
+ "/(~{2})(.+?)\\1/",
+ function ($matches) use ($self) {
+ return '' .
+ $self->parseInlineCallback($matches[2]) .
+ '';
+ },
+ $text
+ );
+
+ return $text;
+ }
+
+ /**
+ * parseBlock
+ *
+ * @param string $text
+ * @param array $lines
+ * @return array
+ */
+ private function parseBlock($text, &$lines)
+ {
+ $lines = explode("\n", $text);
+ $this->_blocks = array();
+ $this->_current = 'normal';
+ $this->_pos = -1;
+ $special = implode("|", array_keys($this->_specialWhiteList));
+ $emptyCount = 0;
+
+ // analyze by line
+ foreach ($lines as $key => $line) {
+ $block = $this->getBlock();
+
+ // code block is special
+ if (preg_match("/^(\s*)(~|`){3,}([^`~]*)$/i", $line, $matches)) {
+ if ($this->isBlock('code')) {
+ $isAfterList = $block[3][2];
+
+ if ($isAfterList) {
+ $this->combineBlock()
+ ->setBlock($key);
+ } else {
+ $this->setBlock($key)
+ ->endBlock();
+ }
+ } else {
+ $isAfterList = false;
+
+ if ($this->isBlock('list')) {
+ $space = $block[3];
+
+ $isAfterList = ($space > 0 && strlen($matches[1]) >= $space)
+ || strlen($matches[1]) > $space;
+ }
+
+ $this->startBlock('code', $key, array(
+ $matches[1], $matches[3], $isAfterList
+ ));
+ }
+
+ continue;
+ } else if ($this->isBlock('code')) {
+ $this->setBlock($key);
+ continue;
+ }
+
+ // html block is special too
+ if (preg_match("/^\s*<({$special})(\s+[^>]*)?>/i", $line, $matches)) {
+ $tag = strtolower($matches[1]);
+ if (!$this->isBlock('html', $tag) && !$this->isBlock('pre')) {
+ $this->startBlock('html', $key, $tag);
+ }
+
+ continue;
+ } else if (preg_match("/<\/({$special})>\s*$/i", $line, $matches)) {
+ $tag = strtolower($matches[1]);
+
+ if ($this->isBlock('html', $tag)) {
+ $this->setBlock($key)
+ ->endBlock();
+ }
+
+ continue;
+ } else if ($this->isBlock('html')) {
+ $this->setBlock($key);
+ continue;
+ }
+
+ switch (true) {
+ // pre block
+ case preg_match("/^ {4}/", $line):
+ $emptyCount = 0;
+
+ if ($this->isBlock('pre') || $this->isBlock('list')) {
+ $this->setBlock($key);
+ } else if ($this->isBlock('normal')) {
+ $this->startBlock('pre', $key);
+ }
+ break;
+
+ // list
+ case preg_match("/^(\s*)((?:[0-9a-z]+\.)|\-|\+|\*)\s+/", $line, $matches):
+ $space = strlen($matches[1]);
+ $emptyCount = 0;
+
+ // opened
+ if ($this->isBlock('list')) {
+ $this->setBlock($key, $space);
+ } else {
+ $this->startBlock('list', $key, $space);
+ }
+ break;
+
+ // footnote
+ case preg_match("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", $line, $matches):
+ $space = strlen($matches[0]) - 1;
+ $this->startBlock('footnote', $key, array(
+ $space, $matches[1]
+ ));
+ break;
+
+ // definition
+ case preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line, $matches):
+ $this->_definitions[$matches[1]] = $this->cleanUrl($matches[2]);
+ $this->startBlock('definition', $key)
+ ->endBlock();
+ break;
+
+ // block quote
+ case preg_match("/^\s*>/", $line):
+ if ($this->isBlock('quote')) {
+ $this->setBlock($key);
+ } else {
+ $this->startBlock('quote', $key);
+ }
+ break;
+
+ // table
+ case preg_match("/^((?:(?:(?:[ :]*\-[ :]*)+(?:\||\+))|(?:(?:\||\+)(?:[ :]*\-[ :]*)+)|(?:(?:[ :]*\-[ :]*)+(?:\||\+)(?:[ :]*\-[ :]*)+))+)$/", $line, $matches):
+ if ($this->isBlock('table')) {
+ $block[3][0][] = $block[3][2];
+ $block[3][2] ++;
+ $this->setBlock($key, $block[3]);
+ } else {
+ $head = 0;
+
+ if (empty($block) ||
+ $block[0] != 'normal' ||
+ preg_match("/^\s*$/", $lines[$block[2]])) {
+ $this->startBlock('table', $key);
+ } else {
+ $head = 1;
+ $this->backBlock(1, 'table');
+ }
+
+ if ($matches[1][0] == '|') {
+ $matches[1] = substr($matches[1], 1);
+
+ if ($matches[1][strlen($matches[1]) - 1] == '|') {
+ $matches[1] = substr($matches[1], 0, -1);
+ }
+ }
+
+ $rows = preg_split("/(\+|\|)/", $matches[1]);
+ $aligns = array();
+ foreach ($rows as $row) {
+ $align = 'none';
+
+ if (preg_match("/^\s*(:?)\-+(:?)\s*$/", $row, $matches)) {
+ if (!empty($matches[1]) && !empty($matches[2])) {
+ $align = 'center';
+ } else if (!empty($matches[1])) {
+ $align = 'left';
+ } else if (!empty($matches[2])) {
+ $align = 'right';
+ }
+ }
+
+ $aligns[] = $align;
+ }
+
+ $this->setBlock($key, array(array($head), $aligns, $head + 1));
+ }
+ break;
+
+ // single heading
+ case preg_match("/^(#+)(.*)$/", $line, $matches):
+ $num = min(strlen($matches[1]), 6);
+ $this->_sh[] = $matches[2]; //nicholas
+ $this->startBlock('sh', $key, $num)
+ ->endBlock();
+ break;
+
+ // multi heading
+ case preg_match("/^\s*((=|-){2,})\s*$/", $line, $matches)
+ && ($block && $block[0] == "normal" && !preg_match("/^\s*$/", $lines[$block[2]])): // check if last line isn't empty
+ if ($this->isBlock('normal')) {
+ $this->backBlock(1, 'mh', $matches[1][0] == '=' ? 1 : 2)
+ ->setBlock($key)
+ ->endBlock();
+ } else {
+ $this->startBlock('normal', $key);
+ }
+ break;
+
+ // hr
+ case preg_match("/^[-\*]{3,}\s*$/", $line):
+ $this->startBlock('hr', $key)
+ ->endBlock();
+ break;
+
+ // TOC
+ case preg_match("/\[TOC\]/", $line):
+ $this->startBlock('toc', $key)
+ ->endBlock();
+ break;
+
+ // normal
+ default:
+ if ($this->isBlock('list')) {
+ if (preg_match("/^(\s*)/", $line)) { // empty line
+ if ($emptyCount > 0) {
+ $this->startBlock('normal', $key);
+ } else {
+ $this->setBlock($key);
+ }
+
+ $emptyCount ++;
+ } else if ($emptyCount == 0) {
+ $this->setBlock($key);
+ } else {
+ $this->startBlock('normal', $key);
+ }
+ } else if ($this->isBlock('footnote')) {
+ preg_match("/^(\s*)/", $line, $matches);
+ if (strlen($matches[1]) >= $block[3][0]) {
+ $this->setBlock($key);
+ } else {
+ $this->startBlock('normal', $key);
+ }
+ } else if ($this->isBlock('table')) {
+ if (false !== strpos($line, '|')) {
+ $block[3][2] ++;
+ $this->setBlock($key, $block[3]);
+ } else {
+ $this->startBlock('normal', $key);
+ }
+ } else if ($this->isBlock('pre')) {
+ if (preg_match("/^\s*$/", $line)) {
+ if ($emptyCount > 0) {
+ $this->startBlock('normal', $key);
+ } else {
+ $this->setBlock($key);
+ }
+
+ $emptyCount ++;
+ } else {
+ $this->startBlock('normal', $key);
+ }
+ } else if ($this->isBlock('quote')) {
+ if (preg_match("/^(\s*)/", $line)) { // empty line
+ if ($emptyCount > 0) {
+ $this->startBlock('normal', $key);
+ } else {
+ $this->setBlock($key);
+ }
+
+ $emptyCount ++;
+ } else if ($emptyCount == 0) {
+ $this->setBlock($key);
+ } else {
+ $this->startBlock('normal', $key);
+ }
+ } else {
+ if (empty($block) || $block[0] != 'normal') {
+ $this->startBlock('normal', $key);
+ } else {
+ $this->setBlock($key);
+ }
+ }
+ break;
+ }
+ }
+
+ return $this->optimizeBlocks($this->_blocks, $lines);
+ }
+
+ /**
+ * @param array $blocks
+ * @param array $lines
+ * @return array
+ */
+ private function optimizeBlocks(array $blocks, array $lines)
+ {
+ $blocks = $this->call('beforeOptimizeBlocks', $blocks, $lines);
+
+ $key = 0;
+ while (isset($blocks[$key])) {
+ $moved = false;
+
+ $block = &$blocks[$key];
+ $prevBlock = isset($blocks[$key - 1]) ? $blocks[$key - 1] : NULL;
+ $nextBlock = isset($blocks[$key + 1]) ? $blocks[$key + 1] : NULL;
+
+ list ($type, $from, $to) = $block;
+
+ if ('pre' == $type) {
+ $isEmpty = array_reduce($lines, function ($result, $line) {
+ return preg_match("/^\s*$/", $line) && $result;
+ }, true);
+
+ if ($isEmpty) {
+ $block[0] = $type = 'normal';
+ }
+ }
+
+ if ('normal' == $type) {
+ // combine two blocks
+ $types = array('list', 'quote');
+
+ if ($from == $to && preg_match("/^\s*$/", $lines[$from])
+ && !empty($prevBlock) && !empty($nextBlock)) {
+ if ($prevBlock[0] == $nextBlock[0] && in_array($prevBlock[0], $types)) {
+ // combine 3 blocks
+ $blocks[$key - 1] = array(
+ $prevBlock[0], $prevBlock[1], $nextBlock[2], NULL
+ );
+ array_splice($blocks, $key, 2);
+
+ // do not move
+ $moved = true;
+ }
+ }
+ }
+
+ if (!$moved) {
+ $key ++;
+ }
+ }
+
+ return $this->call('afterOptimizeBlocks', $blocks, $lines);
+ }
+
+ /**
+ * parseCode
+ *
+ * @param array $lines
+ * @param array $parts
+ * @return string
+ */
+ private function parseCode(array $lines, array $parts)
+ {
+ list ($blank, $lang) = $parts;
+ $lang = trim($lang);
+ $count = strlen($blank);
+
+ if (! preg_match("/^[_a-z0-9-\+\#\:\.]+$/i", $lang)) {
+ $lang = NULL;
+ } else {
+ $parts = explode(':', $lang);
+ if (count($parts) > 1) {
+ list ($lang, $rel) = $parts;
+ $lang = trim($lang);
+ $rel = trim($rel);
+ }
+ }
+
+ $lines = array_map(function ($line) use ($count) {
+ return preg_replace("/^[ ]{{$count}}/", '', $line);
+ }, array_slice($lines, 1, -1));
+ $str = implode("\n", $lines);
+
+ return preg_match("/^\s*$/", $str) ? '' :
+ ''
+ . htmlspecialchars($str) . '
';
+ }
+
+ /**
+ * parsePre
+ *
+ * @param array $lines
+ * @return string
+ */
+ private function parsePre(array $lines)
+ {
+ foreach ($lines as &$line) {
+ $line = htmlspecialchars(substr($line, 4));
+ }
+ $str = implode("\n", $lines);
+
+ return preg_match("/^\s*$/", $str) ? '' : '' . $str . '
';
+ }
+
+ /**
+ * parseSh
+ *
+ * @param array $lines
+ * @param int $num
+ * @return string
+ */
+ private function parseSh(array $lines, $num)
+ {
+ $line = $this->parseInline(trim($lines[0], '# '));
+ $title = trim($line);
+ $content = str_replace(' ', '_', $title);
+ return preg_match("/^\s*$/", $line) ? '' : "{$line}";
+ }
+
+ /**
+ * parseMh
+ *
+ * @param array $lines
+ * @param int $num
+ * @return string
+ */
+ private function parseMh(array $lines, $num)
+ {
+ return $this->parseSh($lines, $num);
+ }
+
+ /**
+ * parseQuote
+ *
+ * @param array $lines
+ * @return string
+ */
+ private function parseQuote(array $lines)
+ {
+ foreach ($lines as &$line) {
+ $line = preg_replace("/^\s*> ?/", '', $line);
+ }
+ $str = implode("\n", $lines);
+
+ return preg_match("/^\s*$/", $str) ? '' : '' . $this->parse($str) . '
';
+ }
+
+ /**
+ * parseList
+ *
+ * @param array $lines
+ * @return string
+ */
+ private function parseList(array $lines)
+ {
+ $html = '';
+ $minSpace = 99999;
+ $rows = array();
+
+ // count levels
+ foreach ($lines as $key => $line) {
+ if (preg_match("/^(\s*)((?:[0-9a-z]+\.?)|\-|\+|\*)(\s+)(.*)$/", $line, $matches)) {
+ $space = strlen($matches[1]);
+ $type = false !== strpos('+-*', $matches[2]) ? 'ul' : 'ol';
+ $minSpace = min($space, $minSpace);
+
+ $rows[] = array($space, $type, $line, $matches[4]);
+ } else {
+ $rows[] = $line;
+ }
+ }
+
+ $found = false;
+ $secondMinSpace = 99999;
+ foreach ($rows as $row) {
+ if (is_array($row) && $row[0] != $minSpace) {
+ $secondMinSpace = min($secondMinSpace, $row[0]);
+ $found = true;
+ }
+ }
+ $secondMinSpace = $found ? $secondMinSpace : $minSpace;
+
+ $lastType = '';
+ $leftLines = array();
+
+ foreach ($rows as $row) {
+ if (is_array($row)) {
+ list ($space, $type, $line, $text) = $row;
+
+ if ($space != $minSpace) {
+ $leftLines[] = preg_replace("/^\s{" . $secondMinSpace . "}/", '', $line);
+ } else {
+ if (!empty($leftLines)) {
+ $html .= "" . $this->parse(implode("\n", $leftLines)) . "";
+ }
+
+ if ($lastType != $type) {
+ if (!empty($lastType)) {
+ $html .= "{$lastType}>";
+ }
+
+ $html .= "<{$type}>";
+ }
+
+ $leftLines = array($text);
+ $lastType = $type;
+ }
+ } else {
+ $leftLines[] = preg_replace("/^\s{" . $secondMinSpace . "}/", '', $row);
+ }
+ }
+
+ if (!empty($leftLines)) {
+ $html .= "" . $this->parse(implode("\n", $leftLines)) . "{$lastType}>";
+ }
+
+ return $html;
+ }
+
+ /**
+ * @param array $lines
+ * @param array $value
+ * @return string
+ */
+ private function parseTable(array $lines, array $value)
+ {
+ list ($ignores, $aligns) = $value;
+ $head = count($ignores) > 0 && array_sum($ignores) > 0;
+
+ $html = '';
+ $body = $head ? NULL : true;
+ $output = false;
+
+ foreach ($lines as $key => $line) {
+ if (in_array($key, $ignores)) {
+ if ($head && $output) {
+ $head = false;
+ $body = true;
+ }
+ continue;
+ }
+
+ $line = trim($line);
+ $output = true;
+
+ if ($line[0] == '|') {
+ $line = substr($line, 1);
+
+ if ($line[strlen($line) - 1] == '|') {
+ $line = substr($line, 0, -1);
+ }
+ }
+
+
+ $rows = array_map(function ($row) {
+ if (preg_match("/^\s+$/", $row)) {
+ return ' ';
+ } else {
+ return trim($row);
+ }
+ }, explode('|', $line));
+ $columns = array();
+ $last = -1;
+
+ foreach ($rows as $row) {
+ if (strlen($row) > 0) {
+ $last ++;
+ $columns[$last] = array(
+ isset($columns[$last]) ? $columns[$last][0] + 1 : 1, $row
+ );
+ } else if (isset($columns[$last])) {
+ $columns[$last][0] ++;
+ } else {
+ $columns[0] = array(1, $row);
+ }
+ }
+
+ if ($head) {
+ $html .= '';
+ } else if ($body) {
+ $html .= '';
+ }
+
+ $html .= '';
+
+ foreach ($columns as $key => $column) {
+ list ($num, $text) = $column;
+ $tag = $head ? 'th' : 'td';
+
+ $html .= "<{$tag}";
+ if ($num > 1) {
+ $html .= " colspan=\"{$num}\"";
+ }
+
+ if (isset($aligns[$key]) && $aligns[$key] != 'none') {
+ $html .= " align=\"{$aligns[$key]}\"";
+ }
+
+ $html .= '>' . $this->parseInline($text) . "{$tag}>";
+ }
+
+ $html .= '
';
+
+ if ($head) {
+ $html .= '';
+ } else if ($body) {
+ $body = false;
+ }
+ }
+
+ if ($body !== NULL) {
+ $html .= '';
+ }
+
+ $html .= '
';
+ return $html;
+ }
+
+ /**
+ * parseHr
+ *
+ * @return string
+ */
+ private function parseHr()
+ {
+ return '
';
+ }
+
+ /**
+ * parseToc
+ *
+ * @return string
+ */
+ private function parseToc(array $lines)
+ {
+ $ret = '';
+ $idx = 0;
+ $level = 0;
+ $parent_level = 0;
+ foreach ($this->_blocks as $key=>$block) {
+ list ($type, $start, $end, $value) = $block;
+ if($type == 'sh'){
+ $title = $this->_sh[$idx];
+ $title = trim($title);
+ $content = str_replace(' ', '_', $title);
+ //echo "start:".$start." end:".$end." val:".$value." key:".$idx." con:".$content;
+ if($value > $parent_level){//down
+ //$level++;
+ $level = $value;
+ while($level-- > $parent_level){
+ $ret .= '- ';
+ }
+ $ret .= ''.$title.'';
+ }else if($value < $parent_level){ //up
+ //$level--;
+ $level = $value;
+ while($level++ < $parent_level){
+ $ret .= '
';
+ }
+ $ret .= ''.$title.'';
+ }else{
+ $last = substr($ret, -4);
+ if($last == ''){
+ $ret .= ''.$title.'';
+ }else{
+ $ret .= ''.$title.'';
+ }
+ }
+
+ $parent_level = $value;
+ $idx++;
+ }
+ }
+ $level = $value;
+ while($level++ < $parent_level){
+ $ret .= '';
+ }
+
+ $ret = ''.$ret.'
';
+ return $ret;
+ }
+
+ /**
+ * parseNormal
+ *
+ * @param array $lines
+ * @return string
+ */
+ private function parseNormal(array $lines)
+ {
+ foreach ($lines as &$line) {
+ $line = $this->parseInline($line);
+ }
+
+ $str = trim(implode("\n", $lines));
+ $str = preg_replace("/(\n\s*){2,}/", "", $str);
+ $str = preg_replace("/\n/", "
", $str);
+
+ return preg_match("/^\s*$/", $str) ? '' : "
{$str}
";
+ }
+
+ /**
+ * parseFootnote
+ *
+ * @param array $lines
+ * @param array $value
+ * @return string
+ */
+ private function parseFootnote(array $lines, array $value)
+ {
+ list($space, $note) = $value;
+ $index = array_search($note, $this->_footnotes);
+
+ if (false !== $index) {
+ $lines[0] = preg_replace("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", '', $lines[0]);
+ $this->_footnotes[$index] = $lines;
+ }
+
+ return '';
+ }
+
+ /**
+ * parseDefine
+ *
+ * @return string
+ */
+ private function parseDefinition()
+ {
+ return '';
+ }
+
+ /**
+ * parseHtml
+ *
+ * @param array $lines
+ * @param string $type
+ * @return string
+ */
+ private function parseHtml(array $lines, $type)
+ {
+ foreach ($lines as &$line) {
+ $line = $this->parseInline($line,
+ isset($this->_specialWhiteList[$type]) ? $this->_specialWhiteList[$type] : '');
+ }
+
+ return implode("\n", $lines);
+ }
+
+ /**
+ * @param $url
+ * @return string
+ */
+ public function cleanUrl($url)
+ {
+ if (preg_match("/^\s*((http|https|ftp|mailto):[x80-xff_a-z0-9-\.\/%#@\?\+=~\|\,&\(\)]+)/i", $url, $matches)) {
+ return $matches[1];
+ } else if (preg_match("/^\s*([x80-xff_a-z0-9-\.\/%#@\?\+=~\|\,&]+)/i", $url, $matches)) {
+ return $matches[1];
+ } else {
+ return '#';
+ }
+ }
+
+ /**
+ * @param $str
+ * @return mixed
+ */
+ public function escapeBracket($str)
+ {
+ return str_replace(
+ array('\[', '\]', '\(', '\)'), array('[', ']', '(', ')'), $str
+ );
+ }
+
+ /**
+ * startBlock
+ *
+ * @param mixed $type
+ * @param mixed $start
+ * @param mixed $value
+ * @return $this
+ */
+ private function startBlock($type, $start, $value = NULL)
+ {
+ $this->_pos ++;
+ $this->_current = $type;
+
+ $this->_blocks[$this->_pos] = array($type, $start, $start, $value);
+
+ return $this;
+ }
+
+ /**
+ * endBlock
+ *
+ * @return $this
+ */
+ private function endBlock()
+ {
+ $this->_current = 'normal';
+ return $this;
+ }
+
+ /**
+ * isBlock
+ *
+ * @param mixed $type
+ * @param mixed $value
+ * @return bool
+ */
+ private function isBlock($type, $value = NULL)
+ {
+ return $this->_current == $type
+ && (NULL === $value ? true : $this->_blocks[$this->_pos][3] == $value);
+ }
+
+ /**
+ * getBlock
+ *
+ * @return array
+ */
+ private function getBlock()
+ {
+ return isset($this->_blocks[$this->_pos]) ? $this->_blocks[$this->_pos] : NULL;
+ }
+
+ /**
+ * setBlock
+ *
+ * @param mixed $to
+ * @param mixed $value
+ * @return $this
+ */
+ private function setBlock($to = NULL, $value = NULL)
+ {
+ if (NULL !== $to) {
+ $this->_blocks[$this->_pos][2] = $to;
+ }
+
+ if (NULL !== $value) {
+ $this->_blocks[$this->_pos][3] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * backBlock
+ *
+ * @param mixed $step
+ * @param mixed $type
+ * @param mixed $value
+ * @return $this
+ */
+ private function backBlock($step, $type, $value = NULL)
+ {
+ if ($this->_pos < 0) {
+ return $this->startBlock($type, 0, $value);
+ }
+
+ $last = $this->_blocks[$this->_pos][2];
+ $this->_blocks[$this->_pos][2] = $last - $step;
+
+ if ($this->_blocks[$this->_pos][1] <= $this->_blocks[$this->_pos][2]) {
+ $this->_pos ++;
+ }
+
+ $this->_current = $type;
+ $this->_blocks[$this->_pos] = array(
+ $type, $last - $step + 1, $last, $value
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ private function combineBlock()
+ {
+ if ($this->_pos < 1) {
+ return $this;
+ }
+
+ $prev = $this->_blocks[$this->_pos - 1];
+ $current = $this->_blocks[$this->_pos];
+
+ $prev[2] = $current[2];
+ $this->_blocks[$this->_pos - 1] = $prev;
+ $this->_current = $prev[0];
+ unset($this->_blocks[$this->_pos]);
+ $this->_pos --;
+
+ return $this;
+ }
+}