From 245d9677d73fe481e6b3e1d3ad878d85d5b3303d Mon Sep 17 00:00:00 2001 From: Stephen Crosby Date: Fri, 1 May 2015 18:53:41 -0700 Subject: [PATCH] Refine hook implementation - hooks are registered to symbol owners rather than classes directly - during registration, add the ability to specify owner as an array to register the same hook to multiple owners - add optional priority during registration as a symbol (:low, :normal, :high) - implement hooks for collections as they are in octopress-hooks, aside from post_init --- features/hooks.feature | 146 +++++++++++++++++++++++++++++++++++++---- lib/jekyll/document.rb | 4 ++ lib/jekyll/hooks.rb | 78 ++++++++++++++++++---- site/_docs/plugins.md | 69 ++++++++++++++----- 4 files changed, 253 insertions(+), 44 deletions(-) diff --git a/features/hooks.feature b/features/hooks.feature index f1256484..6562a90a 100644 --- a/features/hooks.feature +++ b/features/hooks.feature @@ -6,7 +6,7 @@ Feature: Hooks Given I have a _plugins directory And I have a "_plugins/ext.rb" file with content: """ - Jekyll::Hooks.register Jekyll::Site, :reset do |site| + Jekyll::Hooks.register :site, :reset do |site| pageklass = Class.new(Jekyll::Page) do def initialize(site, base) @site = site @@ -32,7 +32,7 @@ Feature: Hooks 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| + Jekyll::Hooks.register :site, :pre_render do |site, payload| payload['site']['injected'] = 'myparam' end """ @@ -46,7 +46,7 @@ Feature: Hooks 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| + Jekyll::Hooks.register :site, :post_read do |site| site.pages.delete_if { |p| p.name == 'page1.html' } end """ @@ -59,7 +59,7 @@ Feature: Hooks 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| + Jekyll::Hooks.register :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) @@ -74,7 +74,7 @@ Feature: Hooks 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| + Jekyll::Hooks.register :page, :post_init do |page| page.name = 'renamed.html' page.process(page.name) end @@ -88,7 +88,7 @@ Feature: Hooks 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| + Jekyll::Hooks.register :page, :pre_render do |page, payload| payload['myparam'] = 'special' if page.name == 'page1.html' end """ @@ -103,7 +103,7 @@ Feature: Hooks 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| + Jekyll::Hooks.register :page, :post_render do |page| page.output = "{{{{{ #{page.output.chomp} }}}}}" end """ @@ -115,7 +115,7 @@ Feature: Hooks 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| + Jekyll::Hooks.register :page, :post_write do |page| require 'fileutils' filename = page.destination(page.site.dest) FileUtils.mv(filename, "#{filename}.moved") @@ -129,7 +129,7 @@ Feature: Hooks And I have a "_plugins/ext.rb" file with content: """ # rot13 translate - Jekyll::Hooks.register Jekyll::Post, :post_init do |post| + Jekyll::Hooks.register :post, :post_init do |post| post.content.tr!('abcdefghijklmnopqrstuvwxyz', 'nopqrstuvwxyzabcdefghijklm') end @@ -148,7 +148,7 @@ Feature: Hooks """ # Add myvar = 'old' to posts before 2015-03-15, and myvar = 'new' for # others - Jekyll::Hooks.register Jekyll::Post, :pre_render do |post, payload| + Jekyll::Hooks.register :post, :pre_render do |post, payload| if post.date < Time.new(2015, 3, 15) payload['myvar'] = 'old' else @@ -170,7 +170,7 @@ Feature: Hooks And I have a "_plugins/ext.rb" file with content: """ # Replace content after rendering - Jekyll::Hooks.register Jekyll::Post, :post_render do |post| + Jekyll::Hooks.register :post, :post_render do |post| post.output.gsub! /42/, 'the answer to life, the universe and everything' end """ @@ -188,7 +188,7 @@ Feature: Hooks And I have a "_plugins/ext.rb" file with content: """ # Log all post filesystem writes - Jekyll::Hooks.register Jekyll::Post, :post_write do |post| + Jekyll::Hooks.register :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}" @@ -203,3 +203,125 @@ Feature: Hooks 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" + + Scenario: Register a hook on multiple owners at the same time + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register [:page, :post], :post_render do |owner| + owner.output = "{{{{{ #{owner.output.chomp} }}}}}" + end + """ + And I have a "index.html" page that contains "WRAP ME" + And I have a _posts directory + And I have the following posts: + | title | date | layout | content | + | entry1 | 2015-03-14 | nil | entry one | + When I run jekyll build + Then I should see "{{{{{ WRAP ME }}}}}" in "_site/index.html" + And I should see "{{{{{

entry one

}}}}}" in "_site/2015/03/14/entry1.html" + + Scenario: Allow hooks to have a named priority + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register :page, :post_render, priority: :normal do |owner| + # first normal runs second + owner.output = "1 #{owner.output.chomp}" + end + Jekyll::Hooks.register :page, :post_render, priority: :high do |owner| + # high runs last + owner.output = "2 #{owner.output.chomp}" + end + Jekyll::Hooks.register :page, :post_render do |owner| + # second normal runs third (normal is default) + owner.output = "3 #{owner.output.chomp}" + end + Jekyll::Hooks.register :page, :post_render, priority: :low do |owner| + # low runs first + owner.output = "4 #{owner.output.chomp}" + end + """ + And I have a "index.html" page that contains "WRAP ME" + When I run jekyll build + Then I should see "2 3 1 4 WRAP ME" in "_site/index.html" + + Scenario: Alter a document right after it is initialized + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register :document, :pre_render do |doc, payload| + doc.data['text'] = doc.data['text'] << ' are belong to us' + end + """ + And I have a "_config.yml" file that contains "collections: [ memes ]" + And I have a _memes directory + And I have a "_memes/doc1.md" file with content: + """ + --- + text: all your base + --- + """ + And I have an "index.md" file with content: + """ + --- + --- + {{ site.memes.first.text }} + """ + When I run jekyll build + Then the _site directory should exist + And I should see "all your base are belong to us" in "_site/index.html" + + Scenario: Update a document after rendering it, but before writing it to disk + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register :document, :post_render do |doc| + doc.output.gsub! /

/, '

' + end + """ + And I have a "_config.yml" file with content: + """ + collections: + memes: + output: true + """ + And I have a _memes directory + And I have a "_memes/doc1.md" file with content: + """ + --- + text: all your base are belong to us + --- + {{ page.text }} + """ + When I run jekyll build + Then the _site directory should exist + And I should see "

all your base are belong to us" in "_site/memes/doc1.html" + + Scenario: Perform an action after every document is written + Given I have a _plugins directory + And I have a "_plugins/ext.rb" file with content: + """ + Jekyll::Hooks.register :document, :post_write do |doc| + open('_site/document-build.log', 'a') do |f| + f.puts "Wrote document #{doc.collection.docs.index doc} at #{Time.now}" + end + end + """ + And I have a "_config.yml" file with content: + """ + collections: + memes: + output: true + """ + And I have a _memes directory + And I have a "_memes/doc1.md" file with content: + """ + --- + text: all your base are belong to us + --- + {{ page.text }} + """ + When I run jekyll build + Then the _site directory should exist + And I should see "Wrote document 0" in "_site/document-build.log" diff --git a/lib/jekyll/document.rb b/lib/jekyll/document.rb index cd407dfa..8b16dca4 100644 --- a/lib/jekyll/document.rb +++ b/lib/jekyll/document.rb @@ -175,11 +175,15 @@ module Jekyll # # Returns nothing. def write(dest) + Jekyll::Hooks.trigger self, :post_render + path = destination(dest) FileUtils.mkdir_p(File.dirname(path)) File.open(path, 'wb') do |f| f.write(output) end + + Jekyll::Hooks.trigger self, :post_write end # Returns merged option hash for File.read of self.site (if exists) diff --git a/lib/jekyll/hooks.rb b/lib/jekyll/hooks.rb index bb551b2d..b660bd6f 100644 --- a/lib/jekyll/hooks.rb +++ b/lib/jekyll/hooks.rb @@ -1,51 +1,101 @@ module Jekyll module Hooks + # Helps look up hooks from the registry by owner's class + OWNER_MAP = { + Jekyll::Site => :site, + Jekyll::Page => :page, + Jekyll::Post => :post, + Jekyll::Document => :document, + }.freeze + + DEFAULT_PRIORITY = 20 + + # compatibility layer for octopress-hooks users + PRIORITY_MAP = { + low: 10, + normal: 20, + high: 30, + }.freeze + # initial empty hooks @registry = { - Jekyll::Site => { + :site => { reset: [], post_read: [], pre_render: [], post_write: [], }, - Jekyll::Page => { + :page => { post_init: [], pre_render: [], post_render: [], post_write: [], }, - Jekyll::Post => { + :post => { post_init: [], pre_render: [], post_render: [], post_write: [], }, + :document => { + pre_render: [], + post_render: [], + post_write: [], + }, } + # map of all hooks and their priorities + @hook_priority = {} + NotAvailable = Class.new(RuntimeError) - # register a hook to be called later - def self.register(klass, event, &block) - unless @registry[klass] + # register hook(s) to be called later + def self.register(owners, event, priority: nil, &block) + Array(owners).each do |owner| + register_one(owner, event, priority: priority_value(priority), &block) + end + end + + # Ensure the priority is a Fixnum + def self.priority_value(priority=nil) + return DEFAULT_PRIORITY unless priority + return priority if priority.is_a?(Fixnum) + PRIORITY_MAP[priority] || DEFAULT_PRIORITY + end + + # register a single hook to be called later + def self.register_one(owner, event, priority: nil, &block) + unless @registry[owner] 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}" + unless @registry[owner][event] + raise NotAvailable, "Invalid hook. #{owner} supports only the " << + "following hooks #{@registry[owner].keys.inspect}" end - @registry[klass][event] << block + insert_hook owner, event, priority, &block + end + + def self.insert_hook(owner, event, priority, &block) + @hook_priority[block] = "#{priority}.#{@hook_priority.size}".to_f + @registry[owner][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] + owner_symbol = OWNER_MAP[instance.class] - @registry[instance.class][event].each do |hook| + # proceed only if there are hooks to call + return unless @registry[owner_symbol] + return unless @registry[owner_symbol][event] + + # hooks to call for this owner and event + hooks = @registry[owner_symbol][event] + + # sort and call hooks according to priority and load order + hooks.sort_by { |h| @hook_priority[h] }.each do |hook| hook.call(instance, *args) end end diff --git a/site/_docs/plugins.md b/site/_docs/plugins.md index e7830e1a..b000c4dc 100644 --- a/site/_docs/plugins.md +++ b/site/_docs/plugins.md @@ -491,16 +491,16 @@ 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| +Jekyll::Hooks.register :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 +Jekyll provides hooks for :site, :page, +:post, and :document. 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: @@ -517,7 +517,7 @@ The complete list of available hooks is below: -

Jekyll::Site

+

:site

:reset

@@ -528,7 +528,7 @@ The complete list of available hooks is below: -

Jekyll::Site

+

:site

:pre_render

@@ -539,7 +539,7 @@ The complete list of available hooks is below: -

Jekyll::Site

+

:site

:post_render

@@ -550,7 +550,7 @@ The complete list of available hooks is below: -

Jekyll::Site

+

:site

:post_write

@@ -561,7 +561,7 @@ The complete list of available hooks is below: -

Jekyll::Page

+

:page

:post_init

@@ -572,7 +572,7 @@ The complete list of available hooks is below: -

Jekyll::Page

+

:page

:pre_render

@@ -583,7 +583,7 @@ The complete list of available hooks is below: -

Jekyll::Page

+

:page

:post_render

@@ -594,7 +594,7 @@ The complete list of available hooks is below: -

Jekyll::Page

+

:page

:post_write

@@ -605,7 +605,7 @@ The complete list of available hooks is below: -

Jekyll::Post

+

:post

:post_init

@@ -616,7 +616,7 @@ The complete list of available hooks is below: -

Jekyll::Post

+

:post

:pre_render

@@ -627,7 +627,7 @@ The complete list of available hooks is below: -

Jekyll::Post

+

:post

:post_render

@@ -638,7 +638,7 @@ The complete list of available hooks is below: -

Jekyll::Post

+

:post

:post_write

@@ -647,6 +647,39 @@ The complete list of available hooks is below:

After writing a post to disk

+ + +

:document

+ + +

:pre_render

+ + +

Just before rendering a document

+ + + + +

:document

+ + +

:post_render

+ + +

After rendering a document, but before writing it to disk

+ + + + +

:document

+ + +

:post_write

+ + +

After writing a document to disk

+ +