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

/plugins/Field/src/Lib/Parsedown.php

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