Update dependencies
We’ve identified that we have two dependencies for our plugin:
- the Shortcode Core plugin, and
- the highlight.php library.
Let’s add the Shortcode Core dependency first. When the plugin is installed via
the admin panel or the gpm
CLI tool, Grav checks the plugin’s
blueprints.yaml
file for dependencies and installs or upgrades those plugins
as required.
Open blueprints.yaml
and add the Shortcode Core plugin, version 4.2.2 or
higher, as shown in the third line of the following: This should be around
line 19 of the file. Why version 4.2.2? I scanned the Shortcode Core
changelog
and selected v4.2.2 on account of the bit about the init()
method override and
the autoloading changes in v4.2.1.
dependencies:
- { name: grav, version: '>=1.6.0' }
- { name: shortcode-core, version: '>=4.2.2' }
That’s a tidy little change to add and commit to git, too.
git add blueprints.yaml
git commit -m "add shortcode-core dependency"
Since we’re not adding our plugin using gpm
yet, let’s also install Shortcode Core.
pushd ../../.. # needs gpm needs to be run from the root of the Grav install
php bin/gpm install shortcode-core
popd # moves you back to the plugin directory
We’ll mostly follow the README
for bringing the highlight.php library into our
plugin, by adding their note on Composer Version Constraints to their composer
command line snippet. That is, from your highlight-php
folder, run:
composer require scrivo/highlight.php:^9.18
This shouldn’t take very long, as it’s a pretty lightweight dependency, but it does install 300 files or so. Most of these are JSON files that define the syntax languages that the library supports, followed by another big group of CSS files that define the colour themes.
git add .
git commit -m "add highlight.php dependency"
Set the stage for our shortcode
The scaffolding from the devtools plugin gives us a great headstart in the file highlight-php.php. Grav has pretty solid documentation on its available event hooks, so read that for more details. Before we write the shortcode itself, let’s configure the under-the-hood bits that will let Grav do its thing.
We’re going to tie into two core Grav events, and one provided by the
shortcode core plugin. We’ll return only onPluginsInitialized
from the
getSubscribedEvents
function call, and handle enabling other events in that
function.
public static function getSubscribedEvents(): array
{
return [
'onPluginsInitialized' => [
['autoload', 100000], // since we're requiring Grav < 1.7
['onPluginsInitialized', 0]
]
];
}
onPluginsInitialized
When the onPluginsInitialized
event is fired, we’ll do a couple of safety
checks before enabling our other events.
- First, we ensure that we’re on the client-facing part of the site by returning early if we’re in the admin view:
// don't proceed if in admin
if ($this->isAdmin()) {
return;
}
- Next, we’ll make sure that the plugin is actually enabled by the user:
// don't proceed if plugin is disabled
if (!$this->config->get('plugins.highlight-php.enabled')) {
return;
}
If we get past both of those checks, we can proceed to enable the shortcode handler event and set the user-selected theme, falling back to a default:
// enable other required events
$this->enable([
'onShortcodeHandlers' => ['onShortcodeHandlers', 0]
]);
// set the configured theme, falling back to 'default' if unset
$theme = $this->config->get('plugins.highlight-php.theme') ?: 'default';
// register the css for our plugin (this function doesn't exist yet)
$this->addHighlightingAssets($theme);
In the above codeblock, we called two functions that don’t exist in our
PhpHighlightPlugin
class yet. Let’s create onShortcodeHandlers
to register
our custom shortcode (which we’re almost ready to start writing) and
addHighlightingAssets
, handle adding our CSS to the Grav Asset Manager.
public function onShortcodeHandlers()
{
// FYI: `onShortCodeHandlers` is fired by the shortcode core at the `onThemesInitialized` event
$this->grav['shortcode']->registerAllShortcodes(__DIR__ . '/shortcodes');
}
private function addHighLightingAssets($theme)
{
// add the syntax highlighting CSS file
$this->grav['assets']->addCss('plugin://highlight-php/vendor/scrivo/highlight.php/styles/' . $theme . '.css');
}
That feels like a good chunk of work to call a commit.
git add .
git commit -m "configure plugin events and register assets"
On to the shortcode itself!
In the onShortcodeHandlers
event in the last codeblock, we referred to the
path that our shortcode will live. The shortcodes
subdirectory name is a
convention that’s followed by many other shortcode plugins in the Grav plugin
repository.While that naming convention isn’t strictly required, sticking
to conventions is often a good practice. (What is required is that the
directory that is passed as an argument to registerAllShortcodes
is the same
directory that houses your shortcodes.) We can make the shortcodes
directory and the file where our ShortCode
like so:
mkdir shortcodes
touch shortcodes/HighlightPhpShortcode.php
The name of our file follows the convention of other shortcode plugins. Let’s scaffold the shortcode and check that our event handler is working as we’d expect.
<?php // shortcodes/HighlightPhpShortcode.php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class HighlightPhpShortcode extends Shortcode
{
public function init()
{
$rawHandlers = $this->shortcode->getRawHandlers();
$rawHandlers->add('hl', function (ShortcodeInterface $sc) {
return "<div>shortcode <span style='font-family: monospace'>hl</span> successfully registered!</div>";
});
}
}
Add the new shortcode to a page — I’ll add it to the home page in the base
Grav installation — and fire up the Grav dev server with php bin/grav
server
.
<!-- 01.home/default.md -->
# Say Hello to Grav!
[hl /] <!-- add this -->
## installation successful...
⋮
The result should be something like this:
Now that we know our plugin is registered and processing our shortcode correctly, we can add the logic that will handle our syntax highlighting. I covered the planned syntax in the second post of this series, so it’s just a matter of turning that desired syntax into something that the highlight.php can use.
Registering the languages
As a refresher, I decided that I’d use the language identifier itself as the tag
name in the shortcodes. Fortunately, the highlight.php library’s Highlighter
class includes a handy method to list all supported languages. The code looks
like this:
// highlight.php::Highlight/Highlighter.php
public static function listRegisteredLanguages($includeAliases = false)
{
if ($includeAliases === true) {
return array_merge(self::$languages, array_keys(self::$aliases));
}
return self::$languages;
}
Back in our shortcode class, we’ll loop over these languages, including the
aliases (so that js
and javascript
work in the same way), and register each
language as its own shortcode. It’s not a bad idea to use plain language in
comments as a kind of pseudocode before implementing the actual code. That’s
what I’ll do here.
<?php // shortcodes/HighlightPhpShortcode.php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class HighlightPhpShortcode extends Shortcode
{
public function init()
{
$rawHandlers = $this->shortcode->getRawHandlers();
// create an instance of the Highlighter class
$hl = new \Highlight\Highlighter();
// store the result of the listRegisteredLanguages helper method,
// passing in the `true` argument to include aliases
$langs = array_unique($hl->listRegisteredLanguages(true));
// loop over the languages...
foreach ($langs as $k) {
// ... and add each one in turn
$rawHandlers->add($k, function (ShortcodeInterface $sc) {
// TODO: update the logic required by the Thunderer Shortcode engine.
return "<div>shortcode <span style='font-family: monospace'>" . $sc->getName() . "</span> successfully registered!</div>";
});
}
}
Still some more work to do, of course, but for now, that little change should be
enough to ensure that we’re on the right track. Instead of testing a single,
hardcoded tag like we did above with hl
, let’s check a few of the languages
known to be supported by highlight.php and their aliases to make sure we’re on
the right track.
<!-- 01.home/default.md -->
# Say Hello to Grav!
[php /]
[javascript /]
[js /]
## installation successful...
⋮
Inline code snippets: self-closing shortcode
Let’s get inline snippets working. Recalling from the second post in this
series, we’d like to turn this: [hl=js code=console.log('hey') /]
into
this: console.log('hey')
. To do so, we need to get the language (which we
just saw we can do using $sc->getName();
) and the code to highlight (which
we can do using $sc->getBbCode();
), and pass those along to the
Highlighter
class instance.
Since we’ll be passing bits of data around, let’s
abstract the highlighting portion into a private function called render
.
private function render(string $lang, string $code)
{
try {
$hl = new \Highlight\Highlighter();
$highlighted = $hl->highlight($lang, $code);
$output = $highlighted->value;
return "<code class='hljs language-$highlighted->language'>$output</code>";
} catch (DomainException $e) {
// if someone uses an unsupported language, we don't want to break the site
return "<code class='hljs whoops-$lang-unknown-language'>$code</code>";
}
}
Now that we’ve got the render
method, let’s update our init
method to handle
grabbing the code, pass the the $lang
and $code
variables from our shortcode
to the render method and return the HTML the that highlight.php gives back
instead of our placeholder text.
// ...
class HighlightPhpShortcode extends Shortcode
{
public function init()
{
// ...
$rawHandlers->add($k, function (ShortcodeInterface $sc) {
$lang = $sc->getName();
$code = $sc->getBbCode();
return $this->render($lang, $code);
});
}
}
Let’s try out our tiny example:
<!-- 01.home/default.md -->
# Say Hello to Grav!
Here goes nothing with our simple inline JavaScript example: [js=console.log('hey') /]
So close! The syntax highlighting is working as expected, but we wanted an
inline code snippet, and our tiny example is on its own line. It turns out that
the default style, along with many others that ship with the plugin, include
this ruleset: .hjls { display: block }
.
Let’s amend our shortcode so that the rendered code
element is always rendered
inline. Taking advantage of CSS specificity rules, we can set a style
attribute to override the ruleset in some of the themes that ship with our
plugin. We’ll do this in the render
function. Here’s the output from
[shell=git diff] after making the change.
- return "<code class='hljs language-$highlighted->language'>$output</code>";
+ return "<code class='hljs language-$highlighted->language' style='display: inline;'>$output</code>";
And here’s the rendered result after making this change:
Longer code samples: paired shortcode
For short snippets that we would prefer to display on their own, or in cases when we want to show mulitple lines of code (as elsewhere on this page)? Back in the second post of this series, I decided that paired shortcodes would handle this. The logic in our code will be basically the same, except that:
- to get the
$code
varible for therender
function, we’ll grab the text between the shortcode tags, using$sc->getContent();
); - to ensure that the output is rendered on its own, we’ll ensure that it has the
CSS rule
{ display: block }
; and - to ensure that the output preserves line breaks, we’ll wrap the output in
<pre></pre>
tags.
Here’s some pseudocode of these new bits to describe how to make this happen:
# in `init`
get $content
create $flag indicating inline or block
if $content is null:
# we're in an inline context
set $code to bbcode
else:
# we're in a block context
set $code to $content
update $flag
# in `render`
receive $lang, $code, $flag
get $highlighted from $lang + $code
if $flag indicates inline:
return $highlighted as is
else:
return $highlighted wrapped in `pre` tags
The init
changes are pretty straightforward: I’ve called the $flag
in my
pseudocode the more descriptive $isInline
. The changes to render
are pretty
similar to the pseudocode, too, although here I’ve added the $display
variable
to make things a little more readable. Here’s the diff
of these changes:
@@ -24,8 +24,14 @@ class HighlightPhpShortcode extends Shortcode
// ... and add each one in turn
$rawHandlers->add($k, function (ShortcodeInterface $sc) {
$lang = $sc->getName();
- $code = $sc->getBbCode();
- return $this->render($lang, $code);
+ $content = $sc->getContent();
+ $isInline = is_null($content);
+ if ($isInline) {
+ $code = $sc->getBbCode();
+ } else {
+ $code = $content;
+ }
+ return $this->render($lang, $code, $isInline);
});
}
}
@@ -33,20 +39,24 @@ class HighlightPhpShortcode extends Shortcode
- private function render(string $lang, string $code)
+ private function render(string $lang, string $code, bool $isInline)
{
try {
$hl = new \Highlight\Highlighter();
$highlighted = $hl->highlight($lang, $code);
$output = $highlighted->value;
- return "<code class='hljs language-$highlighted->language' style='display: inline;'>$output</code>";
+ $display = $isInline ? 'inline' : 'block';
+ $codeElement = "<code class='hljs language-$highlighted->language' style='display: $display'>$output</code>";
+ return $isInline ? $codeElement : "<pre class='hljs'>$codeElement</pre>";
} catch (DomainException $e) {
// if someone uses an unsupported language, we don't want to break the site
- return "<code class='hljs whoops-$lang-unknown-language'>$code</code>";
+ $codeElement = "<code class='hljs whoops-$lang-unknown-language'>$code</code>";
+ return $isInline ? $codeElement : "<pre class='hljs'>$codeElement</pre>";
}
}
}
With those changes made, let’s have a look at how it renders. Updating our base install’s homepage again, now including both a self-closing (i.e., inline) and paired (i.e., block) shortcode example:
<!-- 01.home/default.md -->
# Say Hello to Grav!
Here goes nothing with our simple inline JavaScript example: [js=console.log('hey') /]
[hl=php]
<?php
function add($a, $b) {
return $a + $b;
}
?>
[/hl]
… and we get back almost what we’re after:
To fix that extra whitespace, we’ll just trim our $code
before passing it to
render
.
+ $code = trim($code);
return $this->render($lang, $code, $isInline);
And with that, the logic of our plugin is basically done. Commit, push, and carry on! In the next one, we’ll set up the admin interface for our plugin.