ParsedownExtra.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <?php
  2. /**
  3. * @package Grav\Framework\Parsedown
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Framework\Parsedown;
  9. /*
  10. * Parsedown Extra
  11. * http://parsedown.org
  12. *
  13. * (c) Emanuil Rusev
  14. * http://erusev.com
  15. *
  16. * This file ported from officiall ParsedownExtra repo and kept for compatibility.
  17. */
  18. class ParsedownExtra extends Parsedown
  19. {
  20. # ~
  21. const version = '0.7.0';
  22. # ~
  23. function __construct()
  24. {
  25. if (parent::version < '1.5.0')
  26. {
  27. throw new Exception('ParsedownExtra requires a later version of Parsedown');
  28. }
  29. $this->BlockTypes[':'] []= 'DefinitionList';
  30. $this->BlockTypes['*'] []= 'Abbreviation';
  31. # identify footnote definitions before reference definitions
  32. array_unshift($this->BlockTypes['['], 'Footnote');
  33. # identify footnote markers before before links
  34. array_unshift($this->InlineTypes['['], 'FootnoteMarker');
  35. }
  36. #
  37. # ~
  38. function text($text)
  39. {
  40. $markup = parent::text($text);
  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 isset($Block['type']))
  115. {
  116. return;
  117. }
  118. $Element = array(
  119. 'name' => 'dl',
  120. 'handler' => 'elements',
  121. 'text' => array(),
  122. );
  123. $terms = explode("\n", $Block['element']['text']);
  124. foreach ($terms as $term)
  125. {
  126. $Element['text'] []= array(
  127. 'name' => 'dt',
  128. 'handler' => 'line',
  129. 'text' => $term,
  130. );
  131. }
  132. $Block['element'] = $Element;
  133. $Block = $this->addDdElement($Line, $Block);
  134. return $Block;
  135. }
  136. protected function blockDefinitionListContinue($Line, array $Block)
  137. {
  138. if ($Line['text'][0] === ':')
  139. {
  140. $Block = $this->addDdElement($Line, $Block);
  141. return $Block;
  142. }
  143. else
  144. {
  145. if (isset($Block['interrupted']) and $Line['indent'] === 0)
  146. {
  147. return;
  148. }
  149. if (isset($Block['interrupted']))
  150. {
  151. $Block['dd']['handler'] = 'text';
  152. $Block['dd']['text'] .= "\n\n";
  153. unset($Block['interrupted']);
  154. }
  155. $text = substr($Line['body'], min($Line['indent'], 4));
  156. $Block['dd']['text'] .= "\n" . $text;
  157. return $Block;
  158. }
  159. }
  160. #
  161. # Header
  162. protected function blockHeader($Line)
  163. {
  164. $Block = parent::blockHeader($Line);
  165. if ($Block !== null && preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
  166. {
  167. $attributeString = $matches[1][0];
  168. $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
  169. $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
  170. }
  171. return $Block;
  172. }
  173. #
  174. # Markup
  175. protected function blockMarkupComplete($Block)
  176. {
  177. if ( ! isset($Block['void']))
  178. {
  179. $Block['markup'] = $this->processTag($Block['markup']);
  180. }
  181. return $Block;
  182. }
  183. #
  184. # Setext
  185. protected function blockSetextHeader($Line, array $Block = null)
  186. {
  187. $Block = parent::blockSetextHeader($Line, $Block);
  188. if ($Block !== null && preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
  189. {
  190. $attributeString = $matches[1][0];
  191. $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
  192. $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
  193. }
  194. return $Block;
  195. }
  196. #
  197. # Inline Elements
  198. #
  199. #
  200. # Footnote Marker
  201. protected function inlineFootnoteMarker($Excerpt)
  202. {
  203. if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches))
  204. {
  205. $name = $matches[1];
  206. if ( ! isset($this->DefinitionData['Footnote'][$name]))
  207. {
  208. return;
  209. }
  210. $this->DefinitionData['Footnote'][$name]['count'] ++;
  211. if ( ! isset($this->DefinitionData['Footnote'][$name]['number']))
  212. {
  213. $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » &
  214. }
  215. $Element = array(
  216. 'name' => 'sup',
  217. 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name),
  218. 'handler' => 'element',
  219. 'text' => array(
  220. 'name' => 'a',
  221. 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'),
  222. 'text' => $this->DefinitionData['Footnote'][$name]['number'],
  223. ),
  224. );
  225. return array(
  226. 'extent' => strlen($matches[0]),
  227. 'element' => $Element,
  228. );
  229. }
  230. }
  231. private $footnoteCount = 0;
  232. #
  233. # Link
  234. protected function inlineLink($Excerpt)
  235. {
  236. $Link = parent::inlineLink($Excerpt);
  237. $remainder = $Link !== null ? substr($Excerpt['text'], $Link['extent']) : '';
  238. if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
  239. {
  240. $Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
  241. $Link['extent'] += strlen($matches[0]);
  242. }
  243. return $Link;
  244. }
  245. #
  246. # ~
  247. #
  248. protected function unmarkedText($text)
  249. {
  250. $text = parent::unmarkedText($text);
  251. if (isset($this->DefinitionData['Abbreviation']))
  252. {
  253. foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
  254. {
  255. $pattern = '/\b'.preg_quote($abbreviation, '/').'\b/';
  256. $text = preg_replace($pattern, '<abbr title="'.$meaning.'">'.$abbreviation.'</abbr>', $text);
  257. }
  258. }
  259. return $text;
  260. }
  261. #
  262. # Util Methods
  263. #
  264. protected function addDdElement(array $Line, array $Block)
  265. {
  266. $text = substr($Line['text'], 1);
  267. $text = trim($text);
  268. unset($Block['dd']);
  269. $Block['dd'] = array(
  270. 'name' => 'dd',
  271. 'handler' => 'line',
  272. 'text' => $text,
  273. );
  274. if (isset($Block['interrupted']))
  275. {
  276. $Block['dd']['handler'] = 'text';
  277. unset($Block['interrupted']);
  278. }
  279. $Block['element']['text'] []= & $Block['dd'];
  280. return $Block;
  281. }
  282. protected function buildFootnoteElement()
  283. {
  284. $Element = array(
  285. 'name' => 'div',
  286. 'attributes' => array('class' => 'footnotes'),
  287. 'handler' => 'elements',
  288. 'text' => array(
  289. array(
  290. 'name' => 'hr',
  291. ),
  292. array(
  293. 'name' => 'ol',
  294. 'handler' => 'elements',
  295. 'text' => array(),
  296. ),
  297. ),
  298. );
  299. uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
  300. foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
  301. {
  302. if ( ! isset($DefinitionData['number']))
  303. {
  304. continue;
  305. }
  306. $text = $DefinitionData['text'];
  307. $text = parent::text($text);
  308. $numbers = range(1, $DefinitionData['count']);
  309. $backLinksMarkup = '';
  310. foreach ($numbers as $number)
  311. {
  312. $backLinksMarkup .= ' <a href="#fnref'.$number.':'.$definitionId.'" rev="footnote" class="footnote-backref">&#8617;</a>';
  313. }
  314. $backLinksMarkup = substr($backLinksMarkup, 1);
  315. if (substr($text, - 4) === '</p>')
  316. {
  317. $backLinksMarkup = '&#160;'.$backLinksMarkup;
  318. $text = substr_replace($text, $backLinksMarkup.'</p>', - 4);
  319. }
  320. else
  321. {
  322. $text .= "\n".'<p>'.$backLinksMarkup.'</p>';
  323. }
  324. $Element['text'][1]['text'] []= array(
  325. 'name' => 'li',
  326. 'attributes' => array('id' => 'fn:'.$definitionId),
  327. 'text' => "\n".$text."\n",
  328. );
  329. }
  330. return $Element;
  331. }
  332. # ~
  333. protected function parseAttributeData($attributeString)
  334. {
  335. $Data = array();
  336. $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
  337. foreach ($attributes as $attribute)
  338. {
  339. if ($attribute[0] === '#')
  340. {
  341. $Data['id'] = substr($attribute, 1);
  342. }
  343. else # "."
  344. {
  345. $classes []= substr($attribute, 1);
  346. }
  347. }
  348. if (isset($classes))
  349. {
  350. $Data['class'] = implode(' ', $classes);
  351. }
  352. return $Data;
  353. }
  354. # ~
  355. protected function processTag($elementMarkup) # recursive
  356. {
  357. # http://stackoverflow.com/q/1148928/200145
  358. libxml_use_internal_errors(true);
  359. $DOMDocument = new \DOMDocument;
  360. # http://stackoverflow.com/q/11309194/200145
  361. $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
  362. # http://stackoverflow.com/q/4879946/200145
  363. $DOMDocument->loadHTML($elementMarkup);
  364. $DOMDocument->removeChild($DOMDocument->doctype);
  365. $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
  366. $elementText = '';
  367. if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
  368. {
  369. foreach ($DOMDocument->documentElement->childNodes as $Node)
  370. {
  371. $elementText .= $DOMDocument->saveHTML($Node);
  372. }
  373. $DOMDocument->documentElement->removeAttribute('markdown');
  374. $elementText = "\n".$this->text($elementText)."\n";
  375. }
  376. else
  377. {
  378. foreach ($DOMDocument->documentElement->childNodes as $Node)
  379. {
  380. $nodeMarkup = $DOMDocument->saveHTML($Node);
  381. if ($Node instanceof \DOMElement and ! in_array($Node->nodeName, $this->textLevelElements))
  382. {
  383. $elementText .= $this->processTag($nodeMarkup);
  384. }
  385. else
  386. {
  387. $elementText .= $nodeMarkup;
  388. }
  389. }
  390. }
  391. # because we don't want for markup to get encoded
  392. $DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
  393. $markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
  394. $markup = str_replace('placeholder\x1A', $elementText, $markup);
  395. return $markup;
  396. }
  397. # ~
  398. protected function sortFootnotes($A, $B) # callback
  399. {
  400. return $A['number'] - $B['number'];
  401. }
  402. #
  403. # Fields
  404. #
  405. protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
  406. }