*/}}

ParsedownExtra.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. <?php
  2. #
  3. #
  4. # Parsedown Extra
  5. # https://github.com/erusev/parsedown-extra
  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 ParsedownExtra extends Parsedown
  15. {
  16. # ~
  17. const version = '0.8.0-beta-1';
  18. # ~
  19. function __construct()
  20. {
  21. if (version_compare(parent::version, '1.7.1') < 0)
  22. {
  23. throw new Exception('ParsedownExtra requires a later version of Parsedown');
  24. }
  25. $this->BlockTypes[':'] []= 'DefinitionList';
  26. $this->BlockTypes['*'] []= 'Abbreviation';
  27. # identify footnote definitions before reference definitions
  28. array_unshift($this->BlockTypes['['], 'Footnote');
  29. # identify footnote markers before before links
  30. array_unshift($this->InlineTypes['['], 'FootnoteMarker');
  31. }
  32. #
  33. # ~
  34. function text($text)
  35. {
  36. $Elements = $this->textElements($text);
  37. # convert to markup
  38. $markup = $this->elements($Elements);
  39. # trim line breaks
  40. $markup = trim($markup, "\n");
  41. # merge consecutive dl elements
  42. $markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
  43. # add footnotes
  44. if (isset($this->DefinitionData['Footnote']))
  45. {
  46. $Element = $this->buildFootnoteElement();
  47. $markup .= "\n" . $this->element($Element);
  48. }
  49. return $markup;
  50. }
  51. #
  52. # Blocks
  53. #
  54. #
  55. # Abbreviation
  56. protected function blockAbbreviation($Line)
  57. {
  58. if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
  59. {
  60. $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
  61. $Block = array(
  62. 'hidden' => true,
  63. );
  64. return $Block;
  65. }
  66. }
  67. #
  68. # Footnote
  69. protected function blockFootnote($Line)
  70. {
  71. if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
  72. {
  73. $Block = array(
  74. 'label' => $matches[1],
  75. 'text' => $matches[2],
  76. 'hidden' => true,
  77. );
  78. return $Block;
  79. }
  80. }
  81. protected function blockFootnoteContinue($Line, $Block)
  82. {
  83. if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text']))
  84. {
  85. return;
  86. }
  87. if (isset($Block['interrupted']))
  88. {
  89. if ($Line['indent'] >= 4)
  90. {
  91. $Block['text'] .= "\n\n" . $Line['text'];
  92. return $Block;
  93. }
  94. }
  95. else
  96. {
  97. $Block['text'] .= "\n" . $Line['text'];
  98. return $Block;
  99. }
  100. }
  101. protected function blockFootnoteComplete($Block)
  102. {
  103. $this->DefinitionData['Footnote'][$Block['label']] = array(
  104. 'text' => $Block['text'],
  105. 'count' => null,
  106. 'number' => null,
  107. );
  108. return $Block;
  109. }
  110. #
  111. # Definition List
  112. protected function blockDefinitionList($Line, $Block)
  113. {
  114. if ( ! isset($Block) or $Block['type'] !== 'Paragraph')
  115. {
  116. return;
  117. }
  118. $Element = array(
  119. 'name' => 'dl',
  120. 'elements' => array(),
  121. );
  122. $terms = explode("\n", $Block['element']['handler']['argument']);
  123. foreach ($terms as $term)
  124. {
  125. $Element['elements'] []= array(
  126. 'name' => 'dt',
  127. 'handler' => array(
  128. 'function' => 'lineElements',
  129. 'argument' => $term,
  130. 'destination' => 'elements'
  131. ),
  132. );
  133. }
  134. $Block['element'] = $Element;
  135. $Block = $this->addDdElement($Line, $Block);
  136. return $Block;
  137. }
  138. protected function blockDefinitionListContinue($Line, array $Block)
  139. {
  140. if ($Line['text'][0] === ':')
  141. {
  142. $Block = $this->addDdElement($Line, $Block);
  143. return $Block;
  144. }
  145. else
  146. {
  147. if (isset($Block['interrupted']) and $Line['indent'] === 0)
  148. {
  149. return;
  150. }
  151. if (isset($Block['interrupted']))
  152. {
  153. $Block['dd']['handler']['function'] = 'textElements';
  154. $Block['dd']['handler']['argument'] .= "\n\n";
  155. $Block['dd']['handler']['destination'] = 'elements';
  156. unset($Block['interrupted']);
  157. }
  158. $text = substr($Line['body'], min($Line['indent'], 4));
  159. $Block['dd']['handler']['argument'] .= "\n" . $text;
  160. return $Block;
  161. }
  162. }
  163. #
  164. # Header
  165. protected function blockHeader($Line)
  166. {
  167. $Block = parent::blockHeader($Line);
  168. if (preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))
  169. {
  170. $attributeString = $matches[1][0];
  171. $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
  172. $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
  173. }
  174. return $Block;
  175. }
  176. #
  177. # Markup
  178. protected function blockMarkup($Line)
  179. {
  180. if ($this->markupEscaped or $this->safeMode)
  181. {
  182. return;
  183. }
  184. if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
  185. {
  186. $element = strtolower($matches[1]);
  187. if (in_array($element, $this->textLevelElements))
  188. {
  189. return;
  190. }
  191. $Block = array(
  192. 'name' => $matches[1],
  193. 'depth' => 0,
  194. 'element' => array(
  195. 'rawHtml' => $Line['text'],
  196. 'autobreak' => true,
  197. ),
  198. );
  199. $length = strlen($matches[0]);
  200. $remainder = substr($Line['text'], $length);
  201. if (trim($remainder) === '')
  202. {
  203. if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
  204. {
  205. $Block['closed'] = true;
  206. $Block['void'] = true;
  207. }
  208. }
  209. else
  210. {
  211. if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
  212. {
  213. return;
  214. }
  215. if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
  216. {
  217. $Block['closed'] = true;
  218. }
  219. }
  220. return $Block;
  221. }
  222. }
  223. protected function blockMarkupContinue($Line, array $Block)
  224. {
  225. if (isset($Block['closed']))
  226. {
  227. return;
  228. }
  229. if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
  230. {
  231. $Block['depth'] ++;
  232. }
  233. if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
  234. {
  235. if ($Block['depth'] > 0)
  236. {
  237. $Block['depth'] --;
  238. }
  239. else
  240. {
  241. $Block['closed'] = true;
  242. }
  243. }
  244. if (isset($Block['interrupted']))
  245. {
  246. $Block['element']['rawHtml'] .= "\n";
  247. unset($Block['interrupted']);
  248. }
  249. $Block['element']['rawHtml'] .= "\n".$Line['body'];
  250. return $Block;
  251. }
  252. protected function blockMarkupComplete($Block)
  253. {
  254. if ( ! isset($Block['void']))
  255. {
  256. $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']);
  257. }
  258. return $Block;
  259. }
  260. #
  261. # Setext
  262. protected function blockSetextHeader($Line, array $Block = null)
  263. {
  264. $Block = parent::blockSetextHeader($Line, $Block);
  265. //Yiming: prevent error
  266. if(!$Block) return NULL;
  267. if (preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))
  268. {
  269. $attributeString = $matches[1][0];
  270. $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
  271. $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
  272. }
  273. return $Block;
  274. }
  275. #
  276. # Inline Elements
  277. #
  278. #
  279. # Footnote Marker
  280. protected function inlineFootnoteMarker($Excerpt)
  281. {
  282. if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches))
  283. {
  284. $name = $matches[1];
  285. if ( ! isset($this->DefinitionData['Footnote'][$name]))
  286. {
  287. return;
  288. }
  289. $this->DefinitionData['Footnote'][$name]['count'] ++;
  290. if ( ! isset($this->DefinitionData['Footnote'][$name]['number']))
  291. {
  292. $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » &
  293. }
  294. $Element = array(
  295. 'name' => 'sup',
  296. 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name),
  297. 'element' => array(
  298. 'name' => 'a',
  299. 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'),
  300. 'text' => $this->DefinitionData['Footnote'][$name]['number'],
  301. ),
  302. );
  303. return array(
  304. 'extent' => strlen($matches[0]),
  305. 'element' => $Element,
  306. );
  307. }
  308. }
  309. private $footnoteCount = 0;
  310. #
  311. # Link
  312. protected function inlineLink($Excerpt)
  313. {
  314. $Link = parent::inlineLink($Excerpt);
  315. $remainder = substr($Excerpt['text'], $Link['extent']);
  316. if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
  317. {
  318. $Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
  319. $Link['extent'] += strlen($matches[0]);
  320. }
  321. return $Link;
  322. }
  323. #
  324. # ~
  325. #
  326. private $currentAbreviation;
  327. private $currentMeaning;
  328. protected function insertAbreviation(array $Element)
  329. {
  330. if (isset($Element['text']))
  331. {
  332. $Element['elements'] = self::pregReplaceElements(
  333. '/\b'.preg_quote($this->currentAbreviation, '/').'\b/',
  334. array(
  335. array(
  336. 'name' => 'abbr',
  337. 'attributes' => array(
  338. 'title' => $this->currentMeaning,
  339. ),
  340. 'text' => $this->currentAbreviation,
  341. )
  342. ),
  343. $Element['text']
  344. );
  345. unset($Element['text']);
  346. }
  347. return $Element;
  348. }
  349. protected function inlineText($text)
  350. {
  351. $Inline = parent::inlineText($text);
  352. if (isset($this->DefinitionData['Abbreviation']))
  353. {
  354. foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
  355. {
  356. $this->currentAbreviation = $abbreviation;
  357. $this->currentMeaning = $meaning;
  358. $Inline['element'] = $this->elementApplyRecursiveDepthFirst(
  359. array($this, 'insertAbreviation'),
  360. $Inline['element']
  361. );
  362. }
  363. }
  364. return $Inline;
  365. }
  366. #
  367. # Util Methods
  368. #
  369. protected function addDdElement(array $Line, array $Block)
  370. {
  371. $text = substr($Line['text'], 1);
  372. $text = trim($text);
  373. unset($Block['dd']);
  374. $Block['dd'] = array(
  375. 'name' => 'dd',
  376. 'handler' => array(
  377. 'function' => 'lineElements',
  378. 'argument' => $text,
  379. 'destination' => 'elements'
  380. ),
  381. );
  382. if (isset($Block['interrupted']))
  383. {
  384. $Block['dd']['handler']['function'] = 'textElements';
  385. unset($Block['interrupted']);
  386. }
  387. $Block['element']['elements'] []= & $Block['dd'];
  388. return $Block;
  389. }
  390. protected function buildFootnoteElement()
  391. {
  392. $Element = array(
  393. 'name' => 'div',
  394. 'attributes' => array('class' => 'footnotes'),
  395. 'elements' => array(
  396. array('name' => 'hr'),
  397. array(
  398. 'name' => 'ol',
  399. 'elements' => array(),
  400. ),
  401. ),
  402. );
  403. uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
  404. foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
  405. {
  406. if ( ! isset($DefinitionData['number']))
  407. {
  408. continue;
  409. }
  410. $text = $DefinitionData['text'];
  411. $textElements = parent::textElements($text);
  412. $numbers = range(1, $DefinitionData['count']);
  413. $backLinkElements = array();
  414. foreach ($numbers as $number)
  415. {
  416. $backLinkElements[] = array('text' => ' ');
  417. $backLinkElements[] = array(
  418. 'name' => 'a',
  419. 'attributes' => array(
  420. 'href' => "#fnref$number:$definitionId",
  421. 'rev' => 'footnote',
  422. 'class' => 'footnote-backref',
  423. ),
  424. 'rawHtml' => '&#8617;',
  425. 'allowRawHtmlInSafeMode' => true,
  426. 'autobreak' => false,
  427. );
  428. }
  429. unset($backLinkElements[0]);
  430. $n = count($textElements) -1;
  431. if ($textElements[$n]['name'] === 'p')
  432. {
  433. $backLinkElements = array_merge(
  434. array(
  435. array(
  436. 'rawHtml' => '&#160;',
  437. 'allowRawHtmlInSafeMode' => true,
  438. ),
  439. ),
  440. $backLinkElements
  441. );
  442. unset($textElements[$n]['name']);
  443. $textElements[$n] = array(
  444. 'name' => 'p',
  445. 'elements' => array_merge(
  446. array($textElements[$n]),
  447. $backLinkElements
  448. ),
  449. );
  450. }
  451. else
  452. {
  453. $textElements[] = array(
  454. 'name' => 'p',
  455. 'elements' => $backLinkElements
  456. );
  457. }
  458. $Element['elements'][1]['elements'] []= array(
  459. 'name' => 'li',
  460. 'attributes' => array('id' => 'fn:'.$definitionId),
  461. 'elements' => array_merge(
  462. $textElements
  463. ),
  464. );
  465. }
  466. return $Element;
  467. }
  468. # ~
  469. protected function parseAttributeData($attributeString)
  470. {
  471. $Data = array();
  472. $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
  473. foreach ($attributes as $attribute)
  474. {
  475. if ($attribute[0] === '#')
  476. {
  477. $Data['id'] = substr($attribute, 1);
  478. }
  479. else # "."
  480. {
  481. $classes []= substr($attribute, 1);
  482. }
  483. }
  484. if (isset($classes))
  485. {
  486. $Data['class'] = implode(' ', $classes);
  487. }
  488. return $Data;
  489. }
  490. # ~
  491. protected function processTag($elementMarkup) # recursive
  492. {
  493. # http://stackoverflow.com/q/1148928/200145
  494. libxml_use_internal_errors(true);
  495. $DOMDocument = new DOMDocument;
  496. # http://stackoverflow.com/q/11309194/200145
  497. $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
  498. # http://stackoverflow.com/q/4879946/200145
  499. $DOMDocument->loadHTML($elementMarkup);
  500. $DOMDocument->removeChild($DOMDocument->doctype);
  501. $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
  502. $elementText = '';
  503. if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
  504. {
  505. foreach ($DOMDocument->documentElement->childNodes as $Node)
  506. {
  507. $elementText .= $DOMDocument->saveHTML($Node);
  508. }
  509. $DOMDocument->documentElement->removeAttribute('markdown');
  510. $elementText = "\n".$this->text($elementText)."\n";
  511. }
  512. else
  513. {
  514. foreach ($DOMDocument->documentElement->childNodes as $Node)
  515. {
  516. $nodeMarkup = $DOMDocument->saveHTML($Node);
  517. if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements))
  518. {
  519. $elementText .= $this->processTag($nodeMarkup);
  520. }
  521. else
  522. {
  523. $elementText .= $nodeMarkup;
  524. }
  525. }
  526. }
  527. # because we don't want for markup to get encoded
  528. $DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
  529. $markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
  530. $markup = str_replace('placeholder\x1A', $elementText, $markup);
  531. return $markup;
  532. }
  533. # ~
  534. protected function sortFootnotes($A, $B) # callback
  535. {
  536. return $A['number'] - $B['number'];
  537. }
  538. #
  539. # Fields
  540. #
  541. protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
  542. }