Mike Slinn
Mike Slinn

jekyll_plugin_support

Published 2023-02-12. Last modified 2023-05-18.
Time to read: 4 minutes.

This page is part of the jekyll_plugins collection, categorized under Jekyll, Ruby.

After writing several dozen Jekyll plugins, I distilled the common code into jekyll_plugin_support. This Ruby gem facilitates writing and testing Jekyll plugins, and handles the standard housekeeping that every Jekyll tag and block plugin requires. Logging, parsing arguments, obtaining references to the site and page objects, etc. are all handled. The result is faster Jekyll plugin writing, with fewer bugs.

At present, only Jekyll tags and blocks are supported.

Plugins that were written using Jekyll Plugin Support include:

... and also the demonstration plugins in jekyll_plugin_support

Installation

Jekyll_plugin_support is packaged as a Ruby gem. If your custom plugin will reside in a Jekyll project’s _plugins directory, add the following line to your Jekyll plugin’s Gemfile.

Gemfile
group :jekyll_plugins do 
  gem 'jekyll_plugin_support'
end 

Otherwise, if your custom plugin will be packaged into a gem, add the following to your plugin’s .gemspec:

my_plugin.gemspec
Gem::Specification.new do |spec|
  ... 
  spec.add_dependency 'jekyll_plugin_support'
  ...
end 

Install the jekyll_plugin_support gem in the usual manner:

Shell
$ bundle

About

JekyllSupport::JekyllBlock and JekyllSupport::JekyllTag provide support for Jekyll tag block plugins and Jekyll tag plugins, respectively. They are very similar in construction and usage.

Instead of subclassing your custom Jekyll block tag class from Liquid::Block, subclass from JekyllSupport::JekyllBlock. Similarly, instead of subclassing your custom Jekyll tag class from Liquid::Tag, subclass from JekyllSupport::JekyllTag.

Both JekyllSupport classes instantiate new instances of PluginMetaLogger (called @logger) and JekyllPluginHelper (called @helper).

JekyllPluginHelper defines a generic initialize method, and your tag or block tag class should not need to override it. Also, your tag or block tag class should not define a method called render, because jekyll_plugin_support defines one.

Instead, define a method called render_impl. For tags, render_impl does not accept any parameters. For block tags, a single parameter is required, which contains text passed from your block in the page.

Your implementation of render_impl can parse parameters passed to the tag / block tag, as described in Tag Parameter Parsing.

The following variables are predefined within render. See the Jekyll documentation for more information.

  • @argument_string – Original unparsed string from the tag in the web page
  • @config – Jekyll configuration data
  • @envs – Jekyll environment variables, as shown in the screenshot below
  • @layout – Front matter specified in layouts
  • @modepossible values are: development, production, or test
  • @page – Jekyll page variable
  • @paginator – Only has a value when a paginator is active; they are only available in index files.
  • @site – Jekyll site variable
  • @tag_name – Name of the tag or block plugin
  • @theme – Theme variables (introduced in Jekyll 4.3.0)
Jekyll environments
Jekyll environments

Keyword Options

For all keyword options, values specified in the document may be provided. If a value is not provided, the value true is assumed. Otherwise, if a value is provided, it must be wrapped in single or double quotes.

Examples

The following examples use the die_if_error keyword option for the pre and exec tags from the jekyll_pre plugin.

Specifying Tag Option Values

The following sets die_if_error true:

Implicitly enabling die_if_error
{% pre die_if_error %} ... {% endpre %}

The above is the same as writing:

Explicitly enabling die_if_error
{% pre die_if_error='true' %} ... {% endpre %}

Or writing:

Explicitly enabling die_if_error
{% pre die_if_error="true" %} ... {% endpre %}

Neglecting to provide surrounding quotes around the provided value causes the parser to not recognize the option. Instead, what you had intended to be the keyword/value pair will be parsed as part of the command. For the pre tag, this means the erroneous string becomes part of the label value, unless label is explicitly specified. For the exec tag, this means the erroneous string becomes part of the command to execute. The following demonstrates the error.

Pre: missing quotes around the value of die_if_error
{% pre die_if_error=false %} ... {% endpre %}

The above causes the label to be die_if_error=false.

Exec: missing quotes around the value of die_if_error
{% exec die_if_error=false ls %} ... {% endpre %}

The above causes the command to be executed to be die_if_error=false ls, instead of ls.

Writing Plugins

The following minimal examples define VERSION, which is important because JekyllPluginHelper.register logs that value when registering the plugin.

This is how you would define plugins in the _plugins directory:

Boilerplate for a tag plugin
require 'jekyll_plugin_support'

module Jekyll
  class DemoTag < JekyllSupport::JekyllTag
    VERSION = '0.1.0'.freeze

    def render_impl
      @helper.gem_file __FILE__ # Enables attribution; only works when plugin is a gem
      # Your Jekyll plugin logic goes here
    end

    JekyllPluginHelper.register(self, 'demo_tag')
  end
end
Boilerplate for a tag block plugin
require 'jekyll_plugin_support'

module Jekyll
  class DemoBlock < JekyllSupport::JekyllBlock
    VERSION = '0.1.0'.freeze

    def render_impl(text)
      @helper.gem_file __FILE__ # Enables attribution; only works when plugin is a gem
      # Your Jekyll plugin logic goes here
    end

    JekyllPluginHelper.register(self, 'demo_block')
  end
end

If your plugin is packaged as a gem, then you might need to include version.rb into the plugin class. For example, if your version module looks like this:

lib/my_plugin/version.rb
module MyPluginVersion
  VERSION = '0.5.0'.freeze
end

Then your plugin can incorporate the VERSION constant into your plugin like this:

lib/my_plugin.rb
require 'jekyll_plugin_support'
require_relative 'my_plugin/version'

module Jekyll
  class MyBlock < JekyllSupport::JekyllBlock
    include MyPluginVersion

    def render_impl(text)
      @helper.gem_file __FILE__ # Enables attribution; only works when plugin is a gem
      # Your code here
    end

    JekyllPluginHelper.register(self, 'demo_block')
  end
end

No_arg_parsing Optimization

If your tag or block plugin only needs access to the raw arguments passed from the web page, without tokenization, and you expect that the plugin might be invoked with large amounts of text, derive your plugin from JekyllBlockNoArgParsing or JekyllTagNoArgParsing. See the demo plugins for an example.

This feature is used by the select tag in the jekyll_pre plugin.

Subclass Attribution

JekyllTag and JekyllBlock subclasses of jekyll_plugin_support can utilize the attribution option IFF they are published as a gem. Jekyll­Tag­No­Arg­Parsing and Jekyll­Block­No­Arg­Parsing subclasses cannot.

When used as a keyword option, a default value is used for the attribution string. When used as a name/value option, the attribution string can be specified. Using the attribution option cause subclasses to replace their usual output with HTML that looks like:

<div id="jps_attribute_12345" class="jps_attribute">
  <a href="https://github.com/mslinn/jekyll_outline">
    <b>Generated by <code>jekyll_outline</code>.
  </a>
</div>

The id attribute is in the sample HTML above is randomized so more than one attribution can appear on a page.

Attribution Generation

You can decide where you want the attribution string for your Jekyll tag to appear by invoking @helper.attribute. For example, this is how the jekyll_outline tag generates output:

portion of render_impl in jekyll_outline
<<~HEREDOC
<div class="outer_posts">
#{make_entries(collection)&.join("\n")}
</div>
#{@helper.attribute if @helper.attribution}
HEREDOC

Usage

Typical usage for the attribution tag is:

{% my_tag attribution %}

Normal processing of my_tag is augmented by interpolating the attribution format string, which is a Ruby-compatible interpolated string.

The default attribution format string is:

"Generated by the #{name} #{version} Jekyll plugin, written by #{author} #{date}."

Because jekyll_plugin_suppprt subclasses are gems, their gemfiles define values for name, version, homepage, and authors, as well as many other properties. The date property is obtained from the plugin/gem publishing date.

An alternative attribution string can be specified properties can be output using any of the above properties:

{% my_tag attribution="Generated by the #{name} #{version} Jekyll plugin, written by #{author} #{date}" %}

Demonstration Plugins

The jekyll_plugin_support GitHub project includes a demo website. Three short plugins are demonstrated that show how to access the variables that the Jekyll Plugins Support gem provides.

The following examples use Ruby’s squiggly heredoc operator (<<~). The squiggly heredoc operator removes the outmost indentation. This provides easy-to-read multiline text literals.

require 'jekyll_plugin_support'

module Jekyll
  class DemoTag < JekyllSupport::JekyllTag
    VERSION = '0.1.2'.freeze

    def render_impl
      @keyword1  = @helper.parameter_specified? 'keyword1'
      @keyword2  = @helper.parameter_specified? 'keyword2'
      @name1  = @helper.parameter_specified? 'name1'
      @name2  = @helper.parameter_specified? 'name2'

      <<~END_OUTPUT
        #{@helper.attribute if @helper.attribution}
        <pre>@helper.tag_name=#{@helper.tag_name}

        @helper.attribution=#{@helper.attribution}
        @helper.attribute=#{@helper.attribute}

        @mode=#{@mode}

        # Passed into Liquid::Tag.initialize
        @argument_string="#{@argument_string}"

        @helper.argv=
          #{@helper.argv.join("\n  ")}

        # Liquid variable name/value pairs
        @helper.params=
          #{@helper.params.join(', ')}

        @helper.keys_values=
        #{(@helper.keys_values.map { |k, v| "  #{k}=#{v}\n" }).join("  \n")}

        remaining_markup='#{@helper.remaining_markup}'

        @config['url']='#{@config['url']}'

        @site=#{@site}

        @page['description']=#{@page['description']}

        @page['path']=#{@page['path']}

        @keyword1=#{@keyword1}

        @keyword2=#{@keyword2}

        @name1=#{@name1}

        @name2=#{@name2}

        @envs=#{@envs.keys.join(' ')}</pre>
      END_OUTPUT
    end

    JekyllPluginHelper.register(self, 'demo_tag')
  end
end

require 'jekyll_plugin_support'

module Jekyll
  class DemoBlock < JekyllSupport::JekyllBlock
    VERSION = '0.1.1'.freeze

    def render_impl(text)
      @keyword1  = @helper.parameter_specified? 'keyword1'
      @keyword2  = @helper.parameter_specified? 'keyword2'
      @name1  = @helper.parameter_specified? 'name1'
      @name2  = @helper.parameter_specified? 'name2'

      <<~END_OUTPUT
        #{@helper.attribute if @helper.attribution}
        <pre>@helper.tag_name=#{@helper.tag_name}

        @helper.attribution=#{@helper.attribution}
        @helper.attribute=#{@helper.attribute}

        @mode=#{@mode}

        # Passed into Liquid::Block.initialize
        @argument_string="#{@argument_string}"

        @helper.argv=
          #{@helper.argv.join("\n  ")}

        # Liquid variable name/value pairs
        @helper.params=
          #{@helper.params&.join(', ')}

        @helper.remaining_markup='#{@helper.remaining_markup}'

        @helper.keys_values=
        #{(@helper.keys_values.map { |k, v| "  #{k}=#{v}\n" })&.join("  \n")}

        @config['url']='#{@config['url']}'

        @site.url=# {@site.url}

        @page['description']=#{@page['description']}

        @page['path']=#{@page['path']}

        @keyword1=#{@keyword1}

        @keyword2=#{@keyword2}

        @name1=#{@name1}

        @name2=#{@name2}

        text='#{text}'

        @envs=#{@envs.keys.join(' ')}</pre>
      END_OUTPUT
    end

    JekyllPluginHelper.register(self, 'demo_block')
  end
end

The following is an example of no_arg_parsing optimization.

require 'jekyll_plugin_support'

module Jekyll
  class DemoTagNoArgs < JekyllSupport::JekyllTagNoArgParsing
    VERSION = '0.1.0'.freeze

    def render_impl
      <<~END_OUTPUT
        The raw arguments passed to this <code>DemoTagNoArgs</code> instance are:<br>
        <code>#{@argument_string}</code>
      END_OUTPUT
    end

    JekyllPluginHelper.register(self, 'demo_tag_no_arg')
  end
end