ParsedownGravTrait.php 8.7 KB

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