It’s been a while since last time I used Ruby, and I had the perfect excuse to come back to it when I was looking for a solution to inject content of Github files in Jekyll generated websites, and I could not find a github-pages solution to do that (there’s only a tag for including gists, but not a file or a part of it. See this)

I took some inspiration in a Gem I found for including gists and I started developing my own gem. I never developed a gem before, so I decided to download RubyMine and use the default template they have for that. With the help of the guides in rubygems.org, I adapted some minor stuff to make the .gemfile ready for my gem, so I was ready to start developing.

I decided to create a single module (JekyllGithubContent) to encompass the classes of the Gem. Here is the resulting file (injected using the gem :-):

Github file by francisco-perez-sorrosal
Repo: jekyll-github-content
File: lib/jekyll_github_content.rb Raw view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 3rd party libs
require 'open-uri'
require 'active_support'
require 'liquid'

module JekyllGithubContent

  class GithubUriParser

    GITHUB_URI_ROOT = 'https://github.com/'
    GITHUB_RAW_URI_ROOT = 'https://raw.githubusercontent.com'

    attr_reader :user, :repo, :file

    def initialize(path)
      relative_path = path.sub(/^https?\:\/\//, '').sub(/^github.com/, '')
      url_chunks = relative_path.split('/').delete_if { |e| e.empty? }

      @user, @repo, blob, @branch = url_chunks[0..3]
      @file = File.join(url_chunks[4..-1])
    end

    def get_raw_file_uri
      File.join(GITHUB_RAW_URI_ROOT, @user, @repo, @branch, @file)
    end

    def get_user_uri
      File.join(GITHUB_URI_ROOT, @user)
    end

    def get_repo_uri
      File.join(GITHUB_URI_ROOT, @user, @repo)
    end

  end

  class GithubContentRenderer < ::Liquid::Tag

    def initialize(tag_name, params, tokens)
      path, @initial_line, @end_line = params.split
      @github_file = GithubUriParser.new(path)
      @initial_line, @end_line = parse_line_boundaries(@initial_line, @end_line)
      super
    end

    def render(context)
      file_lines = cache.fetch(@github_file.get_raw_file_uri) do
        open(@github_file.get_raw_file_uri).readlines
      end
      lines_to_print = file_lines[@initial_line..@end_line]
      lines_to_print.join
    end

    private # --------------------------------------------------------------------------------------------------------

    def cache
      @@cache ||= ActiveSupport::Cache::MemoryStore.new
    end

    def parse_line_boundaries(first_line, last_line)
      if first_line.nil? && last_line.nil?
        first_line = 0
        last_line = -1
      elsif last_line.nil?
        last_line = first_line
      end

      [first_line.to_i, last_line.to_i]
    end
  end

  class GithubMetaContentRenderer < GithubContentRenderer

    def render(context)
      <<DELIMITER.strip
<div class="github-file">
  <div class="meta-info">
    Github file by <a href="#{@github_file.get_user_uri}">#{@github_file.user}</a>
    <br>
    Repo: <a href="#{@github_file.get_repo_uri}">#{@github_file.repo}</a>
    <br>
    File: #{@github_file.file} <a href="#{@github_file.get_raw_file_uri}">Raw view</a>
  </div>
</div>
DELIMITER
    end
  end

end

# Register the tag names in Liquid to allow code & medadata insertion in Jekill markdown files
# e.g. {% github_file <my_file_url_in_github> [start_line] [end_line] %}
# e.g. {% github_fileref <my_file_url_in_github> %}
Liquid::Template.register_tag('github_file', JekyllGithubContent::GithubContentRenderer)
Liquid::Template.register_tag('github_fileref', JekyllGithubContent::GithubMetaContentRenderer)

Basically the gem is composed by three classes. The code is very simple and auto-commented, so it’s not worth to add something else:

  • GithubUriParser - A parser that receives a path of a Github file and parses its contents for further use in the other 2 classes.
  • GithubContentRenderer - The responsible of rendering the file contents with the help of the previous class and the Liquid templating engine.
  • GithubMetaContentRenderer - An extension of the previous class that presents only the file metadata.

Finally the last two lines (94 and 95), are responsible the register the two new tags defined by the gem in order to inject the Github files. The Gem usage is described here.

Once the gem was ready, I opened an account in https://rubygems.org/ and I added my gem to the repo:

# Build the gem
$ gem build jekyll_github_content.gemspec
# Push it to the rubygems repo
$ gem push jekyll_github_content-0.0.3.gem
# Add your credentials and that's it!

At some point I had to create new version, and I wanted to get rid of the old ones in the public repo. We can achieve that with gem yank jekyll_github_content -v 0.0.2.

After installing and testing the gem locally with my Jekyll website, I was ready to commit the integration in my Jekyll website, which consists only in adding the gem to the Gemfile as it’s shown below, again using the gem:

1
2
3
4
5
source 'https://rubygems.org'
gem "jekyll"
gem "html-proofer"
gem 'github-pages', group: :jekyll_plugins
gem 'jekyll_github_content', '~> 0.0.3', group: :jekyll_plugins

In order to add the gem to the my github-pages website, as Github does not admit external gems, I had to generate the website statically as I described in this other blog post.