After a long time away from this series, it’s time to make the admin interface nicer with Grav’s blueprints.
As usual, Grav has a pretty good write-up available for the blueprints feature on their documentation site
The plan
In addition to tidying up some of the metadata in the blueprint (low-hanging fruit; we’ll do that last), configuring form elements in the blueprint allow users to alter the plugin’s behaviour from the admin panel. To do that, we’ll need three fields:
- The toggle to enable or disable the plugin.
- A field to override the custom theme directory.
- A field to enter the theme name.
The first of these is already sorted thanks to the scaffolding features of the devtools plugin, as shown below.
Changing the custom themes directory path
For the custom themes directory, let’s edit the text_var
field that the
devtools plugin produced for us. In blueprints.yaml
, let’s rename the field,
and hard-code (for now) the label
and help
fields. We’ll later refactor them
to use languages.yaml
so that the plugin can more readily be
internationalized.
- text_var:
+ custom_styles:
type: text
- label: PLUGIN_HIGHLIGHT_PHP.TEXT_VARIABLE
- help: PLUGIN_HIGHLIGHT_PHP.TEXT_VARIABLE_HELP
+ label: User/custom CSS Styles
+ help: Subdirectory in the /user/custom/ directory to store your custom syntax themes
+ default: 'php-highlight-styles'
+ size: medium
That doesn’t do much on its own; let’s add some code back in highlight-php.php
to create this directory path if it doesn’t already exist.
First, import a couple of classes built into Grav at the top of the file:
use Grav\Common\Grav;
use Grav\Common\Filesystem\Folder;
Then, put them to use by adding this suite of code to the onPluginsInitialized
method:
// create the user/custom directory if it doesn't exist
$customStylesDirName = $this->config->get('plugins.highlight-php.custom_styles');
$locator = Grav::instance()['locator'];
$userCustomDirPath = $locator->findResource('user://') . '/' . 'custom' . '/' . $customStylesDirName;
if (!($locator->findResource($userCustomDirPath))) {
Folder::create($userCustomDirPath);
}
Setting up our blueprint
The Grav team’s plugin for highlight.js writes out the full list of available
themes in
blueprints.yaml
.
Instead of writing out every entry by hand, I’d prefer to generate the list programmatically. That way, if the upstream library adds new styles, our menu will automatically pick them up, and we can add any custom styles the user might add to the top of the list.
Grav allows us to do this by supplying functions as values to data-*@
YAML
keys.See the Grav documentation on advanced blueprint fetaures: using
function
calls,
and also the admin recipe for adding a custom select
field…
which also contains a long, hardcoded list of values.
Let’s start with the easier part in our blueprints.yaml
file. Add the new
theme
key below the custom_styles
entry, being sure to match the indentation
level, and the new subkeys and values. Don’t worry about the data-options@
call to a function that doesn’t exist yet — we’ll write that next.
custom_styles:
type: text
⋮
theme:
type: select
size: medium
classes: fancy
label: Theme
help: 'Select an avaialble theme. Your custom themes appear at the top of the list.'
default: 'default'
data-options@: '\Grav\Plugin\PhpHighlightPlugin::getAvailableThemes'
Now that we’ve told Grav to call the static function getAvailableThemes
, we’ll
need to create it. It was a bit surprising to me to learn that if the function
doesn’t exist, the form still renders correctly: I had expected an error.
Getting the list of themes
We’ll break this into two parts. First, get the list of themes that ship with
the highlight.php library. After that’s working, we’ll add any optional themes
the user has uploaded to their custom/php-highlight-styles
directory.
For reference, here is the form of the key-value pairs that we want to produce
with our getAvailableThemes
function, taken from the Grav team’s plugin that
uses highlight.js:
⋮
a11y-dark: A11y Dark
a11y-light: A11y Light
agate: Agate
⋮
If you compare these to the files in the styles
that ships with the
scrivo/highlight.php
library, the task is pretty clear.
- loop over all CSS files in that directory, ➊
- for each one, make an entry in an array…
- whose key ➋ is the file basename, and
- whose value ➌ is the file basename, with spaces instead of dashes, and in title case.
We’ll use a couple of nice utilities built into Grav to make this a little easier on ourselves. ➍
public static function getAvailableThemes()
{
# make references to objects on our Grav instance
$grav = Grav::instance();
$locator = $grav['locator'];
$config = $grav['config'];
# initialize an empty array
$themes = [];
# ➍ use the findResource method to resolve the plugin stream location; false returns a relative path
$bundledStylesPath = $locator->findResource('plugin://highlight-php/vendor/scrivo/highlight.php/styles', false);
# plain old PHP glob. See https://www.php.net/manual/en/function.glob.php
$cssFiles = glob($bundledStylesPath . '/*.css');
# loop over each file
foreach ($cssFiles as $cssFile) {
# ➋ store our key
$theme = basename($cssFile, ".css");
# ➌ set our value and add it to the array
$themes[$theme] = Inflector::titleize($theme); # ➍ thanks, titleize
}
# return the array
return $themes;
}
That should do it. Let’s save the file, login to the admin interface, and check out the fruits our labour. We should find that ‘default’ is set by default, with a nice long list of options available in the dropdown, and indeed we do:
Supporting custom themes
There are about 90 themes that currently ship with highlight.php — pretty decent selection for most websites. Some users might want something a little more bespoke for whatever reason. We’ve built in support for storing those custom CSS files, but haven’t yet configured the plugin to actually load a custom theme.
First, we’ll add any custom CSS files to our admin dropdown menu (and put them
at the top of the list, since if a user adds a custom theme to their Grav
install, they’ll probably want to actually use it.) Then we’ll update our
addHighlightingAssets
method to load the CSS file from the appropriate
directory, preferring the custom directory (which will allow the user to copy
and modify an inbuilt theme, following a common pattern in Grav customization.)
The same glob-then-loop pattern for listing the inbuilt themes will work equally
well for our custom themes. We’ll grab the custom directory from the plugin
configuration, making our associative array from any CSS files we find there,
then append the list of inbuilt themes to the end. In the getAvailableThemes
method in highlight-php.php
, we can add the following lines between our
initialized $themes
variable and the rest of the code we set up above:
# resolve the custom styles directory
$customStylesDirName = $config->get('plugins.highlight-php.custom_styles');
$customStylesPath = $locator->findResource('user://custom/' . $customStylesDirName, false);
if ($customStylesPath) {
# get our list of custom CSS files
$customCssFiles = glob($customStylesPath . '/*.css');
foreach ($customCssFiles as $cssFile) {
# append a superscript 1 (¹) to prevent naming conflicts if customizing an inbuilt theme
$theme = basename($cssFile, '.css') . '¹';
# indicate to the user that this theme is one of the custom uploads
$themes[$theme] = Inflector::titleize($theme) . ' (custom)';
}
}
It’s quite similar to what was already written; note the means of preventing
name conflicts and communicating to the end user that a given theme comes from
the custom directory. For example, if a user copied the default.css
theme to
the user://custom/highlight-php-styles
directory to make a customization, the
entry in the array that gets passed into the dropdown menu would have the form
array('default¹' => 'Default (custom)')
, although the filename would
still be default.css
.
In the asset loading function, we’ll need to ‘undo’ those changes in the case of
a custom CSS file. We can check for a custom theme by testing whether the theme
name (i.e., the file basename) passed to addHighLightingAssets
ends with ‘¹’.
If so, look up the corresponding file in the configured custom styles directory.
If not, carry on with what we wrote earlier to resolve the path to the builtin style.
private function addHighLightingAssets($theme)
{
$locator = $this->grav['locator'];
if (str_ends_with($theme, '¹')) {
// custom theme
$theme = str_replace('¹', '', $theme);
$customStylesDirName = $this->grav['config']->get('plugins.highlight-php.custom_styles');
$themePath = $locator->findResource('user://custom/' . $customStylesDirName . '/' . $theme . '.css', false);
} else {
// built-in theme
$themePath = $locator->findResource('plugin://highlight-php/vendor/scrivo/highlight.php/styles/' . $theme . '.css', false);
}
$this->grav['assets']->addCss($themePath);
}
Feel free to change a swap between a couple of different builtin theme to verify that it’s working. To check that the logic to handle custom styles is correct, let’s try two cases; in both cases, we need to confirm that all such themes are listed in the admin dropdown, and then properly loaded into the assets pipeline if selected.
In the first case, we want to ensure that a completely new theme in the custom
styles directory is picked up. In the second case, we want to see that if a
builtin theme is copied into the custom_styles
directory and modified, that
those overrides are reflected on the front end.
To check the first case, I’ve grabbed the ‘Green Screen’ theme from the
highlight.js project and copied it into the
user://custom/highlight-php-styles
directory. After saving the file and
refreshing the admin panel, as planned, “Green Screen (custom)” appears at the
top of the dropdown list of available themes:
After selecting it, saving, and refreshing the front-end, lo and behold: a Matrix-like throwback to the iconic IBM 3270 terminal on our inline and block code samples:
To confirm that overrides are also working, copy a builtin theme to the same
directory and make a change that will plainly obvious. In this demo, I’ve copied
the github.css
theme and added a deliberately ridiculous filter effect with
the following rulesets at the bottom of the file:
.hljs {
border:
filter: blur(5px) hue-rotate(120deg) contrast(1.5);
transition: 1s filter ease-in-out, 1s -webkit-filter ease-in-out;
}
.hljs:hover {
filter: none;
}
As before, select and save the theme, then refresh on the front end and take a look:
And with that, the programming parts of the plugin are complete.✨
Completing the blueprint
There are a couple of bookkeeping things to take care of in blueprints.yaml
.
Adding some keywords, updating the demo URL, changing the icon: these don’t
require much explanation. One small postscript of sorts, though, is to update
the form fields to use Grav’s multi-language features for plugins.
This will make it easier to add translations for the plugin in the future.
The devtools
scaffolding already created a languages.yaml
file in the plugin
root directory, following the documented convention for plugins, which “is to
use PLUGIN_PLUGINNAME. as a prefix for all language strings, to avoid any name
conflict.”
It’s a pretty straightforward matter of copying the label and help fields from
our blueprints.yaml
into languages.yaml
:
en:
PLUGIN_HIGHLIGHT_PHP:
- TEXT_VARIABLE: Text Variable
- TEXT_VARIABLE_HELP: Text to add to the top of a page
+ LABEL_CUSTOM_STYLES: User/custom CSS Styles
+ LABEL_CUSTOM_STYLES_HELP: Subdirectory in the /user/custom/ directory to store your custom syntax themes
and then substituting the appropriate values, based on the keys in the languages.yaml
file, back into blueprints.yaml
.
custom_styles:
type: text
- label: User/custom CSS Styles
- help: Subdirectory in the /user/custom/ directory to store your custom syntax themes
+ label: PLUGIN_HIGHLIGHT_PHP.LABEL_CUSTOM_STYLESs
+ help: PLUGIN_HIGHLIGHT_PHP.LABEL_CUSTOM_STYLES_HELP
That’s a wrap. In the next (and probably final) post of this series, I’ll cover documentation considerations and the process for requesting that the plugin be added to the GPM registry.