Merge pull request #953 from mojombo/refactor-configuration

Refactor Jekyll Configuration
This commit is contained in:
Parker Moore 2013-04-13 07:19:03 -07:00
commit 891ccbd656
10 changed files with 251 additions and 142 deletions

View File

@ -28,6 +28,7 @@ require 'pygments'
# internal requires
require 'jekyll/core_ext'
require 'jekyll/configuration'
require 'jekyll/site'
require 'jekyll/convertible'
require 'jekyll/layout'
@ -54,122 +55,20 @@ SafeYAML::OPTIONS[:suppress_warnings] = true
module Jekyll
VERSION = '1.0.0.beta4'
# Default options. Overriden by values in _config.yml.
# Strings rather than symbols are used for compatability with YAML.
DEFAULTS = {
'source' => Dir.pwd,
'destination' => File.join(Dir.pwd, '_site'),
'plugins' => '_plugins',
'layouts' => '_layouts',
'keep_files' => ['.git','.svn'],
'future' => true, # remove and make true just default
'pygments' => true, # remove and make true just default
'markdown' => 'maruku',
'permalink' => 'date',
'baseurl' => '/',
'include' => ['.htaccess'],
'paginate_path' => 'page:num',
'markdown_ext' => 'markdown,mkd,mkdn,md',
'textile_ext' => 'textile',
'port' => '4000',
'host' => '0.0.0.0',
'excerpt_separator' => "\n\n",
'maruku' => {
'use_tex' => false,
'use_divs' => false,
'png_engine' => 'blahtex',
'png_dir' => 'images/latex',
'png_url' => '/images/latex'
},
'rdiscount' => {
'extensions' => []
},
'redcarpet' => {
'extensions' => []
},
'kramdown' => {
'auto_ids' => true,
'footnote_nr' => 1,
'entity_output' => 'as_char',
'toc_levels' => '1..6',
'smart_quotes' => 'lsquo,rsquo,ldquo,rdquo',
'use_coderay' => false,
'coderay' => {
'coderay_wrap' => 'div',
'coderay_line_numbers' => 'inline',
'coderay_line_number_start' => 1,
'coderay_tab_width' => 4,
'coderay_bold_every' => 10,
'coderay_css' => 'style'
}
},
'redcloth' => {
'hard_breaks' => true
}
}
# Public: Generate a Jekyll configuration Hash by merging the default
# options with anything in _config.yml, and adding the given options on top.
#
# override - A Hash of config directives that override any options in both
# the defaults and the config file. See Jekyll::DEFAULTS for a
# the defaults and the config file. See Jekyll::Configuration::DEFAULTS for a
# list of option names and their defaults.
#
# Returns the final configuration Hash.
def self.configuration(override)
# Convert any symbol keys to strings and remove the old key/values
override = override.reduce({}) { |hsh,(k,v)| hsh.merge(k.to_s => v) }
# _config.yml may override default source location, but until
# then, we need to know where to look for _config.yml
source = override['source'] || Jekyll::DEFAULTS['source']
# Get configuration from <source>/_config.yml or <source>/<config_file>
config_files = override.delete('config')
config_files = File.join(source, "_config.yml") if config_files.to_s.empty?
unless config_files.is_a? Array
config_files = [config_files]
end
begin
config = {}
config_files.each do |config_file|
next_config = YAML.safe_load_file(config_file)
raise "Configuration file: (INVALID) #{config_file}" if !next_config.is_a?(Hash)
$stdout.puts "Configuration file: #{config_file}"
config = config.deep_merge(next_config)
end
rescue SystemCallError
# Errno:ENOENT = file not found
$stderr.puts "Configuration file: none"
config = {}
rescue => err
$stderr.puts " " +
"WARNING: Error reading configuration. " +
"Using defaults (and options)."
$stderr.puts "#{err}"
config = {}
end
# Provide backwards-compatibility
if config['auto']
$stderr.puts "Deprecation: ".rjust(20) + "'auto' has been changed to " +
"'watch'. Please update your configuration to use 'watch'."
config['watch'] = config['auto']
end
config = Configuration[Configuration::DEFAULTS]
override = Configuration[override].stringify_keys
config = config.read_config_files(config.config_files(override))
# Merge DEFAULTS < _config.yml < override
Jekyll::DEFAULTS.deep_merge(config).deep_merge(override)
config.deep_merge(override).stringify_keys
end
end

154
lib/jekyll/configuration.rb Normal file
View File

@ -0,0 +1,154 @@
module Jekyll
class Configuration < Hash
# Default options. Overriden by values in _config.yml.
# Strings rather than symbols are used for compatability with YAML.
DEFAULTS = {
'source' => Dir.pwd,
'destination' => File.join(Dir.pwd, '_site'),
'plugins' => '_plugins',
'layouts' => '_layouts',
'keep_files' => ['.git','.svn'],
'future' => true, # remove and make true just default
'pygments' => true, # remove and make true just default
'markdown' => 'maruku',
'permalink' => 'date',
'baseurl' => '/',
'include' => ['.htaccess'],
'paginate_path' => 'page:num',
'markdown_ext' => 'markdown,mkd,mkdn,md',
'textile_ext' => 'textile',
'port' => '4000',
'host' => '0.0.0.0',
'excerpt_separator' => "\n\n",
'maruku' => {
'use_tex' => false,
'use_divs' => false,
'png_engine' => 'blahtex',
'png_dir' => 'images/latex',
'png_url' => '/images/latex'
},
'rdiscount' => {
'extensions' => []
},
'redcarpet' => {
'extensions' => []
},
'kramdown' => {
'auto_ids' => true,
'footnote_nr' => 1,
'entity_output' => 'as_char',
'toc_levels' => '1..6',
'smart_quotes' => 'lsquo,rsquo,ldquo,rdquo',
'use_coderay' => false,
'coderay' => {
'coderay_wrap' => 'div',
'coderay_line_numbers' => 'inline',
'coderay_line_number_start' => 1,
'coderay_tab_width' => 4,
'coderay_bold_every' => 10,
'coderay_css' => 'style'
}
},
'redcloth' => {
'hard_breaks' => true
}
}
# Public: Turn all keys into string
#
# Return a copy of the hash where all its keys are strings
def stringify_keys
reduce({}) { |hsh,(k,v)| hsh.merge(k.to_s => v) }
end
# Public: Directory of the Jekyll source folder
#
# override - the command-line options hash
#
# Returns the path to the Jekyll source directory
def source(override)
override['source'] || self['source'] || DEFAULTS['source']
end
# Public: Generate list of configuration files from the override
#
# override - the command-line options hash
#
# Returns an Array of config files
def config_files(override)
# Get configuration from <source>/_config.yml or <source>/<config_file>
config_files = override.delete('config')
config_files = File.join(source(override), "_config.yml") if config_files.to_s.empty?
config_files = [config_files] unless config_files.is_a? Array
config_files
end
# Public: Read configuration and return merged Hash
#
# file - the path to the YAML file to be read in
#
# Returns this configuration, overridden by the values in the file
def read_config_file(file)
configuration = dup
next_config = YAML.safe_load_file(file)
raise "Configuration file: (INVALID) #{file}" if !next_config.is_a?(Hash)
$stdout.puts "Configuration file: #{file}"
configuration.deep_merge(next_config)
end
# Public: Read in a list of configuration files and merge with this hash
#
# files - the list of configuration file paths
#
# Returns the full configuration, with the defaults overridden by the values in the
# configuration files
def read_config_files(files)
configuration = dup
begin
files.each do |config_file|
configuration = read_config_file(config_file)
end
rescue SystemCallError
# Errno:ENOENT = file not found
$stderr.puts "Configuration file: none"
rescue => err
$stderr.puts " " +
"WARNING: Error reading configuration. " +
"Using defaults (and options)."
$stderr.puts "#{err}"
end
configuration.backwards_compatibilize
end
# Public: Ensure the proper options are set in the configuration to allow for
# backwards-compatibility with Jekyll pre-1.0
#
# Returns the backwards-compatible configuration
def backwards_compatibilize
config = dup
# Provide backwards-compatibility
if config['auto']
$stderr.puts "Deprecation: ".rjust(20) + "'auto' has been changed to " +
"'watch'. Please update your configuration to use 'watch'."
config['watch'] = config['auto']
end
config
end
end
end

View File

@ -97,7 +97,7 @@ module Jekyll
#
# Returns an Array of plugin search paths
def plugins_path
if (config['plugins'] == Jekyll::DEFAULTS['plugins'])
if (config['plugins'] == Jekyll::Configuration::DEFAULTS['plugins'])
[File.join(self.source, config['plugins'])]
else
Array(config['plugins']).map { |d| File.expand_path(d) }

View File

@ -1,6 +1,62 @@
require 'helper'
class TestConfiguration < Test::Unit::TestCase
context "#stringify_keys" do
setup do
@mixed_keys = Configuration[{
'markdown' => 'maruku',
:permalink => 'date',
'baseurl' => '/',
:include => ['.htaccess'],
:source => './'
}]
@string_keys = Configuration[{
'markdown' => 'maruku',
'permalink' => 'date',
'baseurl' => '/',
'include' => ['.htaccess'],
'source' => './'
}]
end
should "stringify symbol keys" do
assert_equal @string_keys, @mixed_keys.stringify_keys
end
should "not mess with keys already strings" do
assert_equal @string_keys, @string_keys.stringify_keys
end
end
context "#config_files" do
setup do
@config = Configuration[{"source" => source_dir}]
@no_override = {}
@one_config_file = {"config" => "config.yml"}
@multiple_files = {"config" => %w[config/site.yml config/deploy.yml configuration.yml]}
end
should "always return an array" do
assert @config.config_files(@no_override).is_a?(Array)
assert @config.config_files(@one_config_file).is_a?(Array)
assert @config.config_files(@multiple_files).is_a?(Array)
end
should "return the default config path if no config files are specified" do
assert_equal [File.join(source_dir, "_config.yml")], @config.config_files(@no_override)
end
should "return the config if given one config file" do
assert_equal %w[config.yml], @config.config_files(@one_config_file)
end
should "return an array of the config files if given many config files" do
assert_equal %w[config/site.yml config/deploy.yml configuration.yml], @config.config_files(@multiple_files)
end
end
context "#backwards_compatibilize" do
setup do
@config = Configuration[{"auto" => true}]
end
should "create 'watch' key for 'auto'" do
assert @config.backwards_compatibilize.has_key? "watch"
assert_equal true, @config.backwards_compatibilize["watch"]
end
end
context "loading configuration" do
setup do
@path = File.join(Dir.pwd, '_config.yml')
@ -9,20 +65,20 @@ class TestConfiguration < Test::Unit::TestCase
should "fire warning with no _config.yml" do
mock(YAML).safe_load_file(@path) { raise SystemCallError, "No such file or directory - #{@path}" }
mock($stderr).puts("Configuration file: none")
assert_equal Jekyll::DEFAULTS, Jekyll.configuration({})
assert_equal Jekyll::Configuration::DEFAULTS, Jekyll.configuration({})
end
should "load configuration as hash" do
mock(YAML).safe_load_file(@path) { Hash.new }
mock($stdout).puts("Configuration file: #{@path}")
assert_equal Jekyll::DEFAULTS, Jekyll.configuration({})
assert_equal Jekyll::Configuration::DEFAULTS, Jekyll.configuration({})
end
should "fire warning with bad config" do
mock(YAML).safe_load_file(@path) { Array.new }
mock($stderr).puts(" WARNING: Error reading configuration. Using defaults (and options).")
mock($stderr).puts("Configuration file: (INVALID) #{@path}")
assert_equal Jekyll::DEFAULTS, Jekyll.configuration({})
assert_equal Jekyll::Configuration::DEFAULTS, Jekyll.configuration({})
end
end
context "loading config from external file" do
@ -37,19 +93,19 @@ class TestConfiguration < Test::Unit::TestCase
should "load default config if no config_file is set" do
mock(YAML).safe_load_file(@paths[:default]) { Hash.new }
mock($stdout).puts("Configuration file: #{@paths[:default]}")
assert_equal Jekyll::DEFAULTS, Jekyll.configuration({})
assert_equal Jekyll::Configuration::DEFAULTS, Jekyll.configuration({})
end
should "load different config if specified" do
mock(YAML).safe_load_file(@paths[:other]) { {"baseurl" => "http://wahoo.dev"} }
mock($stdout).puts("Configuration file: #{@paths[:other]}")
assert_equal Jekyll::DEFAULTS.deep_merge({ "baseurl" => "http://wahoo.dev" }), Jekyll.configuration({ "config" => @paths[:other] })
assert_equal Jekyll::Configuration::DEFAULTS.deep_merge({ "baseurl" => "http://wahoo.dev" }), Jekyll.configuration({ "config" => @paths[:other] })
end
should "load default config if path passed is empty" do
mock(YAML).safe_load_file(@paths[:default]) { Hash.new }
mock($stdout).puts("Configuration file: #{@paths[:default]}")
assert_equal Jekyll::DEFAULTS, Jekyll.configuration({ "config" => @paths[:empty] })
assert_equal Jekyll::Configuration::DEFAULTS, Jekyll.configuration({ "config" => @paths[:empty] })
end
should "load multiple config files" do
@ -57,7 +113,7 @@ class TestConfiguration < Test::Unit::TestCase
mock(YAML).safe_load_file(@paths[:other]) { Hash.new }
mock($stdout).puts("Configuration file: #{@paths[:default]}")
mock($stdout).puts("Configuration file: #{@paths[:other]}")
assert_equal Jekyll::DEFAULTS, Jekyll.configuration({ "config" => [@paths[:default], @paths[:other]] })
assert_equal Jekyll::Configuration::DEFAULTS, Jekyll.configuration({ "config" => [@paths[:default], @paths[:other]] })
end
should "load multiple config files and last config should win" do
@ -65,7 +121,7 @@ class TestConfiguration < Test::Unit::TestCase
mock(YAML).safe_load_file(@paths[:other]) { {"baseurl" => "http://wahoo.dev"} }
mock($stdout).puts("Configuration file: #{@paths[:default]}")
mock($stdout).puts("Configuration file: #{@paths[:other]}")
assert_equal Jekyll::DEFAULTS.deep_merge({ "baseurl" => "http://wahoo.dev" }), Jekyll.configuration({ "config" => [@paths[:default], @paths[:other]] })
assert_equal Jekyll::Configuration::DEFAULTS.deep_merge({ "baseurl" => "http://wahoo.dev" }), Jekyll.configuration({ "config" => [@paths[:default], @paths[:other]] })
end
end
end

View File

@ -5,7 +5,7 @@ class TestGeneratedSite < Test::Unit::TestCase
setup do
clear_dest
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir})
Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir})
end
@site = Site.new(Jekyll.configuration)
@ -46,7 +46,7 @@ class TestGeneratedSite < Test::Unit::TestCase
setup do
clear_dest
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'limit_posts' => 5})
Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'limit_posts' => 5})
end
@site = Site.new(Jekyll.configuration)
@ -62,7 +62,7 @@ class TestGeneratedSite < Test::Unit::TestCase
assert_raise ArgumentError do
clear_dest
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'limit_posts' => 0})
Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'limit_posts' => 0})
end
@site = Site.new(Jekyll.configuration)

View File

@ -15,7 +15,7 @@ class TestPage < Test::Unit::TestCase
context "A Page" do
setup do
clear_dest
stub(Jekyll).configuration { Jekyll::DEFAULTS }
stub(Jekyll).configuration { Jekyll::Configuration::DEFAULTS }
@site = Site.new(Jekyll.configuration)
end

View File

@ -14,7 +14,7 @@ class TestPager < Test::Unit::TestCase
context "pagination disabled" do
setup do
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({
Jekyll::Configuration::DEFAULTS.merge({
'source' => source_dir,
'destination' => dest_dir
})
@ -31,7 +31,7 @@ class TestPager < Test::Unit::TestCase
context "pagination enabled for 2" do
setup do
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({
Jekyll::Configuration::DEFAULTS.merge({
'source' => source_dir,
'destination' => dest_dir,
'paginate' => 2

View File

@ -13,7 +13,7 @@ class TestPost < Test::Unit::TestCase
context "A Post" do
setup do
clear_dest
stub(Jekyll).configuration { Jekyll::DEFAULTS }
stub(Jekyll).configuration { Jekyll::Configuration::DEFAULTS }
@site = Site.new(Jekyll.configuration)
end
@ -344,7 +344,7 @@ class TestPost < Test::Unit::TestCase
context "when in a site" do
setup do
clear_dest
stub(Jekyll).configuration { Jekyll::DEFAULTS }
stub(Jekyll).configuration { Jekyll::Configuration::DEFAULTS }
@site = Site.new(Jekyll.configuration)
@site.posts = [setup_post('2008-02-02-published.textile'),
setup_post('2009-01-27-categories.textile')]
@ -537,7 +537,7 @@ class TestPost < Test::Unit::TestCase
context "converter file extension settings" do
setup do
stub(Jekyll).configuration { Jekyll::DEFAULTS }
stub(Jekyll).configuration { Jekyll::Configuration::DEFAULTS }
@site = Site.new(Jekyll.configuration)
end

View File

@ -3,49 +3,49 @@ require 'helper'
class TestSite < Test::Unit::TestCase
context "configuring sites" do
should "have an array for plugins by default" do
site = Site.new(Jekyll::DEFAULTS)
site = Site.new(Jekyll::Configuration::DEFAULTS)
assert_equal [File.join(Dir.pwd, '_plugins')], site.plugins
end
should "look for plugins under the site directory by default" do
site = Site.new(Jekyll::DEFAULTS.merge({'source' => File.expand_path(source_dir)}))
site = Site.new(Jekyll::Configuration::DEFAULTS.merge({'source' => File.expand_path(source_dir)}))
assert_equal [File.join(source_dir, '_plugins')], site.plugins
end
should "have an array for plugins if passed as a string" do
site = Site.new(Jekyll::DEFAULTS.merge({'plugins' => '/tmp/plugins'}))
site = Site.new(Jekyll::Configuration::DEFAULTS.merge({'plugins' => '/tmp/plugins'}))
assert_equal ['/tmp/plugins'], site.plugins
end
should "have an array for plugins if passed as an array" do
site = Site.new(Jekyll::DEFAULTS.merge({'plugins' => ['/tmp/plugins', '/tmp/otherplugins']}))
site = Site.new(Jekyll::Configuration::DEFAULTS.merge({'plugins' => ['/tmp/plugins', '/tmp/otherplugins']}))
assert_equal ['/tmp/plugins', '/tmp/otherplugins'], site.plugins
end
should "have an empty array for plugins if nothing is passed" do
site = Site.new(Jekyll::DEFAULTS.merge({'plugins' => []}))
site = Site.new(Jekyll::Configuration::DEFAULTS.merge({'plugins' => []}))
assert_equal [], site.plugins
end
should "have an empty array for plugins if nil is passed" do
site = Site.new(Jekyll::DEFAULTS.merge({'plugins' => nil}))
site = Site.new(Jekyll::Configuration::DEFAULTS.merge({'plugins' => nil}))
assert_equal [], site.plugins
end
should "expose default baseurl" do
site = Site.new(Jekyll::DEFAULTS)
assert_equal Jekyll::DEFAULTS['baseurl'], site.baseurl
site = Site.new(Jekyll::Configuration::DEFAULTS)
assert_equal Jekyll::Configuration::DEFAULTS['baseurl'], site.baseurl
end
should "expose baseurl passed in from config" do
site = Site.new(Jekyll::DEFAULTS.merge({'baseurl' => '/blog'}))
site = Site.new(Jekyll::Configuration::DEFAULTS.merge({'baseurl' => '/blog'}))
assert_equal '/blog', site.baseurl
end
end
context "creating sites" do
setup do
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir})
Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir})
end
@site = Site.new(Jekyll.configuration)
end
@ -205,7 +205,7 @@ class TestSite < Test::Unit::TestCase
should "filter symlink entries when safe mode enabled" do
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'safe' => true})
Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'safe' => true})
end
site = Site.new(Jekyll.configuration)
stub(File).symlink?('symlink.js') {true}
@ -221,7 +221,7 @@ class TestSite < Test::Unit::TestCase
should "not include symlinks in safe mode" do
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'safe' => true})
Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'safe' => true})
end
site = Site.new(Jekyll.configuration)
@ -232,7 +232,7 @@ class TestSite < Test::Unit::TestCase
should "include symlinks in unsafe mode" do
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'safe' => false})
Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'safe' => false})
end
site = Site.new(Jekyll.configuration)
@ -244,7 +244,7 @@ class TestSite < Test::Unit::TestCase
context 'error handling' do
should "raise if destination is included in source" do
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => source_dir})
Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => source_dir})
end
assert_raise Jekyll::FatalException do
@ -254,7 +254,7 @@ class TestSite < Test::Unit::TestCase
should "raise if destination is source" do
stub(Jekyll).configuration do
Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => File.join(source_dir, "..")})
Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => File.join(source_dir, "..")})
end
assert_raise Jekyll::FatalException do
@ -303,7 +303,7 @@ class TestSite < Test::Unit::TestCase
end
should 'remove orphaned files in destination - keep_files .svn' do
config = Jekyll::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'keep_files' => ['.svn']})
config = Jekyll::Configuration::DEFAULTS.merge({'source' => source_dir, 'destination' => dest_dir, 'keep_files' => ['.svn']})
@site = Site.new(config)
@site.process
assert !File.exist?(dest_dir('.htpasswd'))

View File

@ -6,7 +6,7 @@ class TestTags < Test::Unit::TestCase
def create_post(content, override = {}, converter_class = Jekyll::Converters::Markdown)
stub(Jekyll).configuration do
Jekyll::DEFAULTS.deep_merge({'pygments' => true}).deep_merge(override)
Jekyll::Configuration::DEFAULTS.deep_merge({'pygments' => true}).deep_merge(override)
end
site = Site.new(Jekyll.configuration)