Permalink
Browse files

Added config includes to solve #514 and #703

This is origonaly form this checkin.
xenji/jekyll@2961201

I have only tried to cherry pick it on top of the current source
tree
including the corresponding tests and fixtures.

You can now use a YAML list to add configuration files to be included
in the order of the list.

The syntax supports globs (to be used with `Dir[pattern]`), relative
paths to the config value of `source` and absolute paths.

Using this sample config:
```
additional_configs:
  - additional_configs/_config_a.yml
  - additional_configs/_config_b.yml
```
or the glob version:
```
additional_configs:
  - additional_configs/_config_*.yml
```
the new deep merge order is in both cases:
`Jekyll::DEFAULTS < _config < _config_a < _config_b`

I've added additional tests for three use cases:
* Having one inclusion
* Having a glob inclusion
* Having one inclusion with override in the main config

Conflicts:
	lib/jekyll/configuration.rb
	test/test_configuration.rb
  • Loading branch information...
Alex Kessinger
Alex Kessinger committed Mar 29, 2013
1 parent 6c6c576 commit e38e4b27a407ea5428a0476408a606a176035795
View
@@ -27,6 +27,7 @@ def require_all(path)
require 'pygments'
# internal requires
+require 'jekyll/configuration'
require 'jekyll/core_ext'
require 'jekyll/site'
require 'jekyll/convertible'
@@ -54,68 +55,6 @@ def require_all(path)
module Jekyll
VERSION = '1.0.0.beta2'
- # 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',
-
- '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.
#
@@ -125,34 +64,7 @@ module Jekyll
#
# 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_file = override.delete('config')
- config_file = File.join(source, "_config.yml") if config_file.to_s.empty?
-
- begin
- config = YAML.safe_load_file(config_file)
- raise "Configuration file: (INVALID) #{config_file}" if !config.is_a?(Hash)
- $stdout.puts "Configuration file: #{config_file}"
- 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
-
- # Merge DEFAULTS < _config.yml < override
- Jekyll::DEFAULTS.deep_merge(config).deep_merge(override)
+ config = Configuration.new
+ config.configuration(override)
end
end
View
@@ -0,0 +1,155 @@
+module Jekyll
+ # Default options. Overriden by values in _config.yml or command-line opts.
+ # 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',
+
+ '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
+ }
+ }
+
+ class Configuration
+
+ def initialize
+ # Keeps the config for further use
+ @merged_config_hash = {}.deep_merge(Jekyll::DEFAULTS)
+
+ # Remember all parsed paths to prevent circular inclusion
+ @parsed_paths = []
+ end
+
+ def 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']
+
+ # Might not be the best idea, needs discussion.
+ @merged_config_hash['source'] = source
+
+
+ # Get configuration from <source>/_config.yml or <source>/<config_file>
+ config_file = override.delete('config')
+ config_file = File.join(source, "_config.yml") if config_file.to_s.empty?
+ self.read_and_merge(config_file)
+
+ # finally merge merged_config and override
+ @merged_config_hash = @merged_config_hash.deep_merge(override)
+ end
+
+ # Reads a configuration from a file and merges it into the instance variable.
+ #
+ # @param [String] config_file
+ # @return [Hash]
+ def read_and_merge(config_file)
+ begin
+ config = YAML.safe_load_file(config_file)
+ raise "Configuration file: (INVALID) #{config_file}" if !config.is_a?(Hash)
+ $stdout.puts "Configuration file: #{config_file}"
+ @merged_config_hash = @merged_config_hash.deep_merge(config)
+
+ if config.has_key? 'additional_configs' and config['additional_configs'].is_a? Array
+ self.process_inclusions(config['additional_configs'], config_file)
+ 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
+
+ end
+
+ # Expands a glob to an array of full qualified paths to each config file.
+ #
+ # @param [String] pattern
+ # @return [Array]
+ def glob_to_file_list(pattern, basedir = '')
+ basedir = @merged_config_hash['source'] unless (pattern[0] == '/' or basedir != '')
+ Dir[File.join(basedir, pattern)].sort
+ end
+
+ # Processes included files.
+ # the parent_file is just as a debug helper for detecting and solving circular references.
+ #
+ # @param [Array] list_of_inclusions
+ # @param [String] parent_file
+ def process_inclusions(list_of_inclusions, parent_file = '')
+ # This is a bit tricky: We want to prevent infinite inclusion loops.
+ # If we add it to the "parsed_path" list right at the top, we do not reflect the true state of the program.
+ # On the other hand, we cannot add it afterwards as the recursion could already happen in the path itself.
+ # Any better ideas than to add it at the start of the processing?
+ list_of_inclusions.each do |path_or_glob|
+ paths = self.glob_to_file_list(path_or_glob)
+ paths.each do |path|
+ if @parsed_paths.include? path
+ $stderr.puts "WARNING: Circular inclusion detected for #{path} in #{parent_file}. Skipping it."
+ next
+ end
+ @parsed_paths << path
+ self.read_and_merge(path)
+ end
+ end
+ end
+ end
+end
@@ -274,4 +274,7 @@ kramdown:
coderay_bold_every: 10
coderay_css: style
+additional_configs:
+ - _configs/*.yml
+
{% endhighlight %}
@@ -0,0 +1,10 @@
+permalink: /blog/:year/:month/:day/:title/index.html
+destination: public
+plugins: plugins
+code_dir: downloads/code
+category_dir: blog/categories
+markdown: rdiscount
+pygments: false # default python pygments have been replaced by pygments.rb
+
+additional_configs:
+ - additional_configs/_config_a.yml
@@ -0,0 +1,10 @@
+permalink: /blog/:year/:month/:day/:title/index.html
+destination: public
+plugins: plugins
+code_dir: downloads/code
+category_dir: blog/categories
+markdown: rdiscount
+pygments: false # default python pygments have been replaced by pygments.rb
+
+additional_configs:
+ - additional_configs/_config_*.yml
@@ -0,0 +1,11 @@
+root: /foobar
+permalink: /blog/:year/:month/:day/:title/index.html
+destination: public
+plugins: plugins
+code_dir: downloads/code
+category_dir: blog/categories
+markdown: rdiscount
+pygments: false # default python pygments have been replaced by pygments.rb
+
+additional_configs:
+ - additional_configs/_config_a.yml
View
@@ -25,6 +25,7 @@ class TestConfiguration < Test::Unit::TestCase
assert_equal Jekyll::DEFAULTS, Jekyll.configuration({})
end
end
+
context "loading config from external file" do
setup do
@paths = {
@@ -50,6 +51,34 @@ class TestConfiguration < Test::Unit::TestCase
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] })
+
+ end
+ end
+
+ context "loading configuration with includes" do
+ setup do
+ @replacement_source_dir = File.expand_path('../fixtures/configuration', __FILE__)
+ end
+
+ should "include config_a when loading the main config file with one inclusion" do
+ mock($stdout).puts("Configuration file: " + @replacement_source_dir + "/one_inclusion/_config.yml")
+ mock($stdout).puts("Configuration file: " + @replacement_source_dir + "/one_inclusion/additional_configs/_config_a.yml")
+ config = Jekyll.configuration({"source" => File.join(@replacement_source_dir, 'one_inclusion')})
+ assert_equal "/foobar", config["root"]
+ end
+ should "include config_a when loading the main config file with glob inclusions" do
+ mock($stdout).puts("Configuration file: " + @replacement_source_dir + "/pattern_include/_config.yml")
+ mock($stdout).puts("Configuration file: " + @replacement_source_dir + "/pattern_include/additional_configs/_config_a.yml")
+ mock($stdout).puts("Configuration file: " + @replacement_source_dir + "/pattern_include/additional_configs/_config_b.yml")
+ config = Jekyll.configuration({"source" => File.join(@replacement_source_dir, 'pattern_include')})
+ assert_equal "/buzz", config["root"]
+ end
+ should "include config_a when loading the main config file with one inclusion, letting the included config overrule them all" do
+ mock($stdout).puts("Configuration file: " + @replacement_source_dir + "/root_config_override/_config.yml")
+ mock($stdout).puts("Configuration file: " + @replacement_source_dir + "/root_config_override/additional_configs/_config_a.yml")
+ config = Jekyll.configuration({"source" => File.join(@replacement_source_dir, 'root_config_override')})
+ assert_equal "/overruled", config["root"]
+
end
end
end

0 comments on commit e38e4b2

Please sign in to comment.