Shortcode: image photo path

Making progress with my first shortcode plugin. However, there is one hurdle that’s giving me a hard time.

Trying to create a client-friendly method to add photos with captions into an article. My shortcode is essentially an HTML figure element with an image, citation (photo credit) and caption. The issue I’m having is an incomplete image path. The img src is resolving to one folder above where it is actually being stored – in the article folder along with item.md.

In researching a solution, I’m coming to the conclusion that some regular expression code-fu or a possible re-direct needs to be applied. Any help with code explanation would be greatly appreciated.

Currently, the shortcode thinks that the image is located at:
http://localhost/siteRoot/blogRoot/sectionRoot/image.jpg

The correct path should be:
http://localhost/siteRoot/blogRoot/sectionRoot/specificArticle/image.jpg

Here is the twig code with the php code below:

<figure class="figur-wrapper clearfix">
    <div class="figur {{ width }} {{ position }}" id="{{ hash ~ key }}">
       <img src="{{ image }}"  class="cover" alt="captioned photo">
      <cite>{{ cite }}</cite>
        <div class="caption">{{ shortcode.getContent() }}</div>
    </div>
</figure>
<?php

namespace Grav\Plugin\Shortcodes;

use Thunder\Shortcode\Shortcode\ShortcodeInterface;

class FigurShortcode extends Shortcode
{
    function init()
    {
        $this->shortcode->getHandlers()->add('figur', function(ShortcodeInterface $sc) {

            // Add assets
            $this->shortcode->addAssets('css', 'plugin://shortcode-greg/css-compiled/sc-sandboxTwo.css');

            $hash = $this->shortcode->getId($sc);

            $output = $this->twig->processTemplate('partials/sc-figur.html.twig', [
                'hash' => $hash,
                'position' => $sc->getParameter('position', 'right'),
                'width' => $sc->getParameter('width', 'one-third'),
                'image' => $sc->getParameter('image', ''),
                'cite' => $sc->getParameter('cite', 'Image by author'),
                'shortcode' => $sc,
            ]);

            return $output;
        });

Came upon this searching for a solution to the same problem. My thought is to expand an existing solution which uses regex to parse out the component parts of the img-element and re-arranging these into a <figure> including <figcaption>.

Now that we have a good plugin for applying PCRE-functions as filters, the replacement should be fairly much easier. You could essentially apply the filter on whichever template needed it. My approach is this:

Take processed Markdown, which outputs this:

<img title="Palestinian refugees escaping the 1948 palestine war" alt="Palestinian refugees 1948" src="http://somewhere.dev/pages/featured/1948.jpg">

Use a regex-pattern (found online):

/<img[^>]*?alt=\x22([^\x22]*)\x22[^>]*?src=\x22([^\x22]*)[^>]*?>|<img[^>]*?src=\x22([^\x22]*)\x22[^>]*?alt=\x22([^\x22]*)\x22[^>]*?>/ig

Replace with this:
<figure>$0<figcaption>$1</figcaption></figure>

And in PHP:

preg_replace("/<img[^>]*?alt=\x22([^\x22]*)\x22[^>]*?src=\x22([^\x22]*)[^>]*?>|<img[^>]*?src=\x22([^\x22]*)\x22[^>]*?alt=\x22([^\x22]*)\x22[^>]*?>/",
					"<figure>$0<figcaption>$1</figcaption></figure>",
					$buffer)

I wrapped this in a plugin, like so:

<?php
namespace Grav\Plugin;

use Grav\Common\Data;
use Grav\Common\Plugin;
use Grav\Common\Grav;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\Event\Event;

class ImgCaptionsPlugin extends Plugin
{
    public static function getSubscribedEvents() {
        return [
            'onPageContentProcessed' => ['onPageContentProcessed', 0]
        ];
    }

    public function onPageContentProcessed(Event $event)
    {
        $page = $event['page'];
        $pluginsobject = (array) $this->config->get('plugins');
        $pageobject = $this->grav['page'];
		if (isset($pluginsobject['imgcaptions'])) {
            if ($pluginsobject['imgcaptions']['enabled']) {
				$buffer = $page->content();
				$url = $page->url();
				$buffer = preg_replace("/<img[^>]*?alt=\x22([^\x22]*)\x22[^>]*?src=\x22([^\x22]*)[^>]*?>|<img[^>]*?src=\x22([^\x22]*)\x22[^>]*?alt=\x22([^\x22]*)\x22[^>]*?>/",
					"<figure>$0<figcaption>$1</figcaption></figure>",
					$buffer);
				$page->setRawContent($buffer);
            }
        }
    }
}

The effect is that all images specified in Markdown (![Alt text](/path/to/img.jpg "Optional title")) are processed, wrapped in <figure>, and the content of the alt-attribute wrapped in <figcaption>.

Ideally it would be an option of the plugin to allow for a limited selection of templates to which it applies, but in my case this was not (at least currently) necessary.

neat! thanks for that info.

In earnest I’d like to apply a HTMLParser instead of Regex, but as a quick path it works.

Utilizing a fairly old HTMLParser, PHP Simple HTML DOM Parser (v1.5), I did a quick proof-of-concept of adding in a srcset-attribute to accommodate responsive sizes of images. Like so:

<?php
namespace Grav\Plugin;

use Grav\Common\Data;
use Grav\Common\Plugin;
use Grav\Common\Grav;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\Event\Event;
include_once('vendor/simplehtmldom/simple_html_dom.php');

class ImgSrcsetPlugin extends Plugin
{
    public static function getSubscribedEvents() {
        return [
            'onPageContentProcessed' => ['onPageContentProcessed', -1]
        ];
    }
    public function onPageContentProcessed(Event $event) 
    {
        $page = $event['page'];
        $pluginsobject = (array) $this->config->get('plugins');
        $pageobject = $this->grav['page'];
		$widths = array('320', '480', '640',  '960',  '1280',  '1600',  '1920',  '2240');
		if (isset($pluginsobject['imgsrcset'])) {
            if ($pluginsobject['imgsrcset']['enabled']) {
				$url = $page->url();
				$buffer = str_get_html($page->content());
				$images = $buffer->find('img');
				foreach($images as $image) {
					$file = pathinfo($image->src);
					$srcsets = '';
					foreach($widths as $width) {
						$srcsets .= '';
						$srcsets .= $file['dirname'].'/'.$file['filename'].'-'.$width.'.'.$file['extension'].' '.$width.'w, ';
					}
					$srcsets = rtrim($srcsets, ", ");
					$src = 'src="'.$file['dirname'].'/'.$file['filename'].'.'.$file['extension'].' '.$srcsets;
					$new = $image;
					$image->srcset = $srcsets;
					$image->sizes = '100vw';
					$buffer = str_replace($image, $new, $buffer);
				}
				$page->setRawContent($buffer);
            }
        }
    }
}

The parser is rather slow, but it’s dependencies are far less than newer alternatives. Ideally it would use the dom-extension of PHP, but including third-party libraries > installing PHP-extensions server-wide.

Does Grav have a preferred HTMLParser (ie. in use at some point, thus a dependancy)? Or does anyone have a suggestion of a faster/newer one that has little overhead?

HTML Dom parsing is notoriously slow. Something I try to stay away from.

I often prefer using regex for “simplicity”, but Dom-parsing is not prone to error from imperfect patterns.

I tried a newer parser, which by its own comparison is faster than other alternatives, and the time compared with regex seems negligible. Required Composer to run, but the 32 seconds saved in rendering (compared to Simple HTML DOM) is definitely worth it. Implementation for reference:

<?php
namespace Grav\Plugin;

use Grav\Common\Data;
use Grav\Common\Plugin;
use Grav\Common\Grav;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\Event\Event;
require __DIR__ . '/vendor/autoload.php';
use DiDom\Document;

class ImgSrcsetPlugin extends Plugin
{
    public static function getSubscribedEvents() {
        return [
            'onPageContentProcessed' => ['onPageContentProcessed', -1]
        ];
    } 
    public function onPageContentProcessed(Event $event)
    {
        $page = $event['page'];
        $pluginsobject = (array) $this->config->get('plugins');
        $pageobject = $this->grav['page'];
		if (isset($pluginsobject['imgsrcset'])) {
            if ($pluginsobject['imgsrcset']['enabled']) {
				$doc = new Document($page->content());
				$widths = array('320', '480', '640',  '960',  '1280',  '1600',  '1920',  '2240');
				$images = $doc->find('img');
				foreach($images as $image) {
					$file = pathinfo($image->src);
					$srcsets = '';
					foreach($widths as $width) {
						$srcsets .= '';
						$srcsets .= $file['dirname'].'/'.$file['filename'].'-'.$width.'.'.$file['extension'].' '.$width.'w, ';
					}
					$srcsets = rtrim($srcsets, ", ");
					$image->setAttribute('srcset', $srcsets);
					$image->setAttribute('sizes', '100vw');
				}
				$page->setRawContent($doc);
            }
        }
    }
}
---

Gingah, will be very interested to see how this plugin develops. Having access to the new(ish) HTML picture element in Grav would be awesome. Bonus points if it’s a client-friendly implementation.
I understand that the picture spec is daunting – so best of luck!

There is some “controversy” around the picture element, and my understanding is that currently the element with nested including srcset’s is preferred. Presumably this is because of the current status of 's implementation (also this, but syntactically I find it preferable.

I suppose a switch between implementation’s is easy to do, but my current focus is on the DiDom-parser. Whilst its clearly fast enough to warrant use, I did have some issues running it on my live server - though I haven’t had the time to troubleshoot this yet.

The technical issue is a bit of a quandary. Post-cache it will return a server error:

PHP Fatal error:  Method DiDom\Document::__toString() must not throw an exception in /root/system/src/Grav/Common/Page/Page.php on line 575

Which is this line:

$divider_pos = mb_strpos($this->content, "<p>{$delimiter}</p>");

I am not quite sure why the summary divider would cause an exception here, as $page->content() is only iterated over to add the srcset- and sizes attributes to the img-elements.

I released this solution - using a different DOMParser - as ImgSrcset.

In reference to the original post, I meant to write that I released a solution as ImgCaptions.

Gingah… Well done. I just had a chance to test-drive your ImgCaptions plugin. It does, indeed, perform exactly as described on the tin. Image with caption is converted to valid figure, img and figcaption elements in the `HTML.

Images are specified with standard markdown format – the exception being the addition of of quoted text – which would be the caption text. For example:

![Jewelry and Lipstick](diamondJewlry.png?cropZoom=300,200 "This is a text that will be used as the caption for the photograph. My sight said the hot VR goggles to be sure.")

Your solution works quite well. My problem is that, perhaps the end result is a bit too vanilla? Developers would be able to write global styles for figure and figcaption but nothing more.

The shortcode that I wrote requires a bit more effort from article authors. However, this extra effort pays off in that the figure element becomes a styled entity (see attached) that is integrated 2016-05-20_10-30-44 2016-05-20_10-31-57 into the page layout. For exa mple, background color, floats, wrapping text and width alternatives.

[figur width="one-third" position="fig-right" image="enclosingFolder/photoName.jpg" cite="Acme Photo Service"]Photo caption would go here. This is a custom GRAV `shortcode` to render an HTML fig/figcaption element. Options include figure **width** (one-third or one-half) and figure **position** (fig-left or fig-right). Image path must be specified with parent folder and file name.[/figur]

My shortcode is far from perfect. Ultimately, I prefer your more straightforward approach as being more friendly to non-technical authors — I just wish that there were more styling options. Maybe this is not possible?

I have an idea for the ImgCaptions-plugin which takes any classes originally set on the Markdown image and applies it to the -element instead. This would allow ultimately all custom styling and easy layout fixes, as any effect meant for the image would now work for the figure, given that the CSS is correctly defined.

Needs a bit more testing though, but that is my thought for the next version. The ImgSrcset-plugin is a bit more plug-and-play, as it just generates the needed srcset-attribute based on a set of rules and assumptions about files. For the next version of that, however, I will implement the -element as an alternative.