Merge branch 'feature/breaking_out_link_and_image_logic' into develop

This commit is contained in:
Andy Miller
2016-10-11 15:56:35 -06:00
7 changed files with 369 additions and 152 deletions

View File

@@ -15,6 +15,7 @@
* Added new `Cache::setEnabled` and `Cache::getEnabled` to enable outside control of cache
* Updated vendor libs including Twig `1.25.0`
* Avoid git ignoring any vendor folder in a Grav site subfolder (but still ignore the main `vendor/` folder)
* Added an option to get just a route back from `Uri::convertUrl()` function
1. [](#bugfix)
* Fixed missing `progress` method in the DirectInstall Command
* `Response` class now handles better unsuccessful requests such as 404 and 401

View File

@@ -0,0 +1,260 @@
<?php
/**
* @package Grav.Common.Helpers
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Common\Page\Medium\Medium;
class Excerpts
{
/**
* Process Grav image media URL from HTML tag
*
* @param $html HTML tag e.g. `<img src="image.jpg" />`
* @param $page The current page object
* @return string Returns final HTML string
*/
public static function processImageHtml($html, $page)
{
$excerpt = static::getExcerptFromHtml($html, 'img');
$original_src = $excerpt['element']['attributes']['src'];
$excerpt['element']['attributes']['href'] = $original_src;
$excerpt = static::processLinkExcerpt($excerpt, $page, 'image');
$excerpt['element']['attributes']['src'] = $excerpt['element']['attributes']['href'];
unset ($excerpt['element']['attributes']['href']);
$excerpt = static::processImageExcerpt($excerpt, $page);
$excerpt['element']['attributes']['data-src'] = $original_src;
$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;
}
/**
* Rebuild HTML tag from an excerpt array
*
* @param $excerpt
* @return string
*/
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 .= '</'.$element['name'].'>';
} else {
$html .= ' />';
}
return $html;
}
/**
* Process a Link excerpt
*
* @param $excerpt
* @param $page
* @param string $type
* @return mixed
*/
public static function processLinkExcerpt($excerpt, $page, $type = 'link')
{
$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;
}
// 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;
}
/**
* Process an image excerpt
*
* @param $excerpt
* @param $page
* @return mixed
*/
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 = isset($excerpt['element']['attributes']['alt']) ? $excerpt['element']['attributes']['alt'] : '';
$title = isset($excerpt['element']['attributes']['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;
}
}

View File

@@ -9,8 +9,7 @@
namespace Grav\Common\Markdown;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Common\Helpers\Excerpts;
use RocketTheme\Toolbox\Event\Event;
trait ParsedownGravTrait
@@ -18,13 +17,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|\})\})\)/';
@@ -42,10 +34,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) {
@@ -203,75 +192,9 @@ trait ParsedownGravTrait
$excerpt = parent::inlineImage($excerpt);
}
// Some stuff we will need
$actions = [];
$media = null;
// if this is an image
// if this is an image process it
if (isset($excerpt['element']['attributes']['src'])) {
$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'] : '';
//get the url and parse it
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['src']));
$this_host = isset($url['host']) && $url['host'] == $this->uri->host();
// if there is no host set but there is a path, the file is local
if ((!isset($url['host']) || $this_host) && isset($url['path'])) {
$path_parts = pathinfo($url['path']);
// get the local path to page media if possible
if ($path_parts['dirname'] == $this->page->url(false, false, false)) {
// get the media objects for this page
$media = $this->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 = $this->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
$medium = $media->all()[$path_parts['basename']];
// if there is a query, then parse it and build action calls
if (isset($url['query'])) {
$url['query'] = htmlspecialchars_decode(urldecode($url['query']));
$actions = array_reduce(explode('&', $url['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(',', urldecode($action['params'])));
}
if (isset($url['fragment'])) {
$medium->urlHash($url['fragment']);
}
$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);
}
}
$excerpt = Excerpts::processImageExcerpt($excerpt, $this->page);
}
return $excerpt;
@@ -299,63 +222,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, $type);
}
return $excerpt;

View File

@@ -766,14 +766,14 @@ class Uri
/**
* Converts links from absolute '/' or relative (../..) to a Grav friendly format
*
* @param Page $page the current page to use as reference
* @param Page $page the current page to use as reference
* @param string $url the URL as it was written in the markdown
* @param string $type the type of URL, image | link
* @param bool $absolute if null, will use system default, if true will use absolute links internally
*
* @param string $type the type of URL, image | link
* @param bool $absolute if null, will use system default, if true will use absolute links internally
* @param bool $route_only only return the route, not full URL path
* @return string the more friendly formatted url
*/
public static function convertUrl(Page $page, $url, $type = 'link', $absolute = false)
public static function convertUrl(Page $page, $url, $type = 'link', $absolute = false, $route_only = false)
{
$grav = Grav::instance();
@@ -922,6 +922,10 @@ class Uri
$url = $url_path;
}
if ($route_only) {
$url = str_replace($base_url, '', $url);
}
return $url;
}

View File

@@ -9,8 +9,6 @@
namespace Grav\Common;
use DateTime;
use DateTimeZone;
use Grav\Common\Grav;
use Grav\Common\Helpers\Truncator;
use RocketTheme\Toolbox\Event\Event;

View File

@@ -0,0 +1,85 @@
<?php
use Codeception\Util\Fixtures;
use Grav\Common\Helpers\Excerpts;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Config\Config;
use Grav\Common\Page\Pages;
use Grav\Common\Language\Language;
/**
* Class ExcerptsTest
*/
class ExcerptsTest extends \Codeception\TestCase\Test
{
/** @var Parsedown $parsedown */
protected $parsedown;
/** @var Grav $grav */
protected $grav;
/** @var Page $page */
protected $page;
/** @var Pages $pages */
protected $pages;
/** @var Config $config */
protected $config;
/** @var Uri $uri */
protected $uri;
/** @var Language $language */
protected $language;
protected $old_home;
protected function _before()
{
$grav = Fixtures::get('grav');
$this->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->assertRegexp('|<img alt="Sample Image" src="\/images\/.*-sample-image.jpe?g\" data-src="sample-image\.jpg\?cropZoom=300,300" \/>|',
Excerpts::processImageHtml('<img src="sample-image.jpg?cropZoom=300,300" alt="Sample Image" />', $this->page));
$this->assertRegexp('|<img alt="Sample Image" class="foo" src="\/images\/.*-sample-image.jpe?g\" data-src="sample-image\.jpg\?classes=foo" \/>|',
Excerpts::processImageHtml('<img src="sample-image.jpg?classes=foo" alt="Sample Image" />', $this->page));
}
}

View File

@@ -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('<p><img src="/tests/fake/nested-site/user/pages/02.item2/02.item2-2/sample-image.jpg" /></p>',
$this->parsedown->text('![](sample-image.jpg)'));
$this->assertRegexp('|<p><img src="\/images\/.*-cache-image.jpe?g\?foo=1" \/><\/p>|',
$this->parsedown->text('![](cache-image.jpg?cropResize=200,200&foo)'));
$this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init();
$this->assertSame('<p><img src="/tests/fake/nested-site/user/pages/02.item2/02.item2-2/sample-image.jpg" /></p>',
@@ -86,15 +96,7 @@ class ParsedownTest extends \Codeception\TestCase\Test
$this->assertSame('<p><img src="/home-missing-image.jpg" alt="" /></p>',
$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('<p><img src="/tests/fake/nested-site/user/pages/02.item2/02.item2-2/sample-image.jpg" /></p>',
$this->parsedown->text('![](sample-image.jpg)'));
$this->assertRegexp('|<p><img src="\/images\/.*-cache-image.jpe?g\?foo=1" \/><\/p>|',
$this->parsedown->text('![](cache-image.jpg?cropResize=200,200&foo)'));
}
public function testImagesSubDir()