PageRenderTime 25ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/system/src/Grav/Common/Markdown/ParsedownGravTrait.php

https://gitlab.com/asun89/socianovation-web
PHP | 360 lines | 216 code | 64 blank | 80 comment | 42 complexity | 325c7520d19f4a05aabd230866d2b311 MD5 | raw file
  1. <?php
  2. namespace Grav\Common\Markdown;
  3. use Grav\Common\GravTrait;
  4. use Grav\Common\Page\Page;
  5. use Grav\Common\Page\Pages;
  6. use Grav\Common\Uri;
  7. use RocketTheme\Toolbox\Event\Event;
  8. /**
  9. * A trait to add some custom processing to the identifyLink() method in Parsedown and ParsedownExtra
  10. */
  11. trait ParsedownGravTrait
  12. {
  13. use GravTrait;
  14. /** @var Page $page */
  15. protected $page;
  16. /** @var Pages $pages */
  17. protected $pages;
  18. /** @var Uri $uri */
  19. protected $uri;
  20. protected $pages_dir;
  21. protected $special_chars;
  22. protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/';
  23. protected $special_protocols = ['xmpp', 'mailto', 'tel', 'sms'];
  24. public $completable_blocks = [];
  25. public $continuable_blocks = [];
  26. /**
  27. * Initialization function to setup key variables needed by the MarkdownGravLinkTrait
  28. *
  29. * @param $page
  30. * @param $defaults
  31. */
  32. protected function init($page, $defaults)
  33. {
  34. $grav = self::getGrav();
  35. $this->page = $page;
  36. $this->pages = $grav['pages'];
  37. $this->uri = $grav['uri'];
  38. $this->BlockTypes['{'] [] = "TwigTag";
  39. $this->pages_dir = self::getGrav()['locator']->findResource('page://');
  40. $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
  41. if ($defaults === null) {
  42. $defaults = self::getGrav()['config']->get('system.pages.markdown');
  43. }
  44. $this->setBreaksEnabled($defaults['auto_line_breaks']);
  45. $this->setUrlsLinked($defaults['auto_url_links']);
  46. $this->setMarkupEscaped($defaults['escape_markup']);
  47. $this->setSpecialChars($defaults['special_chars']);
  48. $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $this]));
  49. }
  50. /**
  51. * Be able to define a new Block type or override an existing one
  52. *
  53. * @param $type
  54. * @param $tag
  55. */
  56. public function addBlockType($type, $tag, $continuable = false, $completable = false)
  57. {
  58. $this->BlockTypes[$type] [] = $tag;
  59. if ($continuable) {
  60. $this->continuable_blocks[] = $tag;
  61. }
  62. if ($completable) {
  63. $this->completable_blocks[] = $tag;
  64. }
  65. }
  66. /**
  67. * Be able to define a new Inline type or override an existing one
  68. *
  69. * @param $type
  70. * @param $tag
  71. */
  72. public function addInlineType($type, $tag)
  73. {
  74. $this->InlineTypes[$type] [] = $tag;
  75. $this->inlineMarkerList .= $type;
  76. }
  77. /**
  78. * Overrides the default behavior to allow for plugin-provided blocks to be continuable
  79. *
  80. * @param $Type
  81. *
  82. * @return bool
  83. */
  84. protected function isBlockContinuable($Type)
  85. {
  86. $continuable = in_array($Type, $this->continuable_blocks) || method_exists($this, 'block' . $Type . 'Continue');
  87. return $continuable;
  88. }
  89. /**
  90. * Overrides the default behavior to allow for plugin-provided blocks to be completable
  91. *
  92. * @param $Type
  93. *
  94. * @return bool
  95. */
  96. protected function isBlockCompletable($Type)
  97. {
  98. $completable = in_array($Type, $this->completable_blocks) || method_exists($this, 'block' . $Type . 'Complete');
  99. return $completable;
  100. }
  101. /**
  102. * Make the element function publicly accessible, Medium uses this to render from Twig
  103. *
  104. * @param array $Element
  105. *
  106. * @return string markup
  107. */
  108. public function elementToHtml(array $Element)
  109. {
  110. return $this->element($Element);
  111. }
  112. /**
  113. * Setter for special chars
  114. *
  115. * @param $special_chars
  116. *
  117. * @return $this
  118. */
  119. function setSpecialChars($special_chars)
  120. {
  121. $this->special_chars = $special_chars;
  122. return $this;
  123. }
  124. /**
  125. * Ensure Twig tags are treated as block level items with no <p></p> tags
  126. */
  127. protected function blockTwigTag($Line)
  128. {
  129. if (preg_match('/(?:{{|{%|{#)(.*)(?:}}|%}|#})/', $Line['body'], $matches)) {
  130. $Block = [
  131. 'markup' => $Line['body'],
  132. ];
  133. return $Block;
  134. }
  135. }
  136. protected function inlineSpecialCharacter($Excerpt)
  137. {
  138. if ($Excerpt['text'][0] === '&' && !preg_match('/^&#?\w+;/', $Excerpt['text'])) {
  139. return [
  140. 'markup' => '&amp;',
  141. 'extent' => 1,
  142. ];
  143. }
  144. if (isset($this->special_chars[$Excerpt['text'][0]])) {
  145. return [
  146. 'markup' => '&' . $this->special_chars[$Excerpt['text'][0]] . ';',
  147. 'extent' => 1,
  148. ];
  149. }
  150. }
  151. protected function inlineImage($excerpt)
  152. {
  153. if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
  154. $excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
  155. $excerpt = parent::inlineImage($excerpt);
  156. $excerpt['element']['attributes']['src'] = $matches[1];
  157. $excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1;
  158. return $excerpt;
  159. } else {
  160. $excerpt['type'] = 'image';
  161. $excerpt = parent::inlineImage($excerpt);
  162. }
  163. // Some stuff we will need
  164. $actions = [];
  165. $media = null;
  166. // if this is an image
  167. if (isset($excerpt['element']['attributes']['src'])) {
  168. $alt = $excerpt['element']['attributes']['alt'] ?: '';
  169. $title = $excerpt['element']['attributes']['title'] ?: '';
  170. $class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : '';
  171. //get the url and parse it
  172. $url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['src']));
  173. $this_host = isset($url['host']) && $url['host'] == $this->uri->host();
  174. // if there is no host set but there is a path, the file is local
  175. if ((!isset($url['host']) || $this_host) && isset($url['path'])) {
  176. $path_parts = pathinfo($url['path']);
  177. // get the local path to page media if possible
  178. if ($path_parts['dirname'] == $this->page->url(false, false, false)) {
  179. // get the media objects for this page
  180. $media = $this->page->media();
  181. } else {
  182. // see if this is an external page to this one
  183. $base_url = rtrim(self::getGrav()['base_url_relative'] . self::getGrav()['pages']->base(), '/');
  184. $page_route = '/' . ltrim(str_replace($base_url, '', $path_parts['dirname']), '/');
  185. $ext_page = $this->pages->dispatch($page_route, true);
  186. if ($ext_page) {
  187. $media = $ext_page->media();
  188. }
  189. }
  190. // if there is a media file that matches the path referenced..
  191. if ($media && isset($media->all()[$path_parts['basename']])) {
  192. // get the medium object
  193. $medium = $media->all()[$path_parts['basename']];
  194. // if there is a query, then parse it and build action calls
  195. if (isset($url['query'])) {
  196. $actions = array_reduce(explode('&', $url['query']), function ($carry, $item) {
  197. $parts = explode('=', $item, 2);
  198. $value = isset($parts[1]) ? $parts[1] : null;
  199. $carry[] = ['method' => $parts[0], 'params' => $value];
  200. return $carry;
  201. }, []);
  202. }
  203. // loop through actions for the image and call them
  204. foreach ($actions as $action) {
  205. $medium = call_user_func_array([$medium, $action['method']],
  206. explode(',', urldecode($action['params'])));
  207. }
  208. if (isset($url['fragment'])) {
  209. $medium->urlHash($url['fragment']);
  210. }
  211. $excerpt['element'] = $medium->parseDownElement($title, $alt, $class, true);
  212. } else {
  213. // not a current page media file, see if it needs converting to relative
  214. $excerpt['element']['attributes']['src'] = Uri::buildUrl($url);
  215. }
  216. }
  217. }
  218. return $excerpt;
  219. }
  220. protected function inlineLink($excerpt)
  221. {
  222. if (isset($excerpt['type'])) {
  223. $type = $excerpt['type'];
  224. } else {
  225. $type = 'link';
  226. }
  227. // do some trickery to get around Parsedown requirement for valid URL if its Twig in there
  228. if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
  229. $excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']);
  230. $excerpt = parent::inlineLink($excerpt);
  231. $excerpt['element']['attributes']['href'] = $matches[1];
  232. $excerpt['extent'] = $excerpt['extent'] + strlen($matches[1]) - 1;
  233. return $excerpt;
  234. } else {
  235. $excerpt = parent::inlineLink($excerpt);
  236. }
  237. // if this is a link
  238. if (isset($excerpt['element']['attributes']['href'])) {
  239. $url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['href']));
  240. // if there is a query, then parse it and build action calls
  241. if (isset($url['query'])) {
  242. $actions = array_reduce(explode('&', $url['query']), function ($carry, $item) {
  243. $parts = explode('=', $item, 2);
  244. $value = isset($parts[1]) ? $parts[1] : true;
  245. $carry[$parts[0]] = $value;
  246. return $carry;
  247. }, []);
  248. // valid attributes supported
  249. $valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
  250. // Unless told to not process, go through actions
  251. if (array_key_exists('noprocess', $actions)) {
  252. unset($actions['noprocess']);
  253. } else {
  254. // loop through actions for the image and call them
  255. foreach ($actions as $attrib => $value) {
  256. $key = $attrib;
  257. if (in_array($attrib, $valid_attributes)) {
  258. // support both class and classes
  259. if ($attrib == 'classes') {
  260. $attrib = 'class';
  261. }
  262. $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
  263. unset($actions[$key]);
  264. }
  265. }
  266. }
  267. $url['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
  268. }
  269. // if no query elements left, unset query
  270. if (empty($url['query'])) {
  271. unset ($url['query']);
  272. }
  273. // set path to / if not set
  274. if (empty($url['path'])) {
  275. $url['path'] = '';
  276. }
  277. // if special scheme, just return
  278. if(isset($url['scheme']) && in_array($url['scheme'], $this->special_protocols)) {
  279. return $excerpt;
  280. }
  281. // handle paths and such
  282. $url = Uri::convertUrl($this->page, $url, $type);
  283. // build the URL from the component parts and set it on the element
  284. $excerpt['element']['attributes']['href'] = Uri::buildUrl($url);
  285. }
  286. return $excerpt;
  287. }
  288. // For extending this class via plugins
  289. public function __call($method, $args)
  290. {
  291. if (isset($this->$method) === true) {
  292. $func = $this->$method;
  293. return call_user_func_array($func, $args);
  294. }
  295. }
  296. }