├── .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
--------------------------------------------------------------------------------