diff --git a/features/collections.feature b/features/collections.feature index d97918bd..1fcc7cce 100644 --- a/features/collections.feature +++ b/features/collections.feature @@ -13,7 +13,7 @@ Feature: Collections And the "_site/methods/configuration.html" file should not exist Scenario: Rendered collection - Given I have an "index.html" page that contains "Collections: {{ site.collections }}" + Given I have an "index.html" page that contains "Collections: output => {{ site.collections[0].output }} label => {{ site.collections[0].label }}" And I have an "collection_metadata.html" page that contains "Methods metadata: {{ site.collections[0].foo }} {{ site.collections[0] }}" And I have fixture collections And I have a "_config.yml" file with content: @@ -25,8 +25,8 @@ Feature: Collections """ When I run jekyll build Then the _site directory should exist - And I should see "Collections: {\"output\"=>true" in "_site/index.html" - And I should see "\"label\"=>\"methods\"," in "_site/index.html" + And I should see "Collections: output => true" in "_site/index.html" + And I should see "label => methods" in "_site/index.html" And I should see "Methods metadata: bar" in "_site/collection_metadata.html" And I should see "

Whatever: foo.bar

" in "_site/methods/configuration.html" @@ -45,7 +45,7 @@ Feature: Collections And I should see "

Whatever: foo.bar

" in "_site/methods/configuration/index.html" Scenario: Rendered document in a layout - Given I have an "index.html" page that contains "Collections: {{ site.collections }}" + Given I have an "index.html" page that contains "Collections: output => {{ site.collections[0].output }} label => {{ site.collections[0].label }} foo => {{ site.collections[0].foo }}" And I have a default layout that contains "
Tom Preston-Werner
{{content}}" And I have fixture collections And I have a "_config.yml" file with content: @@ -57,8 +57,9 @@ Feature: Collections """ When I run jekyll build Then the _site directory should exist - And I should see "Collections: {\"output\"=>true" in "_site/index.html" - And I should see "\"label\"=>\"methods\"," in "_site/index.html" + And I should see "Collections: output => true" in "_site/index.html" + And I should see "label => methods" in "_site/index.html" + And I should see "foo => bar" in "_site/index.html" And I should see "

Run your generators! default

" in "_site/methods/site/generate.html" And I should see "
Tom Preston-Werner
" in "_site/methods/site/generate.html" diff --git a/features/hooks.feature b/features/hooks.feature index 489f256b..b561c00c 100644 --- a/features/hooks.feature +++ b/features/hooks.feature @@ -89,11 +89,11 @@ Feature: Hooks And I have a "_plugins/ext.rb" file with content: """ Jekyll::Hooks.register :pages, :pre_render do |page, payload| - payload['myparam'] = 'special' if page.name == 'page1.html' + payload.page['myparam'] = 'special' if page.name == 'page1.html' end """ - And I have a "page1.html" page that contains "{{ myparam }}" - And I have a "page2.html" page that contains "{{ myparam }}" + And I have a "page1.html" page that contains "{{ page.myparam }}" + And I have a "page2.html" page that contains "{{ page.myparam }}" When I run jekyll build Then I should see "special" in "_site/page1.html" And I should not see "special" in "_site/page2.html" diff --git a/features/post_data.feature b/features/post_data.feature index 6ebfb27d..2792a829 100644 --- a/features/post_data.feature +++ b/features/post_data.feature @@ -233,14 +233,14 @@ Feature: Post data | dir | dir/ | | dir/nested | dir/nested/ | - Scenario: Override page.path variable + Scenario: Cannot override page.path variable Given I have a _posts directory And I have the following post: | title | date | path | content | - | override | 2013-04-12 | override-path.html | Custom path: {{ page.path }} | + | override | 2013-04-12 | override-path.html | Non-custom path: {{ page.path }} | When I run jekyll build Then the _site directory should exist - And I should see "Custom path: override-path.html" in "_site/2013/04/12/override.html" + And I should see "Non-custom path: _posts/2013-04-12-override.markdown" in "_site/2013/04/12/override.html" Scenario: Disable a post from being published Given I have a _posts directory diff --git a/lib/jekyll.rb b/lib/jekyll.rb index 3e8e639d..de6b6c69 100644 --- a/lib/jekyll.rb +++ b/lib/jekyll.rb @@ -32,6 +32,7 @@ require 'colorator' SafeYAML::OPTIONS[:suppress_warnings] = true module Jekyll + StandardError = Class.new(::StandardError) # internal requires autoload :Cleaner, 'jekyll/cleaner' @@ -172,6 +173,7 @@ end require_all 'jekyll/commands' require_all 'jekyll/converters' require_all 'jekyll/converters/markdown' +require_all 'jekyll/drops' require_all 'jekyll/generators' require_all 'jekyll/tags' diff --git a/lib/jekyll/collection.rb b/lib/jekyll/collection.rb index c4a1f456..f74ccf6f 100644 --- a/lib/jekyll/collection.rb +++ b/lib/jekyll/collection.rb @@ -170,14 +170,7 @@ module Jekyll # # Returns a representation of this collection for use in Liquid. def to_liquid - metadata.merge({ - "label" => label, - "docs" => docs, - "files" => files, - "directory" => directory, - "output" => write?, - "relative_directory" => relative_directory - }) + Drops::CollectionDrop.new self end # Whether the collection's documents ought to be written as individual diff --git a/lib/jekyll/convertible.rb b/lib/jekyll/convertible.rb index 016ff245..db34307a 100644 --- a/lib/jekyll/convertible.rb +++ b/lib/jekyll/convertible.rb @@ -210,13 +210,8 @@ module Jekyll while layout Jekyll.logger.debug "Rendering Layout:", path - payload = Utils.deep_merge_hashes( - payload, - { - "content" => output, - "layout" => layout.data - } - ) + payload.content = output + payload.layout = layout.data self.output = render_liquid(layout.content, payload, @@ -250,11 +245,11 @@ module Jekyll Jekyll.logger.debug "Pre-Render Hooks:", self.relative_path Jekyll::Hooks.trigger hook_owner, :pre_render, self, payload - info = { :filters => [Jekyll::Filters], :registers => { :site => site, :page => payload['page'] } } + info = { :filters => [Jekyll::Filters], :registers => { :site => site, :page => payload.page } } # render and transform content (this becomes the final content of the object) - payload["highlighter_prefix"] = converters.first.highlighter_prefix - payload["highlighter_suffix"] = converters.first.highlighter_suffix + payload.highlighter_prefix = converters.first.highlighter_prefix + payload.highlighter_suffix = converters.first.highlighter_suffix if render_with_liquid? Jekyll.logger.debug "Rendering Liquid:", self.relative_path diff --git a/lib/jekyll/document.rb b/lib/jekyll/document.rb index 528fed68..a1bf6bad 100644 --- a/lib/jekyll/document.rb +++ b/lib/jekyll/document.rb @@ -12,11 +12,13 @@ module Jekyll # Create a new Document. # - # site - the Jekyll::Site instance to which this Document belongs # path - the path to the file + # relations - a hash with keys :site and :collection, the values of which + # are the Jekyll::Site and Jekyll::Collection to which this + # Document belong. # # Returns nothing. - def initialize(path, relations) + def initialize(path, relations = {}) @site = relations[:site] @path = path @extname = File.extname(path) @@ -38,12 +40,10 @@ module Jekyll end def output=(output) - @to_liquid = nil @output = output end def content=(content) - @to_liquid = nil @content = content end @@ -181,27 +181,7 @@ module Jekyll # # Returns the Hash of key-value pairs for replacement in the URL. def url_placeholders - { - collection: collection.label, - path: cleaned_relative_path, - output_ext: output_ext, - name: Utils.slugify(basename_without_ext), - title: Utils.slugify(data['slug'], mode: "pretty", cased: true) || Utils - .slugify(basename_without_ext, mode: "pretty", cased: true), - slug: Utils.slugify(data['slug']) || Utils.slugify(basename_without_ext), - year: date.strftime("%Y"), - month: date.strftime("%m"), - day: date.strftime("%d"), - hour: date.strftime("%H"), - minute: date.strftime("%M"), - second: date.strftime("%S"), - i_day: date.strftime("%-d"), - i_month: date.strftime("%-m"), - categories: (data['categories'] || []).map { |c| c.to_s.downcase }.uniq.join('/'), - short_month: date.strftime("%b"), - short_year: date.strftime("%y"), - y_day: date.strftime("%j"), - } + @url_placeholders ||= Drops::UrlDrop.new(self) end # The permalink for this Document. @@ -278,8 +258,6 @@ module Jekyll # # Returns nothing. def read(opts = {}) - @to_liquid = nil - Jekyll.logger.debug "Reading:", relative_path if yaml_file? @@ -353,21 +331,7 @@ module Jekyll # # Returns a Hash representing this Document's data. def to_liquid - @to_liquid ||= if data.is_a?(Hash) - Utils.deep_merge_hashes Utils.deep_merge_hashes({ - "output" => output, - "content" => content, - "relative_path" => relative_path, - "path" => relative_path, - "url" => url, - "collection" => collection.label, - "next" => next_doc, - "previous" => previous_doc, - "id" => id, - }, data), { 'excerpt' => data['excerpt'].to_s } - else - data - end + @to_liquid ||= Drops::DocumentDrop.new(self) end # The inspect string for this document. @@ -391,10 +355,12 @@ module Jekyll # Returns -1, 0, +1 or nil depending on whether this doc's path is less than, # equal or greater than the other doc's path. See String#<=> for more details. def <=>(other) - return nil if !other.respond_to?(:data) - cmp = data['date'] <=> other.data['date'] - cmp = path <=> other.path if cmp == 0 - cmp + return ArgumentError.new("document cannot be compared against #{other}") unless other.respond_to?(:data) + if data['date'] && other.data['date'] && (cmp = data['date'] <=> other.data['date']) != 0 + cmp + else + path <=> other.path + end end # Determine whether this document should be written. diff --git a/lib/jekyll/drops/collection_drop.rb b/lib/jekyll/drops/collection_drop.rb new file mode 100644 index 00000000..ccb3045a --- /dev/null +++ b/lib/jekyll/drops/collection_drop.rb @@ -0,0 +1,22 @@ +# encoding: UTF-8 +require "jekyll/drops/immutable_drop" + +module Jekyll + module Drops + class CollectionDrop < ImmutableDrop + extend Forwardable + + def_delegator :@obj, :write?, :output + def_delegators :@obj, :label, :docs, :files, :directory, + :relative_directory + + def to_s + docs.to_s + end + + private + def_delegator :@obj, :metadata, :fallback_data + + end + end +end diff --git a/lib/jekyll/drops/document_drop.rb b/lib/jekyll/drops/document_drop.rb new file mode 100644 index 00000000..81c9bcd2 --- /dev/null +++ b/lib/jekyll/drops/document_drop.rb @@ -0,0 +1,26 @@ +# encoding: UTF-8 + +module Jekyll + module Drops + class DocumentDrop < ImmutableDrop + extend Forwardable + + def_delegator :@obj, :next_doc, :next + def_delegator :@obj, :previous_doc, :previous + def_delegator :@obj, :relative_path, :path + def_delegators :@obj, :id, :output, :content, :to_s, :relative_path, :url + + def collection + @obj.collection.label + end + + def excerpt + fallback_data['excerpt'].to_s + end + + private + def_delegator :@obj, :data, :fallback_data + + end + end +end diff --git a/lib/jekyll/drops/immutable_drop.rb b/lib/jekyll/drops/immutable_drop.rb new file mode 100644 index 00000000..630cbfe1 --- /dev/null +++ b/lib/jekyll/drops/immutable_drop.rb @@ -0,0 +1,30 @@ +# encoding: UTF-8 + +module Jekyll + module Drops + class ImmutableDrop < Liquid::Drop + IllegalDropModification = Class.new(Jekyll::StandardError) + + def initialize(obj) + @obj = obj + end + + def [](key) + if respond_to? key + public_send key + else + fallback_data[key] + end + end + + def []=(key, val) + if respond_to? key + raise IllegalDropModification.new("Key #{key} cannot be set in the drop.") + else + fallback_data[key] = val + end + end + + end + end +end diff --git a/lib/jekyll/drops/jekyll_drop.rb b/lib/jekyll/drops/jekyll_drop.rb new file mode 100644 index 00000000..c4009da0 --- /dev/null +++ b/lib/jekyll/drops/jekyll_drop.rb @@ -0,0 +1,21 @@ +# encoding: UTF-8 + +module Jekyll + module Drops + class JekyllDrop < Liquid::Drop + class << self + def global + @global ||= JekyllDrop.new + end + end + + def version + Jekyll::VERSION + end + + def environment + Jekyll.env + end + end + end +end diff --git a/lib/jekyll/drops/mutable_drop.rb b/lib/jekyll/drops/mutable_drop.rb new file mode 100644 index 00000000..9dd4048d --- /dev/null +++ b/lib/jekyll/drops/mutable_drop.rb @@ -0,0 +1,28 @@ +# encoding: UTF-8 + +module Jekyll + module Drops + class MutableDrop < Liquid::Drop + + def initialize(obj) + @obj = obj + @mutations = {} + end + + def [](key) + if @mutations.key? key + @mutations[key] + elsif respond_to? key + public_send key + else + fallback_data[key] + end + end + + def []=(key, val) + @mutations[key] = val + end + + end + end +end diff --git a/lib/jekyll/drops/site_drop.rb b/lib/jekyll/drops/site_drop.rb new file mode 100644 index 00000000..61ac8098 --- /dev/null +++ b/lib/jekyll/drops/site_drop.rb @@ -0,0 +1,37 @@ +# encoding: UTF-8 + +module Jekyll + module Drops + class SiteDrop < ImmutableDrop + extend Forwardable + + def_delegator :@obj, :site_data, :data + def_delegators :@obj, :time, :pages, :static_files, :documents, + :tags, :categories + + def [](key) + if @obj.collections.key?(key) && key != "posts" + @obj.collections[key].docs + else + super(key) + end + end + + def posts + @site_posts ||= @obj.posts.docs.sort { |a, b| b <=> a } + end + + def html_pages + @site_html_pages ||= @obj.pages.select { |page| page.html? || page.url.end_with?("/") } + end + + def collections + @site_collections ||= @obj.collections.values.map(&:to_liquid) + end + + private + def_delegator :@obj, :config, :fallback_data + + end + end +end diff --git a/lib/jekyll/drops/unified_payload_drop.rb b/lib/jekyll/drops/unified_payload_drop.rb new file mode 100644 index 00000000..6db48f66 --- /dev/null +++ b/lib/jekyll/drops/unified_payload_drop.rb @@ -0,0 +1,29 @@ +# encoding: UTF-8 + +module Jekyll + module Drops + class UnifiedPayloadDrop < ImmutableDrop + + attr_accessor :page, :layout, :content, :paginator + attr_accessor :highlighter_prefix, :highlighter_suffix + + def initialize(site) + @site = site + end + + def jekyll + JekyllDrop.global + end + + def site + @site_drop ||= SiteDrop.new(@site) + end + + private + def fallback_data + @fallback_data ||= {} + end + + end + end +end diff --git a/lib/jekyll/drops/url_drop.rb b/lib/jekyll/drops/url_drop.rb new file mode 100644 index 00000000..163591dd --- /dev/null +++ b/lib/jekyll/drops/url_drop.rb @@ -0,0 +1,49 @@ +# encoding: UTF-8 + +module Jekyll + module Drops + class UrlDrop < ImmutableDrop + extend Forwardable + + def_delegator :@obj, :cleaned_relative_path, :path + def_delegator :@obj, :output_ext, :output_ext + + def collection + @obj.collection.label + end + + def name + Utils.slugify(@obj.basename_without_ext) + end + + def title + 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) + end + + def categories + category_set = Set.new + Array(@obj.data['categories']).each do |category| + category_set << category.to_s.downcase + end + category_set.to_a.join('/') + end + + def year; @obj.date.strftime("%Y"); end + def month; @obj.date.strftime("%m"); end + def day; @obj.date.strftime("%d"); end + def hour; @obj.date.strftime("%H"); end + def minute; @obj.date.strftime("%M"); end + def second; @obj.date.strftime("%S"); end + def i_day; @obj.date.strftime("%-d"); end + def i_month; @obj.date.strftime("%-m"); end + def short_month; @obj.date.strftime("%b"); end + def short_year; @obj.date.strftime("%y"); end + def y_day; @obj.date.strftime("%j"); end + end + end +end diff --git a/lib/jekyll/page.rb b/lib/jekyll/page.rb index a62a9940..0644e1b6 100644 --- a/lib/jekyll/page.rb +++ b/lib/jekyll/page.rb @@ -117,12 +117,10 @@ module Jekyll # # Returns nothing. def render(layouts, site_payload) - payload = Utils.deep_merge_hashes({ - "page" => to_liquid, - 'paginator' => pager.to_liquid - }, site_payload) + site_payload.page = to_liquid + site_payload.paginator = pager.to_liquid - do_layout(payload, layouts) + do_layout(site_payload, layouts) end # The path to the source file diff --git a/lib/jekyll/renderer.rb b/lib/jekyll/renderer.rb index 529ff84b..09763cca 100644 --- a/lib/jekyll/renderer.rb +++ b/lib/jekyll/renderer.rb @@ -3,12 +3,12 @@ module Jekyll class Renderer - attr_reader :document, :site, :site_payload + attr_reader :document, :site, :payload def initialize(site, document, site_payload = nil) - @site = site - @document = document - @site_payload = site_payload + @site = site + @document = document + @payload = site_payload || site.site_payload end # Determine which converters to use based on this document's @@ -33,12 +33,10 @@ module Jekyll def run Jekyll.logger.debug "Rendering:", document.relative_path - payload = Utils.deep_merge_hashes({ - "page" => document.to_liquid - }, site_payload || site.site_payload) + payload.page = document.to_liquid if document.collection.label == 'posts' && document.is_a?(Document) - payload['site']['related_posts'] = document.related_posts + payload.site['related_posts'] = document.related_posts end Jekyll.logger.debug "Pre-Render Hooks:", document.relative_path @@ -46,12 +44,12 @@ module Jekyll info = { filters: [Jekyll::Filters], - registers: { :site => site, :page => payload['page'] } + registers: { :site => site, :page => payload.page } } # render and transform content (this becomes the final content of the object) - payload["highlighter_prefix"] = converters.first.highlighter_prefix - payload["highlighter_suffix"] = converters.first.highlighter_suffix + payload.highlighter_prefix = converters.first.highlighter_prefix + payload.highlighter_suffix = converters.first.highlighter_suffix output = document.content @@ -135,14 +133,9 @@ module Jekyll used = Set.new([layout]) while layout - payload = Utils.deep_merge_hashes( - payload, - { - "content" => output, - "page" => document.to_liquid, - "layout" => layout.data - } - ) + payload.content = output + payload.page = document.to_liquid + payload.layout = layout.data output = render_liquid( layout.content, diff --git a/lib/jekyll/site.rb b/lib/jekyll/site.rb index 5e9402f4..4dc3ac84 100644 --- a/lib/jekyll/site.rb +++ b/lib/jekyll/site.rb @@ -259,25 +259,7 @@ module Jekyll # "tags" - The Hash of tag values and Posts. # See Site#post_attr_hash for type info. def site_payload - { - "jekyll" => { - "version" => Jekyll::VERSION, - "environment" => Jekyll.env - }, - "site" => Utils.deep_merge_hashes(config, - Utils.deep_merge_hashes(Hash[collections.map{|label, coll| [label, coll.docs]}], { - "time" => time, - "posts" => posts.docs.sort { |a, b| b <=> a }, - "pages" => pages, - "static_files" => static_files, - "html_pages" => pages.select { |page| page.html? || page.url.end_with?("/") }, - "categories" => post_attr_hash('categories'), - "tags" => post_attr_hash('tags'), - "collections" => collections.values.map(&:to_liquid), - "documents" => documents, - "data" => site_data - })) - } + Drops::UnifiedPayloadDrop.new self end # Get the implementation class for the given Converter. diff --git a/lib/jekyll/url.rb b/lib/jekyll/url.rb index 6e8042b0..a001f44d 100644 --- a/lib/jekyll/url.rb +++ b/lib/jekyll/url.rb @@ -59,6 +59,14 @@ module Jekyll # # Returns the unsanitized String URL def generate_url(template) + if @placeholders.is_a? Drops::UrlDrop + generate_url_from_drop(template) + else + generate_url_from_hash(template) + end + end + + def generate_url_from_hash(template) @placeholders.inject(template) do |result, token| break result if result.index(':').nil? if token.last.nil? @@ -70,6 +78,16 @@ module Jekyll end end + def generate_url_from_drop(template) + template.gsub(/:([a-z_]+)/) do |match| + if replacement = @placeholders.public_send(match.sub(':', '')) + self.class.escape_path replacement + else + ''.freeze + end + end.gsub(/\/\//, '/') + end + # Returns a sanitized String URL, stripping "../../" and multiples of "/", # as well as the beginning "/" so we can enforce and ensure it. diff --git a/lib/jekyll/utils.rb b/lib/jekyll/utils.rb index 94da8bd5..a2c6139d 100644 --- a/lib/jekyll/utils.rb +++ b/lib/jekyll/utils.rb @@ -174,13 +174,14 @@ module Jekyll SLUGIFY_PRETTY_REGEXP end - slug = string. - # Strip according to the mode - gsub(re, '-'). - # Remove leading/trailing hyphen - gsub(/^\-|\-$/i, '') + # Strip according to the mode + slug = string.gsub(re, '-') - cased ? slug : slug.downcase + # Remove leading/trailing hyphen + slug.gsub!(/^\-|\-$/i, '') + + slug.downcase! unless cased + slug end # Add an appropriate suffix to template so that it matches the specified diff --git a/test/test_excerpt.rb b/test/test_excerpt.rb index 322ba3c6..61ffded3 100644 --- a/test/test_excerpt.rb +++ b/test/test_excerpt.rb @@ -10,8 +10,7 @@ class TestExcerpt < JekyllUnitTest def do_render(document) @site.layouts = { "default" => Layout.new(@site, source_dir('_layouts'), "simple.html")} - payload = {"site" => {"posts" => []}} - document.output = Jekyll::Renderer.new(@site, document, payload).run + document.output = Jekyll::Renderer.new(@site, document, @site.site_payload).run end context "With extraction disabled" do diff --git a/test/test_page.rb b/test/test_page.rb index 625812aa..16b9e060 100644 --- a/test/test_page.rb +++ b/test/test_page.rb @@ -9,7 +9,7 @@ class TestPage < JekyllUnitTest def do_render(page) layouts = { "default" => Layout.new(@site, source_dir('_layouts'), "simple.html")} - page.render(layouts, {"site" => {"posts" => []}}) + page.render(layouts, @site.site_payload) end context "A Page" do diff --git a/test/test_url.rb b/test/test_url.rb index e4529be7..578e8962 100644 --- a/test/test_url.rb +++ b/test/test_url.rb @@ -54,5 +54,23 @@ class TestURL < JekyllUnitTest ).to_s end + should "handle UrlDrop as a placeholder in addition to a hash" do + site = fixture_site({ + "collections" => { + "methods" => { + "output" => true + } + }, + }) + site.read + doc = site.collections["methods"].docs.find do |doc| + doc.relative_path == "_methods/escape-+ #%20[].md" + end + assert_equal '/methods/escape-+-20/escape-20.html', URL.new( + :template => '/methods/:title/:name:output_ext', + :placeholders => doc.url_placeholders + ).to_s + end + end end