diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7e7725 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.gem diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..8c18f1a --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--format documentation +--color diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..fc48d36 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,10 @@ +Style/AlignHash: + EnforcedHashRocketStyle: table + EnforcedColonStyle: table + +Style/Documentation: + Enabled: false + +Lint/EndAlignment: + AlignWith: variable + AutoCorrect: true diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..276cbf9 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.3.0 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7474c36 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: ruby +rvm: + - 2.3.0 +before_install: gem install bundler -v 1.11.2 +sudo: false +cache: bundler +script: script/cibuild diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..6c99cef --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in jekyll-avatar.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..192fb07 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Ben Balter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2feadc --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# Jekyll Avatar + +*A Jekyll plugin for rendering GitHub avatars* + +Jekyll Avatar makes it easy to add GitHub avatars to your Jekyll site by specifying a username. If performance is a concern, Jekyll Avatar is deeply integrated with the GitHub avatar API, ensuring avatars are cached and load in parallel. + +## Installation + +Add the following to your site's `Gemfile`: + +```ruby +gem 'jekyll-avatar' +``` + +And add the following to your site's `_config.yml` file: + +```yaml +gems: + - jekyll-avatar +``` + +## Usage + +Simply add the following, anywhere you'd like a user's avatar to appear: + +``` +{% avatar [USERNAME] %} +``` + +With `[USERNAME]` being the user's GitHub username: + +``` +{% avatar hubot %} +``` + +That will output: + +```html +hubot +``` + +### Customizing + +You can customize the size of the resulting avatar by passing the size arugment: + +``` +{% avatar hubot size=25 %} +``` + +That will output: + +```html +hubot +``` diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..4c774a2 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new(:spec) + +task default: :spec diff --git a/jekyll-avatar.gemspec b/jekyll-avatar.gemspec new file mode 100644 index 0000000..31efb01 --- /dev/null +++ b/jekyll-avatar.gemspec @@ -0,0 +1,27 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'jekyll/avatar/version' + +Gem::Specification.new do |spec| + spec.name = 'jekyll-avatar' + spec.version = Jekyll::Avatar::VERSION + spec.authors = ['Ben Balter'] + spec.email = ['ben.balter@github.com'] + + spec.summary = 'A Jekyll plugin for rendering GitHub avatars' + spec.homepage = 'https://github.com/benbalter/jekyll-avatar' + spec.license = 'MIT' + + spec.files = `git ls-files -z`.split("\x0").reject do |file| + file.match(%r{^(test|spec|features)/}) + end + + spec.require_paths = ['lib'] + + spec.add_dependency 'jekyll', '>= 2.4' + spec.add_development_dependency 'bundler', '~> 1.11' + spec.add_development_dependency 'rake', '~> 10.0' + spec.add_development_dependency 'rspec', '~> 3.0' + spec.add_development_dependency 'rubocop' +end diff --git a/lib/jekyll/avatar.rb b/lib/jekyll/avatar.rb new file mode 100644 index 0000000..2724be2 --- /dev/null +++ b/lib/jekyll/avatar.rb @@ -0,0 +1,61 @@ +require 'jekyll/avatar/version' + +module Jekyll + class Avatar < Liquid::Tag + SERVERS = 4 + DEFAULT_SIZE = 40 + VERSION = 3 + + def initialize(tag_name, text, tokens) + super + @text = text + end + + def render(_context) + tag = '\"#{username}\"' + tag + end + + private + + def username + @username ||= @text.split(' ').first.sub('@', '') + end + + def size + @size ||= begin + matches = @text.match(/\bsize=(\d+)\b/i) + matches ? matches[1].to_i : DEFAULT_SIZE + end + end + + def path + @path ||= "#{username}?v=#{VERSION}&s=#{size}" + end + + def server_number + @server_number ||= Zlib.crc32(path) % SERVERS + end + + def host + @host ||= "avatars#{server_number}.githubusercontent.com" + end + + def url + @url ||= "https://#{host}/#{path}" + end + end +end + +Liquid::Template.register_tag('avatar', Jekyll::Avatar) diff --git a/lib/jekyll/avatar/version.rb b/lib/jekyll/avatar/version.rb new file mode 100644 index 0000000..36c3f37 --- /dev/null +++ b/lib/jekyll/avatar/version.rb @@ -0,0 +1,6 @@ +module Liquid; class Tag; end; end +module Jekyll + class Avatar < Liquid::Tag + VERSION = '0.1.0' + end +end diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 0000000..654265e --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,5 @@ +#!/bin/sh + +set -ex + +bundle install diff --git a/script/cibuild b/script/cibuild new file mode 100755 index 0000000..d838d0e --- /dev/null +++ b/script/cibuild @@ -0,0 +1,6 @@ +#!/bin/sh + +set -ex + +bundle exec rake spec +bundle exec rubocop diff --git a/spec/jekyll/avatar_spec.rb b/spec/jekyll/avatar_spec.rb new file mode 100644 index 0000000..cdeb0a6 --- /dev/null +++ b/spec/jekyll/avatar_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Jekyll::Avatar do + let(:doc) { doc_with_content(content) } + let(:username) { 'hubot' } + let(:args) { '' } + let(:text) { "#{username} #{args}".squeeze(' ') } + let(:content) { "{% avatar #{text} %}" } + let(:rendered) { subject.render(nil) } + let(:output) do + doc.content = content + doc.output = Jekyll::Renderer.new(doc.site, doc).run + end + subject { Jekyll::Avatar.send(:new, 'avatar', text, nil) } + + it 'has a version number' do + expect(Jekyll::Avatar::VERSION).not_to be nil + end + + it 'outputs the HTML' do + expected = 'hubot' + expect(rendered) + expect(output).to eql("

#{expected}

\n") + end + + it 'parses the username' do + expect(subject.send(:username)).to eql('hubot') + end + + it 'determines the server number' do + expect(subject.send(:server_number)).to eql(3) + end + + it 'builds the host' do + expect(subject.send(:host)).to eql('avatars3.githubusercontent.com') + end + + it 'builds the path' do + expect(subject.send(:path)).to eql('hubot?v=3&s=40') + end + + it 'defaults to the default size' do + expect(subject.send(:size)).to eql(40) + end + + it 'builds the URL' do + expected = 'https://avatars3.githubusercontent.com/hubot?v=3&s=40' + expect(subject.send(:url)).to eql(expected) + end + + context 'when passed @hubot as a username' do + let(:username) { '@hubot' } + + it 'parses the username' do + expect(subject.send(:username)).to eql('hubot') + end + end + + context 'with a size is passed' do + let(:args) { 'size=45' } + + it "parses the user's requested size" do + expect(subject.send(:size)).to eql(45) + end + end + + context 'with a size < 48' do + it 'includes the avatar-small class' do + expect(rendered).to match(/avatar-small/) + end + end + + context 'with a size > 48' do + let(:args) { 'size=80' } + + it "doesn't include the avatar-small class" do + expect(rendered).to_not match(/avatar-small/) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..0db1214 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,44 @@ +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) + +require 'jekyll' +require 'jekyll/avatar' + +TEST_DIR = File.dirname(__FILE__) +TMP_DIR = File.expand_path('../tmp', TEST_DIR) + +def doc_with_content(_content, opts = {}) + my_site = site(opts) + options = { site: my_site, collection: collection(my_site) } + Jekyll::Document.new(source_dir('_test/doc.md'), options) +end + +def tmp_dir(*files) + File.join(TMP_DIR, *files) +end + +def source_dir(*files) + tmp_dir('source', *files) +end + +def dest_dir(*files) + tmp_dir('dest', *files) +end + +def collection(site, label = 'test') + Jekyll::Collection.new(site, label) +end + +def site(opts = {}) + defaults = Jekyll::Configuration::DEFAULTS + opts = opts.merge( + 'source' => source_dir, + 'destination' => dest_dir + ) + conf = Jekyll::Utils.deep_merge_hashes(defaults, opts) + Jekyll::Site.new(conf) +end + +def fixture(name) + path = File.expand_path "./fixtures/#{name}.json", File.dirname(__FILE__) + File.open(path).read +end