HyperDown.php 50 KB

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