Published 2023-02-12.
Last modified 2023-11-20.
Time to read: 6 minutes.
After writing over two 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
site
and page
objects, etc. are all handled.
The result is faster Jekyll plugin writing with fewer bugs.
Jekyll_plugin_support can be used to create simple Jekyll plugins in the _plugins/
directory of your Jekyll project or gem-based Jekyll plugins.
At present, only Jekyll tags and blocks are supported.
Plugins that use jekyll_plugin_support
include:
jekyll_all_collections
jekyll_badge
jekyll_emoji
jekyll_flexible_include
jekyll_href
jekyll_img
jekyll_plugin_template
jekyll_outline
jekyll_pre
jekyll_quote
... 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
.
group :jekyll_plugins do gem 'jekyll_plugin_support', '>= 0.8.0' end
Otherwise, if your custom plugin will be packaged into a gem,
add the following to your plugin’s .gemspec
:
Gem::Specification.new do |spec| ... spec.add_dependency 'jekyll_plugin_support', '>= 0.8.0' ... end
Install the jekyll_plugin_support
gem in the usual manner:
$ bundle
Copy the CSS classes from demo/assets/css/jekyll_plugin_support.css
to your Jekyll project’s CSS file.
About
JekyllSupport::JekyllBlock
and JekyllSupport::JekyllTag
provide support for
Jekyll tag block plugins and
Jekyll inline 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 inline 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@layout
– Front matter specified in layouts-
@mode
– possible values aredevelopment
,production
, ortest
@page
– Jekyllpage
variable-
@paginator
– Only has a value when apaginator
is active; they are only available in index files. @site
– Jekyllsite
variable@tag_name
– Name of the inline tag or block plugin@theme
– Theme variables (introduced in Jekyll 4.3.0)
Argument Parsing
Tag arguments can be obtained within render_impl
.
Both keyword options and name/value parameters are supported.
Both JekyllTag
and JekyllBlock
use the standard Ruby mechanism for parsing command-line options:
shellwords
and
key_value_parser
.
All your code has to do is specify the keywords to search for in the string
passed from the HTML page that your tag is embedded in.
The included demo
website has examples;
both demo/_plugins/demo_inline_tag.rb
and
demo/_plugins/demo_block_tag.rb
contain the following:
@keyword1 = @helper.parameter_specified? 'keyword1' @keyword2 = @helper.parameter_specified? 'keyword2' @name1 = @helper.parameter_specified? 'name1' @name2 = @helper.parameter_specified? 'name2'
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
:
{% pre die_if_error %} ... {% endpre %}
The above is the same as writing:
{% pre die_if_error='true' %} ... {% endpre %}
Or writing:
{% 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 die_if_error=false %} ... {% endpre %}
The above causes the label to be die_if_error=false
.
{% 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
.
Quoting
Parameter values can be quoted.
If the value consists of only one token, then quoting is optional. The following name/value parameters all have the same result:
pay_tuesday="true"
pay_tuesday='true'
pay_tuesday=true
pay_tuesday
If the values consist of more than one token, quotes must be used. The following examples both yield the same result:
pay_tuesday="maybe not"
pay_tuesday='maybe not'
Remaining Markup
After your plugin has parsed all the keyword options and name/value parameters,
call @helper.remaining_markup
to obtain the remaining markup that was passed to your plugin.
Liquid Variable Definitions
jekyll_plugin_support
provides support for
Liquid variables
to be defined in _config.yml
, in a section called liquid_vars
.
The following _config.yml
fragment defines 3 variables called var1
,
var2
and var3
:
liquid_vars: var1: value1 var2: 'value 2' var3: value3
Liquid variables defined in this manner are intended to be embedded in a webpage. They are expanded transparently and can be referenced like any other Liquid variable. These Liquid variables can be passed as parameters to other plugins and includes.
In the following example web page, the Liquid variable called var1
is expanded as part of the displayed page.
The Liquid variables var1
and var2
are expanded and passed to the my_plugin
plugin.
This is the value of var1: {{var1}}. {% my_plugin param1="{{var1}}" param2="{{var2}}" %}
Jekyll_plugin_support
expands all but one of the
plugin variables described above,
replacing Liquid variable references with their values.
The exception is @argument_string
, which is not expanded.
Liquid Variable Values Specific To Production, Development and Test Modes
jekyll_plugin_support
allows Liquid variables defined in _config.yml
to have different values
when Jekyll is running in development
, production
and test
modes.
When injecting variables into your Jekyll website, Jekyll_plugin_support
refers to definitions specific to the current environment
and then refers to other definitions that are not overridden.
Here is an example:
liquid_vars: development: var1: 'http://localhost:4444/demo_block_tag.html' var2: 'http://localhost:4444/demo_inline_tag.html' production: var1: 'https://github.com/django/django/blob/3.1.7' var2: 'https://github.com/django-oscar/django-oscar/blob/3.0.2' var3: 'https://github.com/mslinn'
For the above, the following variable values are set in development
mode:
var1
http://localhost:4444/demo_block_tag.html
var2
http://localhost:4444/demo_inline_tag.html
var3
https://github.com/mslinn
... and the following variable values are set in production
and test
modes:
var1
https://github.com/django/django/blob/3.1.7
var2
https://github.com/django-oscar/django-oscar/blob/3.0.2
var3
https://github.com/mslinn
Liquid Variables in jekyll_plugin_support Subclasses
You can define additional Liquid variables in plugins built using jekyll_plugin_support
.
To achieve this, make entries in _config.yml
under a key named after the value of @tag_name
.
For example, let’s imagine you create a plugin using jekyll_plugin_support
,
and you register it with the name phonetic_alphabet
.
You could define Liquid variables that would be made available to content pages in web applications that
incorporate the phonetic_alphabet
plugin.
The following section in _config.yml
defines variables called x
, y
and z
,
with values xray
, yankee
and zulu
, respectively:
phonetic_alphabet: x: xray y: yankee z: zulu
The above definitions allow you to write content pages that use those variables, like the following:
--- layout: default title: Variable demo --- The letter x is pronounced {{x}}. Similarly, the letters y and z are pronounced {{y}} and {{z}}.
... which expands to:
The letter x is pronounced xray. Similarly, the letters y and z are pronounced yankee and zulu.
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:
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
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:
module MyPluginVersion VERSION = '0.5.0'.freeze end
Then your plugin can incorporate the VERSION
constant into your plugin like this:
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 if they are published as gems.
JekyllTagNoArgParsing
and JekyllBlockNoArgParsing
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 causes 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 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:
<<~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 %}
The 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 gemfile
s 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 using any of the above properties:
{% my_tag attribution="Generated by the #{name} #{version} Jekyll plugin, written by #{author} #{date}" %}
Development
After checking out the jekyll_plugin_suppprt
repository, run bin/setup
to install dependencies.
bin/console
provides an interactive prompt that allows you to experiment.
To build and install this gem on your local machine, run:
$ bundle exec rake install jekyll_plugin_support 0.1.0 built to pkg/jekyll_plugin_support-0.1.0.gem. jekyll_plugin_support (0.1.0) installed.
Examine the newly built gem:
$ gem info jekyll_plugin_support
*** LOCAL GEMS ***
jekyll_plugin_support (0.1.0) Author: Mike Slinn Homepage: https://github.com/mslinn/jekyll_plugin_support License: MIT Installed at: /home/mslinn/.gems
Provides a framework for writing and testing Jekyll plugins.
Pry Breakpoint On StandardError
A pry
breakpoint will be set in the StandardError
handler if
pry_on_standard_error: true
is set in the Liquid variable definition section for your plugin within _config.yml
.
For example, if your plugin is called blah
, enable the breakpoint with the following section:
blah: pry_on_standard_error: true
Demonstration Plugins and Website
The jekyll_plugin_support
GitHub project includes a
demo
website.
It can be used to debug the plugin or to run it freely.
Examining the Demo Plugins
The following example plugins use Ruby’s
squiggly heredoc operator (<<~
).
The squiggly heredoc operator removes the outermost 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 @custom_error = @helper.parameter_specified? 'raise_custom_error' @keyword1 = @helper.parameter_specified? 'keyword1' @keyword2 = @helper.parameter_specified? 'keyword2' @name1 = @helper.parameter_specified? 'name1' @name2 = @helper.parameter_specified? 'name2' @standard_error = @helper.parameter_specified? 'raise_standard_error' if @tag_config @die_on_custom_error = @tag_config['die_on_custom_error'] == true @die_on_standard_error = @tag_config['die_on_standard_error'] == true end raise CustomError, 'Fall down, go boom.' if @custom_error _infinity = 1 / 0 if @standard_error output rescue CustomError => e # jekyll_plugin_support handles StandardError e.shorten_backtrace msg = format_error_message e.message @logger.error "#{e.class} raised #{msg}" raise e if @die_on_custom_error "<div class='custom_error'>#{e.class} raised in #{self.class};\n#{msg}</div>" end private def output <<~END_OUTPUT <pre>@helper.tag_name=#{@helper.tag_name} @mode=#{@mode} # jekyll_plugin_support becomes able to perform variable substitution after this variable is defined. # The value could be updated at a later stage, but no need to add that complexity unless there is a use case. @argument_string="#{@argument_string}" @helper.argv= #{@helper.argv&.join("\n ")} # Liquid variable name/value pairs @helper.params= #{@helper.params&.map { |k, v| "#{k}=#{v}" }&.join("\n ")} # The keys_values property serves no purpose any more, consider it deprecated @helper.keys_values= #{(@helper.keys_values&.map { |k, v| "#{k}=#{v}" })&.join("\n ")} remaining_markup='#{@helper.remaining_markup}' @envs=#{@envs.keys.sort.join(', ')} @config['url']='#{@config['url']}' @site.collection_names=#{@site.collection_names&.sort&.join(', ')} @page['description']=#{@page['description']} @page['path']=#{@page['path']} @keyword1=#{@keyword1} @keyword2=#{@keyword2} @name1=#{@name1} @name2=#{@name2}</pre> END_OUTPUT end JekyllPluginHelper.register(self, 'demo_inline_tag') end end
require 'cgi' require 'jekyll_plugin_support' module Jekyll CustomError = JekyllSupport.define_error class DemoBlock < JekyllSupport::JekyllBlock VERSION = '0.1.2'.freeze def render_impl(text) @custom_error = @helper.parameter_specified? 'raise_custom_error' @keyword1 = @helper.parameter_specified? 'keyword1' @keyword2 = @helper.parameter_specified? 'keyword2' @name1 = @helper.parameter_specified? 'name1' @name2 = @helper.parameter_specified? 'name2' @standard_error = @helper.parameter_specified? 'raise_standard_error' if @tag_config @die_on_custom_error = @tag_config['die_on_custom_error'] == true @die_on_standard_error = @tag_config['die_on_standard_error'] == true end raise CustomError, 'Fall down, go boom.' if @custom_error _infinity = 1 / 0 if @standard_error output text rescue CustomError => e # jekyll_plugin_support handles StandardError e.shorten_backtrace msg = format_error_message e.message @logger.error "#{e.class} raised #{msg}" raise e if @die_on_custom_error "<div class='custom_error'>#{e.class} raised in #{self.class};\n#{msg}</div>" end private def output(text) <<~END_OUTPUT <pre>@helper.tag_name=#{@helper.tag_name} @mode=#{@mode} # jekyll_plugin_support becomes able to perform variable substitution after this variable is defined. # The value could be updated at a later stage, but no need to add that complexity unless there is a use case. @argument_string="#{@argument_string}" @helper.argv= #{@helper.argv&.join("\n ")} # Liquid variable name/value pairs @helper.params= #{@helper.params&.map { |k, v| "#{k}=#{v}" }&.join("\n ")} # The keys_values property serves no purpose any more, consider it deprecated @helper.keys_values= #{(@helper.keys_values&.map { |k, v| "#{k}=#{v}" })&.join("\n ")} @helper.remaining_markup='#{@helper.remaining_markup}' @envs=#{@envs.keys.sort.join(', ')} @config['url']='#{@config['url']}' @site.collection_names=#{@site.collection_names&.sort&.join(', ')} @page['description']=#{@page['description']} @page['path']=#{@page['path']} @keyword1=#{@keyword1} @keyword2=#{@keyword2} @name1=#{@name1} @name2=#{@name2} text=#{text}</pre> END_OUTPUT end JekyllPluginHelper.register(self, 'demo_block_tag') 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_inline_tag_no_arg')
end
end
Run Freely
-
Run from the command line:
Shell$ demo/_bin/debug -r
-
View the generated website,
which might be at
http://localhost:4444
, depending on how you configured it.
Plugin Debugging
- Set breakpoints in Visual Studio Code.
-
Initiate a debug session from the command line by running the
demo/_bin/debug
script:
Shell$ demo/_bin/debug Fetching gem metadata from https://rubygems.org/.......... Resolving dependencies... Fetching public_suffix 5.0.4 Fetching nokogiri 1.15.5 (x86_64-linux) Installing public_suffix 5.0.4 Installing nokogiri 1.15.5 (x86_64-linux) Bundle complete! 17 Gemfile dependencies, 96 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed. INFO PluginMetaLogger: Loaded DraftFilter plugin. INFO PluginMetaLogger: Loaded outline_js v1.2.1 plugin. INFO PluginMetaLogger: Loaded outline v1.2.1 plugin. Configuration file: /mnt/f/jekyll_plugin_support/demo/_config.yml Cleaner: Removing /mnt/f/jekyll_plugin_support/demo/_site... Cleaner: Removing /mnt/f/jekyll_plugin_support/demo/.jekyll-metadata... Cleaner: Removing /mnt/f/jekyll_plugin_support/demo/.jekyll-cache... Cleaner: Nothing to do for .sass-cache. DEBUGGER: Debugger can attach via TCP/IP (127.0.0.1:37177) DEBUGGER: wait for debugger connection...
-
Once the
DEBUGGER: wait for debugger connection...
message appears, run the Visual Studio Code launch configuration calledAttach with rdbg
. -
View the generated website,
which might be at
http://localhost:4444
, depending on how you configured it.