/application/libraries/parsedown.php

https://github.com/medieteknik/Medieteknik.nu · PHP · 1343 lines · 928 code · 355 blank · 60 comment · 95 complexity · c90477e9e1dcda5caf4879e7d4cfbd08 MD5 · raw file

  1. <?php
  2. #
  3. #
  4. # Parsedown
  5. # http://parsedown.org
  6. #
  7. # (c) Emanuil Rusev
  8. # http://erusev.com
  9. #
  10. # For the full license information, view the LICENSE file that was distributed
  11. # with this source code.
  12. #
  13. #
  14. class Parsedown
  15. {
  16. #
  17. # Philosophy
  18. #
  19. # Markdown is intended to be easy-to-read by humans - those of us who read
  20. # line by line, left to right, top to bottom. In order to take advantage of
  21. # this, Parsedown tries to read in a similar way. It breaks texts into
  22. # lines, it iterates through them and it looks at how they start and relate
  23. # to each other.
  24. #
  25. # Setters
  26. #
  27. # Enables GFM line breaks.
  28. function setBreaksEnabled($breaksEnabled)
  29. {
  30. $this->breaksEnabled = $breaksEnabled;
  31. return $this;
  32. }
  33. /**
  34. * For backwards compatibility before PSR-2 naming.
  35. *
  36. * @deprecated Use setBreaksEnabled instead.
  37. */
  38. function set_breaks_enabled($breaks_enabled)
  39. {
  40. return $this->setBreaksEnabled($breaks_enabled);
  41. }
  42. private $breaksEnabled = false;
  43. #
  44. # Methods
  45. #
  46. function parse($text)
  47. {
  48. # standardize line breaks
  49. $text = str_replace("\r\n", "\n", $text);
  50. $text = str_replace("\r", "\n", $text);
  51. # replace tabs with spaces
  52. $text = str_replace("\t", ' ', $text);
  53. # remove surrounding line breaks
  54. $text = trim($text, "\n");
  55. # split text into lines
  56. $lines = explode("\n", $text);
  57. # iterate through lines to identify blocks
  58. $blocks = $this->findBlocks($lines);
  59. # iterate through blocks to build markup
  60. $markup = $this->compile($blocks);
  61. # trim line breaks
  62. $markup = trim($markup, "\n");
  63. # add .markdown div
  64. $markup = '<div class="markdown">'.$markup.'</div>';
  65. return $markup;
  66. }
  67. #
  68. # Private
  69. private function findBlocks(array $lines, $blockContext = null)
  70. {
  71. $block = null;
  72. $context = null;
  73. $contextData = null;
  74. foreach ($lines as $line)
  75. {
  76. $indentedLine = $line;
  77. $indentation = 0;
  78. while(isset($line[$indentation]) and $line[$indentation] === ' ')
  79. {
  80. $indentation++;
  81. }
  82. if ($indentation > 0)
  83. {
  84. $line = ltrim($line);
  85. }
  86. # ~
  87. switch ($context)
  88. {
  89. case null:
  90. $contextData = null;
  91. if ($line === '')
  92. {
  93. continue 2;
  94. }
  95. break;
  96. # ~~~ javascript
  97. # var message = 'Hello!';
  98. case 'fenced code':
  99. if ($line === '')
  100. {
  101. $block['content'][0]['content'] .= "\n";
  102. continue 2;
  103. }
  104. if (preg_match('/^[ ]*'.$contextData['marker'].'{3,}[ ]*$/', $line))
  105. {
  106. $context = null;
  107. }
  108. else
  109. {
  110. if ($block['content'][0]['content'])
  111. {
  112. $block['content'][0]['content'] .= "\n";
  113. }
  114. $string = htmlspecialchars($indentedLine, ENT_NOQUOTES, 'UTF-8');
  115. $block['content'][0]['content'] .= $string;
  116. }
  117. continue 2;
  118. case 'markup':
  119. if (stripos($line, $contextData['start']) !== false) # opening tag
  120. {
  121. $contextData['depth']++;
  122. }
  123. if (stripos($line, $contextData['end']) !== false) # closing tag
  124. {
  125. if ($contextData['depth'] > 0)
  126. {
  127. $contextData['depth']--;
  128. }
  129. else
  130. {
  131. $context = null;
  132. }
  133. }
  134. $block['content'] .= "\n".$indentedLine;
  135. continue 2;
  136. case 'li':
  137. if ($line === '')
  138. {
  139. $contextData['interrupted'] = true;
  140. continue 2;
  141. }
  142. if ($contextData['indentation'] === $indentation and preg_match('/^'.$contextData['marker'].'[ ]+(.*)/', $line, $matches))
  143. {
  144. if (isset($contextData['interrupted']))
  145. {
  146. $nestedBlock['content'] []= '';
  147. unset($contextData['interrupted']);
  148. }
  149. unset($nestedBlock);
  150. $nestedBlock = array(
  151. 'name' => 'li',
  152. 'content type' => 'lines',
  153. 'content' => array(
  154. $matches[1],
  155. ),
  156. );
  157. $block['content'] []= & $nestedBlock;
  158. continue 2;
  159. }
  160. if (empty($contextData['interrupted']))
  161. {
  162. $value = $line;
  163. if ($indentation > $contextData['baseline'])
  164. {
  165. $value = str_repeat(' ', $indentation - $contextData['baseline']) . $value;
  166. }
  167. $nestedBlock['content'] []= $value;
  168. continue 2;
  169. }
  170. if ($indentation > 0)
  171. {
  172. $nestedBlock['content'] []= '';
  173. $value = $line;
  174. if ($indentation > $contextData['baseline'])
  175. {
  176. $value = str_repeat(' ', $indentation - $contextData['baseline']) . $value;
  177. }
  178. $nestedBlock['content'] []= $value;
  179. unset($contextData['interrupted']);
  180. continue 2;
  181. }
  182. $context = null;
  183. break;
  184. case 'quote':
  185. if ($line === '')
  186. {
  187. $contextData['interrupted'] = true;
  188. continue 2;
  189. }
  190. if (preg_match('/^>[ ]?(.*)/', $line, $matches))
  191. {
  192. $block['content'] []= $matches[1];
  193. continue 2;
  194. }
  195. if (empty($contextData['interrupted']))
  196. {
  197. $block['content'] []= $line;
  198. continue 2;
  199. }
  200. $context = null;
  201. break;
  202. case 'code':
  203. if ($line === '')
  204. {
  205. $contextData['interrupted'] = true;
  206. continue 2;
  207. }
  208. if ($indentation >= 4)
  209. {
  210. if (isset($contextData['interrupted']))
  211. {
  212. $block['content'][0]['content'] .= "\n";
  213. unset($contextData['interrupted']);
  214. }
  215. $block['content'][0]['content'] .= "\n";
  216. $string = htmlspecialchars($line, ENT_NOQUOTES, 'UTF-8');
  217. $string = str_repeat(' ', $indentation - 4) . $string;
  218. $block['content'][0]['content'] .= $string;
  219. continue 2;
  220. }
  221. $context = null;
  222. break;
  223. case 'table':
  224. if ($line === '')
  225. {
  226. $context = null;
  227. continue 2;
  228. }
  229. if (strpos($line, '|') !== false)
  230. {
  231. $nestedBlocks = array();
  232. $substring = preg_replace('/^[|][ ]*/', '', $line);
  233. $substring = preg_replace('/[|]?[ ]*$/', '', $substring);
  234. $parts = explode('|', $substring);
  235. foreach ($parts as $index => $part)
  236. {
  237. $substring = trim($part);
  238. $nestedBlock = array(
  239. 'name' => 'td',
  240. 'content type' => 'line',
  241. 'content' => $substring,
  242. );
  243. if (isset($contextData['alignments'][$index]))
  244. {
  245. $nestedBlock['attributes'] = array(
  246. 'align' => $contextData['alignments'][$index],
  247. );
  248. }
  249. $nestedBlocks []= $nestedBlock;
  250. }
  251. $nestedBlock = array(
  252. 'name' => 'tr',
  253. 'content type' => 'blocks',
  254. 'content' => $nestedBlocks,
  255. );
  256. $block['content'][1]['content'] []= $nestedBlock;
  257. continue 2;
  258. }
  259. $context = null;
  260. break;
  261. case 'paragraph':
  262. if ($line === '')
  263. {
  264. $block['name'] = 'p'; # dense li
  265. $context = null;
  266. continue 2;
  267. }
  268. if ($line[0] === '=' and chop($line, '=') === '')
  269. {
  270. $block['name'] = 'h1';
  271. $context = null;
  272. continue 2;
  273. }
  274. if ($line[0] === '-' and chop($line, '-') === '')
  275. {
  276. $block['name'] = 'h2';
  277. $context = null;
  278. continue 2;
  279. }
  280. if (strpos($line, '|') !== false and strpos($block['content'], '|') !== false and chop($line, ' -:|') === '')
  281. {
  282. $values = array();
  283. $substring = trim($line, ' |');
  284. $parts = explode('|', $substring);
  285. foreach ($parts as $part)
  286. {
  287. $substring = trim($part);
  288. $value = null;
  289. if ($substring[0] === ':')
  290. {
  291. $value = 'left';
  292. }
  293. if (substr($substring, -1) === ':')
  294. {
  295. $value = $value === 'left' ? 'center' : 'right';
  296. }
  297. $values []= $value;
  298. }
  299. # ~
  300. $nestedBlocks = array();
  301. $substring = preg_replace('/^[|][ ]*/', '', $block['content']);
  302. $substring = preg_replace('/[|]?[ ]*$/', '', $substring);
  303. $parts = explode('|', $substring);
  304. foreach ($parts as $index => $part)
  305. {
  306. $substring = trim($part);
  307. $nestedBlock = array(
  308. 'name' => 'th',
  309. 'content type' => 'line',
  310. 'content' => $substring,
  311. );
  312. if (isset($values[$index]))
  313. {
  314. $value = $values[$index];
  315. $nestedBlock['attributes'] = array(
  316. 'align' => $value,
  317. );
  318. }
  319. $nestedBlocks []= $nestedBlock;
  320. }
  321. # ~
  322. $block = array(
  323. 'name' => 'table',
  324. 'content type' => 'blocks',
  325. 'content' => array(),
  326. );
  327. $block['content'] []= array(
  328. 'name' => 'thead',
  329. 'content type' => 'blocks',
  330. 'content' => array(),
  331. );
  332. $block['content'] []= array(
  333. 'name' => 'tbody',
  334. 'content type' => 'blocks',
  335. 'content' => array(),
  336. );
  337. $block['content'][0]['content'] []= array(
  338. 'name' => 'tr',
  339. 'content type' => 'blocks',
  340. 'content' => array(),
  341. );
  342. $block['content'][0]['content'][0]['content'] = $nestedBlocks;
  343. # ~
  344. $context = 'table';
  345. $contextData = array(
  346. 'alignments' => $values,
  347. );
  348. # ~
  349. continue 2;
  350. }
  351. break;
  352. default:
  353. throw new Exception('Unrecognized context - '.$context);
  354. }
  355. if ($indentation >= 4)
  356. {
  357. $blocks []= $block;
  358. $string = htmlspecialchars($line, ENT_NOQUOTES, 'UTF-8');
  359. $string = str_repeat(' ', $indentation - 4) . $string;
  360. $block = array(
  361. 'name' => 'pre',
  362. 'content type' => 'blocks',
  363. 'content' => array(
  364. array(
  365. 'name' => 'code',
  366. 'content type' => null,
  367. 'content' => $string,
  368. ),
  369. ),
  370. );
  371. $context = 'code';
  372. continue;
  373. }
  374. switch ($line[0])
  375. {
  376. case '#':
  377. if (isset($line[1]))
  378. {
  379. $blocks []= $block;
  380. $level = 1;
  381. while (isset($line[$level]) and $line[$level] === '#')
  382. {
  383. $level++;
  384. }
  385. $string = trim($line, '# ');
  386. $string = $this->parseLine($string);
  387. $block = array(
  388. 'name' => 'h'.$level,
  389. 'content type' => 'line',
  390. 'content' => $string,
  391. );
  392. $context = null;
  393. continue 2;
  394. }
  395. break;
  396. case '<':
  397. $position = strpos($line, '>');
  398. if ($position > 1)
  399. {
  400. $substring = substr($line, 1, $position - 1);
  401. $substring = chop($substring);
  402. if (substr($substring, -1) === '/')
  403. {
  404. $isClosing = true;
  405. $substring = substr($substring, 0, -1);
  406. }
  407. $position = strpos($substring, ' ');
  408. if ($position)
  409. {
  410. $name = substr($substring, 0, $position);
  411. }
  412. else
  413. {
  414. $name = $substring;
  415. }
  416. $name = strtolower($name);
  417. if ($name[0] == 'h' and strpos('r123456', $name[1]) !== false) # hr, h1, h2, ...
  418. {
  419. if ($name == 'hr')
  420. {
  421. $isClosing = true;
  422. }
  423. }
  424. elseif ( ! ctype_alpha($name))
  425. {
  426. break;
  427. }
  428. if (in_array($name, self::$textLevelElements))
  429. {
  430. break;
  431. }
  432. $blocks []= $block;
  433. $block = array(
  434. 'name' => null,
  435. 'content type' => null,
  436. 'content' => $indentedLine,
  437. );
  438. if (isset($isClosing))
  439. {
  440. unset($isClosing);
  441. continue 2;
  442. }
  443. $context = 'markup';
  444. $contextData = array(
  445. 'start' => '<'.$name.'>',
  446. 'end' => '</'.$name.'>',
  447. 'depth' => 0,
  448. );
  449. if (stripos($line, $contextData['end']) !== false)
  450. {
  451. $context = null;
  452. }
  453. continue 2;
  454. }
  455. break;
  456. case '>':
  457. if (preg_match('/^>[ ]?(.*)/', $line, $matches))
  458. {
  459. $blocks []= $block;
  460. $block = array(
  461. 'name' => 'blockquote',
  462. 'content type' => 'lines',
  463. 'content' => array(
  464. $matches[1],
  465. ),
  466. );
  467. $context = 'quote';
  468. $contextData = array();
  469. continue 2;
  470. }
  471. break;
  472. case '[':
  473. $position = strpos($line, ']:');
  474. if ($position)
  475. {
  476. $reference = array();
  477. $label = substr($line, 1, $position - 1);
  478. $label = strtolower($label);
  479. $substring = substr($line, $position + 2);
  480. $substring = trim($substring);
  481. if ($substring === '')
  482. {
  483. break;
  484. }
  485. if ($substring[0] === '<')
  486. {
  487. $position = strpos($substring, '>');
  488. if ($position === false)
  489. {
  490. break;
  491. }
  492. $reference['link'] = substr($substring, 1, $position - 1);
  493. $substring = substr($substring, $position + 1);
  494. }
  495. else
  496. {
  497. $position = strpos($substring, ' ');
  498. if ($position === false)
  499. {
  500. $reference['link'] = $substring;
  501. $substring = false;
  502. }
  503. else
  504. {
  505. $reference['link'] = substr($substring, 0, $position);
  506. $substring = substr($substring, $position + 1);
  507. }
  508. }
  509. if ($substring !== false)
  510. {
  511. if ($substring[0] !== '"' and $substring[0] !== "'" and $substring[0] !== '(')
  512. {
  513. break;
  514. }
  515. $lastChar = substr($substring, -1);
  516. if ($lastChar !== '"' and $lastChar !== "'" and $lastChar !== ')')
  517. {
  518. break;
  519. }
  520. $reference['title'] = substr($substring, 1, -1);
  521. }
  522. $this->referenceMap[$label] = $reference;
  523. continue 2;
  524. }
  525. break;
  526. case '`':
  527. case '~':
  528. if (preg_match('/^([`]{3,}|[~]{3,})[ ]*(\w+)?[ ]*$/', $line, $matches))
  529. {
  530. $blocks []= $block;
  531. $block = array(
  532. 'name' => 'pre',
  533. 'content type' => 'blocks',
  534. 'content' => array(
  535. array(
  536. 'name' => 'code',
  537. 'content type' => null,
  538. 'content' => '',
  539. ),
  540. ),
  541. );
  542. if (isset($matches[2]))
  543. {
  544. $block['content'][0]['attributes'] = array(
  545. 'class' => 'language-'.$matches[2],
  546. );
  547. }
  548. $context = 'fenced code';
  549. $contextData = array(
  550. 'marker' => $matches[1][0],
  551. );
  552. continue 2;
  553. }
  554. break;
  555. case '-':
  556. case '*':
  557. case '_':
  558. if (preg_match('/^([-*_])([ ]{0,2}\1){2,}[ ]*$/', $line))
  559. {
  560. $blocks []= $block;
  561. $block = array(
  562. 'name' => 'hr',
  563. 'content' => null,
  564. );
  565. continue 2;
  566. }
  567. }
  568. switch (true)
  569. {
  570. case $line[0] <= '-' and preg_match('/^([*+-][ ]+)(.*)/', $line, $matches):
  571. case $line[0] <= '9' and preg_match('/^([0-9]+[.][ ]+)(.*)/', $line, $matches):
  572. $blocks []= $block;
  573. $name = $line[0] >= '0' ? 'ol' : 'ul';
  574. $block = array(
  575. 'name' => $name,
  576. 'content type' => 'blocks',
  577. 'content' => array(),
  578. );
  579. unset($nestedBlock);
  580. $nestedBlock = array(
  581. 'name' => 'li',
  582. 'content type' => 'lines',
  583. 'content' => array(
  584. $matches[2],
  585. ),
  586. );
  587. $block['content'] []= & $nestedBlock;
  588. $baseline = $indentation + strlen($matches[1]);
  589. $marker = $line[0] >= '0' ? '[0-9]+[.]' : '[*+-]';
  590. $context = 'li';
  591. $contextData = array(
  592. 'indentation' => $indentation,
  593. 'baseline' => $baseline,
  594. 'marker' => $marker,
  595. 'lines' => array(
  596. $matches[2],
  597. ),
  598. );
  599. continue 2;
  600. }
  601. if ($context === 'paragraph')
  602. {
  603. $block['content'] .= "\n".$line;
  604. continue;
  605. }
  606. else
  607. {
  608. $blocks []= $block;
  609. $block = array(
  610. 'name' => 'p',
  611. 'content type' => 'line',
  612. 'content' => $line,
  613. );
  614. if ($blockContext === 'li' and empty($blocks[1]))
  615. {
  616. $block['name'] = null;
  617. }
  618. $context = 'paragraph';
  619. }
  620. }
  621. if ($blockContext === 'li' and $block['name'] === null)
  622. {
  623. return $block['content'];
  624. }
  625. $blocks []= $block;
  626. unset($blocks[0]);
  627. return $blocks;
  628. }
  629. private function compile(array $blocks)
  630. {
  631. $markup = '';
  632. foreach ($blocks as $block)
  633. {
  634. $markup .= "\n";
  635. if (isset($block['name']))
  636. {
  637. $markup .= '<'.$block['name'];
  638. if (isset($block['attributes']))
  639. {
  640. foreach ($block['attributes'] as $name => $value)
  641. {
  642. $markup .= ' '.$name.'="'.$value.'"';
  643. }
  644. }
  645. if ($block['content'] === null)
  646. {
  647. $markup .= ' />';
  648. continue;
  649. }
  650. else
  651. {
  652. $markup .= '>';
  653. }
  654. }
  655. switch ($block['content type'])
  656. {
  657. case null:
  658. $markup .= $block['content'];
  659. break;
  660. case 'line':
  661. $markup .= $this->parseLine($block['content']);
  662. break;
  663. case 'lines':
  664. $result = $this->findBlocks($block['content'], $block['name']);
  665. if (is_string($result)) # dense li
  666. {
  667. $markup .= $this->parseLine($result);
  668. break;
  669. }
  670. $markup .= $this->compile($result);
  671. break;
  672. case 'blocks':
  673. $markup .= $this->compile($block['content']);
  674. break;
  675. }
  676. if (isset($block['name']))
  677. {
  678. $markup .= '</'.$block['name'].'>';
  679. }
  680. }
  681. $markup .= "\n";
  682. return $markup;
  683. }
  684. private function parseLine($text, $markers = array(" \n", '![', '&', '*', '<', '[', '\\', '_', '`', 'http', '~~'))
  685. {
  686. if (isset($text[1]) === false or $markers === array())
  687. {
  688. return $text;
  689. }
  690. # ~
  691. $markup = '';
  692. while ($markers)
  693. {
  694. $closestMarker = null;
  695. $closestMarkerIndex = 0;
  696. $closestMarkerPosition = null;
  697. foreach ($markers as $index => $marker)
  698. {
  699. $markerPosition = strpos($text, $marker);
  700. if ($markerPosition === false)
  701. {
  702. unset($markers[$index]);
  703. continue;
  704. }
  705. if ($closestMarker === null or $markerPosition < $closestMarkerPosition)
  706. {
  707. $closestMarker = $marker;
  708. $closestMarkerIndex = $index;
  709. $closestMarkerPosition = $markerPosition;
  710. }
  711. }
  712. # ~
  713. if ($closestMarker === null or isset($text[$closestMarkerPosition + 1]) === false)
  714. {
  715. $markup .= $text;
  716. break;
  717. }
  718. else
  719. {
  720. $markup .= substr($text, 0, $closestMarkerPosition);
  721. }
  722. $text = substr($text, $closestMarkerPosition);
  723. # ~
  724. unset($markers[$closestMarkerIndex]);
  725. # ~
  726. switch ($closestMarker)
  727. {
  728. case " \n":
  729. $markup .= '<br />'."\n";
  730. $offset = 3;
  731. break;
  732. case '![':
  733. case '[':
  734. if (strpos($text, ']') and preg_match('/\[((?:[^][]|(?R))*)\]/', $text, $matches))
  735. {
  736. $element = array(
  737. '!' => $text[0] === '!',
  738. 'text' => $matches[1],
  739. );
  740. $offset = strlen($matches[0]);
  741. if ($element['!'])
  742. {
  743. $offset++;
  744. }
  745. $remainingText = substr($text, $offset);
  746. if ($remainingText[0] === '(' and preg_match('/\([ ]*(.*?)(?:[ ]+[\'"](.+?)[\'"])?[ ]*\)/', $remainingText, $matches))
  747. {
  748. $element['link'] = $matches[1];
  749. if (isset($matches[2]))
  750. {
  751. $element['title'] = $matches[2];
  752. }
  753. $offset += strlen($matches[0]);
  754. }
  755. elseif ($this->referenceMap)
  756. {
  757. $reference = $element['text'];
  758. if (preg_match('/^\s*\[(.*?)\]/', $remainingText, $matches))
  759. {
  760. $reference = $matches[1] === '' ? $element['text'] : $matches[1];
  761. $offset += strlen($matches[0]);
  762. }
  763. $reference = strtolower($reference);
  764. if (isset($this->referenceMap[$reference]))
  765. {
  766. $element['link'] = $this->referenceMap[$reference]['link'];
  767. if (isset($this->referenceMap[$reference]['title']))
  768. {
  769. $element['title'] = $this->referenceMap[$reference]['title'];
  770. }
  771. }
  772. else
  773. {
  774. unset($element);
  775. }
  776. }
  777. else
  778. {
  779. unset($element);
  780. }
  781. }
  782. if (isset($element))
  783. {
  784. $element['link'] = str_replace('&', '&amp;', $element['link']);
  785. $element['link'] = str_replace('<', '&lt;', $element['link']);
  786. if ($element['!'])
  787. {
  788. $markup .= '<img alt="'.$element['text'].'" src="'.$element['link'].'"';
  789. if (isset($element['title']))
  790. {
  791. $markup .= ' title="'.$element['title'].'"';
  792. }
  793. $markup .= ' />';
  794. }
  795. else
  796. {
  797. $element['text'] = $this->parseLine($element['text'], $markers);
  798. $markup .= '<a href="'.$element['link'].'"';
  799. if (isset($element['title']))
  800. {
  801. $markup .= ' title="'.$element['title'].'"';
  802. }
  803. $markup .= '>'.$element['text'].'</a>';
  804. }
  805. unset($element);
  806. }
  807. else
  808. {
  809. $markup .= $closestMarker;
  810. $offset = $closestMarker === '![' ? 2 : 1;
  811. }
  812. break;
  813. case '&':
  814. if (preg_match('/^&#?\w+;/', $text, $matches))
  815. {
  816. $markup .= $matches[0];
  817. $offset = strlen($matches[0]);
  818. }
  819. else
  820. {
  821. $markup .= '&amp;';
  822. $offset = 1;
  823. }
  824. break;
  825. case '*':
  826. case '_':
  827. if ($text[1] === $closestMarker and preg_match(self::$strongRegex[$closestMarker], $text, $matches))
  828. {
  829. $markers[$closestMarkerIndex] = $closestMarker;
  830. $matches[1] = $this->parseLine($matches[1], $markers);
  831. $markup .= '<strong>'.$matches[1].'</strong>';
  832. }
  833. elseif (preg_match(self::$emRegex[$closestMarker], $text, $matches))
  834. {
  835. $markers[$closestMarkerIndex] = $closestMarker;
  836. $matches[1] = $this->parseLine($matches[1], $markers);
  837. $markup .= '<em>'.$matches[1].'</em>';
  838. }
  839. if (isset($matches) and $matches)
  840. {
  841. $offset = strlen($matches[0]);
  842. }
  843. else
  844. {
  845. $markup .= $closestMarker;
  846. $offset = 1;
  847. }
  848. break;
  849. case '<':
  850. if (strpos($text, '>') !== false)
  851. {
  852. if ($text[1] === 'h' and preg_match('/^<(https?:[\/]{2}[^\s]+?)>/i', $text, $matches))
  853. {
  854. $elementUrl = $matches[1];
  855. $elementUrl = str_replace('&', '&amp;', $elementUrl);
  856. $elementUrl = str_replace('<', '&lt;', $elementUrl);
  857. $markup .= '<a href="'.$elementUrl.'">'.$elementUrl.'</a>';
  858. $offset = strlen($matches[0]);
  859. }
  860. elseif (strpos($text, '@') > 1 and preg_match('/<(\S+?@\S+?)>/', $text, $matches))
  861. {
  862. $markup .= '<a href="mailto:'.$matches[1].'">'.$matches[1].'</a>';
  863. $offset = strlen($matches[0]);
  864. }
  865. elseif (preg_match('/^<\/?\w.*?>/', $text, $matches))
  866. {
  867. $markup .= $matches[0];
  868. $offset = strlen($matches[0]);
  869. }
  870. else
  871. {
  872. $markup .= '&lt;';
  873. $offset = 1;
  874. }
  875. }
  876. else
  877. {
  878. $markup .= '&lt;';
  879. $offset = 1;
  880. }
  881. break;
  882. case '\\':
  883. if (in_array($text[1], self::$specialCharacters))
  884. {
  885. $markup .= $text[1];
  886. $offset = 2;
  887. }
  888. else
  889. {
  890. $markup .= '\\';
  891. $offset = 1;
  892. }
  893. break;
  894. case '`':
  895. if (preg_match('/^(`+)[ ]*(.+?)[ ]*(?<!`)\1(?!`)/', $text, $matches))
  896. {
  897. $elementText = $matches[2];
  898. $elementText = htmlspecialchars($elementText, ENT_NOQUOTES, 'UTF-8');
  899. $markup .= '<code>'.$elementText.'</code>';
  900. $offset = strlen($matches[0]);
  901. }
  902. else
  903. {
  904. $markup .= '`';
  905. $offset = 1;
  906. }
  907. break;
  908. case 'http':
  909. if (preg_match('/^https?:[\/]{2}[^\s]+\b\/*/ui', $text, $matches))
  910. {
  911. $elementUrl = $matches[0];
  912. $elementUrl = str_replace('&', '&amp;', $elementUrl);
  913. $elementUrl = str_replace('<', '&lt;', $elementUrl);
  914. $markup .= '<a href="'.$elementUrl.'">'.$elementUrl.'</a>';
  915. $offset = strlen($matches[0]);
  916. }
  917. else
  918. {
  919. $markup .= 'http';
  920. $offset = 4;
  921. }
  922. break;
  923. case '~~':
  924. if (preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $text, $matches))
  925. {
  926. $matches[1] = $this->parseLine($matches[1], $markers);
  927. $markup .= '<del>'.$matches[1].'</del>';
  928. $offset = strlen($matches[0]);
  929. }
  930. else
  931. {
  932. $markup .= '~~';
  933. $offset = 2;
  934. }
  935. break;
  936. }
  937. if (isset($offset))
  938. {
  939. $text = substr($text, $offset);
  940. }
  941. $markers[$closestMarkerIndex] = $closestMarker;
  942. }
  943. return $markup;
  944. }
  945. #
  946. # Static
  947. static function instance($name = 'default')
  948. {
  949. if (isset(self::$instances[$name]))
  950. {
  951. return self::$instances[$name];
  952. }
  953. $instance = new Parsedown();
  954. self::$instances[$name] = $instance;
  955. return $instance;
  956. }
  957. private static $instances = array();
  958. #
  959. # Fields
  960. #
  961. private $referenceMap = array();
  962. #
  963. # Read-only
  964. private static $strongRegex = array(
  965. '*' => '/^[*]{2}((?:[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
  966. '_' => '/^__((?:[^_]|_[^_]*_)+?)__(?!_)/us',
  967. );
  968. private static $emRegex = array(
  969. '*' => '/^[*]((?:[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
  970. '_' => '/^_((?:[^_]|__[^_]*__)+?)_(?!_)\b/us',
  971. );
  972. private static $specialCharacters = array(
  973. '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!',
  974. );
  975. private static $textLevelElements = array(
  976. 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
  977. 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
  978. 'i', 'rp', 'sub', 'code', 'strike', 'marquee',
  979. 'q', 'rt', 'sup', 'font', 'strong',
  980. 's', 'tt', 'var', 'mark',
  981. 'u', 'xm', 'wbr', 'nobr',
  982. 'ruby',
  983. 'span',
  984. 'time',
  985. );
  986. }