diff --git a/system/src/Grav/Common/Helpers/Excerpts.php b/system/src/Grav/Common/Helpers/Excerpts.php new file mode 100644 index 000000000..6c95e069a --- /dev/null +++ b/system/src/Grav/Common/Helpers/Excerpts.php @@ -0,0 +1,234 @@ +` + * @param $page The current page object + * @return string Returns final HTML string + */ + public static function processImageHtml($html, $page) + { + $excerpt = static::getExcerptFromHtml($html, 'img'); + $excerpt = static::processImageExcerpt($excerpt, $page); + $html = static::getHtmlFromExcerpt($excerpt); + + return $html; + } + + /** + * Get an Excerpt array from a chunk of HTML + * + * @param $html Chunk of HTML + * @param $tag a tag, for example `img` + * @return array|null returns nested array excerpt + */ + public static function getExcerptFromHtml($html, $tag) + { + $doc = new \DOMDocument(); + $doc->loadHtml($html); + $images = $doc->getElementsByTagName($tag); + $excerpt = null; + + foreach ($images as $image) { + $attributes = []; + foreach ($image->attributes as $name => $value) { + $attributes[$name] = $value->value; + } + $excerpt = [ + 'element' => [ + 'name' => $image->tagName, + 'attributes' => $attributes + ] + ]; + } + + return $excerpt; + } + + public static function getHtmlFromExcerpt($excerpt) + { + $element = $excerpt['element']; + $html = '<'.$element['name']; + + if (isset($element['attributes'])) { + foreach ($element['attributes'] as $name => $value) { + if ($value === null) { + continue; + } + $html .= ' '.$name.'="'.$value.'"'; + } + } + + if (isset($element['text'])) { + $html .= '>'; + $html .= $element['text']; + $html .= ''; + } else { + $html .= ' />'; + } + + return $html; + } + + public static function processLinkExcerpt($excerpt, $page) + { + $url = $excerpt['element']['attributes']['href']; + + $url_parts = parse_url(htmlspecialchars_decode(urldecode($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)) { + // 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 special scheme, just return + if(isset($url_parts['scheme']) && !Utils::startsWith($url_parts['scheme'], 'http')) { + return $excerpt; + } + + // Get Type + if (isset($excerpt['type'])) { + $type = $excerpt['type']; + } else { + $type = 'link'; + } + + // 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; + } + + public static function processImageExcerpt($excerpt, $page) + { + $url = $excerpt['element']['attributes']['src']; + + $url_parts = parse_url(htmlspecialchars_decode(urldecode($url))); + + $this_host = isset($url_parts['host']) && $url_parts['host'] == Grav::instance()['uri']->host(); + + // if there is no host set but there is a path, the file is local + if ((!isset($url_parts['host']) || $this_host) && isset($url_parts['path'])) { + + $path_parts = pathinfo($url_parts['path']); + $actions = []; + $media = null; + + // get the local path to page media if possible + if ($path_parts['dirname'] == $page->url(false, false, false)) { + // get the media objects for this page + $media = $page->media(); + } else { + // see if this is an external page to this one + $base_url = rtrim(Grav::instance()['base_url_relative'] . Grav::instance()['pages']->base(), '/'); + $page_route = '/' . ltrim(str_replace($base_url, '', $path_parts['dirname']), '/'); + + $ext_page = Grav::instance()['pages']->dispatch($page_route, true); + if ($ext_page) { + $media = $ext_page->media(); + } + } + + // if there is a media file that matches the path referenced.. + if ($media && isset($media->all()[$path_parts['basename']])) { + // get the medium object + /** @var Medium $medium */ + $medium = $media->all()[$path_parts['basename']]; + + // 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]) ? $parts[1] : null; + $carry[] = ['method' => $parts[0], 'params' => $value]; + + return $carry; + }, []); + } + + // loop through actions for the image and call them + foreach ($actions as $action) { + $medium = call_user_func_array([$medium, $action['method']], + explode(',', $action['params'])); + } + + if (isset($url_parts['fragment'])) { + $medium->urlHash($url_parts['fragment']); + } + + $alt = $excerpt['element']['attributes']['alt'] ?: ''; + $title = $excerpt['element']['attributes']['title'] ?: ''; + $class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : ''; + $id = isset($excerpt['element']['attributes']['id']) ? $excerpt['element']['attributes']['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; + } + +} diff --git a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php index 895754e32..a02ca8316 100644 --- a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php +++ b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php @@ -9,7 +9,7 @@ namespace Grav\Common\Markdown; use Grav\Common\Grav; -use Grav\Common\Page\Medium\Medium; +use Grav\Common\Helpers\Excerpts; use Grav\Common\Uri; use Grav\Common\Utils; use RocketTheme\Toolbox\Event\Event; @@ -19,13 +19,6 @@ trait ParsedownGravTrait /** @var Page $page */ protected $page; - /** @var Pages $pages */ -// protected $pages; - - /** @var Uri $uri */ -// protected $uri; - -// protected $pages_dir; protected $special_chars; protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/'; @@ -43,10 +36,7 @@ trait ParsedownGravTrait $grav = Grav::instance(); $this->page = $page; -// $this->pages = $grav['pages']; -// $this->uri = $grav['uri']; $this->BlockTypes['{'] [] = "TwigTag"; -// $this->pages_dir = Grav::instance()['locator']->findResource('page://'); $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot']; if ($defaults === null) { @@ -206,7 +196,7 @@ trait ParsedownGravTrait // if this is an image process it if (isset($excerpt['element']['attributes']['src'])) { - $excerpt = Utils::processImageExcerpt($excerpt, $this->page); + $excerpt = Excerpts::processImageExcerpt($excerpt, $this->page); } return $excerpt; @@ -214,12 +204,6 @@ trait ParsedownGravTrait protected function inlineLink($excerpt) { - if (isset($excerpt['type'])) { - $type = $excerpt['type']; - } else { - $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)) { $excerpt['text'] = str_replace($matches[1], '/', $excerpt['text']); @@ -234,63 +218,7 @@ trait ParsedownGravTrait // if this is a link if (isset($excerpt['element']['attributes']['href'])) { - $url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['href'])); - - // if there is a query, then parse it and build action calls - if (isset($url['query'])) { - $actions = array_reduce(explode('&', $url['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)) { - // support both class and classes - if ($attrib == 'classes') { - $attrib = 'class'; - } - $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value); - unset($actions[$key]); - } - } - } - - $url['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986); - } - - // if no query elements left, unset query - if (empty($url['query'])) { - unset ($url['query']); - } - - // set path to / if not set - if (empty($url['path'])) { - $url['path'] = ''; - } - - // if special scheme, just return - if(isset($url['scheme']) && !Utils::startsWith($url['scheme'], 'http')) { - return $excerpt; - } - - // handle paths and such - $url = Uri::convertUrl($this->page, $url, $type); - - // build the URL from the component parts and set it on the element - $excerpt['element']['attributes']['href'] = Uri::buildUrl($url); + $excerpt = Excerpts::processLinkExcerpt($excerpt, $this->page); } return $excerpt; diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 3c8a9f607..c63aa8ffe 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -9,10 +9,7 @@ namespace Grav\Common; use DateTime; -use DateTimeZone; -use Grav\Common\Grav; use Grav\Common\Helpers\Truncator; -use Grav\Common\Page\Medium\Medium; use RocketTheme\Toolbox\Event\Event; abstract class Utils @@ -429,150 +426,6 @@ abstract class Utils return $root . implode('/', $ret); } - /** - * Process Grav image media URL from HTML tag - * - * @param $html HTML tag e.g. `` - * @param $page The current page object - * @return string Returns final HTML string - */ - public static function processImageHtml($html, $page) - { - $excerpt = static::getExcerptFromHtml($html, 'img'); - $excerpt = static::processImageExcerpt($excerpt, $page); - $html = static::getHtmlFromExcerpt($excerpt); - - return $html; - } - - /** - * Get an Excerpt array from a chunk of HTML - * - * @param $html Chunk of HTML - * @param $tag a tag, for example `img` - * @return array|null returns nested array excerpt - */ - public static function getExcerptFromHtml($html, $tag) - { - $doc = new \DOMDocument(); - $doc->loadHtml($html); - $images = $doc->getElementsByTagName($tag); - $excerpt = null; - - foreach ($images as $image) { - $attributes = []; - foreach ($image->attributes as $name => $value) { - $attributes[$name] = $value->value; - } - $excerpt = [ - 'element' => [ - 'name' => $image->tagName, - 'attributes' => $attributes - ] - ]; - } - - return $excerpt; - } - - public static function getHtmlFromExcerpt($excerpt) - { - $element = $excerpt['element']; - $html = '<'.$element['name']; - - if (isset($element['attributes'])) { - foreach ($element['attributes'] as $name => $value) { - if ($value === null) { - continue; - } - $html .= ' '.$name.'="'.$value.'"'; - } - } - - if (isset($element['text'])) { - $html .= '>'; - $html .= $element['text']; - $html .= ''; - } else { - $html .= ' />'; - } - - return $html; - } - - public static function processImageExcerpt($excerpt, $page) - { - $url = $excerpt['element']['attributes']['src']; - - $url_parts = parse_url(htmlspecialchars_decode(urldecode($url))); - - $this_host = isset($url_parts['host']) && $url_parts['host'] == Grav::instance()['uri']->host(); - - // if there is no host set but there is a path, the file is local - if ((!isset($url_parts['host']) || $this_host) && isset($url_parts['path'])) { - - $path_parts = pathinfo($url_parts['path']); - $actions = []; - $media = null; - - // get the local path to page media if possible - if ($path_parts['dirname'] == $page->url(false, false, false)) { - // get the media objects for this page - $media = $page->media(); - } else { - // see if this is an external page to this one - $base_url = rtrim(Grav::instance()['base_url_relative'] . Grav::instance()['pages']->base(), '/'); - $page_route = '/' . ltrim(str_replace($base_url, '', $path_parts['dirname']), '/'); - - $ext_page = Grav::instance()['pages']->dispatch($page_route, true); - if ($ext_page) { - $media = $ext_page->media(); - } - } - - // if there is a media file that matches the path referenced.. - if ($media && isset($media->all()[$path_parts['basename']])) { - // get the medium object - /** @var Medium $medium */ - $medium = $media->all()[$path_parts['basename']]; - - // 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]) ? $parts[1] : null; - $carry[] = ['method' => $parts[0], 'params' => $value]; - - return $carry; - }, []); - } - - // loop through actions for the image and call them - foreach ($actions as $action) { - $medium = call_user_func_array([$medium, $action['method']], - explode(',', $action['params'])); - } - - if (isset($url_parts['fragment'])) { - $medium->urlHash($url_parts['fragment']); - } - - $alt = $excerpt['element']['attributes']['alt'] ?: ''; - $title = $excerpt['element']['attributes']['title'] ?: ''; - $class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : ''; - $id = isset($excerpt['element']['attributes']['id']) ? $excerpt['element']['attributes']['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); - } - } - - return $excerpt; - } - /** * Check whether a function is disabled in the PHP settings * diff --git a/tests/unit/Grav/Common/Helpers/ExcerptsTest.php b/tests/unit/Grav/Common/Helpers/ExcerptsTest.php new file mode 100644 index 000000000..da3b6921b --- /dev/null +++ b/tests/unit/Grav/Common/Helpers/ExcerptsTest.php @@ -0,0 +1,86 @@ +grav = $grav(); + $this->pages = $this->grav['pages']; + $this->config = $this->grav['config']; + $this->uri = $this->grav['uri']; + $this->language = $this->grav['language']; + $this->old_home = $this->config->get('system.home.alias'); + $this->config->set('system.home.alias', '/item1'); + $this->config->set('system.absolute_urls', false); + $this->config->set('system.languages.supported', []); + + unset($this->grav['language']); + $this->grav['language'] = new Language($this->grav); + + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + $locator->addPath('page', '', 'tests/fake/nested-site/user/pages', false); + $this->pages->init(); + + $defaults = [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ]; + $this->page = $this->pages->dispatch('/item2/item2-2'); + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); + } + + protected function _after() + { + $this->config->set('system.home.alias', $this->old_home); + } + + + public function testProcessImageHtml() + { + + + $this->assertSame('', + Excerpts::processImageHtml('Sample Image', $this->page)); + + } + +} diff --git a/tests/unit/Grav/Common/Markdown/ParsedownTest.php b/tests/unit/Grav/Common/Markdown/ParsedownTest.php index ec4521b4d..104188fb0 100644 --- a/tests/unit/Grav/Common/Markdown/ParsedownTest.php +++ b/tests/unit/Grav/Common/Markdown/ParsedownTest.php @@ -73,6 +73,16 @@ class ParsedownTest extends \Codeception\TestCase\Test public function testImages() { + $this->config->set('system.languages.supported', ['fr','en']); + unset($this->grav['language']); + $this->grav['language'] = new Language($this->grav); + $this->uri->initializeWithURL('http://testing.dev/fr/item2/item2-2')->init(); + + $this->assertSame('

', + $this->parsedown->text('![](sample-image.jpg)')); + $this->assertRegexp('|

<\/p>|', + $this->parsedown->text('![](cache-image.jpg?cropResize=200,200&foo)')); + $this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init(); $this->assertSame('

', @@ -86,15 +96,7 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->assertSame('

', $this->parsedown->text('![](/home-missing-image.jpg)')); - $this->config->set('system.languages.supported', ['fr','en']); - unset($this->grav['language']); - $this->grav['language'] = new Language($this->grav); - $this->uri->initializeWithURL('http://testing.dev/fr/item2/item2-2')->init(); - $this->assertSame('

', - $this->parsedown->text('![](sample-image.jpg)')); - $this->assertRegexp('|

<\/p>|', - $this->parsedown->text('![](cache-image.jpg?cropResize=200,200&foo)')); } public function testImagesSubDir()