Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Parsedown.php 34KB

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