ParsedownExtra.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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.7.0';
  18. # ~
  19. function __construct()
  20. {
  21. if (parent::version < '1.5.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. $markup = parent::text($text);
  37. # merge consecutive dl elements
  38. $markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
  39. # add footnotes
  40. if (isset($this->DefinitionData['Footnote']))
  41. {
  42. $Element = $this->buildFootnoteElement();
  43. $markup .= "\n" . $this->element($Element);
  44. }
  45. return $markup;
  46. }
  47. #
  48. # Blocks
  49. #
  50. #
  51. # Abbreviation
  52. protected function blockAbbreviation($Line)
  53. {
  54. if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
  55. {
  56. $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
  57. $Block = array(
  58. 'hidden' => true,
  59. );
  60. return $Block;
  61. }
  62. }
  63. #
  64. # Footnote
  65. protected function blockFootnote($Line)
  66. {
  67. if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
  68. {
  69. $Block = array(
  70. 'label' => $matches[1],
  71. 'text' => $matches[2],
  72. 'hidden' => true,
  73. );
  74. return $Block;
  75. }
  76. }
  77. protected function blockFootnoteContinue($Line, $Block)
  78. {
  79. if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text']))
  80. {
  81. return;
  82. }
  83. if (isset($Block['interrupted']))
  84. {
  85. if ($Line['indent'] >= 4)
  86. {
  87. $Block['text'] .= "\n\n" . $Line['text'];
  88. return $Block;
  89. }
  90. }
  91. else
  92. {
  93. $Block['text'] .= "\n" . $Line['text'];
  94. return $Block;
  95. }
  96. }
  97. protected function blockFootnoteComplete($Block)
  98. {
  99. $this->DefinitionData['Footnote'][$Block['label']] = array(
  100. 'text' => $Block['text'],
  101. 'count' => null,
  102. 'number' => null,
  103. );
  104. return $Block;
  105. }
  106. #
  107. # Definition List
  108. protected function blockDefinitionList($Line, $Block)
  109. {
  110. if ( ! isset($Block) or isset($Block['type']))
  111. {
  112. return;
  113. }
  114. $Element = array(
  115. 'name' => 'dl',
  116. 'handler' => 'elements',
  117. 'text' => array(),
  118. );
  119. $terms = explode("\n", $Block['element']['text']);
  120. foreach ($terms as $term)
  121. {
  122. $Element['text'] []= array(
  123. 'name' => 'dt',
  124. 'handler' => 'line',
  125. 'text' => $term,
  126. );
  127. }
  128. $Block['element'] = $Element;
  129. $Block = $this->addDdElement($Line, $Block);
  130. return $Block;
  131. }
  132. protected function blockDefinitionListContinue($Line, array $Block)
  133. {
  134. if ($Line['text'][0] === ':')
  135. {
  136. $Block = $this->addDdElement($Line, $Block);
  137. return $Block;
  138. }
  139. else
  140. {
  141. if (isset($Block['interrupted']) and $Line['indent'] === 0)
  142. {
  143. return;
  144. }
  145. if (isset($Block['interrupted']))
  146. {
  147. $Block['dd']['handler'] = 'text';
  148. $Block['dd']['text'] .= "\n\n";
  149. unset($Block['interrupted']);
  150. }
  151. $text = substr($Line['body'], min($Line['indent'], 4));
  152. $Block['dd']['text'] .= "\n" . $text;
  153. return $Block;
  154. }
  155. }
  156. #
  157. # Header
  158. protected function blockHeader($Line)
  159. {
  160. $Block = parent::blockHeader($Line);
  161. if (preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
  162. {
  163. $attributeString = $matches[1][0];
  164. $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
  165. $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
  166. }
  167. return $Block;
  168. }
  169. #
  170. # Markup
  171. protected function blockMarkupComplete($Block)
  172. {
  173. if ( ! isset($Block['void']))
  174. {
  175. $Block['markup'] = $this->processTag($Block['markup']);
  176. }
  177. return $Block;
  178. }
  179. #
  180. # Setext
  181. protected function blockSetextHeader($Line, array $Block = null)
  182. {
  183. $Block = parent::blockSetextHeader($Line, $Block);
  184. if (preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
  185. {
  186. $attributeString = $matches[1][0];
  187. $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
  188. $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
  189. }
  190. return $Block;
  191. }
  192. #
  193. # Inline Elements
  194. #
  195. #
  196. # Footnote Marker
  197. protected function inlineFootnoteMarker($Excerpt)
  198. {
  199. if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches))
  200. {
  201. $name = $matches[1];
  202. if ( ! isset($this->DefinitionData['Footnote'][$name]))
  203. {
  204. return;
  205. }
  206. $this->DefinitionData['Footnote'][$name]['count'] ++;
  207. if ( ! isset($this->DefinitionData['Footnote'][$name]['number']))
  208. {
  209. $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » &
  210. }
  211. $Element = array(
  212. 'name' => 'sup',
  213. 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name),
  214. 'handler' => 'element',
  215. 'text' => array(
  216. 'name' => 'a',
  217. 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'),
  218. 'text' => $this->DefinitionData['Footnote'][$name]['number'],
  219. ),
  220. );
  221. return array(
  222. 'extent' => strlen($matches[0]),
  223. 'element' => $Element,
  224. );
  225. }
  226. }
  227. private $footnoteCount = 0;
  228. #
  229. # Link
  230. protected function inlineLink($Excerpt)
  231. {
  232. $Link = parent::inlineLink($Excerpt);
  233. $remainder = substr($Excerpt['text'], $Link['extent']);
  234. if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
  235. {
  236. $Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
  237. $Link['extent'] += strlen($matches[0]);
  238. }
  239. return $Link;
  240. }
  241. #
  242. # ~
  243. #
  244. protected function unmarkedText($text)
  245. {
  246. $text = parent::unmarkedText($text);
  247. if (isset($this->DefinitionData['Abbreviation']))
  248. {
  249. foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
  250. {
  251. $pattern = '/\b'.preg_quote($abbreviation, '/').'\b/';
  252. $text = preg_replace($pattern, '<abbr title="'.$meaning.'">'.$abbreviation.'</abbr>', $text);
  253. }
  254. }
  255. return $text;
  256. }
  257. #
  258. # Util Methods
  259. #
  260. protected function addDdElement(array $Line, array $Block)
  261. {
  262. $text = substr($Line['text'], 1);
  263. $text = trim($text);
  264. unset($Block['dd']);
  265. $Block['dd'] = array(
  266. 'name' => 'dd',
  267. 'handler' => 'line',
  268. 'text' => $text,
  269. );
  270. if (isset($Block['interrupted']))
  271. {
  272. $Block['dd']['handler'] = 'text';
  273. unset($Block['interrupted']);
  274. }
  275. $Block['element']['text'] []= & $Block['dd'];
  276. return $Block;
  277. }
  278. protected function buildFootnoteElement()
  279. {
  280. $Element = array(
  281. 'name' => 'div',
  282. 'attributes' => array('class' => 'footnotes'),
  283. 'handler' => 'elements',
  284. 'text' => array(
  285. array(
  286. 'name' => 'hr',
  287. ),
  288. array(
  289. 'name' => 'ol',
  290. 'handler' => 'elements',
  291. 'text' => array(),
  292. ),
  293. ),
  294. );
  295. uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
  296. foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
  297. {
  298. if ( ! isset($DefinitionData['number']))
  299. {
  300. continue;
  301. }
  302. $text = $DefinitionData['text'];
  303. $text = parent::text($text);
  304. $numbers = range(1, $DefinitionData['count']);
  305. $backLinksMarkup = '';
  306. foreach ($numbers as $number)
  307. {
  308. $backLinksMarkup .= ' <a href="#fnref'.$number.':'.$definitionId.'" rev="footnote" class="footnote-backref">&#8617;</a>';
  309. }
  310. $backLinksMarkup = substr($backLinksMarkup, 1);
  311. if (substr($text, - 4) === '</p>')
  312. {
  313. $backLinksMarkup = '&#160;'.$backLinksMarkup;
  314. $text = substr_replace($text, $backLinksMarkup.'</p>', - 4);
  315. }
  316. else
  317. {
  318. $text .= "\n".'<p>'.$backLinksMarkup.'</p>';
  319. }
  320. $Element['text'][1]['text'] []= array(
  321. 'name' => 'li',
  322. 'attributes' => array('id' => 'fn:'.$definitionId),
  323. 'text' => "\n".$text."\n",
  324. );
  325. }
  326. return $Element;
  327. }
  328. # ~
  329. protected function parseAttributeData($attributeString)
  330. {
  331. $Data = array();
  332. $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
  333. foreach ($attributes as $attribute)
  334. {
  335. if ($attribute[0] === '#')
  336. {
  337. $Data['id'] = substr($attribute, 1);
  338. }
  339. else # "."
  340. {
  341. $classes []= substr($attribute, 1);
  342. }
  343. }
  344. if (isset($classes))
  345. {
  346. $Data['class'] = implode(' ', $classes);
  347. }
  348. return $Data;
  349. }
  350. # ~
  351. protected function processTag($elementMarkup) # recursive
  352. {
  353. # http://stackoverflow.com/q/1148928/200145
  354. libxml_use_internal_errors(true);
  355. $DOMDocument = new DOMDocument;
  356. # http://stackoverflow.com/q/11309194/200145
  357. $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
  358. # http://stackoverflow.com/q/4879946/200145
  359. $DOMDocument->loadHTML($elementMarkup);
  360. $DOMDocument->removeChild($DOMDocument->doctype);
  361. $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
  362. $elementText = '';
  363. if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
  364. {
  365. foreach ($DOMDocument->documentElement->childNodes as $Node)
  366. {
  367. $elementText .= $DOMDocument->saveHTML($Node);
  368. }
  369. $DOMDocument->documentElement->removeAttribute('markdown');
  370. $elementText = "\n".$this->text($elementText)."\n";
  371. }
  372. else
  373. {
  374. foreach ($DOMDocument->documentElement->childNodes as $Node)
  375. {
  376. $nodeMarkup = $DOMDocument->saveHTML($Node);
  377. if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements))
  378. {
  379. $elementText .= $this->processTag($nodeMarkup);
  380. }
  381. else
  382. {
  383. $elementText .= $nodeMarkup;
  384. }
  385. }
  386. }
  387. # because we don't want for markup to get encoded
  388. $DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
  389. $markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
  390. $markup = str_replace('placeholder\x1A', $elementText, $markup);
  391. return $markup;
  392. }
  393. # ~
  394. protected function sortFootnotes($A, $B) # callback
  395. {
  396. return $A['number'] - $B['number'];
  397. }
  398. #
  399. # Fields
  400. #
  401. protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
  402. }