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('
