Geen omschrijving
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Parser.php 48KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796
  1. <?php
  2. namespace util;
  3. /**
  4. * Parser
  5. *
  6. * @copyright Copyright (c) 2012 SegmentFault Team. (http://segmentfault.com)
  7. * @author Joyqi <joyqi@segmentfault.com>
  8. * @license BSD License
  9. */
  10. class Parser
  11. {
  12. /**
  13. * _whiteList
  14. *
  15. * @var string
  16. */
  17. private $_commonWhiteList = 'kbd|b|i|strong|em|sup|sub|br|code|del|a|hr|small';
  18. /**
  19. * html tags
  20. *
  21. * @var string
  22. */
  23. private $_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';
  24. /**
  25. * _specialWhiteList
  26. *
  27. * @var mixed
  28. * @access private
  29. */
  30. private $_specialWhiteList = array(
  31. 'table' => 'table|tbody|thead|tfoot|tr|td|th',
  32. );
  33. /**
  34. * _footnotes
  35. *
  36. * @var array
  37. */
  38. private $_footnotes;
  39. /**
  40. * @var bool
  41. */
  42. private $_html = false;
  43. /**
  44. * @var bool
  45. */
  46. private $_line = false;
  47. /**
  48. * @var array
  49. */
  50. private $blockParsers = array(
  51. array('code', 10),
  52. array('shtml', 20),
  53. array('pre', 30),
  54. array('ahtml', 40),
  55. array('shr', 50),
  56. array('list', 60),
  57. array('math', 70),
  58. array('html', 80),
  59. array('footnote', 90),
  60. array('definition', 100),
  61. array('quote', 110),
  62. array('table', 120),
  63. array('sh', 130),
  64. array('mh', 140),
  65. array('dhr', 150),
  66. array('default', 9999),
  67. );
  68. /**
  69. * _blocks
  70. *
  71. * @var array
  72. */
  73. private $_blocks;
  74. /**
  75. * _current
  76. *
  77. * @var string
  78. */
  79. private $_current;
  80. /**
  81. * _pos
  82. *
  83. * @var int
  84. */
  85. private $_pos;
  86. /**
  87. * _definitions
  88. *
  89. * @var array
  90. */
  91. private $_definitions;
  92. /**
  93. * @var array
  94. */
  95. private $_hooks = array();
  96. /**
  97. * @var array
  98. */
  99. private $_holders;
  100. /**
  101. * @var string
  102. */
  103. private $_uniqid;
  104. /**
  105. * @var int
  106. */
  107. private $_id;
  108. /**
  109. * @var array
  110. */
  111. private $_parsers = array();
  112. /**
  113. * makeHtml
  114. *
  115. * @param mixed $text
  116. * @return string
  117. */
  118. public function makeHtml($text)
  119. {
  120. $this->_footnotes = array();
  121. $this->_definitions = array();
  122. $this->_holders = array();
  123. $this->_uniqid = md5(uniqid());
  124. $this->_id = 0;
  125. usort($this->blockParsers, function ($a, $b) {
  126. return $a[1] < $b[1] ? -1 : 1;
  127. });
  128. foreach ($this->blockParsers as $parser) {
  129. list($name) = $parser;
  130. if (isset($parser[2])) {
  131. $this->_parsers[$name] = $parser[2];
  132. } else {
  133. $this->_parsers[$name] = array($this, 'parseBlock' . ucfirst($name));
  134. }
  135. }
  136. $text = $this->initText($text);
  137. $html = $this->parse($text);
  138. $html = $this->makeFootnotes($html);
  139. $html = $this->optimizeLines($html);
  140. return $this->call('makeHtml', $html);
  141. }
  142. /**
  143. * @param $html
  144. */
  145. public function enableHtml($html = true)
  146. {
  147. $this->_html = $html;
  148. }
  149. /**
  150. * @param bool $line
  151. */
  152. public function enableLine($line = true)
  153. {
  154. $this->_line = $line;
  155. }
  156. /**
  157. * @param $type
  158. * @param $callback
  159. */
  160. public function hook($type, $callback)
  161. {
  162. $this->_hooks[$type][] = $callback;
  163. }
  164. /**
  165. * @param $str
  166. * @return string
  167. */
  168. public function makeHolder($str)
  169. {
  170. $key = "\r" . $this->_uniqid . $this->_id . "\r";
  171. $this->_id++;
  172. $this->_holders[$key] = $str;
  173. return $key;
  174. }
  175. /**
  176. * @param $text
  177. * @return mixed
  178. */
  179. private function initText($text)
  180. {
  181. $text = str_replace(array("\t", "\r"), array(' ', ''), $text);
  182. return $text;
  183. }
  184. /**
  185. * @param $html
  186. * @return string
  187. */
  188. private function makeFootnotes($html)
  189. {
  190. if (count($this->_footnotes) > 0) {
  191. $html .= '<div class="footnotes"><hr><ol>';
  192. $index = 1;
  193. while ($val = array_shift($this->_footnotes)) {
  194. if (is_string($val)) {
  195. $val .= " <a href=\"#fnref-{$index}\" class=\"footnote-backref\">&#8617;</a>";
  196. } else {
  197. $val[count($val) - 1] .= " <a href=\"#fnref-{$index}\" class=\"footnote-backref\">&#8617;</a>";
  198. $val = count($val) > 1 ? $this->parse(implode("\n", $val)) : $this->parseInline($val[0]);
  199. }
  200. $html .= "<li id=\"fn-{$index}\">{$val}</li>";
  201. $index++;
  202. }
  203. $html .= '</ol></div>';
  204. }
  205. return $html;
  206. }
  207. /**
  208. * parse
  209. *
  210. * @param string $text
  211. * @param bool $inline
  212. * @param int $offset
  213. * @return string
  214. */
  215. private function parse($text, $inline = false, $offset = 0)
  216. {
  217. $blocks = $this->parseBlock($text, $lines);
  218. $html = '';
  219. // inline mode for single normal block
  220. if ($inline && count($blocks) == 1 && $blocks[0][0] == 'normal') {
  221. $blocks[0][3] = true;
  222. }
  223. foreach ($blocks as $block) {
  224. list($type, $start, $end, $value) = $block;
  225. $extract = array_slice($lines, $start, $end - $start + 1);
  226. $method = 'parse' . ucfirst($type);
  227. $extract = $this->call('before' . ucfirst($method), $extract, $value);
  228. $result = $this->{$method}($extract, $value, $start + $offset, $end + $offset);
  229. $result = $this->call('after' . ucfirst($method), $result, $value);
  230. $html .= $result;
  231. }
  232. return $html;
  233. }
  234. /**
  235. * @param $text
  236. * @param $clearHolders
  237. * @return string
  238. */
  239. private function releaseHolder($text, $clearHolders = true)
  240. {
  241. $deep = 0;
  242. while (strpos($text, "\r") !== false && $deep < 10) {
  243. $text = str_replace(array_keys($this->_holders), array_values($this->_holders), $text);
  244. $deep++;
  245. }
  246. if ($clearHolders) {
  247. $this->_holders = array();
  248. }
  249. return $text;
  250. }
  251. /**
  252. * @param $start
  253. * @param int $end
  254. * @return string
  255. */
  256. private function markLine($start, $end = -1)
  257. {
  258. if ($this->_line) {
  259. $end = $end < 0 ? $start : $end;
  260. return '<span class="line" data-start="' . $start
  261. . '" data-end="' . $end . '" data-id="' . $this->_uniqid . '"></span>';
  262. }
  263. return '';
  264. }
  265. /**
  266. * @param array $lines
  267. * @param $start
  268. * @return string[]
  269. */
  270. private function markLines(array $lines, $start)
  271. {
  272. $i = -1;
  273. return $this->_line ? array_map(function ($line) use ($start, &$i) {
  274. $i++;
  275. return $this->markLine($start + $i) . $line;
  276. }, $lines) : $lines;
  277. }
  278. /**
  279. * @param $html
  280. * @return string
  281. */
  282. private function optimizeLines($html)
  283. {
  284. $last = 0;
  285. return $this->_line ?
  286. preg_replace_callback("/class=\"line\" data\-start=\"([0-9]+)\" data\-end=\"([0-9]+)\" (data\-id=\"{$this->_uniqid}\")/",
  287. function ($matches) use (&$last) {
  288. if ($matches[1] != $last) {
  289. $replace = 'class="line" data-start="' . $last . '" data-start-original="' . $matches[1] . '" data-end="' . $matches[2] . '" ' . $matches[3];
  290. } else {
  291. $replace = $matches[0];
  292. }
  293. $last = $matches[2] + 1;
  294. return $replace;
  295. }, $html) : $html;
  296. }
  297. /**
  298. * @param $type
  299. * @param $value
  300. * @return mixed
  301. */
  302. private function call($type, $value)
  303. {
  304. if (empty($this->_hooks[$type])) {
  305. return $value;
  306. }
  307. $args = func_get_args();
  308. $args = array_slice($args, 1);
  309. foreach ($this->_hooks[$type] as $callback) {
  310. $value = call_user_func_array($callback, $args);
  311. $args[0] = $value;
  312. }
  313. return $value;
  314. }
  315. /**
  316. * parseInline
  317. *
  318. * @param string $text
  319. * @param string $whiteList
  320. * @param bool $clearHolders
  321. * @param bool $enableAutoLink
  322. * @return string
  323. */
  324. private function parseInline($text, $whiteList = '', $clearHolders = true, $enableAutoLink = true)
  325. {
  326. $text = $this->call('beforeParseInline', $text);
  327. // code
  328. $text = preg_replace_callback(
  329. "/(^|[^\\\])(`+)(.+?)\\2/",
  330. function ($matches) {
  331. return $matches[1] . $this->makeHolder(
  332. '<code>' . htmlspecialchars($matches[3]) . '</code>'
  333. );
  334. },
  335. $text
  336. );
  337. // mathjax
  338. $text = preg_replace_callback(
  339. "/(^|[^\\\])(\\$+)(.+?)\\2/",
  340. function ($matches) {
  341. return $matches[1] . $this->makeHolder(
  342. $matches[2] . htmlspecialchars($matches[3]) . $matches[2]
  343. );
  344. },
  345. $text
  346. );
  347. // escape
  348. $text = preg_replace_callback(
  349. "/\\\(.)/u",
  350. function ($matches) {
  351. $prefix = preg_match("/^[-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]$/", $matches[1]) ? '' : '\\';
  352. $escaped = htmlspecialchars($matches[1]);
  353. $escaped = str_replace('$', '&dollar;', $escaped);
  354. return $this->makeHolder($prefix . $escaped);
  355. },
  356. $text
  357. );
  358. // link
  359. $text = preg_replace_callback(
  360. "/<(https?:\/\/.+|(?:mailto:)?[_a-z0-9-\.\+]+@[_\w-]+(?:\.[a-z]{2,})+)>/i",
  361. function ($matches) {
  362. $url = $this->cleanUrl($matches[1]);
  363. $link = $this->call('parseLink', $url);
  364. return $this->makeHolder(
  365. "<a href=\"{$url}\">{$link}</a>"
  366. );
  367. },
  368. $text
  369. );
  370. // encode unsafe tags
  371. $text = preg_replace_callback(
  372. "/<(\/?)([a-z0-9-]+)(\s+[^>]*)?>/i",
  373. function ($matches) use ($whiteList) {
  374. if ($this->_html || false !== stripos(
  375. '|' . $this->_commonWhiteList . '|' . $whiteList . '|', '|' . $matches[2] . '|'
  376. )) {
  377. return $this->makeHolder($matches[0]);
  378. } else {
  379. return $this->makeHolder(htmlspecialchars($matches[0]));
  380. }
  381. },
  382. $text
  383. );
  384. if ($this->_html) {
  385. $text = preg_replace_callback("/<!\-\-(.*?)\-\->/", function ($matches) {
  386. return $this->makeHolder($matches[0]);
  387. }, $text);
  388. }
  389. $text = str_replace(array('<', '>'), array('&lt;', '&gt;'), $text);
  390. // footnote
  391. $text = preg_replace_callback(
  392. "/\[\^((?:[^\]]|\\\\\]|\\\\\[)+?)\]/",
  393. function ($matches) {
  394. $id = array_search($matches[1], $this->_footnotes);
  395. if (false === $id) {
  396. $id = count($this->_footnotes) + 1;
  397. $this->_footnotes[$id] = $this->parseInline($matches[1], '', false);
  398. }
  399. return $this->makeHolder(
  400. "<sup id=\"fnref-{$id}\"><a href=\"#fn-{$id}\" class=\"footnote-ref\">{$id}</a></sup>"
  401. );
  402. },
  403. $text
  404. );
  405. // image
  406. $text = preg_replace_callback(
  407. "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/",
  408. function ($matches) {
  409. $escaped = htmlspecialchars($this->escapeBracket($matches[1]));
  410. $url = $this->escapeBracket($matches[2]);
  411. list($url, $title) = $this->cleanUrl($url, true);
  412. $title = empty($title) ? $escaped : " title=\"{$title}\"";
  413. return $this->makeHolder(
  414. "<img src=\"{$url}\" alt=\"{$title}\" title=\"{$title}\">"
  415. );
  416. },
  417. $text
  418. );
  419. $text = preg_replace_callback(
  420. "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/",
  421. function ($matches) {
  422. $escaped = htmlspecialchars($this->escapeBracket($matches[1]));
  423. $result = isset($this->_definitions[$matches[2]]) ?
  424. "<img src=\"{$this->_definitions[$matches[2]]}\" alt=\"{$escaped}\" title=\"{$escaped}\">"
  425. : $escaped;
  426. return $this->makeHolder($result);
  427. },
  428. $text
  429. );
  430. // link
  431. $text = preg_replace_callback(
  432. "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/",
  433. function ($matches) {
  434. $escaped = $this->parseInline(
  435. $this->escapeBracket($matches[1]), '', false, false
  436. );
  437. $url = $this->escapeBracket($matches[2]);
  438. list($url, $title) = $this->cleanUrl($url, true);
  439. $title = empty($title) ? '' : " title=\"{$title}\"";
  440. return $this->makeHolder("<a href=\"{$url}\"{$title}>{$escaped}</a>");
  441. },
  442. $text
  443. );
  444. $text = preg_replace_callback(
  445. "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/",
  446. function ($matches) {
  447. $escaped = $this->parseInline(
  448. $this->escapeBracket($matches[1]), '', false
  449. );
  450. $result = isset($this->_definitions[$matches[2]]) ?
  451. "<a href=\"{$this->_definitions[$matches[2]]}\">{$escaped}</a>"
  452. : $escaped;
  453. return $this->makeHolder($result);
  454. },
  455. $text
  456. );
  457. // strong and em and some fuck
  458. $text = $this->parseInlineCallback($text);
  459. $text = preg_replace(
  460. "/<([_a-z0-9-\.\+]+@[^@]+\.[a-z]{2,})>/i",
  461. "<a href=\"mailto:\\1\">\\1</a>",
  462. $text
  463. );
  464. // autolink url
  465. if ($enableAutoLink) {
  466. $text = preg_replace_callback(
  467. "/(^|[^\"])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\b([-a-zA-Z0-9@:%_\+.~#?&\/=]*)|(?:mailto:)?[_a-z0-9-\.\+]+@[_\w-]+(?:\.[a-z]{2,})+)($|[^\"])/",
  468. function ($matches) {
  469. $url = $this->cleanUrl($matches[2]);
  470. $link = $this->call('parseLink', $matches[2]);
  471. return "{$matches[1]}<a href=\"{$url}\">{$link}</a>{$matches[5]}";
  472. },
  473. $text
  474. );
  475. }
  476. $text = $this->call('afterParseInlineBeforeRelease', $text);
  477. $text = $this->releaseHolder($text, $clearHolders);
  478. $text = $this->call('afterParseInline', $text);
  479. return $text;
  480. }
  481. /**
  482. * @param $text
  483. * @return mixed
  484. */
  485. private function parseInlineCallback($text)
  486. {
  487. $text = preg_replace_callback(
  488. "/(\*{3})(.+?)\\1/",
  489. function ($matches) {
  490. return '<strong><em>' .
  491. $this->parseInlineCallback($matches[2]) .
  492. '</em></strong>';
  493. },
  494. $text
  495. );
  496. $text = preg_replace_callback(
  497. "/(\*{2})(.+?)\\1/",
  498. function ($matches) {
  499. return '<strong>' .
  500. $this->parseInlineCallback($matches[2]) .
  501. '</strong>';
  502. },
  503. $text
  504. );
  505. $text = preg_replace_callback(
  506. "/(\*)(.+?)\\1/",
  507. function ($matches) {
  508. return '<em>' .
  509. $this->parseInlineCallback($matches[2]) .
  510. '</em>';
  511. },
  512. $text
  513. );
  514. $text = preg_replace_callback(
  515. "/(\s+|^)(_{3})(.+?)\\2(\s+|$)/",
  516. function ($matches) {
  517. return $matches[1] . '<strong><em>' .
  518. $this->parseInlineCallback($matches[3]) .
  519. '</em></strong>' . $matches[4];
  520. },
  521. $text
  522. );
  523. $text = preg_replace_callback(
  524. "/(\s+|^)(_{2})(.+?)\\2(\s+|$)/",
  525. function ($matches) {
  526. return $matches[1] . '<strong>' .
  527. $this->parseInlineCallback($matches[3]) .
  528. '</strong>' . $matches[4];
  529. },
  530. $text
  531. );
  532. $text = preg_replace_callback(
  533. "/(\s+|^)(_)(.+?)\\2(\s+|$)/",
  534. function ($matches) {
  535. return $matches[1] . '<em>' .
  536. $this->parseInlineCallback($matches[3]) .
  537. '</em>' . $matches[4];
  538. },
  539. $text
  540. );
  541. $text = preg_replace_callback(
  542. "/(~{2})(.+?)\\1/",
  543. function ($matches) {
  544. return '<del>' .
  545. $this->parseInlineCallback($matches[2]) .
  546. '</del>';
  547. },
  548. $text
  549. );
  550. return $text;
  551. }
  552. /**
  553. * parseBlock
  554. *
  555. * @param string $text
  556. * @param array $lines
  557. * @return array
  558. */
  559. private function parseBlock($text, &$lines)
  560. {
  561. $lines = explode("\n", $text);
  562. $this->_blocks = array();
  563. $this->_current = 'normal';
  564. $this->_pos = -1;
  565. $state = array(
  566. 'special' => implode("|", array_keys($this->_specialWhiteList)),
  567. 'empty' => 0,
  568. 'html' => false,
  569. );
  570. // analyze by line
  571. foreach ($lines as $key => $line) {
  572. $block = $this->getBlock();
  573. $args = array($block, $key, $line, &$state, $lines);
  574. if ($this->_current != 'normal') {
  575. $pass = call_user_func_array($this->_parsers[$this->_current], $args);
  576. if (!$pass) {
  577. continue;
  578. }
  579. }
  580. foreach ($this->_parsers as $name => $parser) {
  581. if ($name != $this->_current) {
  582. $pass = call_user_func_array($parser, $args);
  583. if (!$pass) {
  584. break;
  585. }
  586. }
  587. }
  588. }
  589. return $this->optimizeBlocks($this->_blocks, $lines);
  590. }
  591. /**
  592. * @param $block
  593. * @param $key
  594. * @param $line
  595. * @param $state
  596. * @return bool
  597. */
  598. private function parseBlockList($block, $key, $line, &$state)
  599. {
  600. if ($this->isBlock('list') && !preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line)) {
  601. if (preg_match("/^(\s*)(~{3,}|`{3,})([^`~]*)$/i", $line)) {
  602. // ignore code
  603. return true;
  604. } elseif ($state['empty'] <= 1
  605. && preg_match("/^(\s*)\S+/", $line, $matches)
  606. && strlen($matches[1]) >= ($block[3][0] + $state['empty'])) {
  607. $state['empty'] = 0;
  608. $this->setBlock($key);
  609. return false;
  610. } elseif (preg_match("/^(\s*)$/", $line) && $state['empty'] == 0) {
  611. $state['empty']++;
  612. $this->setBlock($key);
  613. return false;
  614. }
  615. }
  616. if (preg_match("/^(\s*)((?:[0-9]+\.)|\-|\+|\*)\s+/i", $line, $matches)) {
  617. $space = strlen($matches[1]);
  618. $tab = strlen($matches[0]) - $space;
  619. $state['empty'] = 0;
  620. $type = false !== strpos('+-*', $matches[2]) ? 'ul' : 'ol';
  621. // opened
  622. if ($this->isBlock('list')) {
  623. if ($space < $block[3][0] || ($space == $block[3][0] && $type != $block[3][1])) {
  624. $this->startBlock('list', $key, [$space, $type, $tab]);
  625. } else {
  626. $this->setBlock($key);
  627. }
  628. } else {
  629. $this->startBlock('list', $key, [$space, $type, $tab]);
  630. }
  631. return false;
  632. }
  633. return true;
  634. }
  635. /**
  636. * @param $block
  637. * @param $key
  638. * @param $line
  639. * @param $state
  640. * @return bool
  641. */
  642. private function parseBlockCode($block, $key, $line, &$state)
  643. {
  644. if (preg_match("/^(\s*)(~{3,}|`{3,})([^`~]*)$/i", $line, $matches)) {
  645. if ($this->isBlock('code')) {
  646. if ($state['code'] != $matches[2]) {
  647. $this->setBlock($key);
  648. return false;
  649. }
  650. $isAfterList = $block[3][2];
  651. if ($isAfterList) {
  652. $state['empty'] = 0;
  653. $this->combineBlock()
  654. ->setBlock($key);
  655. } else {
  656. $this->setBlock($key)
  657. ->endBlock();
  658. }
  659. } else {
  660. $isAfterList = false;
  661. if ($this->isBlock('list')) {
  662. $space = $block[3][0];
  663. $isAfterList = strlen($matches[1]) >= $space + $state['empty'];
  664. }
  665. $state['code'] = $matches[2];
  666. $this->startBlock('code', $key, array(
  667. $matches[1], $matches[3], $isAfterList,
  668. ));
  669. }
  670. return false;
  671. } elseif ($this->isBlock('code')) {
  672. $this->setBlock($key);
  673. return false;
  674. }
  675. return true;
  676. }
  677. /**
  678. * @param $block
  679. * @param $key
  680. * @param $line
  681. * @param $state
  682. * @return bool
  683. */
  684. private function parseBlockShtml($block, $key, $line, &$state)
  685. {
  686. if ($this->_html) {
  687. if (preg_match("/^(\s*)!!!(\s*)$/", $line, $matches)) {
  688. if ($this->isBlock('shtml')) {
  689. $this->setBlock($key)->endBlock();
  690. } else {
  691. $this->startBlock('shtml', $key);
  692. }
  693. return false;
  694. } elseif ($this->isBlock('shtml')) {
  695. $this->setBlock($key);
  696. return false;
  697. }
  698. }
  699. return true;
  700. }
  701. /**
  702. * @param $block
  703. * @param $key
  704. * @param $line
  705. * @param $state
  706. * @return bool
  707. */
  708. private function parseBlockAhtml($block, $key, $line, &$state)
  709. {
  710. if ($this->_html) {
  711. if (preg_match("/^\s*<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $matches)) {
  712. if ($this->isBlock('ahtml')) {
  713. $this->setBlock($key);
  714. return false;
  715. } elseif (empty($matches[2]) || $matches[2] != '/') {
  716. $this->startBlock('ahtml', $key);
  717. preg_match_all("/<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $allMatches);
  718. $lastMatch = $allMatches[1][count($allMatches[0]) - 1];
  719. if (strpos($line, "</{$lastMatch}>") !== false) {
  720. $this->endBlock();
  721. } else {
  722. $state['html'] = $lastMatch;
  723. }
  724. return false;
  725. }
  726. } elseif (!!$state['html'] && strpos($line, "</{$state['html']}>") !== false) {
  727. $this->setBlock($key)->endBlock();
  728. $state['html'] = false;
  729. return false;
  730. } elseif ($this->isBlock('ahtml')) {
  731. $this->setBlock($key);
  732. return false;
  733. } elseif (preg_match("/^\s*<!\-\-(.*?)\-\->\s*$/", $line, $matches)) {
  734. $this->startBlock('ahtml', $key)->endBlock();
  735. return false;
  736. }
  737. }
  738. return true;
  739. }
  740. /**
  741. * @param $block
  742. * @param $key
  743. * @param $line
  744. * @return bool
  745. */
  746. private function parseBlockMath($block, $key, $line)
  747. {
  748. if (preg_match("/^(\s*)\\$\\$(\s*)$/", $line, $matches)) {
  749. if ($this->isBlock('math')) {
  750. $this->setBlock($key)->endBlock();
  751. } else {
  752. $this->startBlock('math', $key);
  753. }
  754. return false;
  755. } elseif ($this->isBlock('math')) {
  756. $this->setBlock($key);
  757. return false;
  758. }
  759. return true;
  760. }
  761. /**
  762. * @param $block
  763. * @param $key
  764. * @param $line
  765. * @param $state
  766. * @return bool
  767. */
  768. private function parseBlockPre($block, $key, $line, &$state)
  769. {
  770. if (preg_match("/^ {4}/", $line)) {
  771. if ($this->isBlock('pre')) {
  772. $this->setBlock($key);
  773. } else {
  774. $this->startBlock('pre', $key);
  775. }
  776. return false;
  777. } elseif ($this->isBlock('pre') && preg_match("/^\s*$/", $line)) {
  778. $this->setBlock($key);
  779. return false;
  780. }
  781. return true;
  782. }
  783. /**
  784. * @param $block
  785. * @param $key
  786. * @param $line
  787. * @param $state
  788. * @return bool
  789. */
  790. private function parseBlockHtml($block, $key, $line, &$state)
  791. {
  792. if (preg_match("/^\s*<({$state['special']})(\s+[^>]*)?>/i", $line, $matches)) {
  793. $tag = strtolower($matches[1]);
  794. if (!$this->isBlock('html', $tag) && !$this->isBlock('pre')) {
  795. $this->startBlock('html', $key, $tag);
  796. }
  797. return false;
  798. } elseif (preg_match("/<\/({$state['special']})>\s*$/i", $line, $matches)) {
  799. $tag = strtolower($matches[1]);
  800. if ($this->isBlock('html', $tag)) {
  801. $this->setBlock($key)
  802. ->endBlock();
  803. }
  804. return false;
  805. } elseif ($this->isBlock('html')) {
  806. $this->setBlock($key);
  807. return false;
  808. }
  809. return true;
  810. }
  811. /**
  812. * @param $block
  813. * @param $key
  814. * @param $line
  815. * @return bool
  816. */
  817. private function parseBlockFootnote($block, $key, $line)
  818. {
  819. if (preg_match("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", $line, $matches)) {
  820. $space = strlen($matches[0]) - 1;
  821. $this->startBlock('footnote', $key, array(
  822. $space, $matches[1],
  823. ));
  824. return false;
  825. }
  826. return true;
  827. }
  828. /**
  829. * @param $block
  830. * @param $key
  831. * @param $line
  832. * @return bool
  833. */
  834. private function parseBlockDefinition($block, $key, $line)
  835. {
  836. if (preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line, $matches)) {
  837. $this->_definitions[$matches[1]] = $this->cleanUrl($matches[2]);
  838. $this->startBlock('definition', $key)
  839. ->endBlock();
  840. return false;
  841. }
  842. return true;
  843. }
  844. /**
  845. * @param $block
  846. * @param $key
  847. * @param $line
  848. * @return bool
  849. */
  850. private function parseBlockQuote($block, $key, $line)
  851. {
  852. if (preg_match("/^(\s*)>/", $line, $matches)) {
  853. if ($this->isBlock('list') && strlen($matches[1]) > 0) {
  854. $this->setBlock($key);
  855. } elseif ($this->isBlock('quote')) {
  856. $this->setBlock($key);
  857. } else {
  858. $this->startBlock('quote', $key);
  859. }
  860. return false;
  861. }
  862. return true;
  863. }
  864. /**
  865. * @param $block
  866. * @param $key
  867. * @param $line
  868. * @param $state
  869. * @param $lines
  870. * @return bool
  871. */
  872. private function parseBlockTable($block, $key, $line, &$state, $lines)
  873. {
  874. if (preg_match("/^((?:(?:(?:\||\+)(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:[ :]*\-+[ :]*)(?:\||\+)(?:[ :]*\-+[ :]*))|(?:(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:\||\+)(?:[ :]*\-+[ :]*)))+)$/", $line, $matches)) {
  875. if ($this->isBlock('table')) {
  876. $block[3][0][] = $block[3][2];
  877. $block[3][2]++;
  878. $this->setBlock($key, $block[3]);
  879. } else {
  880. $head = 0;
  881. if (empty($block) ||
  882. $block[0] != 'normal' ||
  883. preg_match("/^\s*$/", $lines[$block[2]])) {
  884. $this->startBlock('table', $key);
  885. } else {
  886. $head = 1;
  887. $this->backBlock(1, 'table');
  888. }
  889. if ($matches[1][0] == '|') {
  890. $matches[1] = substr($matches[1], 1);
  891. if ($matches[1][strlen($matches[1]) - 1] == '|') {
  892. $matches[1] = substr($matches[1], 0, -1);
  893. }
  894. }
  895. $rows = preg_split("/(\+|\|)/", $matches[1]);
  896. $aligns = array();
  897. foreach ($rows as $row) {
  898. $align = 'none';
  899. if (preg_match("/^\s*(:?)\-+(:?)\s*$/", $row, $matches)) {
  900. if (!empty($matches[1]) && !empty($matches[2])) {
  901. $align = 'center';
  902. } elseif (!empty($matches[1])) {
  903. $align = 'left';
  904. } elseif (!empty($matches[2])) {
  905. $align = 'right';
  906. }
  907. }
  908. $aligns[] = $align;
  909. }
  910. $this->setBlock($key, array(array($head), $aligns, $head + 1));
  911. }
  912. return false;
  913. }
  914. return true;
  915. }
  916. /**
  917. * @param $block
  918. * @param $key
  919. * @param $line
  920. * @return bool
  921. */
  922. private function parseBlockSh($block, $key, $line)
  923. {
  924. if (preg_match("/^(#+)(.*)$/", $line, $matches)) {
  925. $num = min(strlen($matches[1]), 6);
  926. $this->startBlock('sh', $key, $num)
  927. ->endBlock();
  928. return false;
  929. }
  930. return true;
  931. }
  932. /**
  933. * @param $block
  934. * @param $key
  935. * @param $line
  936. * @param $state
  937. * @param $lines
  938. * @return bool
  939. */
  940. private function parseBlockMh($block, $key, $line, &$state, $lines)
  941. {
  942. if (preg_match("/^\s*((=|-){2,})\s*$/", $line, $matches)
  943. && ($block && $block[0] == "normal" && !preg_match("/^\s*$/", $lines[$block[2]]))) {
  944. // check if last line isn't empty
  945. if ($this->isBlock('normal')) {
  946. $this->backBlock(1, 'mh', $matches[1][0] == '=' ? 1 : 2)
  947. ->setBlock($key)
  948. ->endBlock();
  949. } else {
  950. $this->startBlock('normal', $key);
  951. }
  952. return false;
  953. }
  954. return true;
  955. }
  956. /**
  957. * @param $block
  958. * @param $key
  959. * @param $line
  960. * @return bool
  961. */
  962. private function parseBlockShr($block, $key, $line)
  963. {
  964. if (preg_match("/^(\* *){3,}\s*$/", $line)) {
  965. $this->startBlock('hr', $key)
  966. ->endBlock();
  967. return false;
  968. }
  969. return true;
  970. }
  971. /**
  972. * @param $block
  973. * @param $key
  974. * @param $line
  975. * @return bool
  976. */
  977. private function parseBlockDhr($block, $key, $line)
  978. {
  979. if (preg_match("/^(- *){3,}\s*$/", $line)) {
  980. $this->startBlock('hr', $key)
  981. ->endBlock();
  982. return false;
  983. }
  984. return true;
  985. }
  986. /**
  987. * @param $block
  988. * @param $key
  989. * @param $line
  990. * @param $state
  991. * @return bool
  992. */
  993. private function parseBlockDefault($block, $key, $line, &$state)
  994. {
  995. if ($this->isBlock('footnote')) {
  996. preg_match("/^(\s*)/", $line, $matches);
  997. if (strlen($matches[1]) >= $block[3][0]) {
  998. $this->setBlock($key);
  999. } else {
  1000. $this->startBlock('normal', $key);
  1001. }
  1002. } elseif ($this->isBlock('table')) {
  1003. if (false !== strpos($line, '|')) {
  1004. $block[3][2]++;
  1005. $this->setBlock($key, $block[3]);
  1006. } else {
  1007. $this->startBlock('normal', $key);
  1008. }
  1009. } elseif ($this->isBlock('quote')) {
  1010. if (!preg_match("/^(\s*)$/", $line)) {
  1011. // empty line
  1012. $this->setBlock($key);
  1013. } else {
  1014. $this->startBlock('normal', $key);
  1015. }
  1016. } else {
  1017. if (empty($block) || $block[0] != 'normal') {
  1018. $this->startBlock('normal', $key);
  1019. } else {
  1020. $this->setBlock($key);
  1021. }
  1022. }
  1023. return true;
  1024. }
  1025. /**
  1026. * @param array $blocks
  1027. * @param array $lines
  1028. * @return array
  1029. */
  1030. private function optimizeBlocks(array $blocks, array $lines)
  1031. {
  1032. $blocks = $this->call('beforeOptimizeBlocks', $blocks, $lines);
  1033. $key = 0;
  1034. while (isset($blocks[$key])) {
  1035. $moved = false;
  1036. $block = &$blocks[$key];
  1037. $prevBlock = isset($blocks[$key - 1]) ? $blocks[$key - 1] : null;
  1038. $nextBlock = isset($blocks[$key + 1]) ? $blocks[$key + 1] : null;
  1039. list($type, $from, $to) = $block;
  1040. if ('pre' == $type) {
  1041. $isEmpty = array_reduce(
  1042. array_slice($lines, $block[1], $block[2] - $block[1] + 1),
  1043. function ($result, $line) {
  1044. return preg_match("/^\s*$/", $line) && $result;
  1045. },
  1046. true
  1047. );
  1048. if ($isEmpty) {
  1049. $block[0] = $type = 'normal';
  1050. }
  1051. }
  1052. if ('normal' == $type) {
  1053. // combine two blocks
  1054. $types = array('list', 'quote');
  1055. if ($from == $to && preg_match("/^\s*$/", $lines[$from])
  1056. && !empty($prevBlock) && !empty($nextBlock)) {
  1057. if ($prevBlock[0] == $nextBlock[0] && in_array($prevBlock[0], $types)
  1058. && ($prevBlock[0] != 'list'
  1059. || ($prevBlock[3][0] == $nextBlock[3][0] && $prevBlock[3][1] == $nextBlock[3][1]))) {
  1060. // combine 3 blocks
  1061. $blocks[$key - 1] = array(
  1062. $prevBlock[0], $prevBlock[1], $nextBlock[2], $prevBlock[3] ?? null,
  1063. );
  1064. array_splice($blocks, $key, 2);
  1065. // do not move
  1066. $moved = true;
  1067. }
  1068. }
  1069. }
  1070. if (!$moved) {
  1071. $key++;
  1072. }
  1073. }
  1074. return $this->call('afterOptimizeBlocks', $blocks, $lines);
  1075. }
  1076. /**
  1077. * parseCode
  1078. *
  1079. * @param array $lines
  1080. * @param array $parts
  1081. * @param int $start
  1082. * @return string
  1083. */
  1084. private function parseCode(array $lines, array $parts, $start)
  1085. {
  1086. list($blank, $lang) = $parts;
  1087. $lang = trim($lang);
  1088. $count = strlen($blank);
  1089. if (!preg_match("/^[_a-z0-9-\+\#\:\.]+$/i", $lang)) {
  1090. $lang = null;
  1091. } else {
  1092. $parts = explode(':', $lang);
  1093. if (count($parts) > 1) {
  1094. list($lang, $rel) = $parts;
  1095. $lang = trim($lang);
  1096. $rel = trim($rel);
  1097. }
  1098. }
  1099. $isEmpty = true;
  1100. $lines = array_map(function ($line) use ($count, &$isEmpty) {
  1101. $line = preg_replace("/^[ ]{{$count}}/", '', $line);
  1102. if ($isEmpty && !preg_match("/^\s*$/", $line)) {
  1103. $isEmpty = false;
  1104. }
  1105. return htmlspecialchars($line);
  1106. }, array_slice($lines, 1, -1));
  1107. $str = implode("\n", $this->markLines($lines, $start + 1));
  1108. return $isEmpty ? '' :
  1109. '<pre><code' . (!empty($lang) ? " class=\"{$lang}\"" : '')
  1110. . (!empty($rel) ? " rel=\"{$rel}\"" : '') . '>'
  1111. . $str . '</code></pre>';
  1112. }
  1113. /**
  1114. * parsePre
  1115. *
  1116. * @param array $lines
  1117. * @param mixed $value
  1118. * @param int $start
  1119. * @return string
  1120. */
  1121. private function parsePre(array $lines, $value, $start)
  1122. {
  1123. foreach ($lines as &$line) {
  1124. $line = htmlspecialchars(substr($line, 4));
  1125. }
  1126. $str = implode("\n", $this->markLines($lines, $start));
  1127. return preg_match("/^\s*$/", $str) ? '' : '<pre><code>' . $str . '</code></pre>';
  1128. }
  1129. /**
  1130. * parseAhtml
  1131. *
  1132. * @param array $lines
  1133. * @param mixed $value
  1134. * @param int $start
  1135. * @return string
  1136. */
  1137. private function parseAhtml(array $lines, $value, $start)
  1138. {
  1139. return trim(implode("\n", $this->markLines($lines, $start)));
  1140. }
  1141. /**
  1142. * parseShtml
  1143. *
  1144. * @param array $lines
  1145. * @param mixed $value
  1146. * @param int $start
  1147. * @return string
  1148. */
  1149. private function parseShtml(array $lines, $value, $start)
  1150. {
  1151. return trim(implode("\n", $this->markLines(array_slice($lines, 1, -1), $start + 1)));
  1152. }
  1153. /**
  1154. * parseMath
  1155. *
  1156. * @param array $lines
  1157. * @param mixed $value
  1158. * @param int $start
  1159. * @param int $end
  1160. * @return string
  1161. */
  1162. private function parseMath(array $lines, $value, $start, $end)
  1163. {
  1164. return '<p>' . $this->markLine($start, $end) . htmlspecialchars(implode("\n", $lines)) . '</p>';
  1165. }
  1166. /**
  1167. * parseSh
  1168. *
  1169. * @param array $lines
  1170. * @param int $num
  1171. * @param int $start
  1172. * @param int $end
  1173. * @return string
  1174. */
  1175. private function parseSh(array $lines, $num, $start, $end)
  1176. {
  1177. $line = $this->markLine($start, $end) . $this->parseInline(trim($lines[0], '# '));
  1178. return preg_match("/^\s*$/", $line) ? '' : "<h{$num}>{$line}</h{$num}>";
  1179. }
  1180. /**
  1181. * parseMh
  1182. *
  1183. * @param array $lines
  1184. * @param int $num
  1185. * @param int $start
  1186. * @param int $end
  1187. * @return string
  1188. */
  1189. private function parseMh(array $lines, $num, $start, $end)
  1190. {
  1191. return $this->parseSh($lines, $num, $start, $end);
  1192. }
  1193. /**
  1194. * parseQuote
  1195. *
  1196. * @param array $lines
  1197. * @param mixed $value
  1198. * @param int $start
  1199. * @return string
  1200. */
  1201. private function parseQuote(array $lines, $value, $start)
  1202. {
  1203. foreach ($lines as &$line) {
  1204. $line = preg_replace("/^\s*> ?/", '', $line);
  1205. }
  1206. $str = implode("\n", $lines);
  1207. return preg_match("/^\s*$/", $str) ? '' : '<blockquote>' . $this->parse($str, true, $start) . '</blockquote>';
  1208. }
  1209. /**
  1210. * parseList
  1211. *
  1212. * @param array $lines
  1213. * @param mixed $value
  1214. * @param int $start
  1215. * @return string
  1216. */
  1217. private function parseList(array $lines, $value, $start)
  1218. {
  1219. $html = '';
  1220. list($space, $type, $tab) = $value;
  1221. $rows = array();
  1222. $suffix = '';
  1223. $last = 0;
  1224. foreach ($lines as $key => $line) {
  1225. if (preg_match("/^(\s{" . $space . "})((?:[0-9]+\.?)|\-|\+|\*)(\s+)(.*)$/i", $line, $matches)) {
  1226. if ($type == 'ol' && $key == 0) {
  1227. $start = intval($matches[2]);
  1228. if ($start != 1) {
  1229. $suffix = ' start="' . $start . '"';
  1230. }
  1231. }
  1232. $rows[] = [$matches[4]];
  1233. $last = count($rows) - 1;
  1234. } else {
  1235. $rows[$last][] = preg_replace("/^\s{" . ($tab + $space) . "}/", '', $line);
  1236. }
  1237. }
  1238. foreach ($rows as $row) {
  1239. $html .= "<li>" . $this->parse(implode("\n", $row), true, $start) . "</li>";
  1240. $start += count($row);
  1241. }
  1242. return "<{$type}{$suffix}>{$html}</{$type}>";
  1243. }
  1244. /**
  1245. * @param array $lines
  1246. * @param array $value
  1247. * @param int $start
  1248. * @return string
  1249. */
  1250. private function parseTable(array $lines, array $value, $start)
  1251. {
  1252. list($ignores, $aligns) = $value;
  1253. $head = count($ignores) > 0 && array_sum($ignores) > 0;
  1254. $html = '<table>';
  1255. $body = $head ? null : true;
  1256. $output = false;
  1257. foreach ($lines as $key => $line) {
  1258. if (in_array($key, $ignores)) {
  1259. if ($head && $output) {
  1260. $head = false;
  1261. $body = true;
  1262. }
  1263. continue;
  1264. }
  1265. $line = trim($line);
  1266. $output = true;
  1267. if ($line[0] == '|') {
  1268. $line = substr($line, 1);
  1269. if ($line[strlen($line) - 1] == '|') {
  1270. $line = substr($line, 0, -1);
  1271. }
  1272. }
  1273. $rows = array_map(function ($row) {
  1274. if (preg_match("/^\s*$/", $row)) {
  1275. return ' ';
  1276. } else {
  1277. return trim($row);
  1278. }
  1279. }, explode('|', $line));
  1280. $columns = array();
  1281. $last = -1;
  1282. foreach ($rows as $row) {
  1283. if (strlen($row) > 0) {
  1284. $last++;
  1285. $columns[$last] = array(
  1286. isset($columns[$last]) ? $columns[$last][0] + 1 : 1, $row,
  1287. );
  1288. } elseif (isset($columns[$last])) {
  1289. $columns[$last][0]++;
  1290. } else {
  1291. $columns[0] = array(1, $row);
  1292. }
  1293. }
  1294. if ($head) {
  1295. $html .= '<thead>';
  1296. } elseif ($body) {
  1297. $html .= '<tbody>';
  1298. }
  1299. $html .= '<tr' . ($this->_line ? ' class="line" data-start="'
  1300. . ($start + $key) . '" data-end="' . ($start + $key)
  1301. . '" data-id="' . $this->_uniqid . '"' : '') . '>';
  1302. foreach ($columns as $key => $column) {
  1303. list($num, $text) = $column;
  1304. $tag = $head ? 'th' : 'td';
  1305. $html .= "<{$tag}";
  1306. if ($num > 1) {
  1307. $html .= " colspan=\"{$num}\"";
  1308. }
  1309. if (isset($aligns[$key]) && $aligns[$key] != 'none') {
  1310. $html .= " align=\"{$aligns[$key]}\"";
  1311. }
  1312. $html .= '>' . $this->parseInline($text) . "</{$tag}>";
  1313. }
  1314. $html .= '</tr>';
  1315. if ($head) {
  1316. $html .= '</thead>';
  1317. } elseif ($body) {
  1318. $body = false;
  1319. }
  1320. }
  1321. if ($body !== null) {
  1322. $html .= '</tbody>';
  1323. }
  1324. $html .= '</table>';
  1325. return $html;
  1326. }
  1327. /**
  1328. * parseHr
  1329. *
  1330. * @param array $lines
  1331. * @param array $value
  1332. * @param int $start
  1333. * @return string
  1334. */
  1335. private function parseHr($lines, $value, $start)
  1336. {
  1337. return $this->_line ? '<hr class="line" data-start="' . $start . '" data-end="' . $start . '">' : '<hr>';
  1338. }
  1339. /**
  1340. * parseNormal
  1341. *
  1342. * @param array $lines
  1343. * @param bool $inline
  1344. * @param int $start
  1345. * @return string
  1346. */
  1347. private function parseNormal(array $lines, $inline, $start)
  1348. {
  1349. foreach ($lines as $key => &$line) {
  1350. $line = $this->parseInline($line);
  1351. if (!preg_match("/^\s*$/", $line)) {
  1352. $line = $this->markLine($start + $key) . $line;
  1353. }
  1354. }
  1355. $str = trim(implode("\n", $lines));
  1356. $str = preg_replace_callback("/(\n\s*){2,}/", function () use (&$inline) {
  1357. $inline = false;
  1358. return "</p><p>";
  1359. }, $str);
  1360. $str = preg_replace("/\n/", "<br>", $str);
  1361. return preg_match("/^\s*$/", $str) ? '' : ($inline ? $str : "<p>{$str}</p>");
  1362. }
  1363. /**
  1364. * parseFootnote
  1365. *
  1366. * @param array $lines
  1367. * @param array $value
  1368. * @return string
  1369. */
  1370. private function parseFootnote(array $lines, array $value)
  1371. {
  1372. list($space, $note) = $value;
  1373. $index = array_search($note, $this->_footnotes);
  1374. if (false !== $index) {
  1375. $lines[0] = preg_replace("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", '', $lines[0]);
  1376. $this->_footnotes[$index] = $lines;
  1377. }
  1378. return '';
  1379. }
  1380. /**
  1381. * parseDefine
  1382. *
  1383. * @return string
  1384. */
  1385. private function parseDefinition()
  1386. {
  1387. return '';
  1388. }
  1389. /**
  1390. * parseHtml
  1391. *
  1392. * @param array $lines
  1393. * @param string $type
  1394. * @param int $start
  1395. * @return string
  1396. */
  1397. private function parseHtml(array $lines, $type, $start)
  1398. {
  1399. foreach ($lines as &$line) {
  1400. $line = $this->parseInline($line,
  1401. isset($this->_specialWhiteList[$type]) ? $this->_specialWhiteList[$type] : '');
  1402. }
  1403. return implode("\n", $this->markLines($lines, $start));
  1404. }
  1405. /**
  1406. * @param $url
  1407. * @param bool $parseTitle
  1408. *
  1409. * @return mixed
  1410. */
  1411. private function cleanUrl($url, $parseTitle = false)
  1412. {
  1413. $title = null;
  1414. $url = trim($url);
  1415. if ($parseTitle) {
  1416. $pos = strpos($url, ' ');
  1417. if ($pos !== false) {
  1418. $title = htmlspecialchars(trim(substr($url, $pos + 1), ' "\''));
  1419. $url = substr($url, 0, $pos);
  1420. }
  1421. }
  1422. $url = preg_replace("/[\"'<>\s]/", '', $url);
  1423. if (preg_match("/^(mailto:)?[_a-z0-9-\.\+]+@[_\w-]+(?:\.[a-z]{2,})+$/i", $url, $matches)) {
  1424. if (empty($matches[1])) {
  1425. $url = 'mailto:' . $url;
  1426. }
  1427. }
  1428. if (preg_match("/^\w+:/i", $url) && !preg_match("/^(https?|mailto):/i", $url)) {
  1429. return '#';
  1430. }
  1431. return $parseTitle ? [$url, $title] : $url;
  1432. }
  1433. /**
  1434. * @param $str
  1435. * @return mixed
  1436. */
  1437. private function escapeBracket($str)
  1438. {
  1439. return str_replace(
  1440. array('\[', '\]', '\(', '\)'), array('[', ']', '(', ')'), $str
  1441. );
  1442. }
  1443. /**
  1444. * startBlock
  1445. *
  1446. * @param mixed $type
  1447. * @param mixed $start
  1448. * @param mixed $value
  1449. * @return $this
  1450. */
  1451. private function startBlock($type, $start, $value = null)
  1452. {
  1453. $this->_pos++;
  1454. $this->_current = $type;
  1455. $this->_blocks[$this->_pos] = array($type, $start, $start, $value);
  1456. return $this;
  1457. }
  1458. /**
  1459. * endBlock
  1460. *
  1461. * @return $this
  1462. */
  1463. private function endBlock()
  1464. {
  1465. $this->_current = 'normal';
  1466. return $this;
  1467. }
  1468. /**
  1469. * isBlock
  1470. *
  1471. * @param mixed $type
  1472. * @param mixed $value
  1473. * @return bool
  1474. */
  1475. private function isBlock($type, $value = null)
  1476. {
  1477. return $this->_current == $type
  1478. && (null === $value ? true : $this->_blocks[$this->_pos][3] == $value);
  1479. }
  1480. /**
  1481. * getBlock
  1482. *
  1483. * @return array
  1484. */
  1485. private function getBlock()
  1486. {
  1487. return isset($this->_blocks[$this->_pos]) ? $this->_blocks[$this->_pos] : null;
  1488. }
  1489. /**
  1490. * setBlock
  1491. *
  1492. * @param mixed $to
  1493. * @param mixed $value
  1494. * @return $this
  1495. */
  1496. private function setBlock($to = null, $value = null)
  1497. {
  1498. if (null !== $to) {
  1499. $this->_blocks[$this->_pos][2] = $to;
  1500. }
  1501. if (null !== $value) {
  1502. $this->_blocks[$this->_pos][3] = $value;
  1503. }
  1504. return $this;
  1505. }
  1506. /**
  1507. * backBlock
  1508. *
  1509. * @param mixed $step
  1510. * @param mixed $type
  1511. * @param mixed $value
  1512. * @return $this
  1513. */
  1514. private function backBlock($step, $type, $value = null)
  1515. {
  1516. if ($this->_pos < 0) {
  1517. return $this->startBlock($type, 0, $value);
  1518. }
  1519. $last = $this->_blocks[$this->_pos][2];
  1520. $this->_blocks[$this->_pos][2] = $last - $step;
  1521. if ($this->_blocks[$this->_pos][1] <= $this->_blocks[$this->_pos][2]) {
  1522. $this->_pos++;
  1523. }
  1524. $this->_current = $type;
  1525. $this->_blocks[$this->_pos] = array(
  1526. $type, $last - $step + 1, $last, $value,
  1527. );
  1528. return $this;
  1529. }
  1530. /**
  1531. * @return $this
  1532. */
  1533. private function combineBlock()
  1534. {
  1535. if ($this->_pos < 1) {
  1536. return $this;
  1537. }
  1538. $prev = $this->_blocks[$this->_pos - 1];
  1539. $current = $this->_blocks[$this->_pos];
  1540. $prev[2] = $current[2];
  1541. $this->_blocks[$this->_pos - 1] = $prev;
  1542. $this->_current = $prev[0];
  1543. unset($this->_blocks[$this->_pos]);
  1544. $this->_pos--;
  1545. return $this;
  1546. }
  1547. }