prototype of jekyll hooks, encapsulated

This commit is contained in:
Stephen Crosby 2015-03-06 18:02:08 -08:00
parent ae584e43ed
commit 6ca9633354
9 changed files with 456 additions and 1 deletions

205
features/hooks.feature Normal file
View File

@ -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"

View File

@ -48,6 +48,7 @@ module Jekyll
autoload :External, 'jekyll/external' autoload :External, 'jekyll/external'
autoload :Filters, 'jekyll/filters' autoload :Filters, 'jekyll/filters'
autoload :FrontmatterDefaults, 'jekyll/frontmatter_defaults' autoload :FrontmatterDefaults, 'jekyll/frontmatter_defaults'
autoload :Hooks, 'jekyll/hooks'
autoload :Layout, 'jekyll/layout' autoload :Layout, 'jekyll/layout'
autoload :CollectionReader, 'jekyll/readers/collection_reader' autoload :CollectionReader, 'jekyll/readers/collection_reader'
autoload :DataReader, 'jekyll/readers/data_reader' autoload :DataReader, 'jekyll/readers/data_reader'

View File

@ -236,6 +236,7 @@ module Jekyll
# #
# Returns nothing. # Returns nothing.
def do_layout(payload, layouts) def do_layout(payload, layouts)
Jekyll::Hooks.trigger self, :pre_render, 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) # render and transform content (this becomes the final content of the object)
@ -249,6 +250,7 @@ module Jekyll
self.output = content self.output = content
render_all_layouts(layouts, payload, info) if place_in_layout? render_all_layouts(layouts, payload, info) if place_in_layout?
Jekyll::Hooks.trigger self, :post_render
end end
# Write the generated page file to the destination directory. # Write the generated page file to the destination directory.
@ -262,6 +264,7 @@ module Jekyll
File.open(path, 'wb') do |f| File.open(path, 'wb') do |f|
f.write(output) f.write(output)
end end
Jekyll::Hooks.trigger self, :post_write
end end
# Accessor for data properties by Liquid. # Accessor for data properties by Liquid.

53
lib/jekyll/hooks.rb Normal file
View File

@ -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

View File

@ -35,6 +35,8 @@ module Jekyll
data.default_proc = proc do |hash, key| data.default_proc = proc do |hash, key|
site.frontmatter_defaults.find(File.join(dir, name), type, key) site.frontmatter_defaults.find(File.join(dir, name), type, key)
end end
Jekyll::Hooks.trigger self, :post_init
end end
# The generated directory into which the page will be placed # The generated directory into which the page will be placed

View File

@ -68,6 +68,8 @@ module Jekyll
populate_categories populate_categories
populate_tags populate_tags
Jekyll::Hooks.trigger self, :post_init
end end
def published? def published?

View File

@ -35,6 +35,8 @@ module Jekyll
"page" => document.to_liquid "page" => document.to_liquid
}, site_payload || site.site_payload) }, site_payload || site.site_payload)
Jekyll::Hooks.trigger document, :pre_render, payload
info = { info = {
filters: [Jekyll::Filters], filters: [Jekyll::Filters],
registers: { :site => site, :page => payload['page'] } registers: { :site => site, :page => payload['page'] }

View File

@ -75,6 +75,8 @@ module Jekyll
if limit_posts < 0 if limit_posts < 0
raise ArgumentError, "limit_posts must be a non-negative number" raise ArgumentError, "limit_posts must be a non-negative number"
end end
Jekyll::Hooks.trigger self, :reset
end end
# Load necessary libraries, plugins, converters, and generators. # Load necessary libraries, plugins, converters, and generators.
@ -132,6 +134,7 @@ module Jekyll
def read def read
reader.read reader.read
limit_posts! limit_posts!
Jekyll::Hooks.trigger self, :post_read
end end
# Run each of the Generators. # Run each of the Generators.
@ -150,6 +153,9 @@ module Jekyll
relative_permalinks_are_deprecated relative_permalinks_are_deprecated
payload = site_payload payload = site_payload
Jekyll::Hooks.trigger self, :pre_render, payload
collections.each do |label, collection| collections.each do |label, collection|
collection.docs.each do |document| collection.docs.each do |document|
if regenerator.regenerate?(document) if regenerator.regenerate?(document)
@ -158,7 +164,6 @@ module Jekyll
end end
end end
payload = site_payload
[posts, pages].flatten.each do |page_or_post| [posts, pages].flatten.each do |page_or_post|
if regenerator.regenerate?(page_or_post) if regenerator.regenerate?(page_or_post)
page_or_post.render(layouts, payload) page_or_post.render(layouts, payload)
@ -183,6 +188,7 @@ module Jekyll
item.write(dest) if regenerator.regenerate?(item) item.write(dest) if regenerator.regenerate?(item)
} }
regenerator.write_metadata regenerator.write_metadata
Jekyll::Hooks.trigger self, :post_write
end end
# Construct a Hash of Posts indexed by the specified Post attribute. # Construct a Hash of Posts indexed by the specified Post attribute.

View File

@ -470,6 +470,187 @@ module Jekyll
end end
{% endhighlight %} {% endhighlight %}
## Hooks
<div class="note unreleased">
<h5>Support for hooks is currently unreleased.</h5>
<p>
In order to use this feature, <a href="/docs/installation/#pre-releases">
install the latest development version of Jekyll</a>.
</p>
</div>
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 <code>Jekyll::Site</code>, <code>Jekyll::Page</code>
and <code>Jekyll::Post</code>. In all cases, Jekyll calls your hooks with the
container object as the first callback parameter. But in the case of
<code>:pre_render</code>, 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:
<div class="mobile-side-scroller">
<table>
<thead>
<tr>
<th>Container</th>
<th>Event</th>
<th>Called</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<p><code>Jekyll::Site</code></p>
</td>
<td>
<p><code>:reset</code></p>
</td>
<td>
<p>Just after site reset</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Site</code></p>
</td>
<td>
<p><code>:pre_render</code></p>
</td>
<td>
<p>Just before rendering the whole site</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Site</code></p>
</td>
<td>
<p><code>:post_render</code></p>
</td>
<td>
<p>After rendering the whole site, but before writing any files</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Site</code></p>
</td>
<td>
<p><code>:post_write</code></p>
</td>
<td>
<p>After writing the whole site to disk</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Page</code></p>
</td>
<td>
<p><code>:post_init</code></p>
</td>
<td>
<p>Whenever a page is initialized</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Page</code></p>
</td>
<td>
<p><code>:pre_render</code></p>
</td>
<td>
<p>Just before rendering a page</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Page</code></p>
</td>
<td>
<p><code>:post_render</code></p>
</td>
<td>
<p>After rendering a page, but before writing it to disk</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Page</code></p>
</td>
<td>
<p><code>:post_write</code></p>
</td>
<td>
<p>After writing a page to disk</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Post</code></p>
</td>
<td>
<p><code>:post_init</code></p>
</td>
<td>
<p>Whenever a post is initialized</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Post</code></p>
</td>
<td>
<p><code>:pre_render</code></p>
</td>
<td>
<p>Just before rendering a post</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Post</code></p>
</td>
<td>
<p><code>:post_render</code></p>
</td>
<td>
<p>After rendering a post, but before writing it to disk</p>
</td>
</tr>
<tr>
<td>
<p><code>Jekyll::Post</code></p>
</td>
<td>
<p><code>:post_write</code></p>
</td>
<td>
<p>After writing a post to disk</p>
</td>
</tr>
</tbody>
</table>
</div>
## Available Plugins ## Available Plugins
You can find a few useful plugins at the following locations: You can find a few useful plugins at the following locations: