diff --git a/features/hooks.feature b/features/hooks.feature new file mode 100644 index 00000000..f1256484 --- /dev/null +++ b/features/hooks.feature @@ -0,0 +1,205 @@ +Feature: Hooks + As a plugin author + I want to be able to run code during various stages of the build process + + Scenario: Run some code on site reset + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register Jekyll::Site, :reset do |site| + pageklass = Class.new(Jekyll::Page) do + def initialize(site, base) + @site = site + @base = base + @data = {} + @dir = '/' + @name = 'foo.html' + @content = 'mytinypage' + + self.process(@name) + end + end + + site.pages << pageklass.new(site, site.source) + end + """ + When I run jekyll build + Then the _site directory should exist + And I should see "mytinypage" in "_site/foo.html" + + Scenario: Modify the payload before rendering the site + Given I have a _plugins directory + And I have a "index.html" page that contains "{{ site.injected }}!" + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register Jekyll::Site, :pre_render do |site, payload| + payload['site']['injected'] = 'myparam' + end + """ + When I run jekyll build + Then the _site directory should exist + And I should see "myparam!" in "_site/index.html" + + Scenario: Modify the site contents after reading + Given I have a _plugins directory + And I have a "page1.html" page that contains "page1" + And I have a "page2.html" page that contains "page2" + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register Jekyll::Site, :post_read do |site| + site.pages.delete_if { |p| p.name == 'page1.html' } + end + """ + When I run jekyll build + Then the _site directory should exist + And the "_site/page1.html" file should not exist + And I should see "page2" in "_site/page2.html" + + Scenario: Work with the site files after they've been written to disk + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register Jekyll::Site, :post_write do |site| + firstpage = site.pages.first + content = File.read firstpage.destination(site.dest) + File.write(File.join(site.dest, 'firstpage.html'), content) + end + """ + And I have a "page1.html" page that contains "page1" + When I run jekyll build + Then the _site directory should exist + And I should see "page1" in "_site/firstpage.html" + + Scenario: Alter a page right after it is initialized + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register Jekyll::Page, :post_init do |page| + page.name = 'renamed.html' + page.process(page.name) + end + """ + And I have a "page1.html" page that contains "page1" + When I run jekyll build + Then the _site directory should exist + And I should see "page1" in "_site/renamed.html" + + Scenario: Alter the payload for one page but not another + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register Jekyll::Page, :pre_render do |page, payload| + payload['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 }}" + When I run jekyll build + Then I should see "special" in "_site/page1.html" + And I should not see "special" in "_site/page2.html" + + Scenario: Modify page contents before writing to disk + Given I have a _plugins directory + And I have a "index.html" page that contains "WRAP ME" + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register Jekyll::Page, :post_render do |page| + page.output = "{{{{{ #{page.output.chomp} }}}}}" + end + """ + When I run jekyll build + Then I should see "{{{{{ WRAP ME }}}}}" in "_site/index.html" + + Scenario: Work with a page after writing it to disk + Given I have a _plugins directory + And I have a "index.html" page that contains "HELLO FROM A PAGE" + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register Jekyll::Page, :post_write do |page| + require 'fileutils' + filename = page.destination(page.site.dest) + FileUtils.mv(filename, "#{filename}.moved") + end + """ + When I run jekyll build + Then I should see "HELLO FROM A PAGE" in "_site/index.html.moved" + + Scenario: Alter a post right after it is initialized + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + # rot13 translate + Jekyll::Hooks.register Jekyll::Post, :post_init do |post| + post.content.tr!('abcdefghijklmnopqrstuvwxyz', + 'nopqrstuvwxyzabcdefghijklm') + end + """ + And I have a _posts directory + And I have the following posts: + | title | date | layout | content | + | entry1 | 2015-03-14 | nil | content for entry1. | + When I run jekyll build + Then the _site directory should exist + And I should see "pbagrag sbe ragel1." in "_site/2015/03/14/entry1.html" + + Scenario: Alter the payload for certain posts + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + # Add myvar = 'old' to posts before 2015-03-15, and myvar = 'new' for + # others + Jekyll::Hooks.register Jekyll::Post, :pre_render do |post, payload| + if post.date < Time.new(2015, 3, 15) + payload['myvar'] = 'old' + else + payload['myvar'] = 'new' + end + end + """ + And I have a _posts directory + And I have the following posts: + | title | date | layout | content | + | entry1 | 2015-03-14 | nil | {{ myvar }} post | + | entry2 | 2015-03-15 | nil | {{ myvar }} post | + When I run jekyll build + Then I should see "old post" in "_site/2015/03/14/entry1.html" + And I should see "new post" in "_site/2015/03/15/entry2.html" + + Scenario: Modify post contents before writing to disk + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + # Replace content after rendering + Jekyll::Hooks.register Jekyll::Post, :post_render do |post| + post.output.gsub! /42/, 'the answer to life, the universe and everything' + end + """ + And I have a _posts directory + And I have the following posts: + | title | date | layout | content | + | entry1 | 2015-03-14 | nil | {{ 6 \| times: 7 }} | + | entry2 | 2015-03-15 | nil | {{ 6 \| times: 8 }} | + When I run jekyll build + Then I should see "the answer to life, the universe and everything" in "_site/2015/03/14/entry1.html" + And I should see "48" in "_site/2015/03/15/entry2.html" + + Scenario: Work with a post after writing it to disk + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + # Log all post filesystem writes + Jekyll::Hooks.register Jekyll::Post, :post_write do |post| + filename = post.destination(post.site.dest) + open('_site/post-build.log', 'a') do |f| + f.puts "Wrote #{filename} at #{Time.now}" + end + end + """ + And I have a _posts directory + And I have the following posts: + | title | date | layout | content | + | entry1 | 2015-03-14 | nil | entry one | + | entry2 | 2015-03-15 | nil | entry two | + When I run jekyll build + Then I should see "_site/2015/03/14/entry1.html at" in "_site/post-build.log" + Then I should see "_site/2015/03/15/entry2.html at" in "_site/post-build.log" diff --git a/lib/jekyll.rb b/lib/jekyll.rb index 7506553a..58eda759 100644 --- a/lib/jekyll.rb +++ b/lib/jekyll.rb @@ -48,6 +48,7 @@ module Jekyll autoload :External, 'jekyll/external' autoload :Filters, 'jekyll/filters' autoload :FrontmatterDefaults, 'jekyll/frontmatter_defaults' + autoload :Hooks, 'jekyll/hooks' autoload :Layout, 'jekyll/layout' autoload :CollectionReader, 'jekyll/readers/collection_reader' autoload :DataReader, 'jekyll/readers/data_reader' diff --git a/lib/jekyll/convertible.rb b/lib/jekyll/convertible.rb index fdbf3e85..5767ab02 100644 --- a/lib/jekyll/convertible.rb +++ b/lib/jekyll/convertible.rb @@ -236,6 +236,7 @@ module Jekyll # # Returns nothing. def do_layout(payload, layouts) + Jekyll::Hooks.trigger self, :pre_render, payload info = { :filters => [Jekyll::Filters], :registers => { :site => site, :page => payload['page'] } } # render and transform content (this becomes the final content of the object) @@ -249,6 +250,7 @@ module Jekyll self.output = content render_all_layouts(layouts, payload, info) if place_in_layout? + Jekyll::Hooks.trigger self, :post_render end # Write the generated page file to the destination directory. @@ -262,6 +264,7 @@ module Jekyll File.open(path, 'wb') do |f| f.write(output) end + Jekyll::Hooks.trigger self, :post_write end # Accessor for data properties by Liquid. diff --git a/lib/jekyll/hooks.rb b/lib/jekyll/hooks.rb new file mode 100644 index 00000000..bb551b2d --- /dev/null +++ b/lib/jekyll/hooks.rb @@ -0,0 +1,53 @@ +module Jekyll + module Hooks + # initial empty hooks + @registry = { + Jekyll::Site => { + reset: [], + post_read: [], + pre_render: [], + post_write: [], + }, + Jekyll::Page => { + post_init: [], + pre_render: [], + post_render: [], + post_write: [], + }, + Jekyll::Post => { + post_init: [], + pre_render: [], + post_render: [], + post_write: [], + }, + } + + NotAvailable = Class.new(RuntimeError) + + # register a hook to be called later + def self.register(klass, event, &block) + unless @registry[klass] + raise NotAvailable, "Hooks are only available for the following " << + "classes: #{@registry.keys.inspect}" + end + + unless @registry[klass][event] + raise NotAvailable, "Invalid hook. #{klass} supports only the " << + "following hooks #{@registry[klass].keys.inspect}" + end + + @registry[klass][event] << block + end + + # interface for Jekyll core components to trigger hooks + def self.trigger(instance, event, *args) + # proceed only if there are hooks to call + return unless @registry[instance.class] + return unless @registry[instance.class][event] + + @registry[instance.class][event].each do |hook| + hook.call(instance, *args) + end + end + end +end diff --git a/lib/jekyll/page.rb b/lib/jekyll/page.rb index 36b12aa1..f9040da8 100644 --- a/lib/jekyll/page.rb +++ b/lib/jekyll/page.rb @@ -35,6 +35,8 @@ module Jekyll data.default_proc = proc do |hash, key| site.frontmatter_defaults.find(File.join(dir, name), type, key) end + + Jekyll::Hooks.trigger self, :post_init end # The generated directory into which the page will be placed diff --git a/lib/jekyll/post.rb b/lib/jekyll/post.rb index 780f40f7..252cca42 100644 --- a/lib/jekyll/post.rb +++ b/lib/jekyll/post.rb @@ -68,6 +68,8 @@ module Jekyll populate_categories populate_tags + + Jekyll::Hooks.trigger self, :post_init end def published? diff --git a/lib/jekyll/renderer.rb b/lib/jekyll/renderer.rb index f25ca343..4a772978 100644 --- a/lib/jekyll/renderer.rb +++ b/lib/jekyll/renderer.rb @@ -35,6 +35,8 @@ module Jekyll "page" => document.to_liquid }, site_payload || site.site_payload) + Jekyll::Hooks.trigger document, :pre_render, payload + info = { filters: [Jekyll::Filters], registers: { :site => site, :page => payload['page'] } diff --git a/lib/jekyll/site.rb b/lib/jekyll/site.rb index 92aa0f39..53ac028b 100644 --- a/lib/jekyll/site.rb +++ b/lib/jekyll/site.rb @@ -75,6 +75,8 @@ module Jekyll if limit_posts < 0 raise ArgumentError, "limit_posts must be a non-negative number" end + + Jekyll::Hooks.trigger self, :reset end # Load necessary libraries, plugins, converters, and generators. @@ -132,6 +134,7 @@ module Jekyll def read reader.read limit_posts! + Jekyll::Hooks.trigger self, :post_read end # Run each of the Generators. @@ -150,6 +153,9 @@ module Jekyll relative_permalinks_are_deprecated payload = site_payload + + Jekyll::Hooks.trigger self, :pre_render, payload + collections.each do |label, collection| collection.docs.each do |document| if regenerator.regenerate?(document) @@ -158,7 +164,6 @@ module Jekyll end end - payload = site_payload [posts, pages].flatten.each do |page_or_post| if regenerator.regenerate?(page_or_post) page_or_post.render(layouts, payload) @@ -183,6 +188,7 @@ module Jekyll item.write(dest) if regenerator.regenerate?(item) } regenerator.write_metadata + Jekyll::Hooks.trigger self, :post_write end # Construct a Hash of Posts indexed by the specified Post attribute. diff --git a/site/_docs/plugins.md b/site/_docs/plugins.md index 8fcf564c..e7830e1a 100644 --- a/site/_docs/plugins.md +++ b/site/_docs/plugins.md @@ -470,6 +470,187 @@ module Jekyll end {% endhighlight %} +## Hooks + +
+
Support for hooks is currently unreleased.
+

+ In order to use this feature, + install the latest development version of Jekyll. +

+
+ +Using hooks, your plugin can exercise fine-grained control over various aspects +of the build process. If your plugin defines any hooks, Jekyll will call them +at pre-defined points. + +Hooks are registered to a container and an event name. To register one, you +call Jekyll::Hooks.register, and pass the container, event name, and code to +call whenever the hook is triggered. For example, if you want to execute some +custom functionality every time Jekyll renders a post, you could register a +hook like this: + +{% highlight ruby %} +Jekyll::Hooks.register Jekyll::Post, :post_render do |post| + # code to call after Jekyll renders a post +end +{% endhighlight %} + +Jekyll provides hooks for Jekyll::Site, Jekyll::Page +and Jekyll::Post. In all cases, Jekyll calls your hooks with the +container object as the first callback parameter. But in the case of +:pre_render, your hook will also receive a payload hash as a +second parameter which allows you full control over the variables that are +available while rendering. + +The complete list of available hooks is below: + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ContainerEventCalled
+

Jekyll::Site

+
+

:reset

+
+

Just after site reset

+
+

Jekyll::Site

+
+

:pre_render

+
+

Just before rendering the whole site

+
+

Jekyll::Site

+
+

:post_render

+
+

After rendering the whole site, but before writing any files

+
+

Jekyll::Site

+
+

:post_write

+
+

After writing the whole site to disk

+
+

Jekyll::Page

+
+

:post_init

+
+

Whenever a page is initialized

+
+

Jekyll::Page

+
+

:pre_render

+
+

Just before rendering a page

+
+

Jekyll::Page

+
+

:post_render

+
+

After rendering a page, but before writing it to disk

+
+

Jekyll::Page

+
+

:post_write

+
+

After writing a page to disk

+
+

Jekyll::Post

+
+

:post_init

+
+

Whenever a post is initialized

+
+

Jekyll::Post

+
+

:pre_render

+
+

Just before rendering a post

+
+

Jekyll::Post

+
+

:post_render

+
+

After rendering a post, but before writing it to disk

+
+

Jekyll::Post

+
+

:post_write

+
+

After writing a post to disk

+
+
+ ## Available Plugins You can find a few useful plugins at the following locations: