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 .= '

    '; + $index = 1; + + while ($val = array_shift($this->_footnotes)) { + if (is_string($val)) { + $val .= " "; + } else { + $val[count($val) - 1] .= " "; + $val = count($val) > 1 ? $this->parse(implode("\n", $val)) : $this->parseInline($val[0]); + } + + $html .= "
  1. {$val}
  2. "; + $index ++; + } + + $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( + "{$id}" + ); + }, + $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( + "\"{$escaped}\"" + ); + }, + $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 .= ""; + } + + $html .= "<{$type}>"; + } + + $leftLines = array($text); + $lastType = $type; + } + } else { + $leftLines[] = preg_replace("/^\s{" . $secondMinSpace . "}/", '', $row); + } + } + + if (!empty($leftLines)) { + $html .= "
  • " . $this->parse(implode("\n", $leftLines)) . "
  • "; + } + + 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) . ""; + } + + $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{ + $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; + } +}