ParsedownGravTrait.php 7.8 KB

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