Parsedown.php 38 KB

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