From 99d0c7cb3e48efec6b006a26ce975d8780924941 Mon Sep 17 00:00:00 2001 From: Matias Griese Date: Fri, 17 May 2019 14:48:12 +0300 Subject: [PATCH] Generalized markdown classes so they can be used outside of `Page` scope with a custom `Excerpts` class instance --- CHANGELOG.md | 1 + composer.lock | 2 +- system/src/Grav/Common/Helpers/Excerpts.php | 257 +------------- system/src/Grav/Common/Markdown/Parsedown.php | 16 +- .../Grav/Common/Markdown/ParsedownExtra.php | 16 +- .../Common/Markdown/ParsedownGravTrait.php | 79 +++-- .../Grav/Common/Page/Markdown/Excerpts.php | 328 ++++++++++++++++++ .../Common/Page/Medium/ParsedownHtmlTrait.php | 2 +- system/src/Grav/Common/Page/Page.php | 28 +- system/src/Grav/Common/Utils.php | 20 +- .../Grav/Common/Markdown/ParsedownTest.php | 51 ++- 11 files changed, 487 insertions(+), 313 deletions(-) create mode 100644 system/src/Grav/Common/Page/Markdown/Excerpts.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b2ff74b41..0e8f9208e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Added **page blueprints** to `YamlLinter` CLI and Admin reports * Removed `Gitter` and `Slack` [#2502](https://github.com/getgrav/grav/issues/2502) * Optimizations for Plugin/Theme loading + * Generalized markdown classes so they can be used outside of `Page` scope with a custom `Excerpts` class instance 1. [](#bugfix) * Force question to install demo content in theme update [#2493](https://github.com/getgrav/grav/issues/2493) * Fixed GPM errors from blueprints not being logged [#2505](https://github.com/getgrav/grav/issues/2505) diff --git a/composer.lock b/composer.lock index 32139a7f6..be6bf65b6 100644 --- a/composer.lock +++ b/composer.lock @@ -2024,7 +2024,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", diff --git a/system/src/Grav/Common/Helpers/Excerpts.php b/system/src/Grav/Common/Helpers/Excerpts.php index 094324213..f07c3aa5a 100644 --- a/system/src/Grav/Common/Helpers/Excerpts.php +++ b/system/src/Grav/Common/Helpers/Excerpts.php @@ -9,24 +9,20 @@ namespace Grav\Common\Helpers; -use Grav\Common\Grav; use Grav\Common\Page\Interfaces\PageInterface; -use Grav\Common\Uri; +use Grav\Common\Page\Markdown\Excerpts as ExcerptsObject; use Grav\Common\Page\Medium\Medium; -use Grav\Common\Utils; -use RocketTheme\Toolbox\Event\Event; -use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; class Excerpts { /** * Process Grav image media URL from HTML tag * - * @param string $html HTML tag e.g. `` - * @param PageInterface $page The current page object - * @return string Returns final HTML string + * @param string $html HTML tag e.g. `` + * @param PageInterface|null $page Page, defaults to the current page object + * @return string Returns final HTML string */ - public static function processImageHtml($html, PageInterface $page) + public static function processImageHtml($html, PageInterface $page = null) { $excerpt = static::getExcerptFromHtml($html, 'img'); @@ -112,157 +108,29 @@ class Excerpts * Process a Link excerpt * * @param array $excerpt - * @param PageInterface $page + * @param PageInterface|null $page Page, defaults to the current page object * @param string $type * @return mixed */ - public static function processLinkExcerpt($excerpt, PageInterface $page, $type = 'link') + public static function processLinkExcerpt($excerpt, PageInterface $page = null, $type = 'link') { - $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href'])); + $excerpts = new ExcerptsObject($page); - $url_parts = static::parseUrl($url); - - // If there is a query, then parse it and build action calls. - if (isset($url_parts['query'])) { - $actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) { - $parts = explode('=', $item, 2); - $value = isset($parts[1]) ? rawurldecode($parts[1]) : true; - $carry[$parts[0]] = $value; - - return $carry; - }, []); - - // Valid attributes supported. - $valid_attributes = ['rel', 'target', 'id', 'class', 'classes']; - - // Unless told to not process, go through actions. - if (array_key_exists('noprocess', $actions)) { - unset($actions['noprocess']); - } else { - // Loop through actions for the image and call them. - foreach ($actions as $attrib => $value) { - $key = $attrib; - - if (in_array($attrib, $valid_attributes, true)) { - // support both class and classes. - if ($attrib === 'classes') { - $attrib = 'class'; - } - $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value); - unset($actions[$key]); - } - } - } - - $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986); - } - - // If no query elements left, unset query. - if (empty($url_parts['query'])) { - unset ($url_parts['query']); - } - - // Set path to / if not set. - if (empty($url_parts['path'])) { - $url_parts['path'] = ''; - } - - // If scheme isn't http(s).. - if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) { - // Handle custom streams. - if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) { - $url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}"); - unset($url_parts['stream'], $url_parts['scheme']); - } - - $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); - return $excerpt; - } - - // Handle paths and such. - $url_parts = Uri::convertUrl($page, $url_parts, $type); - - // Build the URL from the component parts and set it on the element. - $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); - - return $excerpt; + return $excerpts->processLinkExcerpt($excerpt, $type); } /** * Process an image excerpt * * @param array $excerpt - * @param PageInterface $page + * @param PageInterface|null $page Page, defaults to the current page object * @return array */ - public static function processImageExcerpt(array $excerpt, PageInterface $page) + public static function processImageExcerpt(array $excerpt, PageInterface $page = null) { - $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src'])); - $url_parts = static::parseUrl($url); + $excerpts = new ExcerptsObject($page); - $media = null; - $filename = null; - - if (!empty($url_parts['stream'])) { - $filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? ''); - - $media = $page->getMedia(); - - } else { - $grav = Grav::instance(); - - // File is also local if scheme is http(s) and host matches. - $local_file = isset($url_parts['path']) - && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true)) - && (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host()); - - if ($local_file) { - $filename = basename($url_parts['path']); - $folder = dirname($url_parts['path']); - - // Get the local path to page media if possible. - if ($folder === $page->url(false, false, false)) { - // Get the media objects for this page. - $media = $page->getMedia(); - } else { - // see if this is an external page to this one - $base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/'); - $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/'); - - /** @var PageInterface $ext_page */ - $ext_page = $grav['pages']->dispatch($page_route, true); - if ($ext_page) { - $media = $ext_page->getMedia(); - } else { - $grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media])); - } - } - } - } - - // If there is a media file that matches the path referenced.. - if ($media && $filename && isset($media[$filename])) { - // Get the medium object. - /** @var Medium $medium */ - $medium = $media[$filename]; - - // Process operations - $medium = static::processMediaActions($medium, $url_parts); - $element_excerpt = $excerpt['element']['attributes']; - - $alt = $element_excerpt['alt'] ?? ''; - $title = $element_excerpt['title'] ?? ''; - $class = $element_excerpt['class'] ?? ''; - $id = $element_excerpt['id'] ?? ''; - - $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true); - - } else { - // Not a current page media file, see if it needs converting to relative. - $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts); - } - - return $excerpt; + return $excerpts->processImageExcerpt($excerpt); } /** @@ -270,104 +138,13 @@ class Excerpts * * @param Medium $medium * @param string|array $url + * @param PageInterface|null $page Page, defaults to the current page object * @return Medium */ - public static function processMediaActions($medium, $url) + public static function processMediaActions($medium, $url, PageInterface $page = null) { - if (!is_array($url)) { - $url_parts = parse_url($url); - } else { - $url_parts = $url; - } + $excerpts = new ExcerptsObject($page); - $actions = []; - - // if there is a query, then parse it and build action calls - if (isset($url_parts['query'])) { - $actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) { - $parts = explode('=', $item, 2); - $value = $parts[1] ?? null; - $carry[] = ['method' => $parts[0], 'params' => $value]; - - return $carry; - }, []); - } - - if (Grav::instance()['config']->get('system.images.auto_fix_orientation')) { - $actions[] = ['method' => 'fixOrientation', 'params' => '']; - } - $defaults = Grav::instance()['config']->get('system.images.defaults'); - if (is_array($defaults) && count($defaults)) { - foreach ($defaults as $method => $params) { - $actions[] = [ - 'method' => $method, - 'params' => $params, - ]; - } - } - - // loop through actions for the image and call them - foreach ($actions as $action) { - $matches = []; - - if (preg_match('/\[(.*)\]/', $action['params'], $matches)) { - $args = [explode(',', $matches[1])]; - } else { - $args = explode(',', $action['params']); - } - - $medium = call_user_func_array([$medium, $action['method']], $args); - } - - if (isset($url_parts['fragment'])) { - $medium->urlHash($url_parts['fragment']); - } - - return $medium; - } - - /** - * Variation of parse_url() which works also with local streams. - * - * @param string $url - * @return array|bool - */ - protected static function parseUrl($url) - { - $url_parts = Utils::multibyteParseUrl($url); - - if (isset($url_parts['scheme'])) { - /** @var UniformResourceLocator $locator */ - $locator = Grav::instance()['locator']; - - // Special handling for the streams. - if ($locator->schemeExists($url_parts['scheme'])) { - if (isset($url_parts['host'])) { - // Merge host and path into a path. - $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : ''); - unset($url_parts['host']); - } - - $url_parts['stream'] = true; - } - } - - return $url_parts; - } - - /** - * @param string $url - * @return bool|string - */ - protected static function resolveStream($url) - { - /** @var UniformResourceLocator $locator */ - $locator = Grav::instance()['locator']; - - if ($locator->isStream($url)) { - return $locator->findResource($url, false) ?: $locator->findResource($url, false, true); - } - - return $url; + return $excerpts->processMediaActions($medium, $url); } } diff --git a/system/src/Grav/Common/Markdown/Parsedown.php b/system/src/Grav/Common/Markdown/Parsedown.php index d2f72fcb7..825a99bb4 100644 --- a/system/src/Grav/Common/Markdown/Parsedown.php +++ b/system/src/Grav/Common/Markdown/Parsedown.php @@ -10,6 +10,7 @@ namespace Grav\Common\Markdown; use Grav\Common\Page\Interfaces\PageInterface; +use Grav\Common\Page\Markdown\Excerpts; class Parsedown extends \Parsedown { @@ -18,12 +19,21 @@ class Parsedown extends \Parsedown /** * Parsedown constructor. * - * @param PageInterface $page + * @param Excerpts $excerpts * @param array|null $defaults */ - public function __construct($page, $defaults) + public function __construct($excerpts, $defaults = null) { - $this->init($page, $defaults); + if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) { + // Deprecated in Grav 1.6.10 + if ($defaults) { + $defaults = ['markdown' => $defaults]; + } + $excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } + + $this->init($excerpts, $defaults); } } diff --git a/system/src/Grav/Common/Markdown/ParsedownExtra.php b/system/src/Grav/Common/Markdown/ParsedownExtra.php index ac0477150..ed86884a3 100644 --- a/system/src/Grav/Common/Markdown/ParsedownExtra.php +++ b/system/src/Grav/Common/Markdown/ParsedownExtra.php @@ -10,6 +10,7 @@ namespace Grav\Common\Markdown; use Grav\Common\Page\Interfaces\PageInterface; +use Grav\Common\Page\Markdown\Excerpts; class ParsedownExtra extends \ParsedownExtra { @@ -18,14 +19,23 @@ class ParsedownExtra extends \ParsedownExtra /** * ParsedownExtra constructor. * - * @param PageInterface $page + * @param Excerpts $excerpts * @param array|null $defaults * @throws \Exception */ - public function __construct($page, $defaults) + public function __construct($excerpts, $defaults = null) { + if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) { + // Deprecated in Grav 1.6.10 + if ($defaults) { + $defaults = ['markdown' => $defaults]; + } + $excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } + parent::__construct(); - $this->init($page, $defaults); + $this->init($excerpts, $defaults); } } diff --git a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php index fa8fa9156..4067d2b82 100644 --- a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php +++ b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php @@ -9,15 +9,13 @@ namespace Grav\Common\Markdown; -use Grav\Common\Grav; -use Grav\Common\Helpers\Excerpts; +use Grav\Common\Page\Markdown\Excerpts; use Grav\Common\Page\Interfaces\PageInterface; -use RocketTheme\Toolbox\Event\Event; trait ParsedownGravTrait { - /** @var PageInterface $page */ - protected $page; + /** @var Excerpts */ + protected $excerpts; protected $special_chars; protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/'; @@ -28,28 +26,49 @@ trait ParsedownGravTrait /** * Initialization function to setup key variables needed by the MarkdownGravLinkTrait * - * @param PageInterface $page + * @param PageInterface|Excerpts $excerpts * @param array|null $defaults */ - protected function init($page, $defaults) + protected function init($excerpts, $defaults = null) { - $grav = Grav::instance(); - - $this->page = $page; - $this->BlockTypes['{'] [] = 'TwigTag'; - $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot']; - - if ($defaults === null) { - $defaults = (array)Grav::instance()['config']->get('system.pages.markdown'); + if (!$excerpts || $excerpts instanceof PageInterface) { + // Deprecated in Grav 1.6.10 + if ($defaults) { + $defaults = ['markdown' => $defaults]; + } + $this->excerpts = new Excerpts($excerpts, $defaults); + 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); + } else { + $this->excerpts = $excerpts; } - $this->setBreaksEnabled($defaults['auto_line_breaks']); - $this->setUrlsLinked($defaults['auto_url_links']); - $this->setMarkupEscaped($defaults['escape_markup']); - $this->setSpecialChars($defaults['special_chars']); + $this->BlockTypes['{'][] = 'TwigTag'; + $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot']; - $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $this, 'page' => $page])); + $defaults = $this->excerpts->getConfig(); + if (isset($defaults['markdown']['auto_line_breaks'])) { + $this->setBreaksEnabled($defaults['markdown']['auto_line_breaks']); + } + if (isset($defaults['markdown']['auto_url_links'])) { + $this->setUrlsLinked($defaults['markdown']['auto_url_links']); + } + if (isset($defaults['markdown']['escape_markup'])) { + $this->setMarkupEscaped($defaults['markdown']['escape_markup']); + } + if (isset($defaults['markdown']['special_chars'])) { + $this->setSpecialChars($defaults['markdown']['special_chars']); + } + + $this->excerpts->fireInitializedEvent($this); + } + + /** + * @return Excerpts + */ + public function getExcerpts() + { + return $this->excerpts; } /** @@ -114,7 +133,8 @@ trait ParsedownGravTrait */ protected function isBlockContinuable($Type) { - $continuable = \in_array($Type, $this->continuable_blocks) || method_exists($this, 'block' . $Type . 'Continue'); + $continuable = \in_array($Type, $this->continuable_blocks, true) + || method_exists($this, 'block' . $Type . 'Continue'); return $continuable; } @@ -128,7 +148,8 @@ trait ParsedownGravTrait */ protected function isBlockCompletable($Type) { - $completable = \in_array($Type, $this->completable_blocks) || method_exists($this, 'block' . $Type . 'Complete'); + $completable = \in_array($Type, $this->completable_blocks, true) + || method_exists($this, 'block' . $Type . 'Complete'); return $completable; } @@ -210,7 +231,7 @@ trait ParsedownGravTrait // if this is an image process it if (isset($excerpt['element']['attributes']['src'])) { - $excerpt = Excerpts::processImageExcerpt($excerpt, $this->page); + $excerpt = $this->excerpts->processImageExcerpt($excerpt); } return $excerpt; @@ -218,11 +239,7 @@ trait ParsedownGravTrait protected function inlineLink($excerpt) { - if (isset($excerpt['type'])) { - $type = $excerpt['type']; - } else { - $type = 'link'; - } + $type = $excerpt['type'] ?? 'link'; // do some trickery to get around Parsedown requirement for valid URL if its Twig in there if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) { @@ -238,13 +255,15 @@ trait ParsedownGravTrait // if this is a link if (isset($excerpt['element']['attributes']['href'])) { - $excerpt = Excerpts::processLinkExcerpt($excerpt, $this->page, $type); + $excerpt = $this->excerpts->processLinkExcerpt($excerpt, $type); } return $excerpt; } - // For extending this class via plugins + /** + * For extending this class via plugins + */ public function __call($method, $args) { if (isset($this->{$method}) === true) { diff --git a/system/src/Grav/Common/Page/Markdown/Excerpts.php b/system/src/Grav/Common/Page/Markdown/Excerpts.php new file mode 100644 index 000000000..3ee4be508 --- /dev/null +++ b/system/src/Grav/Common/Page/Markdown/Excerpts.php @@ -0,0 +1,328 @@ +page = $page ?? Grav::instance()['page'] ?? null; + + // Add defaults to the configuration. + if (null === $config || !isset($config['markdown'], $config['images'])) { + $c = Grav::instance()['config']; + $config = $config ?? []; + $config += [ + 'markdown' => $c->get('system.pages.markdown', []), + 'images' => $c->get('system.images', []) + ]; + } + + $this->config = $config; + } + + public function getPage(): PageInterface + { + return $this->page; + } + + public function getConfig(): array + { + return $this->config; + } + + public function fireInitializedEvent($markdown): void + { + $grav = Grav::instance(); + + $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $markdown, 'page' => $this->page])); + } + + /** + * Process a Link excerpt + * + * @param array $excerpt + * @param string $type + * @return array + */ + public function processLinkExcerpt(array $excerpt, string $type = 'link'): array + { + $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href'])); + + $url_parts = $this->parseUrl($url); + + // If there is a query, then parse it and build action calls. + if (isset($url_parts['query'])) { + $actions = array_reduce( + explode('&', $url_parts['query']), + static function ($carry, $item) { + $parts = explode('=', $item, 2); + $value = isset($parts[1]) ? rawurldecode($parts[1]) : true; + $carry[$parts[0]] = $value; + + return $carry; + }, + [] + ); + + // Valid attributes supported. + $valid_attributes = ['rel', 'target', 'id', 'class', 'classes']; + + // Unless told to not process, go through actions. + if (array_key_exists('noprocess', $actions)) { + unset($actions['noprocess']); + } else { + // Loop through actions for the image and call them. + foreach ($actions as $attrib => $value) { + $key = $attrib; + + if (in_array($attrib, $valid_attributes, true)) { + // support both class and classes. + if ($attrib === 'classes') { + $attrib = 'class'; + } + $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value); + unset($actions[$key]); + } + } + } + + $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986); + } + + // If no query elements left, unset query. + if (empty($url_parts['query'])) { + unset ($url_parts['query']); + } + + // Set path to / if not set. + if (empty($url_parts['path'])) { + $url_parts['path'] = ''; + } + + // If scheme isn't http(s).. + if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) { + // Handle custom streams. + if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) { + $grav = Grav::instance(); + $url_parts['path'] = $grav['base_url_relative'] . '/' . $this->resolveStream("{$url_parts['scheme']}://{$url_parts['path']}"); + unset($url_parts['stream'], $url_parts['scheme']); + } + + $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); + + return $excerpt; + } + + // Handle paths and such. + $url_parts = Uri::convertUrl($this->page, $url_parts, $type); + + // Build the URL from the component parts and set it on the element. + $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); + + return $excerpt; + } + + /** + * Process an image excerpt + * + * @param array $excerpt + * @return array + */ + public function processImageExcerpt(array $excerpt): array + { + $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src'])); + $url_parts = $this->parseUrl($url); + + $media = null; + $filename = null; + + if (!empty($url_parts['stream'])) { + $filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? ''); + + $media = $this->page->getMedia(); + + } else { + $grav = Grav::instance(); + + // File is also local if scheme is http(s) and host matches. + $local_file = isset($url_parts['path']) + && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true)) + && (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host()); + + if ($local_file) { + $filename = basename($url_parts['path']); + $folder = dirname($url_parts['path']); + + // Get the local path to page media if possible. + if ($this->page && $folder === $this->page->url(false, false, false)) { + // Get the media objects for this page. + $media = $this->page->getMedia(); + } else { + // see if this is an external page to this one + $base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/'); + $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/'); + + /** @var PageInterface $ext_page */ + $ext_page = $grav['pages']->dispatch($page_route, true); + if ($ext_page) { + $media = $ext_page->getMedia(); + } else { + $grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media])); + } + } + } + } + + // If there is a media file that matches the path referenced.. + if ($media && $filename && isset($media[$filename])) { + // Get the medium object. + /** @var Medium $medium */ + $medium = $media[$filename]; + + // Process operations + $medium = $this->processMediaActions($medium, $url_parts); + $element_excerpt = $excerpt['element']['attributes']; + + $alt = $element_excerpt['alt'] ?? ''; + $title = $element_excerpt['title'] ?? ''; + $class = $element_excerpt['class'] ?? ''; + $id = $element_excerpt['id'] ?? ''; + + $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true); + + } else { + // Not a current page media file, see if it needs converting to relative. + $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts); + } + + return $excerpt; + } + + /** + * Process media actions + * + * @param Medium $medium + * @param string|array $url + * @return Medium + */ + public function processMediaActions($medium, $url): Medium + { + $url_parts = is_string($url) ? $this->parseUrl($url) : $url; + $actions = []; + + // if there is a query, then parse it and build action calls + if (isset($url_parts['query'])) { + $actions = array_reduce( + explode('&', $url_parts['query']), + static function ($carry, $item) { + $parts = explode('=', $item, 2); + $value = $parts[1] ?? null; + $carry[] = ['method' => $parts[0], 'params' => $value]; + + return $carry; + }, + [] + ); + } + + $config = $this->getConfig(); + if (!empty($config['images']['auto_fix_orientation'])) { + $actions[] = ['method' => 'fixOrientation', 'params' => '']; + } + + $defaults = $config['images']['defaults'] ?? []; + if (count($defaults)) { + foreach ($defaults as $method => $params) { + $actions[] = [ + 'method' => $method, + 'params' => $params, + ]; + } + } + + // loop through actions for the image and call them + foreach ($actions as $action) { + $matches = []; + + if (preg_match('/\[(.*)\]/', $action['params'], $matches)) { + $args = [explode(',', $matches[1])]; + } else { + $args = explode(',', $action['params']); + } + + $medium = call_user_func_array([$medium, $action['method']], $args); + } + + if (isset($url_parts['fragment'])) { + $medium->urlHash($url_parts['fragment']); + } + + return $medium; + } + + /** + * Variation of parse_url() which works also with local streams. + * + * @param string $url + * @return array|bool + */ + protected function parseUrl(string $url) + { + $url_parts = Utils::multibyteParseUrl($url); + + if (isset($url_parts['scheme'])) { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + // Special handling for the streams. + if ($locator->schemeExists($url_parts['scheme'])) { + if (isset($url_parts['host'])) { + // Merge host and path into a path. + $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : ''); + unset($url_parts['host']); + } + + $url_parts['stream'] = true; + } + } + + return $url_parts; + } + + /** + * @param string $url + * @return bool|string + */ + protected function resolveStream(string $url) + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + if ($locator->isStream($url)) { + return $locator->findResource($url, false) ?: $locator->findResource($url, false, true); + } + + return $url; + } +} diff --git a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php index 627c361ff..365cf1d66 100644 --- a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php +++ b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php @@ -33,7 +33,7 @@ trait ParsedownHtmlTrait $element = $this->parsedownElement($title, $alt, $class, $id, $reset); if (!$this->parsedown) { - $this->parsedown = new Parsedown(null, null); + $this->parsedown = new Parsedown(); } return $this->parsedown->elementToHtml($element); diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 82ff71cb2..bcf7539bd 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -19,6 +19,7 @@ use Grav\Common\Markdown\Parsedown; use Grav\Common\Markdown\ParsedownExtra; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Media\Traits\MediaTrait; +use Grav\Common\Page\Markdown\Excerpts; use Grav\Common\Taxonomy; use Grav\Common\Uri; use Grav\Common\Utils; @@ -27,7 +28,6 @@ use Negotiation\Accept; use Negotiation\Negotiator; use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\File\MarkdownFile; -use Symfony\Component\Yaml\Exception\ParseException; define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u'); @@ -819,23 +819,31 @@ class Page implements PageInterface /** @var Config $config */ $config = Grav::instance()['config']; - $defaults = (array)$config->get('system.pages.markdown'); + $markdownDefaults = (array)$config->get('system.pages.markdown'); if (isset($this->header()->markdown)) { - $defaults = array_merge($defaults, $this->header()->markdown); + $markdownDefaults = array_merge($markdownDefaults, $this->header()->markdown); } // pages.markdown_extra is deprecated, but still check it... - if (!isset($defaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) { + if (!isset($markdownDefaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) { user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED); - $defaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra'); + $markdownDefaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra'); } + $extra = $markdownDefaults['extra'] ?? false; + $defaults = [ + 'markdown' => $markdownDefaults, + 'images' => $config->get('system.images', []) + ]; + + $excerpts = new Excerpts($this, $defaults); + // Initialize the preferred variant of Parsedown - if ($defaults['extra']) { - $parsedown = new ParsedownExtra($this, $defaults); + if ($extra) { + $parsedown = new ParsedownExtra($excerpts); } else { - $parsedown = new Parsedown($this, $defaults); + $parsedown = new Parsedown($excerpts); } $this->content = $parsedown->text($this->content); @@ -1397,8 +1405,8 @@ class Page implements PageInterface return $this->template_format; } - // Use content negotitation via the `accept:` header - $http_accept = $_SERVER['HTTP_ACCEPT'] ?? false; + // Use content negotiation via the `accept:` header + $http_accept = $_SERVER['HTTP_ACCEPT'] ?? null; if (is_string($http_accept)) { $negotiator = new Negotiator(); diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 811f2f433..ad9b289f3 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -13,6 +13,7 @@ use Grav\Common\Helpers\Truncator; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Markdown\Parsedown; use Grav\Common\Markdown\ParsedownExtra; +use Grav\Common\Page\Markdown\Excerpts; use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; @@ -1396,7 +1397,7 @@ abstract class Utils $pow = min($pow, count($units) - 1); // Uncomment one of the following alternatives - $bytes /= pow(1024, $pow); + $bytes /= 1024 ** $pow; // $bytes /= (1 << (10 * $pow)); return round($bytes, $precision) . ' ' . $units[$pow]; @@ -1460,14 +1461,21 @@ abstract class Utils */ public static function processMarkdown($string, $block = true) { - $page = Grav::instance()['page'] ?? null; - $defaults = Grav::instance()['config']->get('system.pages.markdown'); + $grav = Grav::instance(); + $page = $grav['page'] ?? null; + $defaults = [ + 'markdown' => $grav['config']->get('system.pages.markdown', []), + 'images' => $grav['config']->get('system.images', []) + ]; + $extra = $defaults['markdown']['extra'] ?? false; + + $excerpts = new Excerpts($page, $defaults); // Initialize the preferred variant of Parsedown - if ($defaults['extra']) { - $parsedown = new ParsedownExtra($page, $defaults); + if ($extra) { + $parsedown = new ParsedownExtra($excerpts); } else { - $parsedown = new Parsedown($page, $defaults); + $parsedown = new Parsedown($excerpts); } if ($block) { diff --git a/tests/unit/Grav/Common/Markdown/ParsedownTest.php b/tests/unit/Grav/Common/Markdown/ParsedownTest.php index e73e11f01..58a1acf61 100644 --- a/tests/unit/Grav/Common/Markdown/ParsedownTest.php +++ b/tests/unit/Grav/Common/Markdown/ParsedownTest.php @@ -2,6 +2,7 @@ use Codeception\Util\Fixtures; use Grav\Common\Grav; +use Grav\Common\Page\Markdown\Excerpts; use Grav\Common\Uri; use Grav\Common\Config\Config; use Grav\Common\Page\Pages; @@ -56,14 +57,19 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->pages->init(); $defaults = [ - 'extra' => false, - 'auto_line_breaks' => false, - 'auto_url_links' => false, - 'escape_markup' => false, - 'special_chars' => ['>' => 'gt', '<' => 'lt'], + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) ]; $page = $this->pages->dispatch('/item2/item2-2'); - $this->parsedown = new Parsedown($page, $defaults); + + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); } protected function _after() @@ -179,14 +185,18 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->uri->initializeWithURL('http://testing.dev/')->init(); $defaults = [ - 'extra' => false, - 'auto_line_breaks' => false, - 'auto_url_links' => false, - 'escape_markup' => false, - 'special_chars' => ['>' => 'gt', '<' => 'lt'], + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) ]; $page = $this->pages->dispatch('/'); - $this->parsedown = new Parsedown($page, $defaults); + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); $this->assertSame('

', $this->parsedown->text('![](home-sample-image.jpg)')); @@ -230,15 +240,18 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->uri->initializeWithURL('http://testing.dev/')->init(); $defaults = [ - 'extra' => false, - 'auto_line_breaks' => false, - 'auto_url_links' => false, - 'escape_markup' => false, - 'special_chars' => ['>' => 'gt', '<' => 'lt'], + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) ]; $page = $this->pages->dispatch('/'); - $this->parsedown = new Parsedown($page, $defaults); - + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); $this->assertSame('

Down a Level

', $this->parsedown->text('[Down a Level](item1-3)'));