*/}}

ParsedownExtra.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  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. if(!isset($Link['extent'])) return $Link;
  316. $remainder = substr($Excerpt['text'], $Link['extent']);
  317. if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
  318. {
  319. $Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
  320. $Link['extent'] += strlen($matches[0]);
  321. }
  322. return $Link;
  323. }
  324. #
  325. # ~
  326. #
  327. private $currentAbreviation;
  328. private $currentMeaning;
  329. protected function insertAbreviation(array $Element)
  330. {
  331. if (isset($Element['text']))
  332. {
  333. $Element['elements'] = self::pregReplaceElements(
  334. '/\b'.preg_quote($this->currentAbreviation, '/').'\b/',
  335. array(
  336. array(
  337. 'name' => 'abbr',
  338. 'attributes' => array(
  339. 'title' => $this->currentMeaning,
  340. ),
  341. 'text' => $this->currentAbreviation,
  342. )
  343. ),
  344. $Element['text']
  345. );
  346. unset($Element['text']);
  347. }
  348. return $Element;
  349. }
  350. protected function inlineText($text)
  351. {
  352. $Inline = parent::inlineText($text);
  353. if (isset($this->DefinitionData['Abbreviation']))
  354. {
  355. foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
  356. {
  357. $this->currentAbreviation = $abbreviation;
  358. $this->currentMeaning = $meaning;
  359. $Inline['element'] = $this->elementApplyRecursiveDepthFirst(
  360. array($this, 'insertAbreviation'),
  361. $Inline['element']
  362. );
  363. }
  364. }
  365. return $Inline;
  366. }
  367. #
  368. # Util Methods
  369. #
  370. protected function addDdElement(array $Line, array $Block)
  371. {
  372. $text = substr($Line['text'], 1);
  373. $text = trim($text);
  374. unset($Block['dd']);
  375. $Block['dd'] = array(
  376. 'name' => 'dd',
  377. 'handler' => array(
  378. 'function' => 'lineElements',
  379. 'argument' => $text,
  380. 'destination' => 'elements'
  381. ),
  382. );
  383. if (isset($Block['interrupted']))
  384. {
  385. $Block['dd']['handler']['function'] = 'textElements';
  386. unset($Block['interrupted']);
  387. }
  388. $Block['element']['elements'] []= & $Block['dd'];
  389. return $Block;
  390. }
  391. protected function buildFootnoteElement()
  392. {
  393. $Element = array(
  394. 'name' => 'div',
  395. 'attributes' => array('class' => 'footnotes'),
  396. 'elements' => array(
  397. array('name' => 'hr'),
  398. array(
  399. 'name' => 'ol',
  400. 'elements' => array(),
  401. ),
  402. ),
  403. );
  404. uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
  405. foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
  406. {
  407. if ( ! isset($DefinitionData['number']))
  408. {
  409. continue;
  410. }
  411. $text = $DefinitionData['text'];
  412. $textElements = parent::textElements($text);
  413. $numbers = range(1, $DefinitionData['count']);
  414. $backLinkElements = array();
  415. foreach ($numbers as $number)
  416. {
  417. $backLinkElements[] = array('text' => ' ');
  418. $backLinkElements[] = array(
  419. 'name' => 'a',
  420. 'attributes' => array(
  421. 'href' => "#fnref$number:$definitionId",
  422. 'rev' => 'footnote',
  423. 'class' => 'footnote-backref',
  424. ),
  425. 'rawHtml' => '&#8617;',
  426. 'allowRawHtmlInSafeMode' => true,
  427. 'autobreak' => false,
  428. );
  429. }
  430. unset($backLinkElements[0]);
  431. $n = count($textElements) -1;
  432. if ($textElements[$n]['name'] === 'p')
  433. {
  434. $backLinkElements = array_merge(
  435. array(
  436. array(
  437. 'rawHtml' => '&#160;',
  438. 'allowRawHtmlInSafeMode' => true,
  439. ),
  440. ),
  441. $backLinkElements
  442. );
  443. unset($textElements[$n]['name']);
  444. $textElements[$n] = array(
  445. 'name' => 'p',
  446. 'elements' => array_merge(
  447. array($textElements[$n]),
  448. $backLinkElements
  449. ),
  450. );
  451. }
  452. else
  453. {
  454. $textElements[] = array(
  455. 'name' => 'p',
  456. 'elements' => $backLinkElements
  457. );
  458. }
  459. $Element['elements'][1]['elements'] []= array(
  460. 'name' => 'li',
  461. 'attributes' => array('id' => 'fn:'.$definitionId),
  462. 'elements' => array_merge(
  463. $textElements
  464. ),
  465. );
  466. }
  467. return $Element;
  468. }
  469. # ~
  470. protected function parseAttributeData($attributeString)
  471. {
  472. $Data = array();
  473. $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
  474. foreach ($attributes as $attribute)
  475. {
  476. if ($attribute[0] === '#')
  477. {
  478. $Data['id'] = substr($attribute, 1);
  479. }
  480. else # "."
  481. {
  482. $classes []= substr($attribute, 1);
  483. }
  484. }
  485. if (isset($classes))
  486. {
  487. $Data['class'] = implode(' ', $classes);
  488. }
  489. return $Data;
  490. }
  491. # ~
  492. protected function processTag($elementMarkup) # recursive
  493. {
  494. # http://stackoverflow.com/q/1148928/200145
  495. libxml_use_internal_errors(true);
  496. $DOMDocument = new DOMDocument;
  497. # http://stackoverflow.com/q/11309194/200145
  498. $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
  499. # http://stackoverflow.com/q/4879946/200145
  500. $DOMDocument->loadHTML($elementMarkup);
  501. $DOMDocument->removeChild($DOMDocument->doctype);
  502. $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
  503. $elementText = '';
  504. if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
  505. {
  506. foreach ($DOMDocument->documentElement->childNodes as $Node)
  507. {
  508. $elementText .= $DOMDocument->saveHTML($Node);
  509. }
  510. $DOMDocument->documentElement->removeAttribute('markdown');
  511. $elementText = "\n".$this->text($elementText)."\n";
  512. }
  513. else
  514. {
  515. foreach ($DOMDocument->documentElement->childNodes as $Node)
  516. {
  517. $nodeMarkup = $DOMDocument->saveHTML($Node);
  518. if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements))
  519. {
  520. $elementText .= $this->processTag($nodeMarkup);
  521. }
  522. else
  523. {
  524. $elementText .= $nodeMarkup;
  525. }
  526. }
  527. }
  528. # because we don't want for markup to get encoded
  529. $DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
  530. $markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
  531. $markup = str_replace('placeholder\x1A', $elementText, $markup);
  532. return $markup;
  533. }
  534. # ~
  535. protected function sortFootnotes($A, $B) # callback
  536. {
  537. return $A['number'] - $B['number'];
  538. }
  539. #
  540. # Fields
  541. #
  542. protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
  543. }