Parsedown.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554
  1. <?php
  2. /**
  3. * @package Grav\Framework\Parsedown
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Framework\Parsedown;
  9. /*
  10. * Parsedown
  11. * http://parsedown.org
  12. *
  13. * (c) Emanuil Rusev
  14. * http://erusev.com
  15. *
  16. * This file ported from officiall Parsedown repo and kept for compatibility.
  17. */
  18. class Parsedown
  19. {
  20. # ~
  21. const version = '1.6.0';
  22. # ~
  23. function text($text)
  24. {
  25. # make sure no definitions are set
  26. $this->DefinitionData = array();
  27. # standardize line breaks
  28. $text = str_replace(array("\r\n", "\r"), "\n", $text);
  29. # remove surrounding line breaks
  30. $text = trim($text, "\n");
  31. # split text into lines
  32. $lines = explode("\n", $text);
  33. # iterate through lines to identify blocks
  34. $markup = $this->lines($lines);
  35. # trim line breaks
  36. $markup = trim($markup, "\n");
  37. return $markup;
  38. }
  39. #
  40. # Setters
  41. #
  42. function setBreaksEnabled($breaksEnabled)
  43. {
  44. $this->breaksEnabled = $breaksEnabled;
  45. return $this;
  46. }
  47. protected $breaksEnabled;
  48. function setMarkupEscaped($markupEscaped)
  49. {
  50. $this->markupEscaped = $markupEscaped;
  51. return $this;
  52. }
  53. protected $markupEscaped;
  54. function setUrlsLinked($urlsLinked)
  55. {
  56. $this->urlsLinked = $urlsLinked;
  57. return $this;
  58. }
  59. protected $urlsLinked = true;
  60. #
  61. # Lines
  62. #
  63. protected $BlockTypes = array(
  64. '#' => array('Header'),
  65. '*' => array('Rule', 'List'),
  66. '+' => array('List'),
  67. '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
  68. '0' => array('List'),
  69. '1' => array('List'),
  70. '2' => array('List'),
  71. '3' => array('List'),
  72. '4' => array('List'),
  73. '5' => array('List'),
  74. '6' => array('List'),
  75. '7' => array('List'),
  76. '8' => array('List'),
  77. '9' => array('List'),
  78. ':' => array('Table'),
  79. '<' => array('Comment', 'Markup'),
  80. '=' => array('SetextHeader'),
  81. '>' => array('Quote'),
  82. '[' => array('Reference'),
  83. '_' => array('Rule'),
  84. '`' => array('FencedCode'),
  85. '|' => array('Table'),
  86. '~' => array('FencedCode'),
  87. );
  88. # ~
  89. protected $unmarkedBlockTypes = array(
  90. 'Code',
  91. );
  92. #
  93. # Blocks
  94. #
  95. protected function lines(array $lines)
  96. {
  97. $CurrentBlock = null;
  98. foreach ($lines as $line)
  99. {
  100. if (chop($line) === '')
  101. {
  102. if (isset($CurrentBlock))
  103. {
  104. $CurrentBlock['interrupted'] = true;
  105. }
  106. continue;
  107. }
  108. if (strpos($line, "\t") !== false)
  109. {
  110. $parts = explode("\t", $line);
  111. $line = $parts[0];
  112. unset($parts[0]);
  113. foreach ($parts as $part)
  114. {
  115. $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
  116. $line .= str_repeat(' ', $shortage);
  117. $line .= $part;
  118. }
  119. }
  120. $indent = 0;
  121. while (isset($line[$indent]) and $line[$indent] === ' ')
  122. {
  123. $indent ++;
  124. }
  125. $text = $indent > 0 ? substr($line, $indent) : $line;
  126. # ~
  127. $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
  128. # ~
  129. if (isset($CurrentBlock['continuable']))
  130. {
  131. $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
  132. if (isset($Block))
  133. {
  134. $CurrentBlock = $Block;
  135. continue;
  136. }
  137. else
  138. {
  139. if ($this->isBlockCompletable($CurrentBlock['type']))
  140. {
  141. $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
  142. }
  143. }
  144. }
  145. # ~
  146. $marker = $text[0];
  147. # ~
  148. $blockTypes = $this->unmarkedBlockTypes;
  149. if (isset($this->BlockTypes[$marker]))
  150. {
  151. foreach ($this->BlockTypes[$marker] as $blockType)
  152. {
  153. $blockTypes []= $blockType;
  154. }
  155. }
  156. #
  157. # ~
  158. foreach ($blockTypes as $blockType)
  159. {
  160. $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
  161. if (isset($Block))
  162. {
  163. $Block['type'] = $blockType;
  164. if ( ! isset($Block['identified']))
  165. {
  166. $Blocks []= $CurrentBlock;
  167. $Block['identified'] = true;
  168. }
  169. if ($this->isBlockContinuable($blockType))
  170. {
  171. $Block['continuable'] = true;
  172. }
  173. $CurrentBlock = $Block;
  174. continue 2;
  175. }
  176. }
  177. # ~
  178. if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
  179. {
  180. $CurrentBlock['element']['text'] .= "\n".$text;
  181. }
  182. else
  183. {
  184. $Blocks []= $CurrentBlock;
  185. $CurrentBlock = $this->paragraph($Line);
  186. $CurrentBlock['identified'] = true;
  187. }
  188. }
  189. # ~
  190. if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
  191. {
  192. $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
  193. }
  194. # ~
  195. $Blocks []= $CurrentBlock;
  196. unset($Blocks[0]);
  197. # ~
  198. $markup = '';
  199. foreach ($Blocks as $Block)
  200. {
  201. if (isset($Block['hidden']))
  202. {
  203. continue;
  204. }
  205. $markup .= "\n";
  206. $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
  207. }
  208. $markup .= "\n";
  209. # ~
  210. return $markup;
  211. }
  212. protected function isBlockContinuable($Type)
  213. {
  214. return method_exists($this, 'block'.$Type.'Continue');
  215. }
  216. protected function isBlockCompletable($Type)
  217. {
  218. return method_exists($this, 'block'.$Type.'Complete');
  219. }
  220. #
  221. # Code
  222. protected function blockCode($Line, $Block = null)
  223. {
  224. if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
  225. {
  226. return;
  227. }
  228. if ($Line['indent'] >= 4)
  229. {
  230. $text = substr($Line['body'], 4);
  231. $Block = array(
  232. 'element' => array(
  233. 'name' => 'pre',
  234. 'handler' => 'element',
  235. 'text' => array(
  236. 'name' => 'code',
  237. 'text' => $text,
  238. ),
  239. ),
  240. );
  241. return $Block;
  242. }
  243. }
  244. protected function blockCodeContinue($Line, $Block)
  245. {
  246. if ($Line['indent'] >= 4)
  247. {
  248. if (isset($Block['interrupted']))
  249. {
  250. $Block['element']['text']['text'] .= "\n";
  251. unset($Block['interrupted']);
  252. }
  253. $Block['element']['text']['text'] .= "\n";
  254. $text = substr($Line['body'], 4);
  255. $Block['element']['text']['text'] .= $text;
  256. return $Block;
  257. }
  258. }
  259. protected function blockCodeComplete($Block)
  260. {
  261. $text = $Block['element']['text']['text'];
  262. $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
  263. $Block['element']['text']['text'] = $text;
  264. return $Block;
  265. }
  266. #
  267. # Comment
  268. protected function blockComment($Line)
  269. {
  270. if ($this->markupEscaped)
  271. {
  272. return;
  273. }
  274. if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
  275. {
  276. $Block = array(
  277. 'markup' => $Line['body'],
  278. );
  279. if (preg_match('/-->$/', $Line['text']))
  280. {
  281. $Block['closed'] = true;
  282. }
  283. return $Block;
  284. }
  285. }
  286. protected function blockCommentContinue($Line, array $Block)
  287. {
  288. if (isset($Block['closed']))
  289. {
  290. return;
  291. }
  292. $Block['markup'] .= "\n" . $Line['body'];
  293. if (preg_match('/-->$/', $Line['text']))
  294. {
  295. $Block['closed'] = true;
  296. }
  297. return $Block;
  298. }
  299. #
  300. # Fenced Code
  301. protected function blockFencedCode($Line)
  302. {
  303. if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
  304. {
  305. $Element = array(
  306. 'name' => 'code',
  307. 'text' => '',
  308. );
  309. if (isset($matches[1]))
  310. {
  311. $class = 'language-'.$matches[1];
  312. $Element['attributes'] = array(
  313. 'class' => $class,
  314. );
  315. }
  316. $Block = array(
  317. 'char' => $Line['text'][0],
  318. 'element' => array(
  319. 'name' => 'pre',
  320. 'handler' => 'element',
  321. 'text' => $Element,
  322. ),
  323. );
  324. return $Block;
  325. }
  326. }
  327. protected function blockFencedCodeContinue($Line, $Block)
  328. {
  329. if (isset($Block['complete']))
  330. {
  331. return;
  332. }
  333. if (isset($Block['interrupted']))
  334. {
  335. $Block['element']['text']['text'] .= "\n";
  336. unset($Block['interrupted']);
  337. }
  338. if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
  339. {
  340. $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
  341. $Block['complete'] = true;
  342. return $Block;
  343. }
  344. $Block['element']['text']['text'] .= "\n".$Line['body'];
  345. return $Block;
  346. }
  347. protected function blockFencedCodeComplete($Block)
  348. {
  349. $text = $Block['element']['text']['text'];
  350. $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
  351. $Block['element']['text']['text'] = $text;
  352. return $Block;
  353. }
  354. #
  355. # Header
  356. protected function blockHeader($Line)
  357. {
  358. if (isset($Line['text'][1]))
  359. {
  360. $level = 1;
  361. while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
  362. {
  363. $level ++;
  364. }
  365. if ($level > 6)
  366. {
  367. return;
  368. }
  369. $text = trim($Line['text'], '# ');
  370. $Block = array(
  371. 'element' => array(
  372. 'name' => 'h' . min(6, $level),
  373. 'text' => $text,
  374. 'handler' => 'line',
  375. ),
  376. );
  377. return $Block;
  378. }
  379. }
  380. #
  381. # List
  382. protected function blockList($Line)
  383. {
  384. list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
  385. if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
  386. {
  387. $Block = array(
  388. 'indent' => $Line['indent'],
  389. 'pattern' => $pattern,
  390. 'element' => array(
  391. 'name' => $name,
  392. 'handler' => 'elements',
  393. ),
  394. );
  395. if($name === 'ol')
  396. {
  397. $listStart = stristr($matches[0], '.', true);
  398. if($listStart !== '1')
  399. {
  400. $Block['element']['attributes'] = array('start' => $listStart);
  401. }
  402. }
  403. $Block['li'] = array(
  404. 'name' => 'li',
  405. 'handler' => 'li',
  406. 'text' => array(
  407. $matches[2],
  408. ),
  409. );
  410. $Block['element']['text'] []= & $Block['li'];
  411. return $Block;
  412. }
  413. }
  414. protected function blockListContinue($Line, array $Block)
  415. {
  416. if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
  417. {
  418. if (isset($Block['interrupted']))
  419. {
  420. $Block['li']['text'] []= '';
  421. unset($Block['interrupted']);
  422. }
  423. unset($Block['li']);
  424. $text = isset($matches[1]) ? $matches[1] : '';
  425. $Block['li'] = array(
  426. 'name' => 'li',
  427. 'handler' => 'li',
  428. 'text' => array(
  429. $text,
  430. ),
  431. );
  432. $Block['element']['text'] []= & $Block['li'];
  433. return $Block;
  434. }
  435. if ($Line['text'][0] === '[' and $this->blockReference($Line))
  436. {
  437. return $Block;
  438. }
  439. if ( ! isset($Block['interrupted']))
  440. {
  441. $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
  442. $Block['li']['text'] []= $text;
  443. return $Block;
  444. }
  445. if ($Line['indent'] > 0)
  446. {
  447. $Block['li']['text'] []= '';
  448. $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
  449. $Block['li']['text'] []= $text;
  450. unset($Block['interrupted']);
  451. return $Block;
  452. }
  453. }
  454. #
  455. # Quote
  456. protected function blockQuote($Line)
  457. {
  458. if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
  459. {
  460. $Block = array(
  461. 'element' => array(
  462. 'name' => 'blockquote',
  463. 'handler' => 'lines',
  464. 'text' => (array) $matches[1],
  465. ),
  466. );
  467. return $Block;
  468. }
  469. }
  470. protected function blockQuoteContinue($Line, array $Block)
  471. {
  472. if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
  473. {
  474. if (isset($Block['interrupted']))
  475. {
  476. $Block['element']['text'] []= '';
  477. unset($Block['interrupted']);
  478. }
  479. $Block['element']['text'] []= $matches[1];
  480. return $Block;
  481. }
  482. if ( ! isset($Block['interrupted']))
  483. {
  484. $Block['element']['text'] []= $Line['text'];
  485. return $Block;
  486. }
  487. }
  488. #
  489. # Rule
  490. protected function blockRule($Line)
  491. {
  492. if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
  493. {
  494. $Block = array(
  495. 'element' => array(
  496. 'name' => 'hr'
  497. ),
  498. );
  499. return $Block;
  500. }
  501. }
  502. #
  503. # Setext
  504. protected function blockSetextHeader($Line, array $Block = null)
  505. {
  506. if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
  507. {
  508. return;
  509. }
  510. if (chop($Line['text'], $Line['text'][0]) === '')
  511. {
  512. $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
  513. return $Block;
  514. }
  515. }
  516. #
  517. # Markup
  518. protected function blockMarkup($Line)
  519. {
  520. if ($this->markupEscaped)
  521. {
  522. return;
  523. }
  524. if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
  525. {
  526. $element = strtolower($matches[1]);
  527. if (in_array($element, $this->textLevelElements))
  528. {
  529. return;
  530. }
  531. $Block = array(
  532. 'name' => $matches[1],
  533. 'depth' => 0,
  534. 'markup' => $Line['text'],
  535. );
  536. $length = strlen($matches[0]);
  537. $remainder = substr($Line['text'], $length);
  538. if (trim($remainder) === '')
  539. {
  540. if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
  541. {
  542. $Block['closed'] = true;
  543. $Block['void'] = true;
  544. }
  545. }
  546. else
  547. {
  548. if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
  549. {
  550. return;
  551. }
  552. if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
  553. {
  554. $Block['closed'] = true;
  555. }
  556. }
  557. return $Block;
  558. }
  559. }
  560. protected function blockMarkupContinue($Line, array $Block)
  561. {
  562. if (isset($Block['closed']))
  563. {
  564. return;
  565. }
  566. if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
  567. {
  568. $Block['depth'] ++;
  569. }
  570. if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
  571. {
  572. if ($Block['depth'] > 0)
  573. {
  574. $Block['depth'] --;
  575. }
  576. else
  577. {
  578. $Block['closed'] = true;
  579. }
  580. }
  581. if (isset($Block['interrupted']))
  582. {
  583. $Block['markup'] .= "\n";
  584. unset($Block['interrupted']);
  585. }
  586. $Block['markup'] .= "\n".$Line['body'];
  587. return $Block;
  588. }
  589. #
  590. # Reference
  591. protected function blockReference($Line)
  592. {
  593. if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
  594. {
  595. $id = strtolower($matches[1]);
  596. $Data = array(
  597. 'url' => $matches[2],
  598. 'title' => null,
  599. );
  600. if (isset($matches[3]))
  601. {
  602. $Data['title'] = $matches[3];
  603. }
  604. $this->DefinitionData['Reference'][$id] = $Data;
  605. $Block = array(
  606. 'hidden' => true,
  607. );
  608. return $Block;
  609. }
  610. }
  611. #
  612. # Table
  613. protected function blockTable($Line, array $Block = null)
  614. {
  615. if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
  616. {
  617. return;
  618. }
  619. if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
  620. {
  621. $alignments = array();
  622. $divider = $Line['text'];
  623. $divider = trim($divider);
  624. $divider = trim($divider, '|');
  625. $dividerCells = explode('|', $divider);
  626. foreach ($dividerCells as $dividerCell)
  627. {
  628. $dividerCell = trim($dividerCell);
  629. if ($dividerCell === '')
  630. {
  631. continue;
  632. }
  633. $alignment = null;
  634. if ($dividerCell[0] === ':')
  635. {
  636. $alignment = 'left';
  637. }
  638. if (substr($dividerCell, - 1) === ':')
  639. {
  640. $alignment = $alignment === 'left' ? 'center' : 'right';
  641. }
  642. $alignments []= $alignment;
  643. }
  644. # ~
  645. $HeaderElements = array();
  646. $header = $Block['element']['text'];
  647. $header = trim($header);
  648. $header = trim($header, '|');
  649. $headerCells = explode('|', $header);
  650. foreach ($headerCells as $index => $headerCell)
  651. {
  652. $headerCell = trim($headerCell);
  653. $HeaderElement = array(
  654. 'name' => 'th',
  655. 'text' => $headerCell,
  656. 'handler' => 'line',
  657. );
  658. if (isset($alignments[$index]))
  659. {
  660. $alignment = $alignments[$index];
  661. $HeaderElement['attributes'] = array(
  662. 'style' => 'text-align: '.$alignment.';',
  663. );
  664. }
  665. $HeaderElements []= $HeaderElement;
  666. }
  667. # ~
  668. $Block = array(
  669. 'alignments' => $alignments,
  670. 'identified' => true,
  671. 'element' => array(
  672. 'name' => 'table',
  673. 'handler' => 'elements',
  674. ),
  675. );
  676. $Block['element']['text'] []= array(
  677. 'name' => 'thead',
  678. 'handler' => 'elements',
  679. );
  680. $Block['element']['text'] []= array(
  681. 'name' => 'tbody',
  682. 'handler' => 'elements',
  683. 'text' => array(),
  684. );
  685. $Block['element']['text'][0]['text'] []= array(
  686. 'name' => 'tr',
  687. 'handler' => 'elements',
  688. 'text' => $HeaderElements,
  689. );
  690. return $Block;
  691. }
  692. }
  693. protected function blockTableContinue($Line, array $Block)
  694. {
  695. if (isset($Block['interrupted']))
  696. {
  697. return;
  698. }
  699. if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
  700. {
  701. $Elements = array();
  702. $row = $Line['text'];
  703. $row = trim($row);
  704. $row = trim($row, '|');
  705. preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
  706. foreach ($matches[0] as $index => $cell)
  707. {
  708. $cell = trim($cell);
  709. $Element = array(
  710. 'name' => 'td',
  711. 'handler' => 'line',
  712. 'text' => $cell,
  713. );
  714. if (isset($Block['alignments'][$index]))
  715. {
  716. $Element['attributes'] = array(
  717. 'style' => 'text-align: '.$Block['alignments'][$index].';',
  718. );
  719. }
  720. $Elements []= $Element;
  721. }
  722. $Element = array(
  723. 'name' => 'tr',
  724. 'handler' => 'elements',
  725. 'text' => $Elements,
  726. );
  727. $Block['element']['text'][1]['text'] []= $Element;
  728. return $Block;
  729. }
  730. }
  731. #
  732. # ~
  733. #
  734. protected function paragraph($Line)
  735. {
  736. $Block = array(
  737. 'element' => array(
  738. 'name' => 'p',
  739. 'text' => $Line['text'],
  740. 'handler' => 'line',
  741. ),
  742. );
  743. return $Block;
  744. }
  745. #
  746. # Inline Elements
  747. #
  748. protected $InlineTypes = array(
  749. '"' => array('SpecialCharacter'),
  750. '!' => array('Image'),
  751. '&' => array('SpecialCharacter'),
  752. '*' => array('Emphasis'),
  753. ':' => array('Url'),
  754. '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
  755. '>' => array('SpecialCharacter'),
  756. '[' => array('Link'),
  757. '_' => array('Emphasis'),
  758. '`' => array('Code'),
  759. '~' => array('Strikethrough'),
  760. '\\' => array('EscapeSequence'),
  761. );
  762. # ~
  763. protected $inlineMarkerList = '!"*_&[:<>`~\\';
  764. #
  765. # ~
  766. #
  767. public function line($text)
  768. {
  769. $markup = '';
  770. # $excerpt is based on the first occurrence of a marker
  771. while ($excerpt = strpbrk($text, $this->inlineMarkerList))
  772. {
  773. $marker = $excerpt[0];
  774. $markerPosition = strpos($text, $marker);
  775. $Excerpt = array('text' => $excerpt, 'context' => $text);
  776. foreach ($this->InlineTypes[$marker] as $inlineType)
  777. {
  778. $Inline = $this->{'inline'.$inlineType}($Excerpt);
  779. if ( ! isset($Inline))
  780. {
  781. continue;
  782. }
  783. # makes sure that the inline belongs to "our" marker
  784. if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
  785. {
  786. continue;
  787. }
  788. # sets a default inline position
  789. if ( ! isset($Inline['position']))
  790. {
  791. $Inline['position'] = $markerPosition;
  792. }
  793. # the text that comes before the inline
  794. $unmarkedText = substr($text, 0, $Inline['position']);
  795. # compile the unmarked text
  796. $markup .= $this->unmarkedText($unmarkedText);
  797. # compile the inline
  798. $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
  799. # remove the examined text
  800. $text = substr($text, $Inline['position'] + $Inline['extent']);
  801. continue 2;
  802. }
  803. # the marker does not belong to an inline
  804. $unmarkedText = substr($text, 0, $markerPosition + 1);
  805. $markup .= $this->unmarkedText($unmarkedText);
  806. $text = substr($text, $markerPosition + 1);
  807. }
  808. $markup .= $this->unmarkedText($text);
  809. return $markup;
  810. }
  811. #
  812. # ~
  813. #
  814. protected function inlineCode($Excerpt)
  815. {
  816. $marker = $Excerpt['text'][0];
  817. if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
  818. {
  819. $text = $matches[2];
  820. $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
  821. $text = preg_replace("/[ ]*\n/", ' ', $text);
  822. return array(
  823. 'extent' => strlen($matches[0]),
  824. 'element' => array(
  825. 'name' => 'code',
  826. 'text' => $text,
  827. ),
  828. );
  829. }
  830. }
  831. protected function inlineEmailTag($Excerpt)
  832. {
  833. if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
  834. {
  835. $url = $matches[1];
  836. if ( ! isset($matches[2]))
  837. {
  838. $url = 'mailto:' . $url;
  839. }
  840. return array(
  841. 'extent' => strlen($matches[0]),
  842. 'element' => array(
  843. 'name' => 'a',
  844. 'text' => $matches[1],
  845. 'attributes' => array(
  846. 'href' => $url,
  847. ),
  848. ),
  849. );
  850. }
  851. }
  852. protected function inlineEmphasis($Excerpt)
  853. {
  854. if ( ! isset($Excerpt['text'][1]))
  855. {
  856. return;
  857. }
  858. $marker = $Excerpt['text'][0];
  859. if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
  860. {
  861. $emphasis = 'strong';
  862. }
  863. elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
  864. {
  865. $emphasis = 'em';
  866. }
  867. else
  868. {
  869. return;
  870. }
  871. return array(
  872. 'extent' => strlen($matches[0]),
  873. 'element' => array(
  874. 'name' => $emphasis,
  875. 'handler' => 'line',
  876. 'text' => $matches[1],
  877. ),
  878. );
  879. }
  880. protected function inlineEscapeSequence($Excerpt)
  881. {
  882. if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
  883. {
  884. return array(
  885. 'markup' => $Excerpt['text'][1],
  886. 'extent' => 2,
  887. );
  888. }
  889. }
  890. protected function inlineImage($Excerpt)
  891. {
  892. if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
  893. {
  894. return;
  895. }
  896. $Excerpt['text']= substr($Excerpt['text'], 1);
  897. $Link = $this->inlineLink($Excerpt);
  898. if ($Link === null)
  899. {
  900. return;
  901. }
  902. $Inline = array(
  903. 'extent' => $Link['extent'] + 1,
  904. 'element' => array(
  905. 'name' => 'img',
  906. 'attributes' => array(
  907. 'src' => $Link['element']['attributes']['href'],
  908. 'alt' => $Link['element']['text'],
  909. ),
  910. ),
  911. );
  912. $Inline['element']['attributes'] += $Link['element']['attributes'];
  913. unset($Inline['element']['attributes']['href']);
  914. return $Inline;
  915. }
  916. protected function inlineLink($Excerpt)
  917. {
  918. $Element = array(
  919. 'name' => 'a',
  920. 'handler' => 'line',
  921. 'text' => null,
  922. 'attributes' => array(
  923. 'href' => null,
  924. 'title' => null,
  925. ),
  926. );
  927. $extent = 0;
  928. $remainder = $Excerpt['text'];
  929. if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
  930. {
  931. $Element['text'] = $matches[1];
  932. $extent += strlen($matches[0]);
  933. $remainder = substr($remainder, $extent);
  934. }
  935. else
  936. {
  937. return;
  938. }
  939. if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches))
  940. {
  941. $Element['attributes']['href'] = $matches[1];
  942. if (isset($matches[2]))
  943. {
  944. $Element['attributes']['title'] = substr($matches[2], 1, - 1);
  945. }
  946. $extent += strlen($matches[0]);
  947. }
  948. else
  949. {
  950. if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
  951. {
  952. $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
  953. $definition = strtolower($definition);
  954. $extent += strlen($matches[0]);
  955. }
  956. else
  957. {
  958. $definition = strtolower($Element['text']);
  959. }
  960. if ( ! isset($this->DefinitionData['Reference'][$definition]))
  961. {
  962. return;
  963. }
  964. $Definition = $this->DefinitionData['Reference'][$definition];
  965. $Element['attributes']['href'] = $Definition['url'];
  966. $Element['attributes']['title'] = $Definition['title'];
  967. }
  968. $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
  969. return array(
  970. 'extent' => $extent,
  971. 'element' => $Element,
  972. );
  973. }
  974. protected function inlineMarkup($Excerpt)
  975. {
  976. if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
  977. {
  978. return;
  979. }
  980. if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
  981. {
  982. return array(
  983. 'markup' => $matches[0],
  984. 'extent' => strlen($matches[0]),
  985. );
  986. }
  987. if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
  988. {
  989. return array(
  990. 'markup' => $matches[0],
  991. 'extent' => strlen($matches[0]),
  992. );
  993. }
  994. if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
  995. {
  996. return array(
  997. 'markup' => $matches[0],
  998. 'extent' => strlen($matches[0]),
  999. );
  1000. }
  1001. }
  1002. protected function inlineSpecialCharacter($Excerpt)
  1003. {
  1004. if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
  1005. {
  1006. return array(
  1007. 'markup' => '&amp;',
  1008. 'extent' => 1,
  1009. );
  1010. }
  1011. $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
  1012. if (isset($SpecialCharacter[$Excerpt['text'][0]]))
  1013. {
  1014. return array(
  1015. 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
  1016. 'extent' => 1,
  1017. );
  1018. }
  1019. }
  1020. protected function inlineStrikethrough($Excerpt)
  1021. {
  1022. if ( ! isset($Excerpt['text'][1]))
  1023. {
  1024. return;
  1025. }
  1026. if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
  1027. {
  1028. return array(
  1029. 'extent' => strlen($matches[0]),
  1030. 'element' => array(
  1031. 'name' => 'del',
  1032. 'text' => $matches[1],
  1033. 'handler' => 'line',
  1034. ),
  1035. );
  1036. }
  1037. }
  1038. protected function inlineUrl($Excerpt)
  1039. {
  1040. if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
  1041. {
  1042. return;
  1043. }
  1044. if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
  1045. {
  1046. $Inline = array(
  1047. 'extent' => strlen($matches[0][0]),
  1048. 'position' => $matches[0][1],
  1049. 'element' => array(
  1050. 'name' => 'a',
  1051. 'text' => $matches[0][0],
  1052. 'attributes' => array(
  1053. 'href' => $matches[0][0],
  1054. ),
  1055. ),
  1056. );
  1057. return $Inline;
  1058. }
  1059. }
  1060. protected function inlineUrlTag($Excerpt)
  1061. {
  1062. if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
  1063. {
  1064. $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
  1065. return array(
  1066. 'extent' => strlen($matches[0]),
  1067. 'element' => array(
  1068. 'name' => 'a',
  1069. 'text' => $url,
  1070. 'attributes' => array(
  1071. 'href' => $url,
  1072. ),
  1073. ),
  1074. );
  1075. }
  1076. }
  1077. # ~
  1078. protected function unmarkedText($text)
  1079. {
  1080. if ($this->breaksEnabled)
  1081. {
  1082. $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
  1083. }
  1084. else
  1085. {
  1086. $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
  1087. $text = str_replace(" \n", "\n", $text);
  1088. }
  1089. return $text;
  1090. }
  1091. #
  1092. # Handlers
  1093. #
  1094. protected function element(array $Element)
  1095. {
  1096. $markup = '<'.$Element['name'];
  1097. if (isset($Element['attributes']))
  1098. {
  1099. foreach ($Element['attributes'] as $name => $value)
  1100. {
  1101. if ($value === null)
  1102. {
  1103. continue;
  1104. }
  1105. $markup .= ' '.$name.'="'.$value.'"';
  1106. }
  1107. }
  1108. if (isset($Element['text']))
  1109. {
  1110. $markup .= '>';
  1111. if (isset($Element['handler']))
  1112. {
  1113. $markup .= $this->{$Element['handler']}($Element['text']);
  1114. }
  1115. else
  1116. {
  1117. $markup .= $Element['text'];
  1118. }
  1119. $markup .= '</'.$Element['name'].'>';
  1120. }
  1121. else
  1122. {
  1123. $markup .= ' />';
  1124. }
  1125. return $markup;
  1126. }
  1127. protected function elements(array $Elements)
  1128. {
  1129. $markup = '';
  1130. foreach ($Elements as $Element)
  1131. {
  1132. $markup .= "\n" . $this->element($Element);
  1133. }
  1134. $markup .= "\n";
  1135. return $markup;
  1136. }
  1137. # ~
  1138. protected function li($lines)
  1139. {
  1140. $markup = $this->lines($lines);
  1141. $trimmedMarkup = trim($markup);
  1142. if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
  1143. {
  1144. $markup = $trimmedMarkup;
  1145. $markup = substr($markup, 3);
  1146. $position = strpos($markup, "</p>");
  1147. $markup = substr_replace($markup, '', $position, 4);
  1148. }
  1149. return $markup;
  1150. }
  1151. #
  1152. # Deprecated Methods
  1153. #
  1154. function parse($text)
  1155. {
  1156. $markup = $this->text($text);
  1157. return $markup;
  1158. }
  1159. #
  1160. # Static Methods
  1161. #
  1162. static function instance($name = 'default')
  1163. {
  1164. if (isset(self::$instances[$name]))
  1165. {
  1166. return self::$instances[$name];
  1167. }
  1168. $instance = new static();
  1169. self::$instances[$name] = $instance;
  1170. return $instance;
  1171. }
  1172. private static $instances = array();
  1173. #
  1174. # Fields
  1175. #
  1176. protected $DefinitionData;
  1177. #
  1178. # Read-Only
  1179. protected $specialCharacters = array(
  1180. '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
  1181. );
  1182. protected $StrongRegex = array(
  1183. '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
  1184. '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
  1185. );
  1186. protected $EmRegex = array(
  1187. '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
  1188. '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
  1189. );
  1190. protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
  1191. protected $voidElements = array(
  1192. 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
  1193. );
  1194. protected $textLevelElements = array(
  1195. 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
  1196. 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
  1197. 'i', 'rp', 'del', 'code', 'strike', 'marquee',
  1198. 'q', 'rt', 'ins', 'font', 'strong',
  1199. 's', 'tt', 'kbd', 'mark',
  1200. 'u', 'xm', 'sub', 'nobr',
  1201. 'sup', 'ruby',
  1202. 'var', 'span',
  1203. 'wbr', 'time',
  1204. );
  1205. }