diff --git a/.rubocop.yml b/.rubocop.yml index 4189be7b..198cb1e6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,51 +7,28 @@ AllCops: - lib/jekyll/collection.rb - lib/jekyll/command.rb - lib/jekyll/configuration.rb - - lib/jekyll/converter.rb - lib/jekyll/converters/identity.rb - - lib/jekyll/converters/markdown - lib/jekyll/converters/markdown/kramdown_parser.rb - - lib/jekyll/converters/markdown/rdiscount_parser.rb - lib/jekyll/converters/markdown/redcarpet_parser.rb - lib/jekyll/converters/markdown.rb - - lib/jekyll/converters/smartypants.rb - lib/jekyll/convertible.rb - lib/jekyll/deprecator.rb - lib/jekyll/document.rb - - lib/jekyll/drops/collection_drop.rb - - lib/jekyll/drops/document_drop.rb - - lib/jekyll/drops/drop.rb - - lib/jekyll/drops/jekyll_drop.rb - lib/jekyll/drops/site_drop.rb - - lib/jekyll/drops/unified_payload_drop.rb - - lib/jekyll/drops/url_drop.rb - lib/jekyll/entry_filter.rb - - lib/jekyll/errors.rb - - lib/jekyll/excerpt.rb - - lib/jekyll/external.rb - lib/jekyll/filters.rb - lib/jekyll/frontmatter_defaults.rb - - lib/jekyll/generator.rb - - lib/jekyll/hooks.rb - lib/jekyll/layout.rb - - lib/jekyll/liquid_extensions.rb - - lib/jekyll/liquid_renderer/file.rb - lib/jekyll/liquid_renderer/table.rb - lib/jekyll/liquid_renderer.rb - - lib/jekyll/log_adapter.rb - lib/jekyll/page.rb - - lib/jekyll/plugin.rb - lib/jekyll/plugin_manager.rb - - lib/jekyll/publisher.rb - lib/jekyll/reader.rb - - lib/jekyll/readers/collection_reader.rb - - lib/jekyll/readers/data_reader.rb - lib/jekyll/readers/layout_reader.rb - lib/jekyll/readers/page_reader.rb - lib/jekyll/readers/post_reader.rb - lib/jekyll/readers/static_file_reader.rb - lib/jekyll/regenerator.rb - - lib/jekyll/related_posts.rb - lib/jekyll/renderer.rb - lib/jekyll/site.rb - lib/jekyll/static_file.rb diff --git a/History.markdown b/History.markdown index 8bf86749..4d6f779b 100644 --- a/History.markdown +++ b/History.markdown @@ -43,6 +43,13 @@ * 3.2.x/master: Fix defaults for Documents (posts/collection docs) (#4808) * Don't rescue LoadError or bundler load errors for Bundler. (#4857) +### Forward Ports + + * From v3.1.4: Add ExcerptDrop and remove excerpt's ability to refer to itself in Liquid (#4941) + * From v3.1.4: Configuration permalink fix and addition of Configuration.from and sorting `site.collections` by label (#4942) + * From v3.1.4: Fix `{{ layout }}` oddities (proper inheritance & fixing overflow of old data) (#4943) + * From v3.1.5: Sort the results of the `require_all` glob (#4944) + ### Development Fixes * Add project maintainer profile links (#4591) @@ -60,6 +67,7 @@ * Update `jekyll/commands*` to pass rubocop rules (#4888) * Clean up many test files to pass Rubocop rules (#4902) * Rubocop cleanup for some utils and further test files (#4916) + * Rubocop: Low hanging fruit (#4936) ### Site Enhancements diff --git a/features/layout_data.feature b/features/layout_data.feature index 78998665..c2af5b70 100644 --- a/features/layout_data.feature +++ b/features/layout_data.feature @@ -35,3 +35,36 @@ Feature: Layout data When I run jekyll build Then the "_site/index.html" file should exist And I should see "page content\n foo: my custom data" in "_site/index.html" + + Scenario: Inherit custom layout data and clear when not present + Given I have a _layouts directory + And I have a "_layouts/default.html" file with content: + """ + --- + bar: i'm default + --- + {{ content }} foo: '{{ layout.foo }}' bar: '{{ layout.bar }}' + """ + And I have a "_layouts/special.html" file with content: + """ + --- + layout: default + foo: my special data + bar: im special + --- + {{ content }} + """ + And I have a "_layouts/page.html" file with content: + """ + --- + layout: default + bar: im page + --- + {{ content }} + """ + And I have an "index.html" page with layout "special" that contains "page content" + And I have an "jekyll.html" page with layout "page" that contains "page content" + When I run jekyll build + Then the "_site/index.html" file should exist + And I should see "page content\n foo: 'my special data' bar: 'im special'" in "_site/index.html" + And I should see "page content\n foo: '' bar: 'im page'" in "_site/jekyll.html" diff --git a/lib/jekyll.rb b/lib/jekyll.rb index e9f4547b..676f6e71 100644 --- a/lib/jekyll.rb +++ b/lib/jekyll.rb @@ -7,7 +7,7 @@ $LOAD_PATH.unshift File.dirname(__FILE__) # For use/testing when no gem is insta # Returns nothing. def require_all(path) glob = File.join(File.dirname(__FILE__), path, '*.rb') - Dir[glob].each do |f| + Dir[glob].sort.each do |f| require f end end @@ -98,18 +98,16 @@ module Jekyll # list of option names and their defaults. # # Returns the final configuration Hash. - def configuration(override = {}) - config = Configuration[Configuration::DEFAULTS] - override = Configuration[override].stringify_keys + def configuration(override = Hash.new) + config = Configuration.new unless override.delete('skip_config_files') config = config.read_config_files(config.config_files(override)) end # Merge DEFAULTS < _config.yml < override - config = Utils.deep_merge_hashes(config, override).stringify_keys - set_timezone(config['timezone']) if config['timezone'] - - config + Configuration.from(Utils.deep_merge_hashes(config, override)).tap do |config| + set_timezone(config['timezone']) if config['timezone'] + end end # Public: Set the TZ environment variable to use the timezone specified @@ -173,6 +171,7 @@ module Jekyll end require "jekyll/drops/drop" +require "jekyll/drops/document_drop" require_all 'jekyll/commands' require_all 'jekyll/converters' require_all 'jekyll/converters/markdown' diff --git a/lib/jekyll/configuration.rb b/lib/jekyll/configuration.rb index 87bfeb46..a88272cc 100644 --- a/lib/jekyll/configuration.rb +++ b/lib/jekyll/configuration.rb @@ -72,7 +72,24 @@ module Jekyll 'hard_wrap' => false, 'footnote_nr' => 1 } - }] + }.map { |k, v| [k, v.freeze] }].freeze + + class << self + # Static: Produce a Configuration ready for use in a Site. + # It takes the input, fills in the defaults where values do not + # exist, and patches common issues including migrating options for + # backwards compatiblity. Except where a key or value is being fixed, + # the user configuration will override the defaults. + # + # user_config - a Hash or Configuration of overrides. + # + # Returns a Configuration filled with defaults and fixed for common + # problems and backwards-compatibility. + def from(user_config) + Utils.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys). + fix_common_issues.add_default_collections + end + end # Public: Turn all keys into string # @@ -169,6 +186,7 @@ module Jekyll begin files.each do |config_file| + next if config_file.nil? or config_file.empty? new_config = read_config_file(config_file) configuration = Utils.deep_merge_hashes(configuration, new_config) end @@ -228,7 +246,6 @@ module Jekyll end %w(include exclude).each do |option| - config[option] ||= [] if config[option].is_a?(String) Jekyll::Deprecator.deprecation_message "The '#{option}' configuration option" \ " must now be specified as an array, but you specified" \ @@ -236,7 +253,7 @@ module Jekyll " as a list of comma-separated values." config[option] = csv_to_array(config[option]) end - config[option].map!(&:to_s) + config[option].map!(&:to_s) if config[option] end if (config['kramdown'] || {}).key?('use_coderay') @@ -271,14 +288,22 @@ module Jekyll def add_default_collections config = clone + # It defaults to `{}`, so this is only if someone sets it to null manually. return config if config['collections'].nil? + # Ensure we have a hash. if config['collections'].is_a?(Array) config['collections'] = Hash[config['collections'].map { |c| [c, {}] }] end - config['collections']['posts'] ||= {} - config['collections']['posts']['output'] = true - config['collections']['posts']['permalink'] = style_to_permalink(config['permalink']) + + config['collections'] = Utils.deep_merge_hashes( + { 'posts' => {} }, config['collections'] + ).tap do |collections| + collections['posts']['output'] = true + if config['permalink'] + collections['posts']['permalink'] ||= style_to_permalink(config['permalink']) + end + end config end diff --git a/lib/jekyll/converters/markdown/rdiscount_parser.rb b/lib/jekyll/converters/markdown/rdiscount_parser.rb index daf8874e..f1679e79 100644 --- a/lib/jekyll/converters/markdown/rdiscount_parser.rb +++ b/lib/jekyll/converters/markdown/rdiscount_parser.rb @@ -5,14 +5,14 @@ module Jekyll def initialize(config) Jekyll::External.require_with_graceful_fail "rdiscount" @config = config - @rdiscount_extensions = @config['rdiscount']['extensions'].map(&:to_sym) + @rdiscount_extensions = @config["rdiscount"]["extensions"].map(&:to_sym) end def convert(content) rd = RDiscount.new(content, *@rdiscount_extensions) html = rd.to_html - if @config['rdiscount']['toc_token'] - html = replace_generated_toc(rd, html, @config['rdiscount']['toc_token']) + if @config["rdiscount"]["toc_token"] + html = replace_generated_toc(rd, html, @config["rdiscount"]["toc_token"]) end html end @@ -21,7 +21,7 @@ module Jekyll def replace_generated_toc(rd, html, toc_token) if rd.generate_toc && html.include?(toc_token) utf8_toc = rd.toc_content - utf8_toc.force_encoding('utf-8') if utf8_toc.respond_to?(:force_encoding) + utf8_toc.force_encoding("utf-8") if utf8_toc.respond_to?(:force_encoding) html.gsub(toc_token, utf8_toc) else html diff --git a/lib/jekyll/convertible.rb b/lib/jekyll/convertible.rb index a473b183..a5d35607 100644 --- a/lib/jekyll/convertible.rb +++ b/lib/jekyll/convertible.rb @@ -209,10 +209,13 @@ module Jekyll used = Set.new([layout]) + # Reset the payload layout data to ensure it starts fresh for each page. + payload["layout"] = nil + while layout Jekyll.logger.debug "Rendering Layout:", path payload["content"] = output - payload["layout"] = Utils.deep_merge_hashes(payload["layout"] || {}, layout.data) + payload["layout"] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {}) self.output = render_liquid(layout.content, payload, diff --git a/lib/jekyll/drops/document_drop.rb b/lib/jekyll/drops/document_drop.rb index b8a0d3e5..10e069ac 100644 --- a/lib/jekyll/drops/document_drop.rb +++ b/lib/jekyll/drops/document_drop.rb @@ -19,13 +19,13 @@ module Jekyll end def excerpt - fallback_data['excerpt'].to_s + fallback_data["excerpt"].to_s end def <=>(other) return nil unless other.is_a? DocumentDrop - cmp = self['date'] <=> other['date'] - cmp = self['path'] <=> other['path'] if cmp.nil? || cmp == 0 + cmp = self["date"] <=> other["date"] + cmp = self["path"] <=> other["path"] if cmp.nil? || cmp == 0 cmp end diff --git a/lib/jekyll/drops/drop.rb b/lib/jekyll/drops/drop.rb index 23ebeb4b..dcabb190 100644 --- a/lib/jekyll/drops/drop.rb +++ b/lib/jekyll/drops/drop.rb @@ -5,7 +5,7 @@ module Jekyll class Drop < Liquid::Drop include Enumerable - NON_CONTENT_METHODS = [:[], :[]=, :inspect, :to_h, :fallback_data, :collapse_document].freeze + NON_CONTENT_METHODS = [:fallback_data, :collapse_document].freeze # Get or set whether the drop class is mutable. # Mutability determines whether or not pre-defined fields may be @@ -15,11 +15,11 @@ module Jekyll # # Returns the mutability of the class def self.mutable(is_mutable = nil) - if is_mutable - @is_mutable = is_mutable - else - @is_mutable = false - end + @is_mutable = if is_mutable + is_mutable + else + false + end end def self.mutable? @@ -88,7 +88,7 @@ module Jekyll # Returns an Array of strings which represent method-specific keys. def content_methods @content_methods ||= ( - self.class.instance_methods(false) - NON_CONTENT_METHODS + self.class.instance_methods - Jekyll::Drops::Drop.instance_methods - NON_CONTENT_METHODS ).map(&:to_s).reject do |method| method.end_with?("=") end @@ -136,7 +136,7 @@ module Jekyll # # Returns a pretty generation of the hash representation of the Drop. def inspect - require 'json' + require "json" JSON.pretty_generate to_h end @@ -165,6 +165,12 @@ module Jekyll keys.each(&block) end + def each(&block) + each_key.each do |key| + yield key, self[key] + end + end + def merge(other, &block) self.dup.tap do |me| if block.nil? diff --git a/lib/jekyll/drops/excerpt_drop.rb b/lib/jekyll/drops/excerpt_drop.rb new file mode 100644 index 00000000..5d61aeb1 --- /dev/null +++ b/lib/jekyll/drops/excerpt_drop.rb @@ -0,0 +1,15 @@ +# encoding: UTF-8 + +module Jekyll + module Drops + class ExcerptDrop < DocumentDrop + def layout + @obj.doc.data["layout"] + end + + def excerpt + nil + end + end + end +end diff --git a/lib/jekyll/drops/site_drop.rb b/lib/jekyll/drops/site_drop.rb index 4d07ebe8..7b083126 100644 --- a/lib/jekyll/drops/site_drop.rb +++ b/lib/jekyll/drops/site_drop.rb @@ -28,7 +28,7 @@ module Jekyll end def collections - @site_collections ||= @obj.collections.values.map(&:to_liquid) + @site_collections ||= @obj.collections.values.sort_by(&:label).map(&:to_liquid) end private diff --git a/lib/jekyll/drops/url_drop.rb b/lib/jekyll/drops/url_drop.rb index 2815edf7..04e48a20 100644 --- a/lib/jekyll/drops/url_drop.rb +++ b/lib/jekyll/drops/url_drop.rb @@ -19,20 +19,20 @@ module Jekyll end def title - Utils.slugify(@obj.data['slug'], :mode => "pretty", :cased => true) || + Utils.slugify(@obj.data["slug"], :mode => "pretty", :cased => true) || Utils.slugify(@obj.basename_without_ext, :mode => "pretty", :cased => true) end def slug - Utils.slugify(@obj.data['slug']) || Utils.slugify(@obj.basename_without_ext) + Utils.slugify(@obj.data["slug"]) || Utils.slugify(@obj.basename_without_ext) end def categories category_set = Set.new - Array(@obj.data['categories']).each do |category| + Array(@obj.data["categories"]).each do |category| category_set << category.to_s.downcase end - category_set.to_a.join('/') + category_set.to_a.join("/") end def year diff --git a/lib/jekyll/excerpt.rb b/lib/jekyll/excerpt.rb index f5884d68..8fef70ed 100644 --- a/lib/jekyll/excerpt.rb +++ b/lib/jekyll/excerpt.rb @@ -7,7 +7,7 @@ module Jekyll attr_writer :output def_delegators :@doc, :site, :name, :ext, :relative_path, :extname, - :render_with_liquid?, :collection, :related_posts + :render_with_liquid?, :collection, :related_posts, :url # Initialize this Excerpt instance. # @@ -59,10 +59,7 @@ module Jekyll end def to_liquid - doc.data['excerpt'] = nil - @to_liquid ||= doc.to_liquid - doc.data['excerpt'] = self - @to_liquid + Jekyll::Drops::ExcerptDrop.new(self) end # Returns the shorthand String identifier of this doc. diff --git a/lib/jekyll/external.rb b/lib/jekyll/external.rb index 69b8d1eb..e84d0857 100644 --- a/lib/jekyll/external.rb +++ b/lib/jekyll/external.rb @@ -17,13 +17,13 @@ module Jekyll # # names - a string gem name or array of gem names # - def require_if_present(names, &block) + def require_if_present(names) Array(names).each do |name| begin require name rescue LoadError Jekyll.logger.debug "Couldn't load #{name}. Skipping." - block.call(name) if block + yield(name) if block_given? false end end @@ -39,7 +39,7 @@ module Jekyll def require_with_graceful_fail(names) Array(names).each do |name| begin - Jekyll.logger.debug "Requiring:", "#{name}" + Jekyll.logger.debug "Requiring:", name.to_s require name rescue LoadError => e Jekyll.logger.error "Dependency Error:", <<-MSG @@ -50,7 +50,7 @@ The full error message from Ruby is: '#{e.message}' If you run into trouble, you can find helpful resources at http://jekyllrb.com/help/! MSG - raise Jekyll::Errors::MissingDependencyException.new(name) + raise Jekyll::Errors::MissingDependencyException, name end end end diff --git a/lib/jekyll/hooks.rb b/lib/jekyll/hooks.rb index 6d8fcd8d..c9a4f794 100644 --- a/lib/jekyll/hooks.rb +++ b/lib/jekyll/hooks.rb @@ -4,38 +4,38 @@ module Jekyll # compatibility layer for octopress-hooks users PRIORITY_MAP = { - :low => 10, + :low => 10, :normal => 20, - :high => 30 + :high => 30 }.freeze # initial empty hooks @registry = { - :site => { - :after_init => [], + :site => { + :after_init => [], :after_reset => [], - :post_read => [], - :pre_render => [], + :post_read => [], + :pre_render => [], :post_render => [], - :post_write => [] + :post_write => [] }, - :pages => { - :post_init => [], - :pre_render => [], + :pages => { + :post_init => [], + :pre_render => [], :post_render => [], - :post_write => [] + :post_write => [] }, - :posts => { - :post_init => [], - :pre_render => [], + :posts => { + :post_init => [], + :pre_render => [], :post_render => [], - :post_write => [] + :post_write => [] }, :documents => { - :post_init => [], - :pre_render => [], + :post_init => [], + :pre_render => [], :post_render => [], - :post_write => [] + :post_write => [] } } @@ -61,10 +61,10 @@ module Jekyll # register a single hook to be called later, internal API def self.register_one(owner, event, priority, &block) @registry[owner] ||={ - :post_init => [], - :pre_render => [], + :post_init => [], + :pre_render => [], :post_render => [], - :post_write => [] + :post_write => [] } unless @registry[owner][event] diff --git a/lib/jekyll/liquid_renderer/file.rb b/lib/jekyll/liquid_renderer/file.rb index 7dcca1b6..63686d8f 100644 --- a/lib/jekyll/liquid_renderer/file.rb +++ b/lib/jekyll/liquid_renderer/file.rb @@ -8,7 +8,7 @@ module Jekyll def parse(content) measure_time do - @template = Liquid::Template.parse(content, line_numbers: true) + @template = Liquid::Template.parse(content, :line_numbers => true) end self diff --git a/lib/jekyll/log_adapter.rb b/lib/jekyll/log_adapter.rb index df0cfdc4..8ac67451 100644 --- a/lib/jekyll/log_adapter.rb +++ b/lib/jekyll/log_adapter.rb @@ -7,7 +7,7 @@ module Jekyll :info => ::Logger::INFO, :warn => ::Logger::WARN, :error => ::Logger::ERROR - } + }.freeze # Public: Create a new instance of a log writer # @@ -98,7 +98,7 @@ module Jekyll # # Returns the formatted message def message(topic, message) - msg = formatted_topic(topic) + message.to_s.gsub(/\s+/, ' ') + msg = formatted_topic(topic) + message.to_s.gsub(/\s+/, " ") messages << msg msg end diff --git a/lib/jekyll/plugin.rb b/lib/jekyll/plugin.rb index 1fb91058..bcc1bf7e 100644 --- a/lib/jekyll/plugin.rb +++ b/lib/jekyll/plugin.rb @@ -1,12 +1,12 @@ module Jekyll class Plugin PRIORITIES = { - :low => -10, + :low => -10, :highest => 100, - :lowest => -100, - :normal => 0, - :high => 10 - } + :lowest => -100, + :normal => 0, + :high => 10 + }.freeze # diff --git a/lib/jekyll/plugin_manager.rb b/lib/jekyll/plugin_manager.rb index 45eeaa79..5acd0e46 100644 --- a/lib/jekyll/plugin_manager.rb +++ b/lib/jekyll/plugin_manager.rb @@ -76,7 +76,7 @@ module Jekyll # # Returns an Array of plugin search paths def plugins_path - if site.config['plugins_dir'] == Jekyll::Configuration::DEFAULTS['plugins_dir'] + if site.config['plugins_dir'].eql? Jekyll::Configuration::DEFAULTS['plugins_dir'] [site.in_source_dir(site.config['plugins_dir'])] else Array(site.config['plugins_dir']).map { |d| File.expand_path(d) } diff --git a/lib/jekyll/publisher.rb b/lib/jekyll/publisher.rb index cc739627..0b67b8c6 100644 --- a/lib/jekyll/publisher.rb +++ b/lib/jekyll/publisher.rb @@ -15,7 +15,7 @@ module Jekyll private def can_be_published?(thing) - thing.data.fetch('published', true) || @site.unpublished + thing.data.fetch("published", true) || @site.unpublished end end end diff --git a/lib/jekyll/readers/data_reader.rb b/lib/jekyll/readers/data_reader.rb index 796c0d29..e7e46bdc 100644 --- a/lib/jekyll/readers/data_reader.rb +++ b/lib/jekyll/readers/data_reader.rb @@ -30,14 +30,14 @@ module Jekyll return unless File.directory?(dir) && !@entry_filter.symlink?(dir) entries = Dir.chdir(dir) do - Dir['*.{yaml,yml,json,csv}'] + Dir['*'].select { |fn| File.directory?(fn) } + Dir["*.{yaml,yml,json,csv}"] + Dir["*"].select { |fn| File.directory?(fn) } end entries.each do |entry| path = @site.in_source_dir(dir, entry) next if @entry_filter.symlink?(path) - key = sanitize_filename(File.basename(entry, '.*')) + key = sanitize_filename(File.basename(entry, ".*")) if File.directory?(path) read_data_to(path, data[key] = {}) else @@ -51,20 +51,20 @@ module Jekyll # Returns the contents of the data file. def read_data_file(path) case File.extname(path).downcase - when '.csv' + when ".csv" CSV.read(path, { - :headers => true, - :encoding => site.config['encoding'] - }).map(&:to_hash) + :headers => true, + :encoding => site.config["encoding"] + }).map(&:to_hash) else SafeYAML.load_file(path) end end def sanitize_filename(name) - name.gsub!(/[^\w\s-]+/, '') + name.gsub!(/[^\w\s-]+/, "") name.gsub!(/(^|\b\s)\s+($|\s?\b)/, '\\1\\2') - name.gsub(/\s+/, '_') + name.gsub(/\s+/, "_") end end end diff --git a/lib/jekyll/related_posts.rb b/lib/jekyll/related_posts.rb index 51ae8d8f..cde1742c 100644 --- a/lib/jekyll/related_posts.rb +++ b/lib/jekyll/related_posts.rb @@ -9,7 +9,7 @@ module Jekyll def initialize(post) @post = post @site = post.site - Jekyll::External.require_with_graceful_fail('classifier-reborn') if site.lsi + Jekyll::External.require_with_graceful_fail("classifier-reborn") if site.lsi end def build diff --git a/lib/jekyll/renderer.rb b/lib/jekyll/renderer.rb index f116cf60..f9eb94be 100644 --- a/lib/jekyll/renderer.rb +++ b/lib/jekyll/renderer.rb @@ -136,10 +136,13 @@ module Jekyll used = Set.new([layout]) + # Reset the payload layout data to ensure it starts fresh for each page. + payload["layout"] = nil + while layout payload['content'] = output payload['page'] = document.to_liquid - payload['layout'] = Utils.deep_merge_hashes(payload['layout'] || {}, layout.data) + payload['layout'] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {}) output = render_liquid( layout.content, diff --git a/lib/jekyll/utils.rb b/lib/jekyll/utils.rb index 179981ec..a70a7916 100644 --- a/lib/jekyll/utils.rb +++ b/lib/jekyll/utils.rb @@ -54,6 +54,10 @@ module Jekyll target.default_proc = overwrite.default_proc end + target.each do |key, val| + target[key] = val.dup if val.frozen? && duplicable?(val) + end + target end @@ -61,6 +65,15 @@ module Jekyll value.is_a?(Hash) || value.is_a?(Drops::Drop) end + def duplicable?(obj) + case obj + when nil, false, true, Symbol, Numeric + false + else + true + end + end + # Read array from the supplied hash favouring the singular key # and then the plural key, and handling any nil entries. # diff --git a/test/helper.rb b/test/helper.rb index 3a3dafa2..9505c762 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -28,7 +28,7 @@ require "minitest/autorun" require "minitest/reporters" require "minitest/profile" require "rspec/mocks" -require "jekyll" +require_relative "../lib/jekyll.rb" Jekyll.logger = Logger.new(StringIO.new) @@ -61,8 +61,30 @@ module Minitest::Assertions end end +module DirectoryHelpers + def dest_dir(*subdirs) + test_dir("dest", *subdirs) + end + + def source_dir(*subdirs) + test_dir("source", *subdirs) + end + + def test_dir(*subdirs) + File.join(File.dirname(__FILE__), *subdirs) + end +end + class JekyllUnitTest < Minitest::Test include ::RSpec::Mocks::ExampleMethods + include DirectoryHelpers + extend DirectoryHelpers + + def mu_pp(obj) + s = obj.is_a?(Hash) ? JSON.pretty_generate(obj) : obj.inspect + s = s.encode Encoding.default_external if defined? Encoding + s + end def mocks_expect(*args) RSpec::Mocks::ExampleMethods::ExpectHost.instance_method(:expect)\ @@ -85,9 +107,12 @@ class JekyllUnitTest < Minitest::Test Jekyll::Site.new(site_configuration(overrides)) end - def build_configs(overrides, base_hash = Jekyll::Configuration::DEFAULTS) + def default_configuration + Marshal.load(Marshal.dump(Jekyll::Configuration::DEFAULTS)) + end + + def build_configs(overrides, base_hash = default_configuration) Utils.deep_merge_hashes(base_hash, overrides) - .fix_common_issues.backwards_compatibilize.add_default_collections end def site_configuration(overrides = {}) @@ -98,14 +123,9 @@ class JekyllUnitTest < Minitest::Test build_configs({ "source" => source_dir }, full_overrides) - end - - def dest_dir(*subdirs) - test_dir("dest", *subdirs) - end - - def source_dir(*subdirs) - test_dir("source", *subdirs) + .fix_common_issues + .backwards_compatibilize + .add_default_collections end def clear_dest @@ -113,10 +133,6 @@ class JekyllUnitTest < Minitest::Test FileUtils.rm_rf(source_dir(".jekyll-metadata")) end - def test_dir(*subdirs) - File.join(File.dirname(__FILE__), *subdirs) - end - def directory_with_contents(path) FileUtils.rm_rf(path) FileUtils.mkdir(path) diff --git a/test/test_configuration.rb b/test/test_configuration.rb index 608dfef0..08cc3bd9 100644 --- a/test/test_configuration.rb +++ b/test/test_configuration.rb @@ -1,7 +1,62 @@ require 'helper' class TestConfiguration < JekyllUnitTest - @@defaults = Jekyll::Configuration::DEFAULTS.add_default_collections.freeze + @@test_config = { + "source" => new(nil).source_dir, + "destination" => dest_dir + } + + context ".from" do + should "create a Configuration object" do + assert_instance_of Configuration, Configuration.from({}) + end + + should "merge input over defaults" do + result = Configuration.from({"source" => "blah"}) + refute_equal result["source"], Configuration::DEFAULTS["source"] + assert_equal result["source"], "blah" + end + + should "fix common mistakes" do + result = Configuration.from({"paginate" => 0}) + assert_nil result["paginate"], "Expected 'paginate' to be corrected to 'nil', but was #{result["paginate"].inspect}" + end + + should "add default collections" do + result = Configuration.from({}) + assert_equal result["collections"], {"posts" => {"output" => true, "permalink" => "/:categories/:year/:month/:day/:title:output_ext"}} + end + + should "NOT backwards-compatibilize" do + assert Configuration.from("watch" => true)["watch"], "Expected the 'watch' key to not be removed." + end + end + + context "#add_default_collections" do + should "no-op if collections is nil" do + result = Configuration[{"collections" => nil}].add_default_collections + assert_nil result["collections"] + end + + should "turn an array into a hash" do + result = Configuration[{"collections" => %w{methods}}].add_default_collections + assert_instance_of Hash, result["collections"] + assert_equal result["collections"], {"posts" => {"output" => true}, "methods" => {}} + end + + should "only assign collections.posts.permalink if a permalink is specified" do + result = Configuration[{"permalink" => "pretty", "collections" => {}}].add_default_collections + assert_equal result["collections"], {"posts" => {"output" => true, "permalink" => "/:categories/:year/:month/:day/:title/"}} + + result = Configuration[{"permalink" => nil, "collections" => {}}].add_default_collections + assert_equal result["collections"], {"posts" => {"output" => true}} + end + + should "forces posts to output" do + result = Configuration[{"collections" => {"posts" => {"output" => false}}}].add_default_collections + assert_equal result["collections"]["posts"]["output"], true + end + end context "#stringify_keys" do setup do @@ -154,27 +209,27 @@ class TestConfiguration < JekyllUnitTest end context "loading configuration" do setup do - @path = File.join(Dir.pwd, '_config.yml') + @path = source_dir('_config.yml') @user_config = File.join(Dir.pwd, "my_config_file.yml") end should "fire warning with no _config.yml" do allow(SafeYAML).to receive(:load_file).with(@path) { raise SystemCallError, "No such file or directory - #{@path}" } allow($stderr).to receive(:puts).with("Configuration file: none".yellow) - assert_equal @@defaults, Jekyll.configuration({}) + assert_equal site_configuration, Jekyll.configuration(@@test_config) end should "load configuration as hash" do allow(SafeYAML).to receive(:load_file).with(@path).and_return(Hash.new) allow($stdout).to receive(:puts).with("Configuration file: #{@path}") - assert_equal @@defaults, Jekyll.configuration({}) + assert_equal site_configuration, Jekyll.configuration(@@test_config) end should "fire warning with bad config" do allow(SafeYAML).to receive(:load_file).with(@path).and_return(Array.new) allow($stderr).to receive(:puts).and_return(("WARNING: ".rjust(20) + "Error reading configuration. Using defaults (and options).").yellow) allow($stderr).to receive(:puts).and_return("Configuration file: (INVALID) #{@path}".yellow) - assert_equal @@defaults, Jekyll.configuration({}) + assert_equal site_configuration, Jekyll.configuration(@@test_config) end should "fire warning when user-specified config file isn't there" do @@ -193,8 +248,8 @@ class TestConfiguration < JekyllUnitTest context "loading config from external file" do setup do @paths = { - :default => File.join(Dir.pwd, '_config.yml'), - :other => File.join(Dir.pwd, '_config.live.yml'), + :default => source_dir('_config.yml'), + :other => source_dir('_config.live.yml'), :toml => source_dir('_config.dev.toml'), :empty => "" } @@ -203,24 +258,31 @@ class TestConfiguration < JekyllUnitTest should "load default plus posts config if no config_file is set" do allow(SafeYAML).to receive(:load_file).with(@paths[:default]).and_return({}) allow($stdout).to receive(:puts).with("Configuration file: #{@paths[:default]}") - assert_equal @@defaults, Jekyll.configuration({}) + assert_equal site_configuration, Jekyll.configuration(@@test_config) end should "load different config if specified" do allow(SafeYAML).to receive(:load_file).with(@paths[:other]).and_return({"baseurl" => "http://wahoo.dev"}) allow($stdout).to receive(:puts).with("Configuration file: #{@paths[:other]}") - assert_equal Utils.deep_merge_hashes(@@defaults, { "baseurl" => "http://wahoo.dev" }), Jekyll.configuration({ "config" => @paths[:other] }) + Jekyll.configuration({ "config" => @paths[:other] }) + assert_equal \ + site_configuration({ "baseurl" => "http://wahoo.dev" }), + Jekyll.configuration(@@test_config.merge({ "config" => @paths[:other] })) end should "load default config if path passed is empty" do allow(SafeYAML).to receive(:load_file).with(@paths[:default]).and_return({}) allow($stdout).to receive(:puts).with("Configuration file: #{@paths[:default]}") - assert_equal @@defaults, Jekyll.configuration({ "config" => @paths[:empty] }) + assert_equal \ + site_configuration, + Jekyll.configuration(@@test_config.merge({ "config" => [@paths[:empty]] })) end should "successfully load a TOML file" do Jekyll.logger.log_level = :warn - assert_equal @@defaults.clone.merge({ "baseurl" => "/you-beautiful-blog-you", "title" => "My magnificent site, wut" }), Jekyll.configuration({ "config" => [@paths[:toml]] }) + assert_equal \ + site_configuration({ "baseurl" => "/you-beautiful-blog-you", "title" => "My magnificent site, wut" }), + Jekyll.configuration(@@test_config.merge({ "config" => [@paths[:toml]] })) Jekyll.logger.log_level = :info end @@ -233,7 +295,9 @@ class TestConfiguration < JekyllUnitTest allow($stdout).to receive(:puts).with("Configuration file: #{@paths[:default]}") allow($stdout).to receive(:puts).with("Configuration file: #{@paths[:other]}") allow($stdout).to receive(:puts).with("Configuration file: #{@paths[:toml]}") - assert_equal @@defaults, Jekyll.configuration({ "config" => [@paths[:default], @paths[:other], @paths[:toml]] }) + assert_equal \ + site_configuration, + Jekyll.configuration(@@test_config.merge({ "config" => [@paths[:default], @paths[:other], @paths[:toml]] })) end should "load multiple config files and last config should win" do @@ -241,7 +305,63 @@ class TestConfiguration < JekyllUnitTest allow(SafeYAML).to receive(:load_file).with(@paths[:other]).and_return({"baseurl" => "http://wahoo.dev"}) allow($stdout).to receive(:puts).with("Configuration file: #{@paths[:default]}") allow($stdout).to receive(:puts).with("Configuration file: #{@paths[:other]}") - assert_equal Utils.deep_merge_hashes(@@defaults, { "baseurl" => "http://wahoo.dev" }), Jekyll.configuration({ "config" => [@paths[:default], @paths[:other]] }) + assert_equal \ + site_configuration({ "baseurl" => "http://wahoo.dev" }), + Jekyll.configuration(@@test_config.merge({ "config" => [@paths[:default], @paths[:other]] })) + end + end + + context "#add_default_collections" do + should "not do anything if collections is nil" do + conf = Configuration[default_configuration].tap {|c| c['collections'] = nil } + assert_equal conf.add_default_collections, conf + assert_nil conf.add_default_collections['collections'] + end + + should "converts collections to a hash if an array" do + conf = Configuration[default_configuration].tap {|c| c['collections'] = ['docs'] } + assert_equal conf.add_default_collections, conf.merge({ + "collections" => { + "docs" => {}, + "posts" => { + "output" => true, + "permalink" => "/:categories/:year/:month/:day/:title:output_ext" + }}}) + end + + should "force collections.posts.output = true" do + conf = Configuration[default_configuration].tap {|c| c['collections'] = {'posts' => {'output' => false}} } + assert_equal conf.add_default_collections, conf.merge({ + "collections" => { + "posts" => { + "output" => true, + "permalink" => "/:categories/:year/:month/:day/:title:output_ext" + }}}) + end + + should "set collections.posts.permalink if it's not set" do + conf = Configuration[default_configuration] + assert_equal conf.add_default_collections, conf.merge({ + "collections" => { + "posts" => { + "output" => true, + "permalink" => "/:categories/:year/:month/:day/:title:output_ext" + }}}) + end + + should "leave collections.posts.permalink alone if it is set" do + posts_permalink = "/:year/:title/" + conf = Configuration[default_configuration].tap do |c| + c['collections'] = { + "posts" => { "permalink" => posts_permalink } + } + end + assert_equal conf.add_default_collections, conf.merge({ + "collections" => { + "posts" => { + "output" => true, + "permalink" => posts_permalink + }}}) end end end diff --git a/test/test_excerpt_drop.rb b/test/test_excerpt_drop.rb new file mode 100644 index 00000000..f8c75f9a --- /dev/null +++ b/test/test_excerpt_drop.rb @@ -0,0 +1,36 @@ +require "helper" + +class TestExcerptDrop < JekyllUnitTest + context "an excerpt drop" do + setup do + @site = fixture_site + @site.read + @doc = @site.docs_to_write.find { |d| !d.data["layout"].nil? } + @doc_drop = @doc.to_liquid + @excerpt = @doc.data["excerpt"] + @excerpt_drop = @excerpt.to_liquid + end + + should "have the right thing" do + assert @doc.is_a? Jekyll::Document + assert @doc_drop.is_a? Jekyll::Drops::DocumentDrop + assert @excerpt.is_a? Jekyll::Excerpt + assert @excerpt_drop.is_a? Jekyll::Drops::ExcerptDrop + end + + should "not have an excerpt" do + assert_nil @excerpt.data["excerpt"] + assert @excerpt_drop.class.invokable? "excerpt" + assert_nil @excerpt_drop["excerpt"] + end + + should "inherit the layout for the drop but not the excerpt" do + assert_nil @excerpt.data["layout"] + assert_equal @excerpt_drop["layout"], @doc_drop["layout"] + end + + should "inherit values from the document" do + assert_equal @excerpt_drop.keys.sort, @doc_drop.keys.sort + end + end +end diff --git a/test/test_front_matter_defaults.rb b/test/test_front_matter_defaults.rb index f1b55ac1..fdad0930 100644 --- a/test/test_front_matter_defaults.rb +++ b/test/test_front_matter_defaults.rb @@ -3,10 +3,8 @@ require "helper" class TestFrontMatterDefaults < JekyllUnitTest context "A site with full front matter defaults" do setup do - @site = Site.new(Jekyll.configuration({ - "source" => source_dir, - "destination" => dest_dir, - "defaults" => [{ + @site = fixture_site({ + "defaults" => [{ "scope" => { "path" => "contacts", "type" => "page" @@ -15,7 +13,7 @@ class TestFrontMatterDefaults < JekyllUnitTest "key" => "val" } }] - })) + }) @site.process @affected = @site.pages.find { |page| page.relative_path == "/contacts/bar.html" } @not_affected = @site.pages.find { |page| page.relative_path == "about.html" } @@ -29,10 +27,8 @@ class TestFrontMatterDefaults < JekyllUnitTest context "A site with front matter type pages and an extension" do setup do - @site = Site.new(Jekyll.configuration({ - "source" => source_dir, - "destination" => dest_dir, - "defaults" => [{ + @site = fixture_site({ + "defaults" => [{ "scope" => { "path" => "index.html" }, @@ -40,7 +36,7 @@ class TestFrontMatterDefaults < JekyllUnitTest "key" => "val" } }] - })) + }) @site.process @affected = @site.pages.find { |page| page.relative_path == "index.html" } @@ -55,10 +51,8 @@ class TestFrontMatterDefaults < JekyllUnitTest context "A site with front matter defaults with no type" do setup do - @site = Site.new(Jekyll.configuration({ - "source" => source_dir, - "destination" => dest_dir, - "defaults" => [{ + @site = fixture_site({ + "defaults" => [{ "scope" => { "path" => "win" }, @@ -66,7 +60,8 @@ class TestFrontMatterDefaults < JekyllUnitTest "key" => "val" } }] - })) + }) + @site.process @affected = @site.posts.docs.find { |page| page.relative_path =~ %r!win\/! } @not_affected = @site.pages.find { |page| page.relative_path == "about.html" } @@ -80,10 +75,8 @@ class TestFrontMatterDefaults < JekyllUnitTest context "A site with front matter defaults with no path and a deprecated type" do setup do - @site = Site.new(Jekyll.configuration({ - "source" => source_dir, - "destination" => dest_dir, - "defaults" => [{ + @site = fixture_site({ + "defaults" => [{ "scope" => { "type" => "page" }, @@ -91,7 +84,8 @@ class TestFrontMatterDefaults < JekyllUnitTest "key" => "val" } }] - })) + }) + @site.process @affected = @site.pages @not_affected = @site.posts.docs @@ -106,10 +100,8 @@ class TestFrontMatterDefaults < JekyllUnitTest context "A site with front matter defaults with no path" do setup do - @site = Site.new(Jekyll.configuration({ - "source" => source_dir, - "destination" => dest_dir, - "defaults" => [{ + @site = fixture_site({ + "defaults" => [{ "scope" => { "type" => "pages" }, @@ -117,7 +109,7 @@ class TestFrontMatterDefaults < JekyllUnitTest "key" => "val" } }] - })) + }) @site.process @affected = @site.pages @not_affected = @site.posts.docs @@ -132,17 +124,15 @@ class TestFrontMatterDefaults < JekyllUnitTest context "A site with front matter defaults with no path or type" do setup do - @site = Site.new(Jekyll.configuration({ - "source" => source_dir, - "destination" => dest_dir, - "defaults" => [{ + @site = fixture_site({ + "defaults" => [{ "scope" => { }, "values" => { "key" => "val" } }] - })) + }) @site.process @affected = @site.pages @not_affected = @site.posts @@ -156,15 +146,13 @@ class TestFrontMatterDefaults < JekyllUnitTest context "A site with front matter defaults with no scope" do setup do - @site = Site.new(Jekyll.configuration({ - "source" => source_dir, - "destination" => dest_dir, - "defaults" => [{ + @site = fixture_site({ + "defaults" => [{ "values" => { "key" => "val" } }] - })) + }) @site.process @affected = @site.pages @not_affected = @site.posts diff --git a/test/test_generated_site.rb b/test/test_generated_site.rb index 0a24172c..8373f0fc 100644 --- a/test/test_generated_site.rb +++ b/test/test_generated_site.rb @@ -4,10 +4,8 @@ class TestGeneratedSite < JekyllUnitTest context "generated sites" do setup do clear_dest - config = Jekyll::Configuration::DEFAULTS.merge({ "source" => source_dir, - "destination" => dest_dir }) - @site = fixture_site(config) + @site = fixture_site @site.process @index = File.read(dest_dir("index.html")) end @@ -65,10 +63,7 @@ OUTPUT context "generating limited posts" do setup do clear_dest - config = Jekyll::Configuration::DEFAULTS.merge({ "source" => source_dir, - "destination" => dest_dir, - "limit_posts" => 5 }) - @site = fixture_site(config) + @site = fixture_site("limit_posts" => 5) @site.process @index = File.read(dest_dir("index.html")) end @@ -80,24 +75,16 @@ OUTPUT should "ensure limit posts is 0 or more" do assert_raises ArgumentError do clear_dest - config = Jekyll::Configuration::DEFAULTS.merge({ - "source" => source_dir, - "destination" => dest_dir, - "limit_posts" => -1 - }) - @site = fixture_site(config) + @site = fixture_site("limit_posts" => -1) end end should "acceptable limit post is 0" do clear_dest - config = Jekyll::Configuration::DEFAULTS.merge({ - "source" => source_dir, - "destination" => dest_dir, - "limit_posts" => 0 - }) - - assert Site.new(config), "Couldn't create a site with the given limit_posts." + assert( + fixture_site("limit_posts" => 0), + "Couldn't create a site with limit_posts=0." + ) end end end diff --git a/test/test_site.rb b/test/test_site.rb index bce20bfa..5e52a7c9 100644 --- a/test/test_site.rb +++ b/test/test_site.rb @@ -3,42 +3,42 @@ require 'helper' class TestSite < JekyllUnitTest context "configuring sites" do should "have an array for plugins by default" do - site = Site.new(Jekyll::Configuration::DEFAULTS) + site = Site.new default_configuration assert_equal [File.join(Dir.pwd, '_plugins')], site.plugins end should "look for plugins under the site directory by default" do site = Site.new(site_configuration) - assert_equal [File.join(source_dir, '_plugins')], site.plugins + assert_equal [source_dir('_plugins')], site.plugins end should "have an array for plugins if passed as a string" do - site = Site.new(Jekyll::Configuration::DEFAULTS.merge({'plugins_dir' => '/tmp/plugins'})) + site = Site.new(site_configuration({ 'plugins_dir' => '/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::Configuration::DEFAULTS.merge({'plugins_dir' => ['/tmp/plugins', '/tmp/otherplugins']})) + site = Site.new(site_configuration({ 'plugins_dir' => ['/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::Configuration::DEFAULTS.merge({'plugins_dir' => []})) + site = Site.new(site_configuration({ 'plugins_dir' => [] })) assert_equal [], site.plugins end - should "have an empty array for plugins if nil is passed" do - site = Site.new(Jekyll::Configuration::DEFAULTS.merge({'plugins_dir' => nil})) - assert_equal [], site.plugins + should "have the default for plugins if nil is passed" do + site = Site.new(site_configuration({ 'plugins_dir' => nil })) + assert_equal [source_dir('_plugins')], site.plugins end should "expose default baseurl" do - site = Site.new(Jekyll::Configuration::DEFAULTS) + site = Site.new(default_configuration) assert_equal Jekyll::Configuration::DEFAULTS['baseurl'], site.baseurl end should "expose baseurl passed in from config" do - site = Site.new(Jekyll::Configuration::DEFAULTS.merge({'baseurl' => '/blog'})) + site = Site.new(site_configuration({ 'baseurl' => '/blog' })) assert_equal '/blog', site.baseurl end end