<?php
namespace vikit;
/**
* Parser
*
* @copyright Copyright (c) 2012 SegmentFault Team. (http://segmentfault.com)
* @author Joyqi <joyqi@segmentfault.com>
* @license BSD License
*/
class parser
{
/**
* _whiteList
*
* @var string
*/
public $_commonWhiteList = 'kbd|b|i|strong|em|sup|sub|br|code|del|a|hr|small';
/**
* html tags
*
* @var string
*/
public $_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';
/**
* _specialWhiteList
*
* @var mixed
* @access private
*/
public $_specialWhiteList = array(
'table' => 'table|tbody|thead|tfoot|tr|td|th'
);
/**
* _footnotes
*
* @var array
*/
public $_footnotes;
/**
* @var bool
*/
public $_html = false;
/**
* @var bool
*/
public $_line = false;
/**
* @var array
*/
public $blockParsers = array(
array('code', 10),
array('shtml', 20),
array('pre', 30),
array('ahtml', 40),
array('list', 50),
array('math', 60),
array('html', 70),
array('footnote', 80),
array('definition', 90),
array('quote', 100),
array('table', 110),
array('sh', 120),
array('mh', 130),
array('hr', 140),
array('default', 9999)
);
/**
* _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 array
*/
private $_parsers = array();
/**
* 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;
usort($this->blockParsers, function ($a, $b) {
return $a[1] < $b[1] ? -1 : 1;
});
foreach ($this->blockParsers as $parser) {
list ($name) = $parser;
if (isset($parser[2])) {
$this->_parsers[$name] = $parser[2];
} else {
$this->_parsers[$name] = array($this, 'parseBlock' . ucfirst($name));
}
}
$text = $this->initText($text);
$html = $this->parse($text);
$html = $this->makeFootnotes($html);
$html = $this->optimizeLines($html);
return $this->call('makeHtml', $html);
}
/**
* @param $html
*/
public function enableHtml($html = true)
{
$this->_html = $html;
}
/**
* @param bool $line
*/
public function enableLine($line = true)
{
$this->_line = $line;
}
/**
* @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 .= '<div class="footnotes"><hr><ol>';
$index = 1;
while ($val = array_shift($this->_footnotes)) {
if (is_string($val)) {
$val .= " <a href=\"#fnref-{$index}\" class=\"footnote-backref\">↩</a>";
} else {
$val[count($val) - 1] .= " <a href=\"#fnref-{$index}\" class=\"footnote-backref\">↩</a>";
$val = count($val) > 1 ? $this->parse(implode("\n", $val)) : $this->parseInline($val[0]);
}
$html .= "<li id=\"fn-{$index}\">{$val}</li>";
$index ++;
}
$html .= '</ol></div>';
}
return $html;
}
/**
* parse
*
* @param string $text
* @param bool $inline
* @param int $offset
* @return string
*/
private function parse($text, $inline = false, $offset = 0)
{
$blocks = $this->parseBlock($text, $lines);
$html = '';
// inline mode for single normal block
if ($inline && count($blocks) == 1 && $blocks[0][0] == 'normal') {
$blocks[0][3] = true;
}
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, $start + $offset, $end + $offset);
$result = $this->call('after' . ucfirst($method), $result, $value);
$html .= $result;
}
return $html;
}
/**
* @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;
}
/**
* @param $start
* @param int $end
* @return string
*/
public function markLine($start, $end = -1)
{
if ($this->_line) {
$end = $end < 0 ? $start : $end;
return '<span class="line" data-start="' . $start
. '" data-end="' . $end . '" data-id="' . $this->_uniqid . '"></span>';
}
return '';
}
/**
* @param array $lines
* @param $start
* @return string
*/
public function markLines(array $lines, $start)
{
$i = -1;
$self = $this;
return $this->_line ? array_map(function ($line) use ($self, $start, &$i) {
$i ++;
return $self->markLine($start + $i) . $line;
}, $lines) : $lines;
}
/**
* @param $html
* @return string
*/
public function optimizeLines($html)
{
$last = 0;
return $this->_line ?
preg_replace_callback("/class=\"line\" data\-start=\"([0-9]+)\" data\-end=\"([0-9]+)\" (data\-id=\"{$this->_uniqid}\")/",
function ($matches) use (&$last) {
if ($matches[1] != $last) {
$replace = 'class="line" data-start="' . $last . '" data-start-original="' . $matches[1] . '" data-end="' . $matches[2] . '" ' . $matches[3];
} else {
$replace = $matches[0];
}
$last = $matches[2] + 1;
return $replace;
}, $html) : $html;
}
/**
* @param $type
* @param $value
* @return mixed
*/
public function call($type, $value)
{
if (empty($this->_hooks[$type])) {