/wp-content/themes/tosin/framework/redux-framework/ReduxCore/inc/fields/raw/parsedown.php

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