PageRenderTime 24ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/bcosca/fatfree/lib/markdown.php

https://github.com/patadejaguar/S.A.F.E.-Open-Source-Microfinance-Suite
PHP | 572 lines | 413 code | 23 blank | 136 comment | 30 complexity | 02acd24ca3b303e24f9e3dbab79cb03f MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-2.0, GPL-3.0, BSD-3-Clause
  1. <?php
  2. /*
  3. Copyright (c) 2009-2017 F3::Factory/Bong Cosca, All rights reserved.
  4. This file is part of the Fat-Free Framework (http://fatfreeframework.com).
  5. This is free software: you can redistribute it and/or modify it under the
  6. terms of the GNU General Public License as published by the Free Software
  7. Foundation, either version 3 of the License, or later.
  8. Fat-Free Framework is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. General Public License for more details.
  12. You should have received a copy of the GNU General Public License along
  13. with Fat-Free Framework. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. //! Markdown-to-HTML converter
  16. class Markdown extends Prefab {
  17. protected
  18. //! Parsing rules
  19. $blocks,
  20. //! Special characters
  21. $special;
  22. /**
  23. * Process blockquote
  24. * @return string
  25. * @param $str string
  26. **/
  27. protected function _blockquote($str) {
  28. $str=preg_replace('/(?<=^|\n)\h?>\h?(.*?(?:\n+|$))/','\1',$str);
  29. return strlen($str)?
  30. ('<blockquote>'.$this->build($str).'</blockquote>'."\n\n"):'';
  31. }
  32. /**
  33. * Process whitespace-prefixed code block
  34. * @return string
  35. * @param $str string
  36. **/
  37. protected function _pre($str) {
  38. $str=preg_replace('/(?<=^|\n)(?: {4}|\t)(.+?(?:\n+|$))/','\1',
  39. $this->esc($str));
  40. return strlen($str)?
  41. ('<pre><code>'.
  42. $this->esc($this->snip($str)).
  43. '</code></pre>'."\n\n"):
  44. '';
  45. }
  46. /**
  47. * Process fenced code block
  48. * @return string
  49. * @param $hint string
  50. * @param $str string
  51. **/
  52. protected function _fence($hint,$str) {
  53. $str=$this->snip($str);
  54. $fw=Base::instance();
  55. if ($fw->HIGHLIGHT) {
  56. switch (strtolower($hint)) {
  57. case 'php':
  58. $str=$fw->highlight($str);
  59. break;
  60. case 'apache':
  61. preg_match_all('/(?<=^|\n)(\h*)'.
  62. '(?:(<\/?)(\w+)((?:\h+[^>]+)*)(>)|'.
  63. '(?:(\w+)(\h.+?)))(\h*(?:\n+|$))/',
  64. $str,$matches,PREG_SET_ORDER);
  65. $out='';
  66. foreach ($matches as $match)
  67. $out.=$match[1].
  68. ($match[3]?
  69. ('<span class="section">'.
  70. $this->esc($match[2]).$match[3].
  71. '</span>'.
  72. ($match[4]?
  73. ('<span class="data">'.
  74. $this->esc($match[4]).
  75. '</span>'):
  76. '').
  77. '<span class="section">'.
  78. $this->esc($match[5]).
  79. '</span>'):
  80. ('<span class="directive">'.
  81. $match[6].
  82. '</span>'.
  83. '<span class="data">'.
  84. $this->esc($match[7]).
  85. '</span>')).
  86. $match[8];
  87. $str='<code>'.$out.'</code>';
  88. break;
  89. case 'html':
  90. preg_match_all(
  91. '/(?:(?:<(\/?)(\w+)'.
  92. '((?:\h+(?:\w+\h*=\h*)?".+?"|[^>]+)*|'.
  93. '\h+.+?)(\h*\/?)>)|(.+?))/s',
  94. $str,$matches,PREG_SET_ORDER
  95. );
  96. $out='';
  97. foreach ($matches as $match) {
  98. if ($match[2]) {
  99. $out.='<span class="xml_tag">&lt;'.
  100. $match[1].$match[2].'</span>';
  101. if ($match[3]) {
  102. preg_match_all(
  103. '/(?:\h+(?:(?:(\w+)\h*=\h*)?'.
  104. '(".+?")|(.+)))/',
  105. $match[3],$parts,PREG_SET_ORDER
  106. );
  107. foreach ($parts as $part)
  108. $out.=' '.
  109. (empty($part[3])?
  110. ((empty($part[1])?
  111. '':
  112. ('<span class="xml_attr">'.
  113. $part[1].'</span>=')).
  114. '<span class="xml_data">'.
  115. $part[2].'</span>'):
  116. ('<span class="xml_tag">'.
  117. $part[3].'</span>'));
  118. }
  119. $out.='<span class="xml_tag">'.
  120. $match[4].'&gt;</span>';
  121. }
  122. else
  123. $out.=$this->esc($match[5]);
  124. }
  125. $str='<code>'.$out.'</code>';
  126. break;
  127. case 'ini':
  128. preg_match_all(
  129. '/(?<=^|\n)(?:'.
  130. '(;[^\n]*)|(?:<\?php.+?\?>?)|'.
  131. '(?:\[(.+?)\])|'.
  132. '(.+?)\h*=\h*'.
  133. '((?:\\\\\h*\r?\n|.+?)*)'.
  134. ')((?:\r?\n)+|$)/',
  135. $str,$matches,PREG_SET_ORDER
  136. );
  137. $out='';
  138. foreach ($matches as $match) {
  139. if ($match[1])
  140. $out.='<span class="comment">'.$match[1].
  141. '</span>';
  142. elseif ($match[2])
  143. $out.='<span class="ini_section">['.$match[2].']'.
  144. '</span>';
  145. elseif ($match[3])
  146. $out.='<span class="ini_key">'.$match[3].
  147. '</span>='.
  148. ($match[4]?
  149. ('<span class="ini_value">'.
  150. $match[4].'</span>'):'');
  151. else
  152. $out.=$match[0];
  153. if (isset($match[5]))
  154. $out.=$match[5];
  155. }
  156. $str='<code>'.$out.'</code>';
  157. break;
  158. default:
  159. $str='<code>'.$this->esc($str).'</code>';
  160. break;
  161. }
  162. }
  163. else
  164. $str='<code>'.$this->esc($str).'</code>';
  165. return '<pre>'.$str.'</pre>'."\n\n";
  166. }
  167. /**
  168. * Process horizontal rule
  169. * @return string
  170. **/
  171. protected function _hr() {
  172. return '<hr />'."\n\n";
  173. }
  174. /**
  175. * Process atx-style heading
  176. * @return string
  177. * @param $type string
  178. * @param $str string
  179. **/
  180. protected function _atx($type,$str) {
  181. $level=strlen($type);
  182. return '<h'.$level.' id="'.Web::instance()->slug($str).'">'.
  183. $this->scan($str).'</h'.$level.'>'."\n\n";
  184. }
  185. /**
  186. * Process setext-style heading
  187. * @return string
  188. * @param $str string
  189. * @param $type string
  190. **/
  191. protected function _setext($str,$type) {
  192. $level=strpos('=-',$type)+1;
  193. return '<h'.$level.' id="'.Web::instance()->slug($str).'">'.
  194. $this->scan($str).'</h'.$level.'>'."\n\n";
  195. }
  196. /**
  197. * Process ordered/unordered list
  198. * @return string
  199. * @param $str string
  200. **/
  201. protected function _li($str) {
  202. // Initialize list parser
  203. $len=strlen($str);
  204. $ptr=0;
  205. $dst='';
  206. $first=TRUE;
  207. $tight=TRUE;
  208. $type='ul';
  209. // Main loop
  210. while ($ptr<$len) {
  211. if (preg_match('/^\h*[*-](?:\h?[*-]){2,}(?:\n+|$)/',
  212. substr($str,$ptr),$match)) {
  213. $ptr+=strlen($match[0]);
  214. // Embedded horizontal rule
  215. return (strlen($dst)?
  216. ('<'.$type.'>'."\n".$dst.'</'.$type.'>'."\n\n"):'').
  217. '<hr />'."\n\n".$this->build(substr($str,$ptr));
  218. }
  219. elseif (preg_match('/(?<=^|\n)([*+-]|\d+\.)\h'.
  220. '(.+?(?:\n+|$))((?:(?: {4}|\t)+.+?(?:\n+|$))*)/s',
  221. substr($str,$ptr),$match)) {
  222. $match[3]=preg_replace('/(?<=^|\n)(?: {4}|\t)/','',$match[3]);
  223. $found=FALSE;
  224. foreach (array_slice($this->blocks,0,-1) as $regex)
  225. if (preg_match($regex,$match[3])) {
  226. $found=TRUE;
  227. break;
  228. }
  229. // List
  230. if ($first) {
  231. // First pass
  232. if (is_numeric($match[1]))
  233. $type='ol';
  234. if (preg_match('/\n{2,}$/',$match[2].
  235. ($found?'':$match[3])))
  236. // Loose structure; Use paragraphs
  237. $tight=FALSE;
  238. $first=FALSE;
  239. }
  240. // Strip leading whitespaces
  241. $ptr+=strlen($match[0]);
  242. $tmp=$this->snip($match[2].$match[3]);
  243. if ($tight) {
  244. if ($found)
  245. $tmp=$match[2].$this->build($this->snip($match[3]));
  246. }
  247. else
  248. $tmp=$this->build($tmp);
  249. $dst.='<li>'.$this->scan(trim($tmp)).'</li>'."\n";
  250. }
  251. }
  252. return strlen($dst)?
  253. ('<'.$type.'>'."\n".$dst.'</'.$type.'>'."\n\n"):'';
  254. }
  255. /**
  256. * Ignore raw HTML
  257. * @return string
  258. * @param $str string
  259. **/
  260. protected function _raw($str) {
  261. return $str;
  262. }
  263. /**
  264. * Process paragraph
  265. * @return string
  266. * @param $str string
  267. **/
  268. protected function _p($str) {
  269. $str=trim($str);
  270. if (strlen($str)) {
  271. if (preg_match('/^(.+?\n)([>#].+)$/s',$str,$parts))
  272. return $this->_p($parts[1]).$this->build($parts[2]);
  273. $str=preg_replace_callback(
  274. '/([^<>\[]+)?(<[\?%].+?[\?%]>|<.+?>|\[.+?\]\s*\(.+?\))|'.
  275. '(.+)/s',
  276. function($expr) {
  277. $tmp='';
  278. if (isset($expr[4]))
  279. $tmp.=$this->esc($expr[4]);
  280. else {
  281. if (isset($expr[1]))
  282. $tmp.=$this->esc($expr[1]);
  283. $tmp.=$expr[2];
  284. if (isset($expr[3]))
  285. $tmp.=$this->esc($expr[3]);
  286. }
  287. return $tmp;
  288. },
  289. $str
  290. );
  291. $str=preg_replace('/\s{2}\r?\n/','<br />',$str);
  292. return '<p>'.$this->scan($str).'</p>'."\n\n";
  293. }
  294. return '';
  295. }
  296. /**
  297. * Process strong/em/strikethrough spans
  298. * @return string
  299. * @param $str string
  300. **/
  301. protected function _text($str) {
  302. $tmp='';
  303. while ($str!=$tmp)
  304. $str=preg_replace_callback(
  305. '/(?<=\s|^)(?<!\\\\)([*_]{1,3})(.*?)(?!\\\\)\1(?=[\s[:punct:]]|$)/',
  306. function($expr) {
  307. switch (strlen($expr[1])) {
  308. case 1:
  309. return '<em>'.$expr[2].'</em>';
  310. case 2:
  311. return '<strong>'.$expr[2].'</strong>';
  312. case 3:
  313. return '<strong><em>'.$expr[2].'</em></strong>';
  314. }
  315. },
  316. preg_replace(
  317. '/(?<!\\\\)~~(.*?)(?!\\\\)~~(?=[\s[:punct:]]|$)/',
  318. '<del>\1</del>',
  319. $tmp=$str
  320. )
  321. );
  322. return $str;
  323. }
  324. /**
  325. * Process image span
  326. * @return string
  327. * @param $str string
  328. **/
  329. protected function _img($str) {
  330. return preg_replace_callback(
  331. '/!(?:\[(.+?)\])?\h*\(<?(.*?)>?(?:\h*"(.*?)"\h*)?\)/',
  332. function($expr) {
  333. return '<img src="'.$expr[2].'"'.
  334. (empty($expr[1])?
  335. '':
  336. (' alt="'.$this->esc($expr[1]).'"')).
  337. (empty($expr[3])?
  338. '':
  339. (' title="'.$this->esc($expr[3]).'"')).' />';
  340. },
  341. $str
  342. );
  343. }
  344. /**
  345. * Process anchor span
  346. * @return string
  347. * @param $str string
  348. **/
  349. protected function _a($str) {
  350. return preg_replace_callback(
  351. '/(?<!\\\\)\[(.+?)(?!\\\\)\]\h*\(<?(.*?)>?(?:\h*"(.*?)"\h*)?\)/',
  352. function($expr) {
  353. return '<a href="'.$this->esc($expr[2]).'"'.
  354. (empty($expr[3])?
  355. '':
  356. (' title="'.$this->esc($expr[3]).'"')).
  357. '>'.$this->scan($expr[1]).'</a>';
  358. },
  359. $str
  360. );
  361. }
  362. /**
  363. * Auto-convert links
  364. * @return string
  365. * @param $str string
  366. **/
  367. protected function _auto($str) {
  368. return preg_replace_callback(
  369. '/`.*?<(.+?)>.*?`|<(.+?)>/',
  370. function($expr) {
  371. if (empty($expr[1]) && parse_url($expr[2],PHP_URL_SCHEME)) {
  372. $expr[2]=$this->esc($expr[2]);
  373. return '<a href="'.$expr[2].'">'.$expr[2].'</a>';
  374. }
  375. return $expr[0];
  376. },
  377. $str
  378. );
  379. }
  380. /**
  381. * Process code span
  382. * @return string
  383. * @param $str string
  384. **/
  385. protected function _code($str) {
  386. return preg_replace_callback(
  387. '/`` (.+?) ``|(?<!\\\\)`(.+?)(?!\\\\)`/',
  388. function($expr) {
  389. return '<code>'.
  390. $this->esc(empty($expr[1])?$expr[2]:$expr[1]).'</code>';
  391. },
  392. $str
  393. );
  394. }
  395. /**
  396. * Convert characters to HTML entities
  397. * @return string
  398. * @param $str string
  399. **/
  400. function esc($str) {
  401. if (!$this->special)
  402. $this->special=[
  403. '...'=>'&hellip;',
  404. '(tm)'=>'&trade;',
  405. '(r)'=>'&reg;',
  406. '(c)'=>'&copy;'
  407. ];
  408. foreach ($this->special as $key=>$val)
  409. $str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str);
  410. return htmlspecialchars($str,ENT_COMPAT,
  411. Base::instance()->ENCODING,FALSE);
  412. }
  413. /**
  414. * Reduce multiple line feeds
  415. * @return string
  416. * @param $str string
  417. **/
  418. protected function snip($str) {
  419. return preg_replace('/(?:(?<=\n)\n+)|\n+$/',"\n",$str);
  420. }
  421. /**
  422. * Scan line for convertible spans
  423. * @return string
  424. * @param $str string
  425. **/
  426. function scan($str) {
  427. $inline=['img','a','text','auto','code'];
  428. foreach ($inline as $func)
  429. $str=$this->{'_'.$func}($str);
  430. return $str;
  431. }
  432. /**
  433. * Assemble blocks
  434. * @return string
  435. * @param $str string
  436. **/
  437. protected function build($str) {
  438. if (!$this->blocks) {
  439. // Regexes for capturing entire blocks
  440. $this->blocks=[
  441. 'blockquote'=>'/^(?:\h?>\h?.*?(?:\n+|$))+/',
  442. 'pre'=>'/^(?:(?: {4}|\t).+?(?:\n+|$))+/',
  443. 'fence'=>'/^`{3}\h*(\w+)?.*?[^\n]*\n+(.+?)`{3}[^\n]*'.
  444. '(?:\n+|$)/s',
  445. 'hr'=>'/^\h*[*_-](?:\h?[\*_-]){2,}\h*(?:\n+|$)/',
  446. 'atx'=>'/^\h*(#{1,6})\h?(.+?)\h*(?:#.*)?(?:\n+|$)/',
  447. 'setext'=>'/^\h*(.+?)\h*\n([=-])+\h*(?:\n+|$)/',
  448. 'li'=>'/^(?:(?:[*+-]|\d+\.)\h.+?(?:\n+|$)'.
  449. '(?:(?: {4}|\t)+.+?(?:\n+|$))*)+/s',
  450. 'raw'=>'/^((?:<!--.+?-->|'.
  451. '<(address|article|aside|audio|blockquote|canvas|dd|'.
  452. 'div|dl|fieldset|figcaption|figure|footer|form|h\d|'.
  453. 'header|hgroup|hr|noscript|object|ol|output|p|pre|'.
  454. 'section|table|tfoot|ul|video).*?'.
  455. '(?:\/>|>(?:(?>[^><]+)|(?R))*<\/\2>))'.
  456. '\h*(?:\n{2,}|\n*$)|<[\?%].+?[\?%]>\h*(?:\n?$|\n*))/s',
  457. 'p'=>'/^(.+?(?:\n{2,}|\n*$))/s'
  458. ];
  459. }
  460. // Treat lines with nothing but whitespaces as empty lines
  461. $str=preg_replace('/\n\h+(?=\n)/',"\n",$str);
  462. // Initialize block parser
  463. $len=strlen($str);
  464. $ptr=0;
  465. $dst='';
  466. // Main loop
  467. while ($ptr<$len) {
  468. if (preg_match('/^ {0,3}\[([^\[\]]+)\]:\s*<?(.*?)>?\s*'.
  469. '(?:"([^\n]*)")?(?:\n+|$)/s',substr($str,$ptr),$match)) {
  470. // Reference-style link; Backtrack
  471. $ptr+=strlen($match[0]);
  472. $tmp='';
  473. // Catch line breaks in title attribute
  474. $ref=preg_replace('/\h/','\s',preg_quote($match[1],'/'));
  475. while ($dst!=$tmp) {
  476. $dst=preg_replace_callback(
  477. '/(?<!\\\\)\[('.$ref.')(?!\\\\)\]\s*\[\]|'.
  478. '(!?)(?:\[([^\[\]]+)\]\s*)?'.
  479. '(?<!\\\\)\[('.$ref.')(?!\\\\)\]/',
  480. function($expr) use($match) {
  481. return (empty($expr[2]))?
  482. // Anchor
  483. ('<a href="'.$this->esc($match[2]).'"'.
  484. (empty($match[3])?
  485. '':
  486. (' title="'.
  487. $this->esc($match[3]).'"')).'>'.
  488. // Link
  489. $this->scan(
  490. empty($expr[3])?
  491. (empty($expr[1])?
  492. $expr[4]:
  493. $expr[1]):
  494. $expr[3]
  495. ).'</a>'):
  496. // Image
  497. ('<img src="'.$match[2].'"'.
  498. (empty($expr[2])?
  499. '':
  500. (' alt="'.
  501. $this->esc($expr[3]).'"')).
  502. (empty($match[3])?
  503. '':
  504. (' title="'.
  505. $this->esc($match[3]).'"')).
  506. ' />');
  507. },
  508. $tmp=$dst
  509. );
  510. }
  511. }
  512. else
  513. foreach ($this->blocks as $func=>$regex)
  514. if (preg_match($regex,substr($str,$ptr),$match)) {
  515. $ptr+=strlen($match[0]);
  516. $dst.=call_user_func_array(
  517. [$this,'_'.$func],
  518. count($match)>1?array_slice($match,1):$match
  519. );
  520. break;
  521. }
  522. }
  523. return $dst;
  524. }
  525. /**
  526. * Render HTML equivalent of markdown
  527. * @return string
  528. * @param $txt string
  529. **/
  530. function convert($txt) {
  531. $txt=preg_replace_callback(
  532. '/(<code.*?>.+?<\/code>|'.
  533. '<[^>\n]+>|\([^\n\)]+\)|"[^"\n]+")|'.
  534. '\\\\(.)/s',
  535. function($expr) {
  536. // Process escaped characters
  537. return empty($expr[1])?$expr[2]:$expr[1];
  538. },
  539. $this->build(preg_replace('/\r\n|\r/',"\n",$txt))
  540. );
  541. return $this->snip($txt);
  542. }
  543. }