Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
benbalter committed Sep 8, 2017
1 parent 1bb332d commit b3e0822
Show file tree
Hide file tree
Showing 22 changed files with 634 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
Gemfile.lock
spec/examples.txt
*.gem
tmp/
1 change: 1 addition & 0 deletions .rspec
@@ -0,0 +1 @@
--require spec_helper
10 changes: 10 additions & 0 deletions .rubocop.yml
@@ -0,0 +1,10 @@
inherit_gem:
jekyll: .rubocop.yml

AllCops:
Exclude:
- vendor/**/*

Metrics/BlockLength:
Exclude:
- spec/**/*
3 changes: 3 additions & 0 deletions Gemfile
@@ -0,0 +1,3 @@
source "https://rubygems.org"

gemspec
23 changes: 23 additions & 0 deletions jekyll-remote-theme.gemspec
@@ -0,0 +1,23 @@
# encoding: utf-8

$LOAD_PATH.unshift File.expand_path("lib", __dir__)
require "jekyll-remote-theme/version"

Gem::Specification.new do |s|
s.name = "jekyll-remote-theme"
s.version = Jekyll::RemoteTheme::VERSION
s.authors = ["Ben Balter"]
s.email = ["ben.balter@github.com"]
s.homepage = "https://github.com/benbalter/jekyll-remote-theme"
s.summary = ""

s.files = `git ls-files app lib`.split("\n")
s.platform = Gem::Platform::RUBY
s.require_paths = ["lib"]
s.license = "MIT"

s.add_dependency "jekyll", "~> 3.5"
s.add_development_dependency "rubocop", "~> 0.4"
s.add_development_dependency "rspec", "~> 3.0"
s.add_development_dependency "jekyll-theme-primer"
end
22 changes: 22 additions & 0 deletions lib/jekyll-remote-theme.rb
@@ -0,0 +1,22 @@
require "jekyll"
$LOAD_PATH.unshift(File.dirname(__FILE__))

module Jekyll
module RemoteTheme
autoload :VERSION, "jekyll-remote-theme/version"
autoload :Theme, "jekyll-remote-theme/theme"
autoload :Cloner, "jekyll-remote-theme/cloner"
autoload :Munger, "jekyll-remote-theme/munger"
autoload :MockGemspec, "jekyll-remote-theme/mock_gemspec"

CONFIG_KEY = "remote_theme".freeze

def self.init(site)
Munger.new(site).munge!
end
end
end

Jekyll::Hooks.register :site, :after_reset do |site|
Jekyll::RemoteTheme.init(site)
end
65 changes: 65 additions & 0 deletions lib/jekyll-remote-theme/cloner.rb
@@ -0,0 +1,65 @@
require "open3"

module Jekyll
module RemoteTheme
class Cloner
class CloneError < StandardError; end

FLAGS = [
"--recurse-submodules",
"--depth", "1",
].freeze

attr_reader :git_url, :git_ref, :path

# Initialize a cloner instance
#
# git_url - the remote clone location (string)
# git_ref - the remote reference to check out in our working dir (string)
# path - the absolute path to a clone location on the local disk (string)
def initialize(git_url: nil, git_ref: "master", path: nil)
@git_url = git_url
@git_ref = git_ref
@path = path
end

def run
return false unless path

if clone_dir_exists?
msg = "_theme directory already exists in site source. "
msg << "Not cloning remote theme."
Jekyll.logger.warn "Remote theme: ", msg
return false
end

Jekyll.logger.info "Remote theme: ", "Cloning into #{git_url}"
output, status = Open3.capture2e(*clone_command)
raise CloneError, output if status.exitstatus != 0
@cloned = true
end

private

def clone_command
[
"git",
"clone",
*FLAGS,
"--branch", git_ref,
"--verbose",
git_url,
path,
].compact
end

def cloned?
@cloned ||= clone_dir_exists?
end

def clone_dir_exists?
path && Dir.exist?(path)
end
end
end
end
23 changes: 23 additions & 0 deletions lib/jekyll-remote-theme/mock_gemspec.rb
@@ -0,0 +1,23 @@
module Jekyll
module RemoteTheme
# Jekyll::Theme expects the theme's gemspec to tell it things like
# the path to the theme and runtime dependencies. MockGemspec serves as a
# stand in, since remote themes don't have Gemspecs
class MockGemspec
extend Forwardable
def_delegator :theme, :root, :full_gem_path

def initialize(theme)
@theme = theme
end

def runtime_dependencies
[]
end

private

attr_reader :theme
end
end
end
64 changes: 64 additions & 0 deletions lib/jekyll-remote-theme/munger.rb
@@ -0,0 +1,64 @@
module Jekyll
module RemoteTheme
class Munger
extend Forwardable
def_delegator :site, :config
attr_writer :cloner
attr_reader :site

def initialize(site)
@site = site
end

def munge!
return unless theme

unless theme.remote?
msg = "The theme `#{theme.name}` was requested, but exists locally. "
msg << "The Gem-based theme will be used instead."
Jekyll.logger.warn "Remote theme: ", msg
return false
end

cloner.run
configure_theme
theme
end

private

def theme
return unless raw_theme && raw_theme.is_a?(String)
@theme ||= Theme.new(raw_theme, theme_path)
end

def raw_theme
config[CONFIG_KEY]
end

def theme_path
@theme_path ||= File.expand_path "_theme", config["source"]
end

def cloner
@cloner ||= Cloner.new(
:git_url => theme.git_url,
:git_ref => theme.git_ref,
:path => theme_path
)
end

def theme_dir_exists?
@theme_dir_exists ||= Dir.exist?(theme_path)
end

def configure_theme
return unless theme
site.config["theme"] = theme.name
site.theme = theme
site.theme.configure_sass
site.send(:configure_include_paths)
end
end
end
end
85 changes: 85 additions & 0 deletions lib/jekyll-remote-theme/theme.rb
@@ -0,0 +1,85 @@
module Jekyll
module RemoteTheme
class Theme < Jekyll::Theme
THEME_REGEX = %r!\A([a-z0-9\-_]+)(?:/([a-z0-9\-_]+)(?:@([a-z0-9]+))?)?\z!i
GIT_HOST = "https://github.com".freeze

attr_reader :root

# Initializes a new Jekyll::RemoteTheme::Theme
#
# raw_theme can be in the form of:
#
# 1. theme-name - a gem-based theme
# 2. owner/theme-name - a GitHub owner + theme-name string
# 3. owner/theme-name@git_ref - a GitHub owner + theme-name + Git ref string
def initialize(raw_theme, root = nil)
@raw_theme = raw_theme.downcase.strip
@root = root
super(name)
end

def name
theme_parts[2] || theme_parts[1]
end

def owner
theme_parts[1] if theme_parts[2]
end

def name_with_owner
[name, owner].join("/")
end
alias_method :nwo, :name_with_owner

def valid?
local? || remote?
end

def invalid?
!valid?
end

def git_url
"#{GIT_HOST}/#{owner}/#{name}" if remote?
end

def git_ref
theme_parts[3] || "master" if remote?
end

def remote?
!owner.nil? && !gem?
end

def local?
owner.nil? && gem?
end

def inspect
"#<Jekyll::RemoteTheme::Theme owner=\"#{owner}\" name=\"#{name}\">"
end

private

def theme_parts
@theme_parts ||= @raw_theme.match(THEME_REGEX)
end

def gem?
return @gem if defined? @gem
@gem = !Gem::Specification.find_by_name(name).nil?
rescue Gem::LoadError
@gem = false
end

def gemspec
@gemspec ||= if gem?
Gem::Specification.find_by_name(name)
else
MockGemspec.new(self)
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/jekyll-remote-theme/version.rb
@@ -0,0 +1,5 @@
module Jekyll
module RemoteTheme
VERSION = "0.1.0".freeze
end
end
3 changes: 3 additions & 0 deletions script/bootstrap
@@ -0,0 +1,3 @@
#!/bin/sh

bundle install
7 changes: 7 additions & 0 deletions script/cibuild
@@ -0,0 +1,7 @@
#!/bin/sh

set -e

bundle exec rspec
bundle exec rubocop
gem build jekyll-remote-theme.gemspec
Empty file added spec/fixtures/site/index.html
Empty file.
1 change: 1 addition & 0 deletions spec/fixtures/theme/_includes/include.html
@@ -0,0 +1 @@
REMOTE THEME INCLUDE
3 changes: 3 additions & 0 deletions spec/fixtures/theme/_layouts/default.html
@@ -0,0 +1,3 @@
REMOTE THEME LAYOUT

{{ content }}
4 changes: 4 additions & 0 deletions spec/fixtures/theme/_sass/style.scss
@@ -0,0 +1,4 @@
---
---

.remote-theme { padding: 20px }
60 changes: 60 additions & 0 deletions spec/jekyll-remote-theme/cloner_spec.rb
@@ -0,0 +1,60 @@
RSpec.describe Jekyll::RemoteTheme::Cloner do
let(:git_url) { git_repo }
let(:git_ref) { "master" }
let(:path) { File.expand_path "_theme", dest_dir }

subject { described_class.new(:git_url => git_url, :git_ref => git_ref, :path => path) }

before { reset_tmp_dir }
before { write_git_repo }
before { Jekyll.logger.log_level = :error }

it "stores the git_url" do
expect(subject.git_url).to eql(git_url)
end

it "stores the git_ref" do
expect(subject.git_ref).to eql(git_ref)
end

it "stores the path" do
expect(subject.path).to eql(path)
end

context "when path is nil" do
subject do
described_class.new(:git_url => git_url, :git_ref => git_ref, :path => nil)
end

it "doesn't clone" do
expect(subject.run).to be_falsy
end
end

context "when the theme dir exists" do
before { FileUtils.mkdir_p path }

it "doesn't clone" do
expect(subject.run).to be_falsy
end
end

context "a valid theme" do
it "clones" do
layout_path = File.expand_path "_layouts/default.html", path
expect(File.exist?(layout_path)).to be_falsy

expect(subject.run).to be_truthy
expect(File.exist?(layout_path)).to be_truthy
end
end

context "an invalid theme" do
let(:git_url) { File.expand_path "foo", tmp_dir }
before { FileUtils.rm_rf git_url }

it "raises an error" do
expect { subject.run }.to raise_error(Jekyll::RemoteTheme::Cloner::CloneError)
end
end
end

0 comments on commit b3e0822

Please sign in to comment.