Mike Slinn
Mike Slinn

My Jekyll Plugins

Published 2020-10-03. Last modified 2022-04-24.
Time to read: about 42 minutes.

This site is categorized under Jekyll, Ruby.

This is a Jekyll-powered web site. Jekyll is a free open-source preprocessor that generates static web sites. You can extend Jekyll by using the Liquid language to write includes. Includes are just macros for Jekyll.

I prefer to write and use Jekyll plugins instead of Jekyll includes. Non-trivial Jekyll includes require logic to be expressed in the Liquid language. Liquid is an interesting language, but it is quite verbose, syntax can be awkward, some expressions are impossible to formulate, and there are no debugging tools.

In contrast, plugins are written in Ruby. Plugin usage syntax is more flexible and require less typing for users.

The argument against writing plugins is that the Ruby language is subtle and powerful, and could be overwhelming for novice programmers. However, just as the Apache Spark framework allows novice Scala programmers to write in Just Enough Scala for Spark, and the Ruby on Rails framework allows novice Ruby programmers to write Just Enough Ruby for Rails, writing plugins for the Jekyll framework generally does not require total mastery of Ruby.

Below are some of my Jekyll plugins. The source code for a plugin can be copied to the clipboard whenever you click on this icon at the top right corner of the code: Copy to clipboard.

All plugins are RubyGems that I maintain on GitHub.

archive_create

Originally called make_archive, archive_create creates tar and zip archives according to the make_archive entry in _config.yml. Archives are placed in the top-level of the Jekyll project, and are copied to _site by Jekyll’s normal build process. Entries are created in .gitignore for each of the generated archives.

In production mode, the archives are built each time Jekyll generates the web site.

In development mode, the archives are only built if they do not already exist, or if delete: true is set for that archive in _config.yml.

File Specifications

This plugin supports 4 types of file specifications:

  1. Absolute filenames (start with /).
  2. Filenames relative to the top-level directory of the Jekyll web site (Do not preface with . or /).
  3. Filenames relative to the user home directory (preface with ~).
  4. Executable programs on the PATH (preface with !).

_config.yml Syntax

Any number of archives can be specified. Each archive has 3 properties: archive_name, delete (defaults to true) and files. Take care that the dashes have exactly 2 spaces before them, and that the 2 lines following each dash have exactly 4 spaces in front.

make_archive:
  -
    archive_name: cloud9.zip
    delete: true  # This is the default, and need not be specified.
    files: [ index.html, 404.html, ~/.ssh/config, /etc/passwd, '!update' ]
  -
    archive_name: cloud9.tar
    delete: false  # Do not overwrite the archive if it already exists
    files: [ index.html, 404.html, ~/.ssh/config, /etc/passwd, '!update' ]

GitHub Project and RubyGem

The GitHub project is github.com/mslinn/jekyll_archive_create and the gem is provided at rubygems.org/gems/jekyll_archive_create.

archive_display

Lists the names and contents of each file in a tar file.

Syntax

{% archive_display /relative/or/absolute/path/to/filename.tar %}

Output

For each text file within the tar file, the following HTML is emitted:

<div class='codeLabel'>{entry.full_name}{entry.mime_type}</div>
<pre data-lt-active='false'><code>{tar_entry.file_contents}</pre>
Sample CSS
.codeLabel {
  color: white;
  background-color: #666;
  margin-bottom: 0;
  padding-bottom: 0;
  padding-left: 1em;
}
.codeLabel+pre {
  margin-top: 0;
}
.codeLabel, pre {
  font-family: Monaco,"Bitstream Vera Sans Mono","Lucida Console",Terminal,monospace;
  font-stretch: semi-condensed;
}
pre {
  background: #78B4DB21;
  border: solid 1px #cecece;
  overflow: auto;
  padding: 4px;
  text-indent: initial;
  text-shadow: none;
}
pre {
  font-size: 11pt;
  margin-bottom: 30px;
  margin-left: 0;
  margin-right: 0;
}

Using the above CSS, text files within a tar file are displayed like this:

my_script.sh (text/x-shellscript; charset=us-ascii)
#!/bin/sh
echo "Hello, world"

Binary files are displayed like this:

usr/bin/ruby2.7 (application/x-sharedlib; charset=binary)
Binary file

Installation

  1. Install libmagic.
    Ubuntu & WSL
    Shell
    $ sudo apt install libmagic-dev
    Mac
    Shell
    $ brew install libmagic
  2. Add the following to Gemfile:
    Gemfile
    group :jekyll_plugins do
    gem 'jekyll_archive_display'
    end
  3. Add the following to _config.yml:
    _config.yml
    plugins:
    - archive_display
  4. From your Jekyll site's top-level directory, type:
    Shell
    $ bundle install
  5. Restart Jekyll.

GitHub Project and RubyGem

The GitHub project is github.com/mslinn/jekyll_archive_display and the gem is provided at rubygems.org/gems/jekyll_archive_display.

basename, dirname and basename_without_extension

These filters all return portions of a string. They are all defined in the same plugin.

  • basename — Filters a string containing a path, returning the filename and extension.
  • dirname — Filters a string containing a path, returning the portion before the filename and extension.
  • basename_without_extension — Filters a string containing a path, returning the filename without the extension.

Syntax

{{ "blah/blah/filename.ext" | dirname }} => blah/blah
{{ "blah/blah/filename.ext" | basename }} => filename.ext
{{ "blah/blah/filename.ext" | basename_without_extension  }} => filename

GitHub Project and RubyGem

The GitHub project is github.com/mslinn/jekyll_basename_dirname and the gem is provided at rubygems.org/gems/jekyll_basename_dirname.

begins_with, does_not_begin_with, ends_with, does_not_end_with, and append_suffix_if_does_not_start_with

These filters all return portions of a string. They are all defined in the same plugin.

  • begins_with – returns true if a string starts with a given substring.
  • does_not_begin_with – returns false if a string starts with a given substring.
  • ends_with – returns true if a string end with a given substring.
  • does_not_end_with – returns false if a string end with a given substring.
  • append_suffix_if_does_not_start_with – appends a suffix to the string if the string does not start with a substring

Important: the name of each of these filters must be followed by a colon (:). If you fail to do that an error will be generated and the Jekyll site building process will halt. The error message looks something like this: Liquid Warning: Liquid syntax error (line 285): Expected end_of_string but found string in "{{ lines | begins_with 'blah' | xml_escape }}" in /some_directory/some_files.html Liquid Exception: Liquid error (line 285): wrong number of arguments (given 1, expected 2) in /some_directory/some_file.html Error: Liquid error (line 285): wrong number of arguments (given 1, expected 2)

Examples

begins_with

First example
{% assign url = "https:/asdf.com" %}
{% assign isAbsolute = url | begins_with: 'http' %}
Second example
{% assign url = "https:/asdf.com" %}
{% if url | begins_with: 'http' %}
  <p>Absolute</p>
{% else %}
  <p>Relative</p>
{% endif %}

does_not_begin_with

First example
{% assign url = "https:/asdf.com" %}
{% assign isRelative = url | does_not_begin_with: 'http' %}
Second example
{% assign url = "https:/asdf.com" %}
{% if url | does_not_begin_with: 'http' %}
  <p>Relative</p>
{% else %}
  <p>Absolute</p>
{% endif %}

ends_with

First example
{% assign url = "https:/asdf.com" %}
{% assign isDotCom = url | ends_with: '.com' %}
Second example
{% assign url = "https:/asdf.com" %}
{% if url | ends_with: '.com' %}
  <p>.com found</p>
{% else %}
  <p>Not a .com</p>
{% endif %}

does_not_end_with

First example
{% assign url = "https:/asdf.com" %}
{% assign isNotDotCom = url | does_not_end_with: '.com' %}
Second example
{% assign url = "https:/asdf.com" %}
{% if url | does_not_end_with: '.com' %}
  <p>Not a .com</p>
{% else %}
  <p>.com found</p>
{% endif %}

append_suffix_if_does_not_start_with

This filter was created to make asset reloading work better.

Given a portion of _layouts/default.html that looks like this:

{% assign csses = page.css | default: layout.css %}
{% assign nowMillis = site.time | date: '%s' %}
{% assign suffix = '?v=' | append: nowMillis %}
{% for css in csses %}
  <link rel="stylesheet" href="{{ css | append_suffix_if_does_not_start_with: 'http', suffix }}" type="text/css">
{% endfor %}

And given index.html with front matter that looks like this:

Jekyll Front Matter
---
css: [
  https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css,
  /order/order.css
]
---

The following is generated. Note that the suffix s?v=1612879301 in only applied to the relative URL for order.css.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css" type="text/css">
<link rel="stylesheet" href="/order/order.css?v=1612879301" type="text/css">

GitHub Project and RubyGem

The GitHub project is github.com/mslinn/jekyll_begin_end and the gem is provided at rubygems.org/gems/jekyll_begin_end.

This plugin generates a link to the given URI, which must be a file on the server. The file name can be absolute or relative to the top-level directory of the web site.

Syntax

{% download_link uri %}

Usage Example

{% download_link cloud9.tar %}

Generates:

<a href="/cloud9.tar"><code>cloud9.tar</code></a> (4.5 KB)

Which renders as: cloud9.tar (4.5 KB)

The GitHub project is github.com/mslinn/jekyll_download_link and the gem is provided at rubygems.org/gems/jekyll_download_link.

flexible_include

Jekyll's built-in include tag does not support including files outside of the _includes folder. Originally called include_absolute, this plugin name is now called flexible_include because it no longer just includes absolute file names. This plugin now supports 4 types of includes:

  1. Filenames relative to the top-level directory of the Jekyll web site. It is unnecessary to preface these paths with ./).
  2. Absolute filenames (first character is /). This feature can be modified or denied where security is a concern by specifying an array of Ruby glob expressions in the FLEXIBLE_INCLUDE_PATHS environment variable.
  3. Filenames relative to the user home directory (first character is ~). This feature can also be modified or denied where security is a concern by specifying an array of Ruby glob expressions in the FLEXIBLE_INCLUDE_PATHS environment variable.
  4. Executable filenames on the PATH (first character is !). This feature can be disabled by defining the DISABLE_FLEXIBLE_INCLUDE environment variable before launching Jekyll.

In addition, filenames that require environment expansion because they contain a $ character are expanded according to the environment variables defined when the jekyll build process was launched.

Syntax

The following are equivalent:

  • {% flexible_include path [ OPTIONS ] %}
  • {% flexible_include 'path' [ OPTIONS ] %}
  • {% flexible_include "path" [ OPTIONS ] %}
  • {% flexible_include file='path' [ OPTIONS ] %}
  • {% flexible_include file="path" [ OPTIONS ] %}

By default, the included file will escape characters <, { and } unless do_not_escape is specified. Note that the [square brackets] merely indicate optional parameters and are not intended to be written literally.

The file must be specified as a relative or absolute path on the server, or a command to execute. A URL cannot be provided (you cannot write a file name that starts with http: or https:). This capability is under consideration for a possible future release.

Options

  • do_not_escape includes the content without HTML escaping it.
  • pre causes the included file to be wrapped inside a <pre></pre> tag, no label is generated.

The following options imply pre:

  • copyButton draws an icon at the top right of the <pre></pre> area, which causes the included contents to be copied to the clipboard.
  • download uses the name of the file as a label, and displays it above the <pre></pre> tag. Clicking the label causes the file to be downloaded.
  • label specifies that an automatically generated label be placed above the contents. There is no need to specify this option if download or copyButton options are provided.
  • label="blah blah" specifies a label for the contents; this value overrides the default label. The value can be enclosed in single or double quotes. If you want to display a text message other than the file name, use this option.
  • number numbers the lines.

Usage Examples

This example shows a typical set of options. A label is automatically generated from the file name or process output that is included:

{% flexible_include ~/.mem_settings.yaml download copyButton %}

Here is what the above looks like when rendered by a web browser:

append_file_name: sample.html
output_format: qt

... and without the download option, but still including the copyButton option:

.mem_settings.yaml
append_file_name: sample.html
output_format: qt

... now with just the pre option:

.mem_settings.yaml
append_file_name: sample.html
output_format: qt

... and finally this is the result of using flexible_include without any options. The generated text is outlined in a <pre></pre> tag so it is noticable. The file has a trailing newline, which is apparent below, however flexible_include trims any leading and trailing whitespace when the pre option is specified or implied.

append_file_name: sample.html
output_format: qt

Dark Mode

Normally my website uses light colors, however some content displays better on a dark background. The dark option causes the generated <pre> tag to have a dark class applied. You can define the CSS for the dark and darkLabel classes. The CSS that defines those classes for this web site is here.

.mem_settings.yaml
append_file_name: sample.html
output_format: qt

Home Directory

The included file path can use a tilde (~) to denote the user $HOME directory.

{% flexible_include ~/.mem_settings.yaml %}
append_file_name: sample.html
output_format: qt

Environment variables can be used; they will be expanded according to the environment variables that were current in the process when the Jekyll generator launched.

{% flexible_include '$HOME/.gitconfig' %}
[alias]
    lol=log --graph --decorate --pretty=oneline --abbrev-commit
    lola=log --graph --decorate --pretty=oneline --abbrev-commit --all
    ls=ls-files
    st = status
    ci = commit
    br = branch
    co = checkout
    df = diff
    dc = diff --cached
    dif = diff --word-diff=color --ignore-space-at-eol
    lg = log -p
    ign = ls-files -o -i --exclude-standard
	pwd = !pwd
[branch "master"]
	remote = origin
	merge = refs/heads/master
[core]
	filemode = false
	autocrlf = input
	safecrlf = false
	excludesfile = C:\\Users\\Mike Slinn\\Documents\\gitignore_global.txt
[color]
	status = auto
	branch = auto
    ui = auto
[gui]
	trustmtime = true
[push]
	default = simple
[user]
	name = Mike Slinn
	email = mslinn@micronauticsresearch.com
[rebase]
	autostash = true
[diff "exif"]
	textconv = exiftool
[diff]
	compactionHeuristic = true
	renames = 0
[hub]
	protocol = git
[pull]
	rebase = false
[init]
	defaultBranch = master

Environment Variable Expansion

This example includes the output of running the bash command which jekyll, according to the Ruby environment that was current when the Jekyll generator was launched:

{% flexible_include '!which jekyll' %}
/home/mslinn/.rbenv/versions/3.1.0/bin/jekyll

Does This Make Your Brain Hurt?

Ready to have your mind twisted? This invocation:

{% flexible_include '~/.mem_settings.yaml' download copyButton
  label='&lcub;% flexible_include download copyButton
    ~/.mem_settings.yaml %&rcub;' %}

Renders like this:

append_file_name: sample.html
output_format: qt

Hint: &lcub; and &rcub; are HTML entities for { and }, respectively.

Highlighting Text

A regular expression can be passed to the highlight option. This causes text that matches the regex pattern to be wrapped within a <span class="bg_color"></span> tag.

The following highlights filenames in a directory listing that contain django-admin that might contain dots, underscores, dashes and forward slashes:

{% flexible_include
  highlight="[\w./\-_]*django-admin[\w.\-_]*"
  label="ls ~/venv/aw/bin/*"
  file="!ls ~/venv/aw/bin/*"
%}

Renders as:

ls ~/venv/aw/bin/*
/home/mslinn/venv/aw/bin/activate
/home/mslinn/venv/aw/bin/activate.csh
/home/mslinn/venv/aw/bin/activate.fish
/home/mslinn/venv/aw/bin/activate.ps1
/home/mslinn/venv/aw/bin/activate.xsh
/home/mslinn/venv/aw/bin/activate_this.py
/home/mslinn/venv/aw/bin/django-admin
/home/mslinn/venv/aw/bin/django-admin.py
/home/mslinn/venv/aw/bin/easy_install
/home/mslinn/venv/aw/bin/easy_install-3.8
/home/mslinn/venv/aw/bin/easy_install3
/home/mslinn/venv/aw/bin/faker
/home/mslinn/venv/aw/bin/pip
/home/mslinn/venv/aw/bin/pip3
/home/mslinn/venv/aw/bin/pip3.8
/home/mslinn/venv/aw/bin/pip3.9
/home/mslinn/venv/aw/bin/pybabel
/home/mslinn/venv/aw/bin/python
/home/mslinn/venv/aw/bin/python3
/home/mslinn/venv/aw/bin/python3.8
/home/mslinn/venv/aw/bin/sqlformat
/home/mslinn/venv/aw/bin/wheel
/home/mslinn/venv/aw/bin/wheel-3.8
/home/mslinn/venv/aw/bin/wheel3

/home/mslinn/venv/aw/bin/__pycache__:
django-admin.cpython-39.pyc

Numbering Lines

{% flexible_include
  label="ls ~/venv/aw/*"
  file="!ls ~/venv/aw/*"
  number
%}

Renders as (notice that the numbers are unselectable):

ls ~/venv/aw/*
  1: /home/mslinn/venv/aw/pyvenv.cfg
  2: 
  3: /home/mslinn/venv/aw/bin:
  4: __pycache__
  5: activate
  6: activate.csh
  7: activate.fish
  8: activate.ps1
  9: activate.xsh
 10: activate_this.py
 11: django-admin
 12: django-admin.py
 13: easy_install
 14: easy_install-3.8
 15: easy_install3
 16: faker
 17: pip
 18: pip3
 19: pip3.8
 20: pip3.9
 21: pybabel
 22: python
 23: python3
 24: python3.8
 25: sqlformat
 26: wheel
 27: wheel-3.8
 28: wheel3
 29: 
 30: /home/mslinn/venv/aw/lib:
 31: python3.8
 32: python3.9

CSS

Below are the CSS declarations that I defined for the flexible_include tag that produced the above output. This CSS is the same as used by pre.

pre.dark {
  color: #eee;
  background-color: #222;
}

pre.dark .unselectable,
pre.dark .unselectable > code {
  color: rgb(155, 150, 150);
}

.bg_yellow {
  background-color: yellow;
  padding: 2px;
}

.codeLabel {
  color: white;
  background-color: #666;
  margin-bottom: 0;
  padding-bottom: 2px;
  padding-left: 10px;
  padding-right: 10px;
  padding-top: 2px;
  font-family: $mono-font;
  font-stretch: semi-condensed;
}

.darkLabel {
  color: ivory;
}

li div.codeLabel {
  padding-left: 2em;
}

.codeLabel.unselectable, div.codeLabel.unselectable > code {
  color: yellow;
}

.codeLabel + pre {
  margin-top: 0;
}

.codeLabel a {
  color: #c0e6fb;
}

.codeLabel a:hover {
  color: #FFE59F;
}

.copyBtn {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -webkit-appearance: none;
  background-color: #eee;
  background-image: linear-gradient(#fcfcfc, #eee);
  border: 1px solid #d5d5d5;
  border-radius: 3px;
  color: #333;
  cursor: pointer;
  float: right;
  font-size: 13px;
  font-weight: 700;
  line-height: 20px;
  padding: 2px 2px 0 4px;;
  position: -webkit-sticky;
  position: sticky;
  right: 4px;
  top: 0;
  user-select: none;
  z-index: 1;
}

.copyContainer {
  position: relative;
}

ol li .codeLabel {
  padding-left: 1.75em;
}

.maxOneScreenHigh {
  max-height: 500px;
}

.numbered_line,
.unselectable.numbered_line,
.numbered_line.unselectable {
  color: #5fb25f;
}

.unselectable {
  color: #7922f9;
  -moz-user-select: none;
  -khtml-user-select: none;
  user-select: none;
}

Restricting Directory Access

By default, flexible_include can read from all directories according to the permissions of the user account that launched the jekyll process. For security-conscience environments, the accessible paths can be restricted.

Defining an environment variable called FLEXIBLE_INCLUDE_PATHS prior to launching Jekyll will restrict the paths that flexible_include will be able to read from. This environment variable consists of a colon-delimited set of regular expressions. For example, the following restricts access to only the files within:

  1. The ~/my_dir directory tree of the account of the user that launched Jekyll.
  2. The directory tree rooted at /var/files.
  3. The directory tree rooted at the expanded value of the $work environment variable.
Shell
$ export FLEXIBLE_INCLUDE_PATHS='~/my_dir/.*:/var/files/.*:$work/.*'

If a reference to an unauthorized file is intercepted, a big red message will appear on the generated web page that says something like

Access to #{path} denied by FLEXIBLE_INCLUDE_PATHS value.

... and an error message will be logged on the console that looks something like:

ERROR FlexibleInclude: _posts/2020/2020-10-03-jekyll-plugins.html - Access to #{path} denied by FLEXIBLE_INCLUDE_PATHS value.

Restricting Arbitrary Processes

By default, flexible_include can execute any command. You can disable that by setting the environment variable DISABLE_FLEXIBLE_INCLUDE to any non-empty value.

Shell
$ export DISABLE_FLEXIBLE_INCLUDE=true

If a potential command execution is intercepted, a big red message will appear on the generated web page that says:

Arbitrary command execution denied by DISABLE_FLEXIBLE_INCLUDE value.

... and an error message will be logged on the console that looks something like:

ERROR FlexibleInclude: #{path} - Arbitrary command execution denied by DISABLE_FLEXIBLE_INCLUDE value.

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_flexible_include_plugin. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_flexible_include.

from, to and until

These filters all return portions of a multiline string. They are all defined in the same plugin. A regular expression is used to specify the match; the simplest regular expression is a string.

  • from — returns the portion beginning with the line that satisfies a regular expression to the end of the multiline string.
  • to — returns the portion from the first line to the line that satisfies a regular expression, including the matched line.
  • until — returns the portion from the first line to the line that satisfies a regular expression, excluding the matched line.

from

All of these examples perform identically.
{{ sourceOfLines | from: 'regex' }}
{{ sourceOfLines | from: "regex" }}
{{ sourceOfLines | from: regex }}

to

All of these examples perform identically.
{{ sourceOfLines | to: 'regex' }}
{{ sourceOfLines | to: "regex" }}
{{ sourceOfLines | to: regex }}

until

All of these examples perform identically.
{{ sourceOfLines | until: 'regex' }}
{{ sourceOfLines | until: "regex" }}
{{ sourceOfLines | until: regex }}

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_from_to_until. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_from_to_until.

href

Generates an a href tag with target="_blank" and rel=nofollow. Also provides a convenient way to generate formatted and clickable URIs.

Syntax

{% href [match | [follow] [notarget]] [url] text to display %}

The url should not be enclosed in quotes.

If no url is provided then the text to display is assumed to be a URI, and is formatted into a clickable link.

Usage Examples

Defaults

{% href https://www.mslinn.com The Awesome %}

This generates:

<a href='https://www.mslinn.com' target='_blank' rel='nofollow'>The Awesome</a>

Which renders as: The Awesome

follow

{% href follow https://www.mslinn.com The Awesome %}

This generates:

<a href='https://www.mslinn.com' target='_blank'>The Awesome</a>

notarget

{% href notarget https://www.mslinn.com The Awesome %}

This generates:

<a href='https://www.mslinn.com' rel='nofollow'>The Awesome</a>

follow notarget

{% href follow notarget https://www.mslinn.com The Awesome %}

This generates:

<a href='https://www.mslinn.com'>The Awesome</a>

match

Looks for a post with a matching URL.
{% href match setting-up-django-oscar.html tutorial site %}

This might generate:

tutorial site

URI

{% href mslinn.com %}

This generates:

<a href='https://mslinn.com' target='_blank' rel='nofollow'><code>mslinn.com</code></a>

Which renders as: mslinn.com

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_href. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_href.

nth

nth is a Liquid filter that returns item n of array, origin 0.

Usage Example

This example obtains the second item of the array. Note that because Liquid does not provide a syntax for defining arrays, the following transforms a string of numbers to an array of numbers using split.

{{ "1,2,3,4,5" | split: ',' | nth: 2 }}  # returns 3

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_nth. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_nth.

jekyll_plugin_logger

Formerly known as logger_factory, jekyll_plugin_logger is Jekyll plugin packaged as a Ruby gem that provides colored console logs.

All of my plugins use jekyll_plugin_logger, for notifying the user that the plugin has loaded, and for generating log messages. I wrote it up separately.

GitHub Project and RubyGem

The GitHub project is github.com/mslinn/jekyll_plugin_logger and the gem is provided at rubygems.org/gems/jekyll_plugin_logger.

pre

This Jekyll plugin provides 2 new Liquid tags that work together:

  • A pre block tag that can optionally display a labeled heading and a copy button.
    {% pre [class="class1 class2"] [clear] [copyButton] [dark] [shell]
           [style="font-weight: bold;"]
           [highlight="regex"]
           [headline words] %}
    Contents of pre tag
    {% endpre %}
    The clear keyword ensures no floated elements overlap the rendered pre tag.
  • A noselect tag that can render HTML content passed to it unselectable.
    {% pre [copyButton] [dark] %}
    {% noselect [text string, defaults to $] %}Contents of pre tag
    {% endpre %}

Clipboard Javascript

The clipboard functionality requires Javascript. You might want to load it in a Jekyll layout.

The clipboard button is fuctionally equivalent to the following HTML:

<button class="copyBtn" data-clipboard-target="#id098814fabaf3" title="Copy to clipboard">
  <img src="/assets/images/clippy.svg" alt="Copy to clipboard" style="width: 13px">
</button>

The value of the data-clipboard-target attribute is the id of the container holding the text to be copied.

clipboard.js must be loaded by the web page. For example:

<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js"></script>

After the JavaScript loads, a new `ClipboardJS` instance must be created. The constructor needs to know the CSS selector for the buttons that the user will click on when they want to copy text to the clipboard. In this example, all of the buttons on the web page have class .copyBtn.

new ClipboardJS('.copyBtn');

Usage

Dark Mode Example

Normally my website uses light colors, however some content displays better on a dark background. You can define the CSS any way you like.

{% pre copyButton dark Dark Mode Example %}
{% noselect >>> %}Contents of pre tag
{% noselect How now brown cow %}
{% endpre %}

Renders as:

Dark Mode Example
>>> Contents of pre tag
How now brown cow 

Example 1

This example does not generate a copy button and does not demonstrate noselect.

{% pre %}Contents of pre tag{% endpre %}

Generates:

<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id377433c30186'>Contents of pre tag</pre>

Which renders as:

Contents of pre tag

Example 2

This example generates a copy button and does not demonstrate noselect.

{% pre copyButton %}
Contents of pre tag
{% endpre %}

Generates:

<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6a831a3e8992'><button class='copyBtn' data-clipboard-target='#id6a831a3e8992' title='Copy to clipboard'><img src='images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>Contents of pre tag</pre>

Which renders as (note the clipboard icon at the far right):


Contents of pre tag

Example 3

This example generates a copy button and demonstrates the default usage of noselect, which renders an unselectable dollar sign followed by a space.

{% pre copyButton %}
{% noselect %}Contents of pre tag
{% endpre %}

Generates:

<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1e4a8fe53480'><button class='copyBtn' data-clipboard-target='#id1e4a8fe53480' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>Contents of pre tag</pre>

Which renders as:


$ Contents of pre tag

Example 4

This example generates a copy button and demonstrates the noselect being used twice: the first time to render an unselectable custom prompt, and the second time to render unselectable output.

{% pre copyButton %}
{% noselect >>> %}Contents of pre tag
{% noselect How now brown cow %}{% endpre %}

Generates:

<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb58a6cf1761c'><button class='copyBtn' data-clipboard-target='#idb58a6cf1761c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>> </span>contents of pre tag
<span class='unselectable'>How now brown cow</span></pre>

Which renders as:

$ Contents of pre tag
>>> How now brown cow 

Example 5

A regular expression can be passed to the highlight option. This causes text that matches the regex pattern to be wrapped within a <span class="bg_color"></span> tag.

This example demonstrates highlighting text that matches a regular expression. Regular expressions match against lines, which are delimited via newlines (\n).

{% pre copyButton highlight="Line 2" %}
Line 1
  Line 2
    Line 3
      Line 4
    Line 5
  Line 6
Line 7
{% endpre %}

Which renders as:

Line 1
  Line 2
    Line 3
      Line 4
    Line 5
  Line 6
Line 7

Example 6

Regular expressions match against lines, which are delimited view newlines (\n). Thus to match an entire line that contains a phrase, specify the regex as .*phrase.*. The following matches 3 possible phrases (2, 4 or 6), then selects the entire line if matched.

{% pre copyButton highlight=".*(2|4|6).*" %}
Line 1
  Line 2
    Line 3
      Line 4
    Line 5
  Line 6
Line 7
{% endpre %}

Which renders as:

Line 1
  Line 2
    Line 3
      Line 4
    Line 5
  Line 6
Line 7

CSS

Below are the CSS declarations that I defined for the pre tag that produced the above output. This CSS is the same as used by flexible_include.

pre.dark {
  color: #eee;
  background-color: #222;
}

pre.dark .unselectable,
pre.dark .unselectable > code {
  color: rgb(155, 150, 150);
}

.bg_yellow {
  background-color: yellow;
  padding: 2px;
}

.codeLabel {
  color: white;
  background-color: #666;
  margin-bottom: 0;
  padding-bottom: 2px;
  padding-left: 10px;
  padding-right: 10px;
  padding-top: 2px;
  font-family: $mono-font;
  font-stretch: semi-condensed;
}

.darkLabel {
  color: ivory;
}

li div.codeLabel {
  padding-left: 2em;
}

.codeLabel.unselectable, div.codeLabel.unselectable > code {
  color: yellow;
}

.codeLabel + pre {
  margin-top: 0;
}

.codeLabel a {
  color: #c0e6fb;
}

.codeLabel a:hover {
  color: #FFE59F;
}

.copyBtn {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -webkit-appearance: none;
  background-color: #eee;
  background-image: linear-gradient(#fcfcfc, #eee);
  border: 1px solid #d5d5d5;
  border-radius: 3px;
  color: #333;
  cursor: pointer;
  float: right;
  font-size: 13px;
  font-weight: 700;
  line-height: 20px;
  padding: 2px 2px 0 4px;;
  position: -webkit-sticky;
  position: sticky;
  right: 4px;
  top: 0;
  user-select: none;
  z-index: 1;
}

.copyContainer {
  position: relative;
}

ol li .codeLabel {
  padding-left: 1.75em;
}

.maxOneScreenHigh {
  max-height: 500px;
}

.numbered_line,
.unselectable.numbered_line,
.numbered_line.unselectable {
  color: #5fb25f;
}

.unselectable {
  color: #7922f9;
  -moz-user-select: none;
  -khtml-user-select: none;
  user-select: none;
}

Comprehensive Example

The code that generates the above CSS is a good example of how the plugins work together with the from and to tags from my from_to_until plugin:

{% capture css %}{% flexible_include '_sass/mystyle.scss' %}{% endcapture %}
{% pre copyButton %}{{ css | from: '.copyBtn' | to: '^$' | strip }}

{{ css | from: '.copyContainer' | to: '^$' | strip }}

{{ css | from: '.maxOneScreenHigh' | to: '^$' | strip }}

{{ css | from: '.unselectable' | to: '^$' | strip }}
{% endpre %}

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_pre. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_pre.

random_hex_string

This Liquid filter generates a random hexadecimal string of any length. Each byte displays as two characters. You can specify the number of bytes in the hex string; if you do not, 6 random bytes (12 characters) will be generated.

Usage Example

This example generates a random hex string 6 bytes long and stores the result in a Liquid variable called id. Both of the following do the same thing:

{% assign id = random_hex_string %}
{% assign id = random_hex_string 6 %}

The generated 6 bytes (12 characters) might be: d5ba76573c2a.

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_random_hex. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_random_hex.

run

jekyll_run is a Jekyll tag plugin that executes a program and returns the output from STDOUT. Because the output includes the command that was executed, and contains unselectable <span> tags, this plugin is intended to be embedded within a {% pre %}{% endpre %} tags.

Usage Example

{% run echo "asdf" %}

The generated HTML is:

<span class='unselectable'>$ </span>#{@command}\n<span class='unselectable'>#{output}</span>

Which renders as the following, when enclosed within {% pre %}{% endpre %}:

$ echo "asdf" 
asdf

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_run. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_run.

site_inspector

Dumps lots of information from site when enabled by the site_inspector setting in _config.yml.

_config.yml Syntax

site_inspector: true   # Run in development mode
site_inspector: force  # Run in development and production modes
site_inspector: false  # The default is to not run

Sample Output

site is of type Jekyll::Site
site.time = 2020-10-05 05:18:27 -0400
site.config['env']['JEKYLL_ENV'] = development
site.collections.posts
site.collections.expertArticles
site.config.source = '/mnt/_/www/www.mslinn.com'
site.config.destination = '/mnt/_/www/www.mslinn.com/_site'
site.config.collections_dir = ''
site.config.plugins_dir = '_plugins'
site.config.layouts_dir = '_layouts'
site.config.data_dir = '_data'
site.config.includes_dir = '_includes'
site.config.collections = '{"posts"=>{"output"=>true, "permalink"=>"/blog/:year/:month/:day/:title:output_ext"}, "expertArticles"=>{"output"=>true, "relative_directory"=>"_expertArticles", "sort_by"=>"order"}}'
site.config.safe = 'false'
site.config.include = '[".htaccess"]'
site.config.exclude = '["_bin", ".ai", ".git", ".github", ".gitignore", "Gemfile", "Gemfile.lock", "script", ".jekyll-cache/assets"]'
site.config.keep_files = '[".git", ".svn", "cloud9.tar"]'
site.config.encoding = 'utf-8'
site.config.markdown_ext = 'markdown,mkdown,mkdn,mkd,md'
site.config.strict_front_matter = 'false'
site.config.show_drafts = 'true'
site.config.limit_posts = '0'
site.config.future = 'true'
site.config.unpublished = 'false'
site.config.whitelist = '[]'
site.config.plugins = '["classifier-reborn", "html-proofer", "jekyll", "jekyll-admin", "jekyll-assets", "jekyll-docs", "jekyll-environment-variables", "jekyll-feed", "jekyll-gist", "jekyll-sitemap", "kramdown"]'
site.config.markdown = 'kramdown'
site.config.lsi = 'false'
site.config.excerpt_separator = '

'
site.config.incremental = 'true'
site.config.detach = 'false'
site.config.port = '4000'
site.config.host = '127.0.0.1'
site.config.baseurl = ''
site.config.show_dir_listing = 'false'
site.config.permalink = '/blog/:year/:month/:day/:title:output_ext'
site.config.paginate_path = '/page:num'
site.config.timezone = ''
site.config.quiet = 'false'
site.config.verbose = 'false'
site.config.defaults = '[]'
site.config.liquid = '{"error_mode"=>"warn", "strict_filters"=>false, "strict_variables"=>false}'
site.config.rdiscount = '{"extensions"=>[]}'
site.config.redcarpet = '{"extensions"=>[]}'
site.config.kramdown = '{"auto_ids"=>true, "toc_levels"=>"1..6", "entity_output"=>"as_char", "smart_quotes"=>"lsquo,rsquo,ldquo,rdquo", "input"=>"GFM", "hard_wrap"=>false, "footnote_nr"=>1, "show_warnings"=>false}'
site.config.author = 'Mike Slinn'
site.config.compress_html = '{"blanklines"=>false, "clippings"=>"all", "comments"=>[""], "endings"=>"all", "ignore"=>{"envs"=>["development"]}, "profile"=>false, "startings"=>["html", "head", "body"]}'
site.config.email = 'mslinn@mslinn.com'
site.config.feed = '{"categories"=>["AI", "Blockchain", "Scala", "Software-Expert"]}'
site.config.ignore_theme_config = 'true'
site.config.site_inspector = 'false'
site.config.make_archive = '[{"archive_name"=>"cloud9.tar", "delete"=>true, "files"=>["!killPortFwdLocal", "!killPortFwdOnJumper", "!tunnelToJumper"]}]'
site.config.sass = '{"style"=>"compressed"}'
site.config.title = 'Mike Slinn'
site.config.twitter = '{"username"=>"mslinn", "card"=>"summary"}'
site.config.url = 'http://localhost:4000'
site.config.livereload = 'true'
site.config.livereload_port = '35729'
site.config.serving = 'true'
site.config.watch = 'true'
site.config.assets = '{}'
site.config.tag_data = '[]'
site.keep_files: [".git", ".svn", "cloud9.tar"]

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_site_inspector. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_site_inspector.

sort_natural

jekyll_sort is a Jekyll plugin that sorts hashes using String#casecmp, the case-insensitive version of String#<=>.

Usage Example

Case_insensitive category sort
{% assign sorted_categories = site.categories | sort_natural %}

For More Information

By default, Enumerable#sort uses <=> for comparisons

See Add sort_natural to jekyll/filters.rb. This plugin overrides sort_natural provided by Liquid 4.

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_sort_natural. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_sort_natural.

This plugin contains code modified from tkrotoff.

time_since

time_since is a collection of Liquid filters that returns the elaped time since a date or date and time. The filters are years_since, months_since, days_since, hours_since, minutes_since and seconds_since.

Usage Examples

These tags all work in a similar manner; they accept one parameter, which is either a date string (YYYY-MM-dd) or an ISO 8601-1:2019 datetime string (YYYY-MM-ddThh:mm). Just a year can be specified, which implies Jan 1 at midnight.

The date and time strings can be surrounded with quotes or double quotes. Time zone can be omitted, or specified as GMT, Z, or +/- HHMM.

{{ "1959" | years_since }} {{ '1959' | years_since }}
{{ "1959-02-03" | years_since }} {{ '1959-02-03' | years_since }}
{{ "1959-02-03T01:02" | years_since }} {{ '1959-02-03T01:02' | years_since }}
{{ "1959-02-03T01:02:00Z" | years_since }} {{ "1959-02-03T01:02:00-0400" | years_since }}

{{ "1959" | months_since }} {{ months_since '1959' }}
{{ "1959-02-03" months_since }} {{ '1959-02-03' | months_since }}
{{ "1959-02-03T01:02" | months_since }} {{ '1959-02-03T01:02' | months_since }}
{{ "1959-02-03T01:02:00Z" | months_since }} {{ "1959-02-03T01:02:00-0400" | months_since }}

{{ "1959" | weeks_since }} {{ '1959' | weeks_since }}
{{ "1959-02-03" | weeks_since }} {{ '1959-02-03' | weeks_since }}
{{ "1959-02-03T01:02" | weeks_since }} {{ '1959-02-03T01:02' | weeks_since }}
{{ "1959-02-03T01:02:00Z" | weeks_since }} {{ "1959-02-03T01:02:00-0400" | weeks_since }}

{{ "1959" | days_since }} {{ '1959' | days_since }}
{{ "1959-02-03" | days_since }} {{ '1959-02-03' | days_since }}
{{ "1959-02-03T01:02" | days_since }} {{ '1959-02-03T01:02' | days_since }}
{{ "1959-02-03T01:02:00Z" | days_since }} {{ "1959-02-03T01:02:00-0400" | days_since }}

{{ "1959" | hours_since }} {{ '1959' | hours_since }}
{{ "1959-02-03" | hours_since }} {{ '1959-02-03' | hours_since }}
{{ "1959-02-03T01:02" | hours_since }} {{ '1959-02-03T01:02' | hours_since }}
{{ "1959-02-03T01:02:00Z" | hours_since }} {{ "1959-02-03T01:02:00-0400" | hours_since }}

{{ "1959" | minutes_since }} {{ '1959' | minutes_since }}
{{ "1959-02-03" | minutes_since }} {{ '1959-02-03' | minutes_since }}
{{ "1959-02-03T01:02" | minutes_since }} {{ '1959-02-03T01:02' | minutes_since }}
{{ "1959-02-03T01:02:00Z" | minutes_since }} {{ "1959-02-03T01:02:00-0400" | minutes_since }}

{{ "1959" | seconds_since }} {{ '1959' | seconds_since }}
{{ "1959-02-03" | seconds_since }} {{ '1959-02-03' | seconds_since }}
{{ "1959-02-03T01:02" | seconds_since }} {{ '1959-02-03T01:02' | seconds_since }}
{{ seconds_since "1959-02-03T01:02:00" }} {{ seconds_since '1959-02-03T01:02:00' }}

GitHub Project and RubyGem

More information is available about this plugin from its GitHub project at github.com/mslinn/jekyll_time_since. This plugin is provided as a Ruby gem at rubygems.org/gems/jekyll_time_since.