A downloads manager for Jekyll

[Tweet : ADN : nvALT]

On my WordPress blog I ran a plugin called Download Monitor which allowed me to create download ids that could be inserted via short tags. When I updated a download version, any mention of it throughout the site would be updated to show the latest version and link to the most recent download package. I needed something similar on my Jekyll blog to keep things up to date. The following system is geared toward Jekyll but the concept could be adapted to any static blog.

I started by using a script that looped through all of the downloads listed in my WordPress database and generated a CSV file of all of my downloads and their associated metadata. For example:

id,title,file,version,description,info,icon,updated
1,Clippable to Evernote Service,/share/clippable-to-evernote.zip,1.0,"A Snow Leopard System Service [...]",http://brettterpstra.com/code/clippable-to-evernote-service/,/images/serviceicon.jpg,Tue Feb 23 21:55:00 -0600 2010
Copy

This file is editable in a plain text editor or in a spreadsheet app like Numbers, and doesn’t require an existing WordPress database to start, just a plain old CSV file with the appropriate data.

Next, I just needed a tag plugin that would let me create a tag like:

{% download 59 %}
Copy

The tag would insert a templated “download card” with the appropriate data:

Example download card

The current tag plugin I’m using has the template hardcoded and needs to be updated to handle an actual external template file, but it should give you the idea:

# Title: Download tag for Jekyll
# Author: Brett Terpstra http://brettterpstra.com
# Description: Create updateable download cards for downloadable projects. Reads from a downloads.csv file in the root folder
#
# Example: {% download id %} to read download [id] from the CSV
module Jekyll
class DownloadTag < Liquid::Tag
require 'csv'
@title = nil
@file = ''
@info = ''
@icon = nil
@template = 'link'
def initialize(tag_name, markup, tokens)
if markup =~ /^(\d+)( .*)?$/
req_id = $1.strip
@format = $2.strip.to_i unless $2.nil?
CSV.read("downloads.csv").each do |line|
if line[0] == req_id
id, title, file, version, description, info, icon, updated = line
@file = file
@version = version == '' ? '' : %Q{ v#{version}}
@title = %Q{#{title}#{@version}}
@icon = icon == '' ? '' : %Q{<img src="#{icon}" width="100" height="100">}
@updated = updated == '' ? '' : %Q{<p class="dl_updated">Updated #{updated.sub(/[\d:]+ -\d+ (\d+)/,"\\1").strip}.</p>}
@description = description == '' ? '' : %Q{<p class="dl_description">#{description}</p>#{@updated}}
@info = info == '' ? '' : %Q{<p class="dl_info"><a href="#{info}" title="More information on #{title}">More info&hellip;</a></p>}
break
end
end
end
super
end
# TODO: Make this read templates
# Example:
# Dir.chdir(includes_dir) do
# choices = Dir['**/*'].reject { |x| File.symlink?(x) }
# if choices.include?(file)
# source = File.read(file)
# partial = Liquid::Template.parse(source)
# context.stack do
# rtn = rtn + partial.render(context)
# end
# else
# rtn = rtn + "Included file '#{file}' not found in _includes directory"
# end
# end
def render(context)
output = super
# TODO: Enable template selection
if @title
download = %Q{<div class="download"><h4>#{@title}</h4><p class="dl_icon"><a href="#{@file}" title="Download #{@title}">#{@icon}</a></p><div class="dl_body"><p class="dl_link"><a href="#{@file}">Download #{@title}</a></p>#{@description}#{@info}</div></div>}
else
"Error processing input, expected syntax: {% download title filename [url/to/related/post] %}"
end
end
end
end
Liquid::Template.register_tag('download', Jekyll::DownloadTag)
view raw download.rb hosted with ❤ by GitHub

Updating a download’s row in the CSV file with a new download link, version number, update time and/or description change will recreate all of the references to it in the site next time I build it.

I also added a Rake task for searching my downloads and finding the ID for use in the tag:

desc "Find a download ID"
task :find_download, :term do |t, args|
raise "### You haven't created a download csv yet." unless File.exists?('downloads.csv')
results = CSV.read("downloads.csv").delete_if {|row|
row[0].strip =~ /^\d+$/ && row[1] + " " + row[4] =~ /.*#{args.term}.*/i ? false : true
}
results.sort! { |a,b|
a[0].to_i <=> b[0].to_i
}.map! { |res|
res[0] + ": " + res[1] + " v" + res[3]
}
results_menu(results,"download")
print ("Select download")
while line = Readline.readline(": ", true)
if !line || line =~ /^[a-z]/i
puts "## Canceled"
Process.exit 0
end
line = line.to_i
if (line > 0 && line <= results.length)
id = results[line.to_i - 1].match(/^\d+/)[0]
download_tag = "{% download #{id} %}"
%x{echo "#{download_tag}\\c"| pbcopy}
puts %Q{Download tag in clipboard: "#{download_tag}"}
Process.exit 0
else
puts "## Selection out of range"
Process.exit 0
end
end
end
# creates a user menu from a hash or array
def results_menu(res, type = "file")
counter = 1
puts
res.reverse!
res.each do |match|
match = match.class == String ? match : match[:path]
if type == "file"
display = match.sub(/^.*?\/([^\/]+)$/,"\\1")
display.gsub!(/^[\d-]+/,'')
display.gsub!(/\.(md|markdown)$/,'')
display.gsub!(/-/,' ')
elsif type == "download"
display = match.sub(/^\d+:\s*/,'')
else
display = match.strip
end
printf("%2d ) %s\n", counter, display)
counter += 1
end
puts
end
view raw Rakefile.rb hosted with ❤ by GitHub

Now typing rake find_download[nvalt] will show me all downloads matching the search term “nvalt,” offer a menu of matches and put the complete Liquid tag for the selected result in my clipboard:

$ rake find_download[nvalt]

 1 ) OmniFocus Clipper Plugins for Chrome v1.0
 2 ) nvALT 2.2 BETA v2.2b101
 3 ) Marked Watcher Scripts v1.1
 4 ) QuickQuestion v1.1
 5 ) nvALT v2.1

Select download: 2
Download tag in clipboard: "{% download 59 %}"
Copy

I also plan to add a Rake task to make adding new downloads and updating existing versions from the command line as simple as possible.

With a little modification, this system could easily be used to generate a “Downloads” page for my site, though I’ve decided that’s really not necessary. I may change my mind in the future, though.

Todo

What I’ve shared here is a functioning system that is currently in use. Before it’s “complete” and ready to share in my plugins repo, there are a few things I’d like to polish:

  • A template system
    • reads a template name from the tag
    • works with multiple templates
  • Rake tasks for adding and editing downloads more easily
  • script for adding downloads via Service or droplet
  • (Possibly) a plugin for generating a Downloads page