├── .gitignore ├── README.textile ├── Rakefile ├── VERSION ├── bin └── ejekyll ├── jekyll_ext.gemspec ├── lib └── aop.rb └── specs └── aop_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. jekyll_ext 2 | 3 | jekyll_ext allows you to extend the Jekyll static blog generator without forking and modifying its codebase. 4 | With this code, not only do your extensions live in your blog directory, but they can also be shared and reutilized. 5 | 6 | More information can be found here: "Jekyll Extensions -= Pain":http://rfelix.com/2010/01/19/jekyll-extensions-minus-equal-pain/ 7 | 8 | h2. Installation 9 | 10 | gem install jekyll_ext 11 | 12 | Now you just need to create the directory _extensions in your blog where all your extensions will live. Any .rb file in that directory or subdirectory will automatically be loaded by jekyll_ext. 13 | 14 | IMPORTANT: Instead of using the jekyll command, you should now use ejekyll instead (which is just a jekyll wrapper that loads your extensions). 15 | 16 | To get you started, take a look at "my own extensions directory":http://github.com/rfelix/my_jekyll_extensions. 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "rake/clean" 3 | require "mg" 4 | 5 | MG.new("jekyll_ext.gemspec") -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.4 2 | -------------------------------------------------------------------------------- /bin/ejekyll: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Code based on gist supplied by Alban Peignier (http://gist.github.com/379361) 4 | 5 | require 'rubygems' 6 | require 'jekyll' 7 | 8 | require File.join(File.dirname(__FILE__), '..', 'lib', 'aop.rb') 9 | 10 | module Kernel 11 | # From http://mentalized.net/journal/2010/04/02/suppress_warnings_from_ruby/ 12 | def suppress_warnings 13 | original_verbosity = $VERBOSE 14 | $VERBOSE = nil 15 | result = yield 16 | $VERBOSE = original_verbosity 17 | return result 18 | end 19 | end 20 | 21 | module Jekyll 22 | # For jekyll versions above 0.6.2 (inclusive) 23 | if const_defined? :VERSION 24 | suppress_warnings { VERSION = VERSION + " (Extended)" } 25 | end 26 | 27 | class << self 28 | alias_method :configuration_without_extensions, :configuration 29 | def configuration_with_extensions(options) 30 | options = configuration_without_extensions(options) 31 | Dir[File.join(options['source'], "_extensions", "*.rb"), File.join(options['source'], "_extensions", "*", "*.rb")].each do |f| 32 | puts "Loading Extension: #{File.basename(f)}" 33 | load f 34 | end 35 | options 36 | end 37 | alias_method :configuration, :configuration_with_extensions 38 | 39 | # For jekyll versions below 0.6.2 (exclusive) 40 | if method_defined? :version 41 | alias_method :original_version, :version 42 | def version 43 | original_version + " (Extended)" 44 | end 45 | end 46 | 47 | end 48 | 49 | end 50 | 51 | original_jekyll = ENV['PATH'].split(File::PATH_SEPARATOR).collect { |d| "#{d}/jekyll" }.find{ |p| File.exists?(p) } 52 | load original_jekyll 53 | -------------------------------------------------------------------------------- /jekyll_ext.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = 'jekyll_ext' 5 | spec.summary = "Create Jekyll extensions that are local to your blog and that can be shared with others" 6 | spec.version = File.read(File.dirname(__FILE__) + '/VERSION').strip 7 | spec.authors = ['Raoul Felix'] 8 | spec.email = 'gems@rfelix.com' 9 | spec.description = <<-END 10 | jekyll_ext allows you to extend the Jekyll static blog generator without forking 11 | and modifying it's codebase. With this code, not only do your extensions live in 12 | your blog directory, but they can also be shared and reutilized. 13 | END 14 | spec.executables = ['ejekyll'] 15 | spec.add_development_dependency('mg') 16 | spec.files = Dir['lib/*', 'vendor/*', 'bin/*'] + ['Rakefile', 'README.textile'] 17 | spec.homepage = 'http://rfelix.com/2010/01/19/jekyll-extensions-minus-equal-pain/' 18 | end -------------------------------------------------------------------------------- /lib/aop.rb: -------------------------------------------------------------------------------- 1 | if RUBY_VERSION >= "1.9" 2 | require 'continuation' 3 | end 4 | 5 | module AOP 6 | extend self 7 | 8 | # Intercept the +meth_name+ method of +klass+ and execute +block+ before the 9 | # original method. 10 | # +klass+ Class that method to be intercepted belongs to 11 | # +meth_name+ Name of method to be intercepted 12 | # +block+ Code to executed before method, and can receive as parameters: 13 | # 1. the instance that has been intercepted 14 | # 2. the arguments passed to the original method 15 | # 16 | def before(klass, meth_name, &block) 17 | intercept(klass, meth_name, :before, &block) 18 | end 19 | 20 | # Intercept the +meth_name+ method of +klass+ and execute +block+ after the 21 | # original method 22 | # +klass+ Class that method to be intercepted belongs to 23 | # +meth_name+ Name of method to be intercepted 24 | # +block+ Code to executed before method, and can receive as parameters: 25 | # 1. the instance that has been intercepted 26 | # 2. the arguments passed to the original method 27 | # 28 | def after(klass, meth_name, &block) 29 | intercept(klass, meth_name, :after, &block) 30 | end 31 | 32 | # Intercept the +meth_name+ method of +klass+ and execute +block+ before and 33 | # after the original method, but needs explicit calling of a Ruby proc/lambda. 34 | # +klass+ Class that method to be intercepted belongs to 35 | # +meth_name+ Name of method to be intercepted 36 | # +block+ Code to executed before method, and can receive as parameters: 37 | # 1. the instance that has been intercepted 38 | # 2. the arguments passed to the original method 39 | # 3. the proc that, if called, will proceed with the execution of the method 40 | # 4. the proc that, if called, will abort the execution of the method returning 41 | # whatever was passed as arguments to the block 42 | # 43 | def around(klass, meth_name, &block) 44 | intercept(klass, meth_name, :around, &block) 45 | end 46 | 47 | private 48 | 49 | # Use Ruby metaprogramming capabilities to intercept the method only once, making 50 | # it execute the blocks defined for before, after, and around at the correct 51 | # time before, after, or around the calling of the original method. 52 | # +klass+ Class that method to be intercepted belongs to 53 | # +meth_name+ Name of method to be intercepted 54 | # +type+ Type of interception to be made (before, after, or around) 55 | # +block+ Code to executed before/after/around method 56 | # 57 | def intercept(klass, meth_name, type, &block) 58 | orig_name = "aop_orig_#{meth_name}".to_sym 59 | meth_name = meth_name.to_sym 60 | @intercepted_methods ||= Hash.new do |h,k| 61 | # h[class_name] = hash 62 | h[k] = Hash.new do |h,k| 63 | # h[class_name][method_name] = hash 64 | h[k] = Hash.new do |h,k| 65 | # h[class_name][method_name][interception_type] = array 66 | h[k] = [] 67 | end 68 | end 69 | end 70 | 71 | make_interception = !@intercepted_methods[klass].has_key?(meth_name) 72 | @intercepted_methods[klass][meth_name][type] << block 73 | method_chain = @intercepted_methods[klass][meth_name] 74 | 75 | if make_interception 76 | klass.class_eval do 77 | alias_method orig_name, meth_name 78 | define_method(meth_name) do |*args| 79 | method_chain[:before].each { |m| m.call(self, args) } 80 | # The result of the callcc block will either be the last line in the actual 81 | # ruby block, or it will be whatever is passed as arguments when calling the 82 | # +abort_continuation+ proc 83 | callcc do |abort_continuation| 84 | # First lambda in chain is the call to the original method 85 | call_lambda = lambda { send(orig_name, *args) } 86 | method_chain[:around].each do |m| 87 | # Make a chain of lambdas that calls the previouly defined 88 | # lambda, thus creating a chain of around blocks that will 89 | # all finally reach the original method block 90 | prev_call_lambda = call_lambda 91 | call_lambda = lambda { 92 | # If +prev_call_lambda+ is called, the next around block in 93 | # chain until the last one which corresponds to the original method call 94 | # if +abort_continuation+ is called, then this loop is aborted and the 95 | # callcc block returns whatever was passed as an argument to the proc call 96 | m.call(self, args, prev_call_lambda, abort_continuation) 97 | } 98 | end 99 | result = call_lambda.call 100 | method_chain[:after].each { |m| m.call(self, result, args) } 101 | result 102 | end 103 | end 104 | end 105 | end 106 | end 107 | end -------------------------------------------------------------------------------- /specs/aop_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', 'aop', 'aop.rb') 2 | require 'stringio' 3 | 4 | def catch_output 5 | orig = $stdout 6 | fake = StringIO.new 7 | $stdout = fake 8 | begin 9 | yield 10 | ensure 11 | $stdout = orig 12 | end 13 | fake.string 14 | end 15 | 16 | class GuineaPig 17 | end 18 | 19 | def get_output_from_eat 20 | catch_output { GuineaPig.new.eat } 21 | end 22 | 23 | def get_output_from_feed(*food) 24 | catch_output { GuineaPig.new.feed(*food) } 25 | end 26 | 27 | describe AOP do 28 | before(:each) do 29 | # Reset intercepted methods 30 | AOP.instance_variable_set(:@intercepted_methods, nil) 31 | # Clean GuineaPig class 32 | class GuineaPig 33 | def eat 34 | print "food" 35 | "food" 36 | end 37 | 38 | attr_reader :food 39 | def feed(*food) 40 | @food = food 41 | end 42 | end 43 | end 44 | 45 | context "#before" do 46 | it "must display 'eat some food'" do 47 | AOP.before(GuineaPig, :eat) { print "eat some " } 48 | get_output_from_eat.should == "eat some food" 49 | end 50 | 51 | it "must display 'eat some food' with 2 before blocks " do 52 | AOP.before(GuineaPig, :eat) { print "eat " } 53 | AOP.before(GuineaPig, :eat) { print "some " } 54 | get_output_from_eat.should == "eat some food" 55 | end 56 | 57 | it "must have access to the instance and the arguments passed to the original method" do 58 | AOP.before(GuineaPig, :feed) { |instance, args| 59 | args.each { |arg| print "#{arg} " } 60 | raise "food instance variable should be nil" unless instance.food.nil? 61 | } 62 | get_output_from_feed('apple', 'orange').should == "apple orange " 63 | end 64 | end 65 | 66 | context "#after" do 67 | it "must display 'food must be eaten'" do 68 | AOP.after(GuineaPig, :eat) { print " must be eaten" } 69 | get_output_from_eat.should == "food must be eaten" 70 | end 71 | 72 | it "must display 'food must be eaten' with 2 after blocks " do 73 | AOP.after(GuineaPig, :eat) { print " must" } 74 | AOP.after(GuineaPig, :eat) { print " be eaten" } 75 | get_output_from_eat.should == "food must be eaten" 76 | end 77 | 78 | it "must have access to the instance and the arguments passed to the original method" do 79 | AOP.after(GuineaPig, :feed) { |instance, args| 80 | args.each { |arg| print "#{arg} " } 81 | instance.food.each { |food| print "#{food} " } 82 | } 83 | get_output_from_feed('apple', 'orange').should == "apple orange apple orange " 84 | end 85 | end 86 | 87 | context "#around" do 88 | it "must display 'eat food now'" do 89 | AOP.around(GuineaPig, :eat) do |instance, args, proceed| 90 | print "eat " 91 | proceed.call 92 | print " now" 93 | end 94 | get_output_from_eat.should == "eat food now" 95 | end 96 | 97 | it "must display 'must eat food right now' with 2 after blocks" do 98 | AOP.around(GuineaPig, :eat) do |instance, args, proceed| 99 | print "eat " 100 | proceed.call 101 | print " right" 102 | end 103 | 104 | AOP.around(GuineaPig, :eat) do |instance, args, proceed| 105 | print "must " 106 | proceed.call 107 | print " now" 108 | end 109 | 110 | get_output_from_eat.should == "must eat food right now" 111 | end 112 | 113 | it "must return 'food please" do 114 | AOP.around(GuineaPig, :eat) do |instance, args, proceed| 115 | proceed.call + " please" 116 | end 117 | catch_output { GuineaPig.new.eat.should == "food please" } 118 | end 119 | 120 | it "must return 'bring me food pretty please" do 121 | AOP.around(GuineaPig, :eat) do |instance, args, proceed| 122 | "me " + proceed.call + " pretty" 123 | end 124 | 125 | AOP.around(GuineaPig, :eat) do |instance, args, proceed| 126 | "bring " + proceed.call + " please" 127 | end 128 | 129 | catch_output { GuineaPig.new.eat.should == "bring me food pretty please" } 130 | end 131 | 132 | it "must return 'diet' when aborted" do 133 | AOP.around(GuineaPig, :eat) do |instance, args, proceed, abort| 134 | proceed.call 135 | end 136 | 137 | AOP.around(GuineaPig, :eat) do |instance, args, proceed, abort| 138 | abort.call("diet") 139 | proceed.call 140 | end 141 | 142 | GuineaPig.new.eat.should == "diet" 143 | end 144 | 145 | it "must have access to the instance and the arguments passed to the original method" do 146 | AOP.around(GuineaPig, :feed) { |instance, args, proceed, abort| 147 | args.each { |arg| print "#{arg} " } 148 | proceed.call 149 | instance.food.each { |food| print "#{food} " } 150 | } 151 | get_output_from_feed('apple', 'orange').should == "apple orange apple orange " 152 | end 153 | end 154 | end --------------------------------------------------------------------------------