Skip to content

How to Build a Grav Plugin: Part 6

Making the admin interface useful

  • code
  • tutorial
  • series

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:

  1. The toggle to enable or disable the plugin.
  2. A field to override the custom theme directory.
  3. 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.

A look at our plugin form fields before we make any changes
A look at our plugin form fields before we make any changes

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.

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:

Our dropdown is providing the list of styles we expect
Our dropdown is providing the list of styles we expect

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:

"Green Screen (custom)" as the first entry of the admin interface dropdown, as desired.
“Green Screen (custom)” as the first entry of the admin interface dropdown, as desired.

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:

And green-screen.css is successfully loaded by the asset pipeline and rendering our code blocks on the front end.
And green-screen.css is successfully loaded by the asset pipeline and rendering our code blocks on the front end.

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:

A looped GIF demonstrating that our overrides of a builtin theme are working Sweet.
A looped GIF demonstrating that our overrides of a builtin theme are working Sweet.

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.