ParsedownGravTrait.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <?php
  2. /**
  3. * @package Grav.Common.Markdown
  4. *
  5. * @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Markdown;
  9. use Grav\Common\Grav;
  10. use Grav\Common\Helpers\Excerpts;
  11. use Grav\Common\Page\Page;
  12. use RocketTheme\Toolbox\Event\Event;
  13. trait ParsedownGravTrait
  14. {
  15. /** @var Page $page */
  16. protected $page;
  17. protected $special_chars;
  18. protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/';
  19. public $completable_blocks = [];
  20. public $continuable_blocks = [];
  21. /**
  22. * Initialization function to setup key variables needed by the MarkdownGravLinkTrait
  23. *
  24. * @param $page
  25. * @param $defaults
  26. */
  27. protected function init($page, $defaults)
  28. {
  29. $grav = Grav::instance();
  30. $this->page = $page;
  31. $this->BlockTypes['{'] [] = 'TwigTag';
  32. $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
  33. if ($defaults === null) {
  34. $defaults = Grav::instance()['config']->get('system.pages.markdown');
  35. }
  36. $this->setBreaksEnabled($defaults['auto_line_breaks']);
  37. $this->setUrlsLinked($defaults['auto_url_links']);
  38. $this->setMarkupEscaped($defaults['escape_markup']);
  39. $this->setSpecialChars($defaults['special_chars']);
  40. $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $this]));
  41. }
  42. /**
  43. * Be able to define a new Block type or override an existing one
  44. *
  45. * @param $type
  46. * @param $tag
  47. * @param bool $continuable
  48. * @param bool $completable
  49. * @param $index
  50. */
  51. public function addBlockType($type, $tag, $continuable = false, $completable = false, $index = null)
  52. {
  53. $block = &$this->unmarkedBlockTypes;
  54. if ($type) {
  55. if (!isset($this->BlockTypes[$type])) {
  56. $this->BlockTypes[$type] = [];
  57. }
  58. $block = &$this->BlockTypes[$type];
  59. }
  60. if (null === $index) {
  61. $block[] = $tag;
  62. } else {
  63. array_splice($block, $index, 0, [$tag]);
  64. }
  65. if ($continuable) {
  66. $this->continuable_blocks[] = $tag;
  67. }
  68. if ($completable) {
  69. $this->completable_blocks[] = $tag;
  70. }
  71. }
  72. /**
  73. * Be able to define a new Inline type or override an existing one
  74. *
  75. * @param $type
  76. * @param $tag
  77. * @param $index
  78. */
  79. public function addInlineType($type, $tag, $index = null)
  80. {
  81. if (null === $index || !isset($this->InlineTypes[$type])) {
  82. $this->InlineTypes[$type] [] = $tag;
  83. } else {
  84. array_splice($this->InlineTypes[$type], $index, 0, [$tag]);
  85. }
  86. if (strpos($this->inlineMarkerList, $type) === false) {
  87. $this->inlineMarkerList .= $type;
  88. }
  89. }
  90. /**
  91. * Overrides the default behavior to allow for plugin-provided blocks to be continuable
  92. *
  93. * @param $Type
  94. *
  95. * @return bool
  96. */
  97. protected function isBlockContinuable($Type)
  98. {
  99. $continuable = \in_array($Type, $this->continuable_blocks) || method_exists($this, 'block' . $Type . 'Continue');
  100. return $continuable;
  101. }
  102. /**
  103. * Overrides the default behavior to allow for plugin-provided blocks to be completable
  104. *
  105. * @param $Type
  106. *
  107. * @return bool
  108. */
  109. protected function isBlockCompletable($Type)
  110. {
  111. $completable = \in_array($Type, $this->completable_blocks) || method_exists($this, 'block' . $Type . 'Complete');
  112. return $completable;
  113. }
  114. /**
  115. * Make the element function publicly accessible, Medium uses this to render from Twig
  116. *
  117. * @param array $Element
  118. *
  119. * @return string markup
  120. */
  121. public function elementToHtml(array $Element)
  122. {
  123. return $this->element($Element);
  124. }
  125. /**
  126. * Setter for special chars
  127. *
  128. * @param $special_chars
  129. *
  130. * @return $this
  131. */
  132. public function setSpecialChars($special_chars)
  133. {
  134. $this->special_chars = $special_chars;
  135. return $this;
  136. }
  137. /**
  138. * Ensure Twig tags are treated as block level items with no <p></p> tags
  139. *
  140. * @param array $line
  141. * @return array|null
  142. */
  143. protected function blockTwigTag($line)
  144. {
  145. if (preg_match('/(?:{{|{%|{#)(.*)(?:}}|%}|#})/', $line['body'], $matches)) {
  146. return ['markup' => $line['body']];
  147. }
  148. return null;
  149. }
  150. protected function inlineSpecialCharacter($excerpt)
  151. {
  152. if ($excerpt['text'][0] === '&' && !preg_match('/^&#?\w+;/', $excerpt['text'])) {
  153. return [
  154. 'markup' => '&amp;',
  155. 'extent' => 1,
  156. ];
  157. }
  158. if (isset($this->special_chars[$excerpt['text'][0]])) {
  159. return [
  160. 'markup' => '&' . $this->special_chars[$excerpt['text'][0]] . ';',
  161. 'extent' => 1,
  162. ];
  163. }
  164. return null;
  165. }
  166. protected function inlineImage($excerpt)
  167. {
  168. if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
  169. $excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
  170. $excerpt = parent::inlineImage($excerpt);
  171. $excerpt['element']['attributes']['src'] = $matches[1];
  172. $excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1;
  173. return $excerpt;
  174. }
  175. $excerpt['type'] = 'image';
  176. $excerpt = parent::inlineImage($excerpt);
  177. // if this is an image process it
  178. if (isset($excerpt['element']['attributes']['src'])) {
  179. $excerpt = Excerpts::processImageExcerpt($excerpt, $this->page);
  180. }
  181. return $excerpt;
  182. }
  183. protected function inlineLink($excerpt)
  184. {
  185. if (isset($excerpt['type'])) {
  186. $type = $excerpt['type'];
  187. } else {
  188. $type = 'link';
  189. }
  190. // do some trickery to get around Parsedown requirement for valid URL if its Twig in there
  191. if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
  192. $excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
  193. $excerpt = parent::inlineLink($excerpt);
  194. $excerpt['element']['attributes']['href'] = $matches[1];
  195. $excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1;
  196. return $excerpt;
  197. }
  198. $excerpt = parent::inlineLink($excerpt);
  199. // if this is a link
  200. if (isset($excerpt['element']['attributes']['href'])) {
  201. $excerpt = Excerpts::processLinkExcerpt($excerpt, $this->page, $type);
  202. }
  203. return $excerpt;
  204. }
  205. // For extending this class via plugins
  206. public function __call($method, $args)
  207. {
  208. if (isset($this->{$method}) === true) {
  209. $func = $this->{$method};
  210. return \call_user_func_array($func, $args);
  211. }
  212. return null;
  213. }
  214. }