Mike Slinn
Mike Slinn

I've Been Writing Jekyll Plugins

Published 2020-10-03.

This article is categorized under Jekyll, Ruby

This is a Jekyll-powered web site. Jekyll is a free open-source preprocessor that generates static web sites. Until recently I followed the documentation and used the Liquid language to write includes. Includes are just macros for Jekyll.

I now 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 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.

Here are some of my 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

archive_display

Lists the names and contents of each file in a tar file. For each text file, the following HTML is emitted:

<div class='codeLabel'>{tar_entry.full_name}</div>
<pre data-lt-active='false'><code>{tar_entry.file_contents}</pre>

Binary files are displayed like this:

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

Syntax

{% archive_display filename.tar %}

Sample output is:

killPortFwdLocal (text/x-shellscript; charset=us-ascii)
#!/bin/bash

# Kill any existing port forwarding on this machine
for X in "$( ps -ef | grep "[s]sh -f" | awk '{print $2}' )"; do 
  if [ "$X" ]; then kill $X; fi
done
killPortFwdOnJumper (text/x-shellscript; charset=us-ascii)
#!/bin/bash

# If an argument is found, assume it is the desired port to forward
export PORT=22222
if [ "$1" ]; then 
  export PORT="$1"
  shift
fi

# Kill port forwarding for $PORT on the remote machine
CMD="for X in \"\$( sudo lsof -i TCP:$PORT | tail -n +2 | awk '{print \$2}' | sort | uniq )\"; do if [ "\$X" ]; then kill \$X; fi; done"
ssh -T jumper "$CMD" 2> >( sed '/Warning: No xauth data/d')

# Check no ports are still listening on jump server
CMD="ssh jumper \"ss --no-header --options state listening \\\"sport = $PORT\\\"\" 2> /dev/null"
if [ "$( $CMD 2> /dev/null )" ]; then
  2> echo "Error: failed to terminate ssh port forwarding for port $PORT on jump server"
  exit 1
fi
tunnelToJumper (text/x-shellscript; charset=us-ascii)
#!/bin/bash

# If an argument is found, assume it is the desired port to forward
export PORT=22222
if [ "$1" ]; then 
  PORT="$1"
  shift
fi

killPortFwdLocal
killPortFwdOnJumper

ssh -f -N -T -R $PORT:localhost:22 jumper

Source Code

_plugins/archive_display.rb
# Copyright 2020 Michael Slinn
#
# Apache 2 License
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

module Jekyll
  class ArchiveDisplay < Liquid::Tag

    def initialize(archive_display, archive_name, tokens)
      super
      archive_name.strip!
      @archive_name = archive_name
    end

    # Modified from https://gist.github.com/sinisterchipmunk/1335041/5be4e6039d899c9b8cca41869dc6861c8eb71f13
    def traverse_tar(tar_name)
      require 'rubygems/package'
      require 'ruby-filemagic'
      # sudo apt install libmagic-dev
      # brew install libmagic

      fileMagic = FileMagic.new(FileMagic::MAGIC_MIME)
      File.open(tar_name, "rb") do |file|
        Gem::Package::TarReader.new(file) do |tar|
          return tar.each.map { |entry|
            if entry.file?
              content = entry.read
              fm_type = fileMagic.buffer(content)
              {
                :name => entry.full_name,
                :content => content.strip,
                :is_text => (fm_type.start_with? "text"),
                :fm_type => fm_type
              }
            end
          }.compact.sort_by { |entry| entry[:name] }.map { |entry|
            heading = "<div class='codeLabel'>#{entry[:name]} <span style='font-size: smaller'>(#{entry[:fm_type]})</span></div>"
            if entry[:is_text]
              "#{heading}\n<pre data-lt-active='false'>#{entry[:content]}</pre>"
            else
              "#{heading}\n<p><i>Binary file</i></pre>"
            end
          }
        end
      end
    end

    def render(context)
      source = context.environments.first["site"]["source"]
      tar_name = "#{source}/#{@archive_name}"
      #puts("archive_display: tar_name=#{tar_name}")
      traverse_tar(tar_name)
    end
  end
end

Liquid::Template.register_tag('archive_display', Jekyll::ArchiveDisplay)

      # begin
      # rescue NameError
      #   #File.delete(tar_name)
      #   abort "Problem reading #{@archive_name}. Please restart Jekyll.\n"
      #   # Should recreate tar and reread. Crash exit for now instead
      # end

Installation

  1. Install libmagic.
    Ubuntu & WSL
    $ sudo apt install libmagic-dev
    Mac
    $ brew install libmagic
  2. Add this line to Gemfile in your Jekyll site's top-level directory:
    gem 'ruby-filemagic'
  3. Install the ruby-filemagic gem. From your Jekyll site's top-level directory, type:
    $ bundle install
  4. Copy archive_display.rb into /_plugins.
  5. Restart Jekyll.

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. Absolute filenames (first character is /).
  2. Filenames relative to the top-level directory of the Jekyll web site (unnecessary to preface with ./).
  3. Filenames relative to the user home directory (first character is ~).
  4. Executable filenames on the PATH (first character is !).

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

Syntax

{% flexible_include 'path' optionalParam1='yes' optionalParam2='green' %}

The optional parameters can have any name. The included file will have parameters substituted.

Usage Examples

  1. Include files without parameters; all four types of includes are shown.
    {% flexible_include '../../folder/outside/jekyll/site/foo.html' %}
    {% flexible_include 'folder/within/jekyll/site/bar.js' %}
    {% flexible_include '/etc/passwd' %}
    {% flexible_include '~/.ssh/config' %}
    
    Here is another flexible_include invocation using environment variables:
    {% flexible_include '$HOME/.bash_aliases' %}
    
  2. Include a file and pass parameters to it.
    {% flexible_include '~/folder/under/home/directory/foo.html'
      param1='yes' param2='green' %}

Source Code

This code lives in a GitHub repository.

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.

Rubular is a handy online tool to try out regular expressions.

Syntax

The regular expression may be enclosed in single quotes, double quotes, or nothing.

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 }}

Important: the name of the filter 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 | from '2' | until: '4' | 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)

Usage Examples

Some of the following examples use a multiline string containing 5 lines, called lines, which was created this way:

{% capture lines %}line 1
line 2
line 3
line 4
line 5
{% endcapture %}

Other examples use a multiline string containing the contents of .gitignore, which looks like this:

.gitignore
*.gz
*.sublime*
*.swp
*.out
*.Identifier
*.log
.idea*
*.iml
*.tmp
*~
.DS_Store
.idea
.jekyll-cache/
.jekyll-metadata
.sass-cache/
__pycache__/
__MACOSX
_site/
~*
bin/*.class
doc/
node_modules/
Notepad++/
package/
cloud9.tar
cloud9.zip
instances.json
rescue_ubuntu2010
landingPageShortName.md
test.html
RUNNING_PID

From the third line of string

These examples return the lines of the file from the beginning of the until a line with the string "3" is found, including the matched line. The only difference between the examples is the delimiter around the regular expression.

{{ lines | from: '3' }}
{{ lines | from: "3" }}
{{ lines | from: 3 }}

These all generate:

line 3
line 4
line 5

From Line In a File Containing 'PID'

{{ flexible_include '.gitignore' | from: 'PID' }}

This generates:

RUNNING_PID

To the third line of string

These examples return the lines of the file from the first line until a line with the string "3" is found, including the matched line. The only difference between the examples is the delimiter around the regular expression.

{{ lines | to: '3' }}
{{ lines | to: "3" }}
{{ lines | to: 3 }}

These all generate:

line 1
line 2
line 3

To Line In a File Containing 'idea'

{{ flexible_include '.gitignore' | to: 'idea' }}

This generates:

*.gz
*.sublime*
*.swp
*.out
*.Identifier
*.log
.idea*

Until the third line of string

These examples return the lines of the file until a line with the string "3" is found, excluding the matched line. The only difference between the examples is the delimiter around the regular expression.

{{ lines | until: '3' }}
{{ lines | until: "3" }}
{{ lines | until: 3 }}

These all generate:

line 1
line 2

Until Line In a File Containing 'idea'

{{ flexible_include '.gitignore' | until: 'idea' }}

This generates:

*.gz
*.sublime*
*.swp
*.out
*.Identifier
*.log

From the string "2" until the string "4"

These examples return the lines of the file until a line with the string "3" is found, excluding the matched line. The only difference between the examples is the delimiter around the regular expression.

{{ lines | from: '2' | until: '4' }}
{{ lines | from: "2" | until: "4" }}
{{ lines | from: 2 | until: 4 }}

These all generate:

line 2
line 3

From Line In a File Containing 'idea' Until no match

The .gitignore file does not contain the string xx. If we attempt to match against that string the remainder of the file is returned for the to and until filter, and the empty string is returned for the from filter.

{{ flexible_include '.gitignore' | from: 'PID' | until: 'xx' }}

This generates:

RUNNING_PID

More Complex Regular Expressions

The from, to and until filters can all accept more complex regular expressions. This regular expression matches lines that have either the string sun or cloud at the beginning of the line.

{{ flexible_include '.gitignore' | from: '^(cloud|sun)' }}

This generates:

cloud9.tar
cloud9.zip
instances.json
rescue_ubuntu2010
landingPageShortName.md
test.html
RUNNING_PID

Source Code

_plugins/from_to_until.rb
# Copyright 2020 Michael Slinn
#
# Apache 2 License
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

module From
  # Filters a multiline string, returning the portion beginning with the line that satisfies a regex.
  # The regex could be enclosed in single quotes, double quotes, or nothing.
  #
  # Usage:
  #   {{ flexible_include '/blog/2020/10/03/jekyll-plugins.html' | from 'module' }}
  def from(input_string, regex)
    if not check_parameters(input_string, regex) then return "" end

    regex = remove_quotations(regex.to_s.strip)
    matched = false
    result = ""
    input_string.each_line do |line|
      if ! matched and line =~ /#{regex}/ then matched = true end
      if matched then
        result += line
      end
    end
    result
  end

  # Filters a multiline string, returning the portion from the beginning until and including the line that satisfies a regex.
  # The regex could be enclosed in single quotes, double quotes, or nothing.
  #
  # Usage:
  #   {{ flexible_include '/blog/2020/10/03/jekyll-plugins.html' | to 'module' }}
  def to(input_string, regex)
    if not check_parameters(input_string, regex) then return "" end

    regex = remove_quotations(regex.to_s.strip)
    result = ""
    input_string.each_line do |line|
      result += line
      if line =~ /#{regex}/ then return result end
    end
    result
  end

  # Filters a multiline string, returning the portion from the beginning until but not including the line that satisfies a regex.
  # The regex could be enclosed in single quotes, double quotes, or nothing.
  #
  # Usage:
  #   {{ flexible_include '/blog/2020/10/03/jekyll-plugins.html' | until 'module' }}
  def until(input_string, regex)
    if not check_parameters(input_string, regex) then return "" end

    regex = remove_quotations(regex.to_s.strip)
    result = ""
    input_string.each_line do |line|
      if line =~ /#{regex}/ then return result end
      result += line
    end
    result
  end

  private

  def check_parameters(input_string, regex)
    if input_string.nil? or input_string.empty? then
      puts "Warning: Plugin 'from' received no input."
      return false
    end

    regex = regex.to_s
    if regex.nil? or regex.empty? then
      puts "Warning: Plugin 'from' received no regex."
      return false
    end
    true
  end

  def remove_quotations(str)
    if (str.start_with?('"') and str.end_with?('"')) or
       (str.start_with?("'") and str.end_with?("'"))
      str = str.slice(1..-2)
    end

    str
  end
end


Liquid::Template.register_filter(From)

Installation

  1. Copy from_to_until.rb into /_plugins.
  2. Restart Jekyll.

href

Generates an a href tag with target="_blank" and rel=nofollow.

Syntax

{% href url text to display %}

The url should not be enclosed in quotes.

Usage Examples

Default

{% 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>

Source Code

_plugins/href.rb
# Copyright 2020 Michael Slinn
#
# Apache 2 License
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

module Jekyll
  # Generates an href. By default the link opens in a new tab or window, with rel=nofollow.
  # To suppress the nofollow attribute value, preface the link with the word follow.
  # To suppress the target attribute value, preface the link with the word notarget.
  class ExternalHref < Liquid::Tag

    def initialize(href, command_line, tokens)
      super

      @follow = " rel='nofollow'"
      @target = " target='_blank'"

      tokens = command_line.strip.split(" ")

      followIndex = tokens.index("follow")
      if followIndex then
        tokens.delete_at(followIndex)
        @follow = ""
      end

      targetIndex = tokens.index("notarget")
      if targetIndex then
        tokens.delete_at(targetIndex)
        @target = ""
      end

      @link = tokens.shift
      @text = tokens.join(" ")
    end

    def render(context)
      "<a href='#{@link}'#{@target}#{@follow}>#{@text}</a>"
    end
  end
end

Liquid::Template.register_tag('href', Jekyll::ExternalHref)

Installation

  1. Copy href.rb into /_plugins.
  2. Restart Jekyll.

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

{% link uri %}

Usage Example

{% link cloud9.tar %}

Generates:

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

Which renders as: cloud9.tar (4.5 KB)

Source Code

_plugins/link.rb
# Copyright 2020 Michael Slinn
#
# Apache 2 License
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

module Jekyll
  # Generates an href to a file for the user to download from the site.
  # Also shows the file size in a human-readable format.
  class Linker < Liquid::Tag
    # text contains the name of the file, relative to the website top level directory
    def initialize(link, text, tokens)
      super
      @filename = text.delete('"').delete("'").strip
    end

    def render(context)
      source = context.environments.first["site"]["source"]
      file_fq = File.join(source, @filename)
      if !File.exist?(file_fq)
        abort("Error: '#{file_fq}' not found. See the link tag in")
      end

      "<a href='/#{@filename}'><code>#{@filename}</code></a> (#{as_size(File.size(file_fq))})"
    end

    def as_size(s)
      units = %W(B KB MB GB TB)

      size, unit = units.reduce(s.to_f) do |(fsize, _), utype|
        fsize > 512 ? [fsize / 1024, utype] : (break [fsize, utype])
      end

      "#{size > 9 || size.modulo(1) < 0.1 ? '%d' : '%.1f'} %s" % [size, unit]
    end
  end
end

Liquid::Template.register_tag('link', Jekyll::Linker)

Installation

  1. Copy link.rb into /_plugins.
  2. Restart Jekyll.

make_archive

Creates tar and zip archives according to the make_archive entry in _config.yml. 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. 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.

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 filenames 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, error.html, ~/.ssh/config, /etc/passwd, '!update' ]
  -
    archive_name: cloud9.tar
    delete: false  # Do not overwrite the archive if it already exists
    files: [ index.html, error.html, ~/.ssh/config, /etc/passwd, '!update' ]

Source Code

_plugins/make_archive.rb
# Copyright 2020 Michael Slinn
#
# Apache 2 License
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

require 'fileutils'
require 'ptools'
require 'rubygems'

module Jekyll
  class ArchiveGen < Generator
    priority :high

    def generate(site)
      @live_reload = site.config['livereload']

      archive_config = site.config['make_archive']
      return if archive_config.nil?
      archive_config.each do |config|
        @archive_name = config['archive_name']  # Relative to _site
        abort("Error: archive_name was not specified in _config.yml.") if @archive_name.nil?

        if @archive_name.end_with? ".zip"
          @archive_type = :zip
        elsif @archive_name.end_with? ".tar"
          @archive_type = :tar
        else
          abort("Error: archive must be zip or tar; #{@archive_name} is of an unknown archive type.")
        end

        @archive_files = config['files'].compact
        abort("Error: archive files were not specified in _config.yml.") if @archive_files.nil?

        delete_archive = config['delete']
        @force_delete = if delete_archive.nil? then !@live_reload else delete_archive end

        #puts("@archive_name=#{@archive_name}; @live_reload=#{@live_reload}; @force_delete=#{@force_delete}; @archive_files=#{@archive_files}")

        doit(site.source)
        site.keep_files << @archive_name
      end
    end

    def doit(source)
      archive_name_full = "#{source}/#{@archive_name}"
      archive_exists = File.exist?(archive_name_full)
      return if archive_exists && @live_reload

      puts("#{archive_name_full} exists? #{archive_exists}")
      if archive_exists && @force_delete
        puts("Deleting old #{archive_name_full}")
        File.delete(archive_name_full)
      end

      if !archive_exists || @force_delete
        puts("Making #{archive_name_full}")
        if @archive_type == :tar
          make_tar(archive_name_full, source)
        elsif @archive_type == :zip
          make_zip(archive_name_full, source)
        end
      end

      if !File.foreach(".gitignore").grep(/^#{@archive_name}/).any?
        puts("#{@archive_name} not found in .gitignore, adding entry.")
        File.open('.gitignore', 'a') do |f|
          f.puts File.basename(@archive_name)
        end
      end
    end

    def make_tar(tar_name, source)
      require 'rubygems/package'
      require 'tmpdir'
      require 'zlib'

      Dir.mktmpdir do |dirname|
        @archive_files.each do |filename|
          fn, filename_full = qualify_file_name(filename, source)
          puts("Copying #{filename_full} to temporary directory #{dirname}; filename=#{filename}; fn=#{fn}")
          FileUtils.copy(filename_full, dirname)
        end

        # Modified from https://gist.github.com/sinisterchipmunk/1335041/5be4e6039d899c9b8cca41869dc6861c8eb71f13
        File.open(tar_name, "wb") do |tarfile|
          Gem::Package::TarWriter.new(tarfile) do |tar|
            Dir[File.join(dirname, "**/*")].each do |file|
              mode = File.stat(file).mode
              relative_file = file.sub /^#{Regexp::escape dirname}\/?/, ''
              if File.directory?(file)
                tar.mkdir relative_file, mode
              else
                tar.add_file relative_file, mode do |tf|
                  File.open(file, "rb") { |f| tf.write f.read }
                end
              end
            end
          end
        end
      end
    end

    def make_zip(zip_name, source)
      require 'zip'
      Zip.default_compression = Zlib::DEFAULT_COMPRESSION
      Zip::File.open(zip_name, Zip::File::CREATE) do |zipfile|
        @archive_files.each do |filename|
          filename_in_archive, filename_original = qualify_file_name(filename, source)
          #puts("make_zip: adding #{filename_original} to #{zip_name} as #{filename_in_archive}")
          zipfile.add(filename_in_archive, filename_original)
        end
      end
    end

    def qualify_file_name(path, source)
      # Returns tuple of filename (without path) and fully qualified filename
      if path.start_with? "/"  # Is the file absolute?
        puts("Absolute filename: #{path}")
        return File.basename(path), path
      elsif path.start_with? "!"  # Should the file be found on the PATH?
        clean_path = path[1..-1]
        filename_full = File.which(clean_path)
        abort("Error: #{clean_path} is not on the PATH.") if filename_full.nil?
        puts("File on PATH: #{clean_path} -> #{filename_full}")
        return File.basename(clean_path), filename_full
      elsif path.start_with? "~"  # Is the file relative to user's home directory?
        clean_path = path[2..-1]
        filename_full = File.join(ENV['HOME'], clean_path)
        puts("File in home directory: #{clean_path} -> #{filename_full}")
        return File.basename(clean_path), filename_full
      else  # The file is relative to website top-level directory
        puts("Relative filename: #{path}")
        return File.basename(path), File.join(source, path)  # join yields the fully qualified path
      end
    end
  end
end

Installation

  1. Copy make_archive.rb into /_plugins.
  2. Restart Jekyll.

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: 8793f244b097.

Source Code

_plugins/random_hex.rb
# Copyright 2020 Michael Slinn
#
# Apache 2 License
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

# Outputs a string of random hexadecimal characters of any length
# Defaults to a six-character string
#
# Usage:
#   {{ random_hex_string 10 }}

module Jekyll
  class RandomNumberTag < Liquid::Tag
    def initialize(tag_name, text, context)
      super
      text.to_s.strip!
      if text.empty?
        @n = 6
      else
        tokens = text.split(" ")
        if tokens.length > 1
          abort("random_hex_string error - more than one token was provided: '#{text}'")
        end

        if ! Integer(text, exception: false)
          abort("random_hex_string error: '#{text}' is not a valid integer")
        end

        @n = text.to_i
      end
    end

    def render(context)
      require 'securerandom'

      SecureRandom.hex(@n)
    end
  end
end

Liquid::Template.register_tag('random_hex_string', Jekyll::RandomNumberTag)

Installation

  1. Save the above code as random_hex.rb and place the file in the _plugins directory.
  2. Restart Jekyll.

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.highlighter = 'rouge'
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"]

Source Code

_plugins/site_inspector.rb
# Copyright 2020 Michael Slinn
#
# Apache 2 License
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

class SiteInspector < Jekyll::Generator
  # Dumps lots of information from site if in development mode and site_inspector: true in _config.yml.

  def generate(site)
    mode = site.config['env']['JEKYLL_ENV']

    config = site.config['site_inspector']
    return if config.nil?

    inspector_enabled = config != false
    return if !inspector_enabled

    force = config == "force"

    if force || mode == "development"
      puts("site is of type #{site.class}")
      puts("site.time = #{site.time}")
      puts("site.config['env']['JEKYLL_ENV'] = #{mode}")
      site.collections.each do |key, value|
        puts "site.collections.#{key}"
        #value.each { |key2, value2| puts "  #{key2}" }  # Generates too much output!
      end

      # key env contains all environment variables, quite verpose so output is suppressed
      site.config.sort.each {|key, value| puts "site.config.#{key} = '#{value}'" unless key == "env" }

      site.data.sort.each {|key, value| puts "site.data.#{key} = '#{value}'" }
      #site.documents.each {|key, value| puts "site.documents.#{key}" }  # Generates too much output!
      puts "site.keep_files: #{site.keep_files.sort}"
      #site.pages.each {|key, value| puts "site.pages.#{key}'" }  # Generates too much output!
      #site.posts.each {|key, value| puts "site.posts.#{key}" }   # Generates too much output!
      site.tags.sort.each {|key, value| puts "site.tags.#{key} = '#{value}'" }
    end
  end
end

Installation

  1. Copy site_inspector.rb into /_plugins.
  2. Restart Jekyll.