. */ //! Markdown-to-HTML converter class Markdown extends Prefab { protected //! Parsing rules $blocks, //! Special characters $special; /** * Process blockquote * @return string * @param $str string **/ protected function _blockquote($str) { $str=preg_replace('/(?<=^|\n)\h?>\h?(.*?(?:\n+|$))/','\1',$str); return strlen($str)? ('
'.$this->build($str).''."\n\n"):''; } /** * Process whitespace-prefixed code block * @return string * @param $str string **/ protected function _pre($str) { $str=preg_replace('/(?<=^|\n)(?: {4}|\t)(.+?(?:\n+|$))/','\1', $this->esc($str)); return strlen($str)? ('
'.
$this->esc($this->snip($str)).
'
'."\n\n"):
'';
}
/**
* Process fenced code block
* @return string
* @param $hint string
* @param $str string
**/
protected function _fence($hint,$str) {
$str=$this->snip($str);
$fw=Base::instance();
if ($fw->HIGHLIGHT) {
switch (strtolower($hint)) {
case 'php':
$str=$fw->highlight($str);
break;
case 'apache':
preg_match_all('/(?<=^|\n)(\h*)'.
'(?:(<\/?)(\w+)((?:\h+[^>]+)*)(>)|'.
'(?:(\w+)(\h.+?)))(\h*(?:\n+|$))/',
$str,$matches,PREG_SET_ORDER);
$out='';
foreach ($matches as $match)
$out.=$match[1].
($match[3]?
(''.
$this->esc($match[2]).$match[3].
''.
($match[4]?
(''.
$this->esc($match[4]).
''):
'').
''.
$this->esc($match[5]).
''):
(''.
$match[6].
''.
''.
$this->esc($match[7]).
'')).
$match[8];
$str=''.$out.'
';
break;
case 'html':
preg_match_all(
'/(?:(?:<(\/?)(\w+)'.
'((?:\h+(?:\w+\h*=\h*)?".+?"|[^>]+)*|'.
'\h+.+?)(\h*\/?)>)|(.+?))/s',
$str,$matches,PREG_SET_ORDER
);
$out='';
foreach ($matches as $match) {
if ($match[2]) {
$out.='<'.
$match[1].$match[2].'';
if ($match[3]) {
preg_match_all(
'/(?:\h+(?:(?:(\w+)\h*=\h*)?'.
'(".+?")|(.+)))/',
$match[3],$parts,PREG_SET_ORDER
);
foreach ($parts as $part)
$out.=' '.
(empty($part[3])?
((empty($part[1])?
'':
(''.
$part[1].'=')).
''.
$part[2].''):
(''.
$part[3].''));
}
$out.=''.
$match[4].'>';
}
else
$out.=$this->esc($match[5]);
}
$str=''.$out.'
';
break;
case 'ini':
preg_match_all(
'/(?<=^|\n)(?:'.
'(;[^\n]*)|(?:<\?php.+?\?>?)|'.
'(?:\[(.+?)\])|'.
'(.+?)(\h*=\h*)'.
'((?:\\\\\h*\r?\n|.+?)*)'.
')((?:\r?\n)+|$)/',
$str,$matches,PREG_SET_ORDER
);
$out='';
foreach ($matches as $match) {
if ($match[1])
$out.=''.$match[1].
'';
elseif ($match[2])
$out.='['.$match[2].']'.
'';
elseif ($match[3])
$out.=''.$match[3].
''.$match[4].
($match[5]?
(''.
$match[5].''):'');
else
$out.=$match[0];
if (isset($match[6]))
$out.=$match[6];
}
$str=''.$out.'
';
break;
default:
$str=''.$this->esc($str).'
';
break;
}
}
else
$str=''.$this->esc($str).'
';
return ''.$str.''."\n\n"; } /** * Process horizontal rule * @return string **/ protected function _hr() { return '
'.$this->scan($str).'
'."\n\n"; } return ''; } /** * Process strong/em/strikethrough spans * @return string * @param $str string **/ protected function _text($str) { $tmp=''; while ($str!=$tmp) $str=preg_replace_callback( '/(?<=\s|^)(?'.$expr[4].''; if ($expr[2]) return ''.$expr[4].''; return ''.$expr[4].''; }, preg_replace( '/(?\1', $tmp=$str ) ); return $str; } /** * Process image span * @return string * @param $str string **/ protected function _img($str) { return preg_replace_callback( '/!(?:\[(.+?)\])?\h*\((.*?)>?(?:\h*"(.*?)"\h*)?\)/', function($expr) { return ''; }, $str ); } /** * Process anchor span * @return string * @param $str string **/ protected function _a($str) { return preg_replace_callback( '/(??(?:\h*"(.*?)"\h*)?\)/', function($expr) { return ''.$this->scan($expr[1]).''; }, $str ); } /** * Auto-convert links * @return string * @param $str string **/ protected function _auto($str) { return preg_replace_callback( '/`.*?<(.+?)>.*?`|<(.+?)>/', function($expr) { if (empty($expr[1]) && parse_url($expr[2],PHP_URL_SCHEME)) { $expr[2]=$this->esc($expr[2]); return ''.$expr[2].''; } return $expr[0]; }, $str ); } /** * Process code span * @return string * @param $str string **/ protected function _code($str) { return preg_replace_callback( '/`` (.+?) ``|(?'. $this->esc(empty($expr[1])?$expr[2]:$expr[1]).''; }, $str ); } /** * Convert characters to HTML entities * @return string * @param $str string **/ function esc($str) { if (!$this->special) $this->special=[ '...'=>'…', '(tm)'=>'™', '(r)'=>'®', '(c)'=>'©' ]; foreach ($this->special as $key=>$val) $str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str); return htmlspecialchars($str,ENT_COMPAT, Base::instance()->ENCODING,FALSE); } /** * Reduce multiple line feeds * @return string * @param $str string **/ protected function snip($str) { return preg_replace('/(?:(?<=\n)\n+)|\n+$/',"\n",$str); } /** * Scan line for convertible spans * @return string * @param $str string **/ function scan($str) { $inline=['img','a','text','auto','code']; foreach ($inline as $func) $str=$this->{'_'.$func}($str); return $str; } /** * Assemble blocks * @return string * @param $str string **/ protected function build($str) { if (!$this->blocks) { // Regexes for capturing entire blocks $this->blocks=[ 'blockquote'=>'/^(?:\h?>\h?.*?(?:\n+|$))+/', 'pre'=>'/^(?:(?: {4}|\t).+?(?:\n+|$))+/', 'fence'=>'/^`{3}\h*(\w+)?.*?[^\n]*\n+(.+?)`{3}[^\n]*'. '(?:\n+|$)/s', 'hr'=>'/^\h*[*_-](?:\h?[\*_-]){2,}\h*(?:\n+|$)/', 'atx'=>'/^\h*(#{1,6})\h?(.+?)\h*(?:#.*)?(?:\n+|$)/', 'setext'=>'/^\h*(.+?)\h*\n([=-])+\h*(?:\n+|$)/', 'li'=>'/^(?:(?:[*+-]|\d+\.)\h.+?(?:\n+|$)'. '(?:(?: {4}|\t)+.+?(?:\n+|$))*)+/s', 'raw'=>'/^((?:|'. '<(address|article|aside|audio|blockquote|canvas|dd|'. 'div|dl|fieldset|figcaption|figure|footer|form|h\d|'. 'header|hgroup|hr|noscript|object|ol|output|p|pre|'. 'section|table|tfoot|ul|video).*?'. '(?:\/>|>(?:(?>[^><]+)|(?R))*<\/\2>))'. '\h*(?:\n{2,}|\n*$)|<[\?%].+?[\?%]>\h*(?:\n?$|\n*))/s', 'p'=>'/^(.+?(?:\n{2,}|\n*$))/s' ]; } // Treat lines with nothing but whitespaces as empty lines $str=preg_replace('/\n\h+(?=\n)/',"\n",$str); // Initialize block parser $len=strlen($str); $ptr=0; $dst=''; // Main loop while ($ptr<$len) { if (preg_match('/^ {0,3}\[([^\[\]]+)\]:\s*(.*?)>?\s*'. '(?:"([^\n]*)")?(?:\n+|$)/s',substr($str,$ptr),$match)) { // Reference-style link; Backtrack $ptr+=strlen($match[0]); $tmp=''; // Catch line breaks in title attribute $ref=preg_replace('/\h/','\s',preg_quote($match[1],'/')); while ($dst!=$tmp) { $dst=preg_replace_callback( '/(?esc($match[2]).'"'. (empty($match[3])? '': (' title="'. $this->esc($match[3]).'"')).'>'. // Link $this->scan( empty($expr[3])? (empty($expr[1])? $expr[4]: $expr[1]): $expr[3] ).''): // Image (''); }, $tmp=$dst ); } } else foreach ($this->blocks as $func=>$regex) if (preg_match($regex,substr($str,$ptr),$match)) { $ptr+=strlen($match[0]); $dst.=call_user_func_array( [$this,'_'.$func], count($match)>1?array_slice($match,1):$match ); break; } } return $dst; } /** * Render HTML equivalent of markdown * @return string * @param $txt string **/ function convert($txt) { $txt=preg_replace_callback( '/(