Merge pull request #4595 from jekyll/themes

Merge pull request 4595
This commit is contained in:
jekyllbot 2016-04-21 17:18:59 -07:00
commit 30f2bdff5b
22 changed files with 390 additions and 47 deletions

View File

@ -25,6 +25,7 @@ env:
branches: branches:
only: only:
- master - master
- themes
notifications: notifications:
irc: irc:

View File

@ -22,6 +22,7 @@ group :test do
gem "rspec-mocks" gem "rspec-mocks"
gem "nokogiri" gem "nokogiri"
gem "rspec" gem "rspec"
gem "test-theme", path: File.expand_path("./test/fixtures/test-theme", File.dirname(__FILE__))
end end
# #

61
features/theme.feature Normal file
View File

@ -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 "<span class=\"sample\">include.html from test-theme</span>" 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 "<span class=\"sample\">include.html from test-theme</span>" 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 "<span class=\"sample\">include.html from test-theme</span>" 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"

View File

@ -66,6 +66,7 @@ module Jekyll
autoload :Site, 'jekyll/site' autoload :Site, 'jekyll/site'
autoload :StaticFile, 'jekyll/static_file' autoload :StaticFile, 'jekyll/static_file'
autoload :Stevenson, 'jekyll/stevenson' autoload :Stevenson, 'jekyll/stevenson'
autoload :Theme, 'jekyll/theme'
autoload :URL, 'jekyll/url' autoload :URL, 'jekyll/url'
autoload :Utils, 'jekyll/utils' autoload :Utils, 'jekyll/utils'
autoload :VERSION, 'jekyll/version' autoload :VERSION, 'jekyll/version'

View File

@ -39,7 +39,7 @@ module Jekyll
filename = File.join(base, name) filename = File.join(base, name)
begin 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)) Utils.merged_file_read_opts(site, opts))
if content =~ Document::YAML_FRONT_MATTER_REGEXP if content =~ Document::YAML_FRONT_MATTER_REGEXP
self.content = $POSTMATCH self.content = $POSTMATCH
@ -215,9 +215,9 @@ module Jekyll
payload["layout"] = Utils.deep_merge_hashes(payload["layout"] || {}, layout.data) payload["layout"] = Utils.deep_merge_hashes(payload["layout"] || {}, layout.data)
self.output = render_liquid(layout.content, self.output = render_liquid(layout.content,
payload, payload,
info, info,
File.join(site.config['layouts_dir'], layout.name)) layout.relative_path)
# Add layout to dependency tree # Add layout to dependency tree
site.regenerator.add_dependency( site.regenerator.add_dependency(

View File

@ -29,7 +29,14 @@ module Jekyll
@site = site @site = site
@base = base @base = base
@name = name @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 = {} self.data = {}
@ -45,5 +52,13 @@ module Jekyll
def process(name) def process(name)
self.ext = File.extname(name) self.ext = File.extname(name)
end 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
end end

View File

@ -40,6 +40,7 @@ module Jekyll
@base = base @base = base
@dir = dir @dir = dir
@name = name @name = name
@path = site.in_source_dir(base, dir, name)
process(name) process(name)
read_yaml(File.join(base, dir), name) read_yaml(File.join(base, dir), name)

View File

@ -7,8 +7,12 @@ module Jekyll
end end
def read def read
layout_entries.each do |f| layout_entries.each do |layout_file|
@layouts[layout_name(f)] = Layout.new(site, layout_directory, f) @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 end
@layouts @layouts
@ -18,11 +22,23 @@ module Jekyll
@layout_directory ||= (layout_directory_in_cwd || layout_directory_inside_source) @layout_directory ||= (layout_directory_in_cwd || layout_directory_inside_source)
end end
def theme_layout_directory
@theme_layout_directory ||= site.theme.layouts_path if site.theme
end
private private
def layout_entries 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 = [] entries = []
within(layout_directory) do within(dir) do
entries = EntryFilter.new(site).filter(Dir['**/*.*']) entries = EntryFilter.new(site).filter(Dir['**/*.*'])
end end
entries entries

View File

@ -145,7 +145,7 @@ module Jekyll
layout.content, layout.content,
payload, payload,
info, info,
File.join(site.config['layouts_dir'], layout.name) layout.relative_path
) )
# Add layout to dependency tree # Add layout to dependency tree

View File

@ -8,10 +8,10 @@ module Jekyll
:exclude, :include, :lsi, :highlighter, :permalink_style, :exclude, :include, :lsi, :highlighter, :permalink_style,
:time, :future, :unpublished, :safe, :plugins, :limit_posts, :time, :future, :unpublished, :safe, :plugins, :limit_posts,
:show_drafts, :keep_files, :baseurl, :data, :file_read_opts, :show_drafts, :keep_files, :baseurl, :data, :file_read_opts,
:gems, :plugin_manager :gems, :plugin_manager, :theme
attr_accessor :converters, :generators, :reader attr_accessor :converters, :generators, :reader
attr_reader :regenerator, :liquid_renderer attr_reader :regenerator, :liquid_renderer, :includes_load_paths
# Public: Initialize a new Site. # Public: Initialize a new Site.
# #
@ -52,6 +52,12 @@ module Jekyll
self.plugin_manager = Jekyll::PluginManager.new(self) self.plugin_manager = Jekyll::PluginManager.new(self)
self.plugins = plugin_manager.plugins_path 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 = {}
self.file_read_opts[:encoding] = config['encoding'] if config['encoding'] self.file_read_opts[:encoding] = config['encoding'] if config['encoding']
@ -367,6 +373,19 @@ module Jekyll
end end
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. # Public: Prefix a given path with the destination directory.
# #
# paths - (optional) path elements to a file or directory within the # paths - (optional) path elements to a file or directory within the

View File

@ -12,8 +12,6 @@ module Jekyll
end end
class IncludeTag < Liquid::Tag class IncludeTag < Liquid::Tag
attr_reader :includes_dir
VALID_SYNTAX = /([\w-]+)\s*=\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+))/ VALID_SYNTAX = /([\w-]+)\s*=\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+))/
VARIABLE_SYNTAX = /(?<variable>[^{]*(\{\{\s*[\w\-\.]+\s*(\|.*)?\}\}[^\s{}]*)+)(?<params>.*)/ VARIABLE_SYNTAX = /(?<variable>[^{]*(\{\{\s*[\w\-\.]+\s*(\|.*)?\}\}[^\s{}]*)+)(?<params>.*)/
@ -98,20 +96,29 @@ eos
end end
end end
def tag_includes_dir(context) def tag_includes_dirs(context)
context.registers[:site].config['includes_dir'].freeze 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 end
def render(context) def render(context)
site = context.registers[:site] site = context.registers[:site]
@includes_dir = tag_includes_dir(context)
dir = resolved_includes_dir(context)
file = render_variable(context) || @file file = render_variable(context) || @file
validate_file_name(file) validate_file_name(file)
path = File.join(dir, file) path = locate_include_file(context, file, site.safe)
validate_path(path, dir, site.safe) return unless path
# Add include to dependency tree # Add include to dependency tree
if context.registers[:page] && context.registers[:page].key?("path") if context.registers[:page] && context.registers[:page].key?("path")
@ -121,16 +128,16 @@ eos
) )
end end
begin #begin
partial = load_cached_partial(path, context) partial = load_cached_partial(path, context)
context.stack do context.stack do
context['include'] = parse_params(context) if @params context['include'] = parse_params(context) if @params
partial.render!(context) partial.render!(context)
end end
rescue => e #rescue => e
raise IncludeTagError.new e.message, File.join(@includes_dir, @file) #raise IncludeTagError.new e.message, path
end #end
end end
def load_cached_partial(path, context) def load_cached_partial(path, context)
@ -144,24 +151,18 @@ eos
end end
end end
def resolved_includes_dir(context) def valid_include_file?(path, dir, safe)
context.registers[:site].in_source_dir(@includes_dir) !(outside_site_source?(path, dir, safe) || !File.exist?(path))
end end
def validate_path(path, dir, safe) def outside_site_source?(path, dir, safe)
if safe && !realpath_prefixed_with?(path, dir) 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}"), ""))
end end
def realpath_prefixed_with?(path, dir) def realpath_prefixed_with?(path, dir)
File.exist?(path) && File.realpath(path).start_with?(dir) File.exist?(path) && File.realpath(path).start_with?(dir)
rescue
false
end end
# This method allows to modify the file content by inheriting from the class. # This method allows to modify the file content by inheriting from the class.
@ -171,16 +172,17 @@ eos
end end
class IncludeRelativeTag < IncludeTag class IncludeRelativeTag < IncludeTag
def tag_includes_dir(context) def tag_includes_dirs(context)
'.'.freeze Array(page_path(context)).freeze
end end
def page_path(context) def page_path(context)
context.registers[:page].nil? ? includes_dir : File.dirname(context.registers[:page]["path"]) if context.registers[:page].nil?
end context.registers[:site].source
else
def resolved_includes_dir(context) current_doc_dir = File.dirname(context.registers[:page]["path"])
context.registers[:site].in_source_dir(page_path(context)) context.registers[:site].in_source_dir current_doc_dir
end
end end
end end
end end

56
lib/jekyll/theme.rb Normal file
View File

@ -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

View File

@ -103,7 +103,7 @@ namespace :site do
desc "Create a nicely formatted history page for the jekyll site based on the repo history." desc "Create a nicely formatted history page for the jekyll site based on the repo history."
task :history do task :history do
siteify_file('History.markdown') siteify_file('History.markdown', { "title" => "History" })
end end
desc "Copy the Code of Conduct" desc "Copy the Code of Conduct"

View File

@ -26,6 +26,7 @@
- permalinks - permalinks
- pagination - pagination
- plugins - plugins
- themes
- extras - extras
- title: Deployment - title: Deployment

89
site/_docs/themes.md Normal file
View File

@ -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 = '<THEME TITLE>'
s.version = '0.1.0'
s.license = 'MIT'
s.summary = '<THEME DESCRIPTION>'
s.author = '<YOUR NAME>'
s.email = '<YOUR 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

View File

@ -0,0 +1 @@
<span class="sample">include.html from test-theme</span>

View File

@ -0,0 +1 @@
default.html from test-theme: {{ content }}

View File

@ -0,0 +1,3 @@
.sample {
color: black;
}

1
test/fixtures/test-theme/_symlink vendored Symbolic link
View File

@ -0,0 +1 @@
_layouts

View File

@ -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

View File

@ -616,7 +616,7 @@ title: Include symlink
CONTENT CONTENT
create_post(content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true, 'safe' => true }) create_post(content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true, 'safe' => true })
end 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
end end
@ -757,7 +757,7 @@ CONTENT
exception = assert_raises IOError do exception = assert_raises IOError do
create_post(@content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true}) create_post(@content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true})
end 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
end end
@ -839,7 +839,7 @@ CONTENT
exception = assert_raises IOError do exception = assert_raises IOError do
create_post(@content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true}) create_post(@content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true})
end 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
end end
@ -894,7 +894,7 @@ title: Include symlink
CONTENT CONTENT
create_post(content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true, 'safe' => true }) create_post(content, {'permalink' => 'pretty', 'source' => source_dir, 'destination' => dest_dir, 'read_posts' => true, 'safe' => true })
end 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 end
end end

65
test/test_theme.rb Normal file
View File

@ -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