diff --git a/.travis.yml b/.travis.yml
index 56a8b42c..451d94a6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -25,6 +25,7 @@ env:
branches:
only:
- master
+ - themes
notifications:
irc:
diff --git a/Gemfile b/Gemfile
index 5a080750..afb4513b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,6 +22,7 @@ group :test do
gem "rspec-mocks"
gem "nokogiri"
gem "rspec"
+ gem "test-theme", path: File.expand_path("./test/fixtures/test-theme", File.dirname(__FILE__))
end
#
diff --git a/features/theme.feature b/features/theme.feature
new file mode 100644
index 00000000..40877f84
--- /dev/null
+++ b/features/theme.feature
@@ -0,0 +1,61 @@
+Feature: Writing themes
+ As a hacker who likes to share my expertise
+ I want to be able to make a gemified theme
+ In order to share my awesome style skillz with other Jekyllites
+
+ Scenario: A theme with SCSS
+ Given I have a configuration file with "theme" set to "test-theme"
+ And I have a css directory
+ And I have a "css/main.scss" page that contains "@import 'style';"
+ When I run jekyll build
+ Then I should get a zero exit status
+ And the _site directory should exist
+ And I should see ".sample {\n color: black; }" in "_site/css/main.css"
+
+ Scenario: A theme with an include
+ Given I have a configuration file with "theme" set to "test-theme"
+ And I have an _includes directory
+ And I have an "_includes/in_project.html" file that contains "I'm in the project."
+ And I have an "index.html" page that contains "{% include in_project.html %} {% include include.html %}"
+ When I run jekyll build
+ Then I should get a zero exit status
+ And the _site directory should exist
+ And I should see "I'm in the project." in "_site/index.html"
+ And I should see "include.html from test-theme" in "_site/index.html"
+
+ Scenario: A theme with a layout
+ Given I have a configuration file with "theme" set to "test-theme"
+ And I have an _layouts directory
+ And I have an "_layouts/post.html" file that contains "post.html from the project: {{ content }}"
+ And I have an "index.html" page with layout "default" that contains "I'm content."
+ And I have a "post.html" page with layout "post" that contains "I'm more content."
+ When I run jekyll build
+ Then I should get a zero exit status
+ And the _site directory should exist
+ And I should see "default.html from test-theme: I'm content." in "_site/index.html"
+ And I should see "post.html from the project: I'm more content." in "_site/post.html"
+
+ Scenario: Complicated site that puts it all together
+ Given I have a configuration file with "theme" set to "test-theme"
+ And I have a _posts directory
+ And I have the following posts:
+ | title | date | layout | content |
+ | entry1 | 2016-04-21 | post | I am using a local layout. {% include include.html %} |
+ | entry2 | 2016-04-21 | default | I am using a themed layout. {% include include.html %} {% include in_project.html %} |
+ And I have a _layouts directory
+ And I have a "_layouts/post.html" page with layout "default" that contains "I am a post layout! {{ content }}"
+ And I have an _includes directory
+ And I have an "_includes/in_project.html" file that contains "I am in the project, not the theme."
+ When I run jekyll build
+ Then I should get a zero exit status
+ And the _site directory should exist
+ And I should see "I am in the project, not the theme." in "_site/2016/04/21/entry2.html"
+ And I should see "include.html from test-theme" in "_site/2016/04/21/entry2.html"
+ And I should see "default.html from test-theme:" in "_site/2016/04/21/entry2.html"
+ And I should see "I am using a themed layout." in "_site/2016/04/21/entry2.html"
+ And I should not see "I am a post layout!" in "_site/2016/04/21/entry2.html"
+ And I should not see "I am in the project, not the theme." in "_site/2016/04/21/entry1.html"
+ And I should see "include.html from test-theme" in "_site/2016/04/21/entry1.html"
+ And I should see "default.html from test-theme:" in "_site/2016/04/21/entry1.html"
+ And I should see "I am using a local layout." in "_site/2016/04/21/entry1.html"
+ And I should see "I am a post layout!" in "_site/2016/04/21/entry1.html"
diff --git a/lib/jekyll.rb b/lib/jekyll.rb
index 6dc42a92..8afdf06a 100644
--- a/lib/jekyll.rb
+++ b/lib/jekyll.rb
@@ -66,6 +66,7 @@ module Jekyll
autoload :Site, 'jekyll/site'
autoload :StaticFile, 'jekyll/static_file'
autoload :Stevenson, 'jekyll/stevenson'
+ autoload :Theme, 'jekyll/theme'
autoload :URL, 'jekyll/url'
autoload :Utils, 'jekyll/utils'
autoload :VERSION, 'jekyll/version'
diff --git a/lib/jekyll/convertible.rb b/lib/jekyll/convertible.rb
index 26c88fd6..a473b183 100644
--- a/lib/jekyll/convertible.rb
+++ b/lib/jekyll/convertible.rb
@@ -39,7 +39,7 @@ module Jekyll
filename = File.join(base, name)
begin
- self.content = File.read(site.in_source_dir(base, name),
+ self.content = File.read(@path || site.in_source_dir(base, name),
Utils.merged_file_read_opts(site, opts))
if content =~ Document::YAML_FRONT_MATTER_REGEXP
self.content = $POSTMATCH
@@ -215,9 +215,9 @@ module Jekyll
payload["layout"] = Utils.deep_merge_hashes(payload["layout"] || {}, layout.data)
self.output = render_liquid(layout.content,
- payload,
- info,
- File.join(site.config['layouts_dir'], layout.name))
+ payload,
+ info,
+ layout.relative_path)
# Add layout to dependency tree
site.regenerator.add_dependency(
diff --git a/lib/jekyll/layout.rb b/lib/jekyll/layout.rb
index c29f353f..72005277 100644
--- a/lib/jekyll/layout.rb
+++ b/lib/jekyll/layout.rb
@@ -29,7 +29,14 @@ module Jekyll
@site = site
@base = base
@name = name
- @path = site.in_source_dir(base, name)
+
+ if site.theme && site.theme.layouts_path.eql?(base)
+ @base_dir = site.theme.root
+ @path = site.in_theme_dir(base, name)
+ else
+ @base_dir = site.source
+ @path = site.in_source_dir(base, name)
+ end
self.data = {}
@@ -45,5 +52,13 @@ module Jekyll
def process(name)
self.ext = File.extname(name)
end
+
+ # The path to the layout, relative to the site source.
+ #
+ # Returns a String path which represents the relative path
+ # from the site source to this layout
+ def relative_path
+ @relative_path ||= Pathname.new(path).relative_path_from(Pathname.new(@base_dir)).to_s
+ end
end
end
diff --git a/lib/jekyll/page.rb b/lib/jekyll/page.rb
index 6c402ee3..2bdcdf6f 100644
--- a/lib/jekyll/page.rb
+++ b/lib/jekyll/page.rb
@@ -40,6 +40,7 @@ module Jekyll
@base = base
@dir = dir
@name = name
+ @path = site.in_source_dir(base, dir, name)
process(name)
read_yaml(File.join(base, dir), name)
diff --git a/lib/jekyll/readers/layout_reader.rb b/lib/jekyll/readers/layout_reader.rb
index f0620294..df8931cb 100644
--- a/lib/jekyll/readers/layout_reader.rb
+++ b/lib/jekyll/readers/layout_reader.rb
@@ -7,8 +7,12 @@ module Jekyll
end
def read
- layout_entries.each do |f|
- @layouts[layout_name(f)] = Layout.new(site, layout_directory, f)
+ layout_entries.each do |layout_file|
+ @layouts[layout_name(layout_file)] = Layout.new(site, layout_directory, layout_file)
+ end
+
+ theme_layout_entries.each do |layout_file|
+ @layouts[layout_name(layout_file)] ||= Layout.new(site, theme_layout_directory, layout_file)
end
@layouts
@@ -18,11 +22,23 @@ module Jekyll
@layout_directory ||= (layout_directory_in_cwd || layout_directory_inside_source)
end
+ def theme_layout_directory
+ @theme_layout_directory ||= site.theme.layouts_path if site.theme
+ end
+
private
def layout_entries
+ entries_in layout_directory
+ end
+
+ def theme_layout_entries
+ theme_layout_directory ? entries_in(theme_layout_directory) : []
+ end
+
+ def entries_in(dir)
entries = []
- within(layout_directory) do
+ within(dir) do
entries = EntryFilter.new(site).filter(Dir['**/*.*'])
end
entries
diff --git a/lib/jekyll/renderer.rb b/lib/jekyll/renderer.rb
index b158f421..f116cf60 100644
--- a/lib/jekyll/renderer.rb
+++ b/lib/jekyll/renderer.rb
@@ -145,7 +145,7 @@ module Jekyll
layout.content,
payload,
info,
- File.join(site.config['layouts_dir'], layout.name)
+ layout.relative_path
)
# Add layout to dependency tree
diff --git a/lib/jekyll/site.rb b/lib/jekyll/site.rb
index c4fe9f0c..9afdbde3 100644
--- a/lib/jekyll/site.rb
+++ b/lib/jekyll/site.rb
@@ -8,10 +8,10 @@ module Jekyll
:exclude, :include, :lsi, :highlighter, :permalink_style,
:time, :future, :unpublished, :safe, :plugins, :limit_posts,
:show_drafts, :keep_files, :baseurl, :data, :file_read_opts,
- :gems, :plugin_manager
+ :gems, :plugin_manager, :theme
attr_accessor :converters, :generators, :reader
- attr_reader :regenerator, :liquid_renderer
+ attr_reader :regenerator, :liquid_renderer, :includes_load_paths
# Public: Initialize a new Site.
#
@@ -52,6 +52,12 @@ module Jekyll
self.plugin_manager = Jekyll::PluginManager.new(self)
self.plugins = plugin_manager.plugins_path
+ self.theme = nil
+ self.theme = Jekyll::Theme.new(config["theme"]) if config["theme"]
+
+ @includes_load_paths = Array(in_source_dir(config["includes_dir"].to_s))
+ @includes_load_paths << theme.includes_path if self.theme
+
self.file_read_opts = {}
self.file_read_opts[:encoding] = config['encoding'] if config['encoding']
@@ -367,6 +373,19 @@ module Jekyll
end
end
+ # Public: Prefix a given path with the theme directory.
+ #
+ # paths - (optional) path elements to a file or directory within the
+ # theme directory
+ #
+ # Returns a path which is prefixed with the theme root directory.
+ def in_theme_dir(*paths)
+ return nil unless theme
+ paths.reduce(theme.root) do |base, path|
+ Jekyll.sanitized_path(base, path)
+ end
+ end
+
# Public: Prefix a given path with the destination directory.
#
# paths - (optional) path elements to a file or directory within the
diff --git a/lib/jekyll/tags/include.rb b/lib/jekyll/tags/include.rb
index 847e6638..d4f31f2a 100644
--- a/lib/jekyll/tags/include.rb
+++ b/lib/jekyll/tags/include.rb
@@ -12,8 +12,6 @@ module Jekyll
end
class IncludeTag < Liquid::Tag
- attr_reader :includes_dir
-
VALID_SYNTAX = /([\w-]+)\s*=\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+))/
VARIABLE_SYNTAX = /(?[^{]*(\{\{\s*[\w\-\.]+\s*(\|.*)?\}\}[^\s{}]*)+)(?.*)/
@@ -98,20 +96,29 @@ eos
end
end
- def tag_includes_dir(context)
- context.registers[:site].config['includes_dir'].freeze
+ def tag_includes_dirs(context)
+ context.registers[:site].includes_load_paths.freeze
+ end
+
+ def locate_include_file(context, file, safe)
+ includes_dirs = tag_includes_dirs(context)
+ includes_dirs.each do |dir|
+ path = File.join(dir, file)
+ return path if valid_include_file?(path, dir, safe)
+ end
+ raise IOError, "Could not locate the included file '#{file}' in any of #{includes_dirs}." \
+ " Ensure it exists in one of those directories and, if it is a symlink, " \
+ "does not point outside your site source."
end
def render(context)
site = context.registers[:site]
- @includes_dir = tag_includes_dir(context)
- dir = resolved_includes_dir(context)
file = render_variable(context) || @file
validate_file_name(file)
- path = File.join(dir, file)
- validate_path(path, dir, site.safe)
+ path = locate_include_file(context, file, site.safe)
+ return unless path
# Add include to dependency tree
if context.registers[:page] && context.registers[:page].key?("path")
@@ -121,16 +128,16 @@ eos
)
end
- begin
+ #begin
partial = load_cached_partial(path, context)
context.stack do
context['include'] = parse_params(context) if @params
partial.render!(context)
end
- rescue => e
- raise IncludeTagError.new e.message, File.join(@includes_dir, @file)
- end
+ #rescue => e
+ #raise IncludeTagError.new e.message, path
+ #end
end
def load_cached_partial(path, context)
@@ -144,24 +151,18 @@ eos
end
end
- def resolved_includes_dir(context)
- context.registers[:site].in_source_dir(@includes_dir)
+ def valid_include_file?(path, dir, safe)
+ !(outside_site_source?(path, dir, safe) || !File.exist?(path))
end
- def validate_path(path, dir, safe)
- if safe && !realpath_prefixed_with?(path, dir)
- raise IOError.new "The included file '#{path}' should exist and should not be a symlink"
- elsif !File.exist?(path)
- raise IOError.new "Included file '#{path_relative_to_source(dir, path)}' not found"
- end
- end
-
- def path_relative_to_source(dir, path)
- File.join(@includes_dir, path.sub(Regexp.new("^#{dir}"), ""))
+ def outside_site_source?(path, dir, safe)
+ safe && !realpath_prefixed_with?(path, dir)
end
def realpath_prefixed_with?(path, dir)
File.exist?(path) && File.realpath(path).start_with?(dir)
+ rescue
+ false
end
# This method allows to modify the file content by inheriting from the class.
@@ -171,16 +172,17 @@ eos
end
class IncludeRelativeTag < IncludeTag
- def tag_includes_dir(context)
- '.'.freeze
+ def tag_includes_dirs(context)
+ Array(page_path(context)).freeze
end
def page_path(context)
- context.registers[:page].nil? ? includes_dir : File.dirname(context.registers[:page]["path"])
- end
-
- def resolved_includes_dir(context)
- context.registers[:site].in_source_dir(page_path(context))
+ if context.registers[:page].nil?
+ context.registers[:site].source
+ else
+ current_doc_dir = File.dirname(context.registers[:page]["path"])
+ context.registers[:site].in_source_dir current_doc_dir
+ end
end
end
end
diff --git a/lib/jekyll/theme.rb b/lib/jekyll/theme.rb
new file mode 100644
index 00000000..ed9cc68d
--- /dev/null
+++ b/lib/jekyll/theme.rb
@@ -0,0 +1,56 @@
+module Jekyll
+ class Theme
+ extend Forwardable
+ attr_reader :name
+ def_delegator :gemspec, :version, :version
+
+ def initialize(name)
+ @name = name.downcase.strip
+ configure_sass
+ end
+
+ def root
+ @root ||= gemspec.full_gem_path
+ end
+
+ def includes_path
+ path_for :includes
+ end
+
+ def layouts_path
+ path_for :layouts
+ end
+
+ def sass_path
+ path_for :sass
+ end
+
+ def configure_sass
+ return unless sass_path
+ require 'sass'
+ Sass.load_paths << sass_path
+ end
+
+ private
+
+ def path_for(folder)
+ resolved_dir = realpath_for(folder)
+ return unless resolved_dir
+
+ path = Jekyll.sanitized_path(root, resolved_dir)
+ path if Dir.exists?(path)
+ end
+
+ def realpath_for(folder)
+ File.realpath(Jekyll.sanitized_path(root, "_#{folder}"))
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
+ nil
+ end
+
+ def gemspec
+ @gemspec ||= Gem::Specification.find_by_name(name)
+ rescue Gem::LoadError
+ raise Jekyll::Errors::MissingDependencyException, "The #{name} theme could not be found."
+ end
+ end
+end
diff --git a/rake/site.rake b/rake/site.rake
index f1af29ba..4d1a4c24 100644
--- a/rake/site.rake
+++ b/rake/site.rake
@@ -103,7 +103,7 @@ namespace :site do
desc "Create a nicely formatted history page for the jekyll site based on the repo history."
task :history do
- siteify_file('History.markdown')
+ siteify_file('History.markdown', { "title" => "History" })
end
desc "Copy the Code of Conduct"
diff --git a/site/_data/docs.yml b/site/_data/docs.yml
index 7cb85198..071b5a14 100644
--- a/site/_data/docs.yml
+++ b/site/_data/docs.yml
@@ -26,6 +26,7 @@
- permalinks
- pagination
- plugins
+ - themes
- extras
- title: Deployment
diff --git a/site/_docs/themes.md b/site/_docs/themes.md
new file mode 100644
index 00000000..2b7155bb
--- /dev/null
+++ b/site/_docs/themes.md
@@ -0,0 +1,89 @@
+---
+layout: docs
+title: Themes
+permalink: /docs/themes/
+---
+
+Jekyll has an extensive theme system, which allows you to leverage community-maintained templates and styles to customize your site's presentation. Jekyll themes package layouts, includes, and stylesheets in a way that can be overridden by your site's content.
+
+## Installing a theme
+
+1. To install a theme, first, add the theme to your site's `Gemfile`:
+
+ gem 'my-awesome-jekyll-theme'
+
+2. Save the changes to your `Gemfile`
+3. Run the command `bundle install` to install the theme
+4. Finally, activate the theme by adding the following to your site's `_config.yml`:
+
+ theme: my-awesome-jekyll-theme
+
+You can have multiple themes listed in your site's Gemfile, but only one theme can be selected in your site's `_config.yml`.
+{: .note .info }
+
+## Overriding theme defaults
+
+Jekyll themes set default layouts, includes, and stylesheets, that can be overridden by your site's content. For example, if your selected theme has a `page` layout, you can override the theme's layout by creating your own `page` layout in the `_layouts` folder (e.g., `_layouts/page.html`).
+
+Jekyll will look first to your site's content, before looking to the theme's defaults, for any requested file in the following folders:
+
+* `/_layouts`
+* `/_includes`
+* `/_sass`
+
+Refer to your selected theme's documentation and source repository for more information on what files you can override.
+{: .note .info}
+
+## Creating a theme
+
+Jekyll themes are distributed as Ruby gems. The only required file is the [Ruby Gemspec](http://guides.rubygems.org/specification-reference/). Here's an example of a minimal Gemspec for the `my-awesome-jekyll-theme` theme, saved as `/my-awsome-jekyll-theme.gemspec`:
+
+{% highlight ruby %}
+Gem::Specification.new do |s|
+ s.name = ''
+ s.version = '0.1.0'
+ s.license = 'MIT'
+ s.summary = ''
+ s.author = ''
+ s.email = ''
+ s.homepage = 'https://github.com/jekyll/my-awesome-jekyll-theme'
+ s.files = `git ls-files -z`.split("\x0").grep(%r{^_(sass|includes|layouts)/})
+end
+{% endhighlight %}
+
+### Layouts and includes
+
+Theme layouts and includes work just like they work in any Jekyll site. Place layouts in your theme's `/_layouts` folder, and place includes in your themes `/_includes` folder.
+
+For example, if your theme has a `/_layouts/page.html` file, and a page has `layout: page` in its YAML front matter, Jekyll will first look to the site's `_layouts` folder for a the `page` layout, and if none exists, will use your theme's `page` layout.
+
+### Stylesheets
+
+Your theme's stylesheets should be placed in your theme's `/_sass` folder, again, just as you would when authoring a Jekyll site. Your theme's styles can be included in the user's stylesheet using the `@import` directive.
+
+### Documenting your theme
+
+Your theme should include a `/README.md` file, which explains how site authors can install and use your theme. What layouts are included? What includes? Do they need to add anything special to their site's configuration file?
+
+### Adding a screenshot
+
+Themes are visual. Show users what your theme looks like by including a screenshot as `/screenshot.png` within your theme's repository where it can be retrieved programatically. You can also include this screenshot within your theme's documentation.
+
+### Previewing your theme
+
+To preview your theme as you're authoring it, it may be helpful to add dummy content in, for example, `/index.html` and `/page.html` files. This will allow you to use the `jekyll build` and `jekyll serve` commands to preview your theme, just as you'd preview a Jekyll site.
+
+If you do preview your theme locally, be sure to add `/_site` to your theme's `.gitignore` file to prevent the compiled site from also being included when you distribute your theme.
+{: .info .note}
+
+### Publishing your theme
+
+Themes are published via [RubyGems.org](https://rubygems.org). You'll need a RubyGems account, which you can [create for free](https://rubygems.org/sign_up).
+
+1. First, package your theme, by running the following command, replacing `my-awesome-jekyll-theme` with the name of your theme:
+
+ gem build my-awesome-jekyll-theme.gemspec
+
+2. Next, push your packaged theme up to the RubyGems service, by running the following command, again replacing `my-awesome-jekyll-theme` with the name of your theme:
+
+ gem push my-awesome-jekyll-theme-*.gem
diff --git a/test/fixtures/test-theme/_includes/include.html b/test/fixtures/test-theme/_includes/include.html
new file mode 100644
index 00000000..98608392
--- /dev/null
+++ b/test/fixtures/test-theme/_includes/include.html
@@ -0,0 +1 @@
+include.html from test-theme
diff --git a/test/fixtures/test-theme/_layouts/default.html b/test/fixtures/test-theme/_layouts/default.html
new file mode 100644
index 00000000..902c61c3
--- /dev/null
+++ b/test/fixtures/test-theme/_layouts/default.html
@@ -0,0 +1 @@
+default.html from test-theme: {{ content }}
diff --git a/test/fixtures/test-theme/_sass/style.scss b/test/fixtures/test-theme/_sass/style.scss
new file mode 100644
index 00000000..a1e07da6
--- /dev/null
+++ b/test/fixtures/test-theme/_sass/style.scss
@@ -0,0 +1,3 @@
+.sample {
+ color: black;
+}
diff --git a/test/fixtures/test-theme/_symlink b/test/fixtures/test-theme/_symlink
new file mode 120000
index 00000000..d2d6aac5
--- /dev/null
+++ b/test/fixtures/test-theme/_symlink
@@ -0,0 +1 @@
+_layouts
\ No newline at end of file
diff --git a/test/fixtures/test-theme/test-theme.gemspec b/test/fixtures/test-theme/test-theme.gemspec
new file mode 100644
index 00000000..5f950ae1
--- /dev/null
+++ b/test/fixtures/test-theme/test-theme.gemspec
@@ -0,0 +1,9 @@
+Gem::Specification.new do |s|
+ s.name = 'test-theme'
+ s.version = '0.1.0'
+ s.licenses = ['MIT']
+ s.summary = "This is a theme used to test Jekyll"
+ s.authors = ["Jekyll"]
+ s.files = ["lib/example.rb"]
+ s.homepage = 'https://github.com/jekyll/jekyll'
+end
diff --git a/test/test_tags.rb b/test/test_tags.rb
index ee7b5074..1dab0feb 100644
--- a/test/test_tags.rb
+++ b/test/test_tags.rb
@@ -616,7 +616,7 @@ title: Include symlink
CONTENT
create_post(content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true, 'safe' => true })
end
- assert_match(/should exist and should not be a symlink/, ex.message)
+ assert_match "Could not locate the included file 'tmp/pages-test-does-not-exist' in any of [\"#{source_dir}/_includes\"].", ex.message
end
end
@@ -757,7 +757,7 @@ CONTENT
exception = assert_raises IOError do
create_post(@content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true})
end
- assert_equal 'Included file \'_includes/missing.html\' not found', exception.message
+ assert_match "Could not locate the included file 'missing.html' in any of [\"#{source_dir}/_includes\"].", exception.message
end
end
@@ -839,7 +839,7 @@ CONTENT
exception = assert_raises IOError do
create_post(@content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true})
end
- assert_equal 'Included file \'./missing.html\' not found', exception.message
+ assert_match "Could not locate the included file 'missing.html' in any of [\"#{source_dir}\"].", exception.message
end
end
@@ -894,7 +894,7 @@ title: Include symlink
CONTENT
create_post(content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true, 'safe' => true })
end
- assert_match(/should exist and should not be a symlink/, ex.message)
+ assert_match /Ensure it exists in one of those directories and, if it is a symlink, does not point outside your site source./, ex.message
end
end
end
diff --git a/test/test_theme.rb b/test/test_theme.rb
new file mode 100644
index 00000000..9767946e
--- /dev/null
+++ b/test/test_theme.rb
@@ -0,0 +1,65 @@
+require 'helper'
+
+class TestTheme < JekyllUnitTest
+ def setup
+ @theme = Theme.new('test-theme')
+ @expected_root = File.expand_path "./fixtures/test-theme", File.dirname(__FILE__)
+ end
+
+ context "initializing" do
+ should "normalize the theme name" do
+ theme = Theme.new(' Test-Theme ')
+ assert_equal "test-theme", theme.name
+ end
+
+ should "know the theme root" do
+ assert_equal @expected_root, @theme.root
+ end
+
+ should "know the theme version" do
+ assert_equal Gem::Version.new("0.1.0"), @theme.version
+ end
+
+ should "raise an error for invalid themes" do
+ assert_raises Jekyll::Errors::MissingDependencyException do
+ Theme.new("foo").version
+ end
+ end
+
+ should "add itself to sass's load path" do
+ @theme.configure_sass
+ assert Sass.load_paths.include?(@theme.sass_path), "Sass load paths should include the theme sass dir"
+ end
+ end
+
+ context "path generation" do
+ [:layouts, :includes, :sass].each do |folder|
+ should "know the #{folder} path" do
+ expected = File.expand_path("_#{folder}", @expected_root)
+ assert_equal expected, @theme.public_send("#{folder}_path")
+ end
+ end
+
+ should "generate folder paths" do
+ expected = File.expand_path("./_sass", @expected_root)
+ assert_equal expected, @theme.send(:path_for, :sass)
+ end
+
+ should "not allow paths outside of the theme root" do
+ assert_equal nil, @theme.send(:path_for, "../../source")
+ end
+
+ should "return nil for paths that don't exist" do
+ assert_equal nil, @theme.send(:path_for, "foo")
+ end
+
+ should "return the resolved path when a symlink & resolved path exists" do
+ expected = File.expand_path("./_layouts", @expected_root)
+ assert_equal expected, @theme.send(:path_for, :symlink)
+ end
+ end
+
+ should "retrieve the gemspec" do
+ assert_equal "test-theme-0.1.0", @theme.send(:gemspec).full_name
+ end
+end