Created
July 21, 2010 20:49
-
-
Save judofyr/485111 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Example of the implementation | |
class Template | |
module CompileSite; end | |
if RUBY_VERSION < '1.9' | |
def compile(klass, name, source) | |
klass.class_eval <<-RUBY | |
::Template::CompileSite.class_eval do | |
def #{name}(locals) | |
#{source} | |
end | |
end | |
RUBY | |
end | |
else | |
def compile(klass, name, source) | |
klass.class_eval <<-RUBY | |
def self.definer | |
::Template::CompileSite.send(:define_method, #{name.inspect}) do |locals, &blk| | |
(Thread.current[:tilt_blk] ||= []) << blk | |
begin | |
#{source} | |
ensure | |
Thread.current[:tilt_blk].pop | |
end | |
end | |
end | |
definer do |*a, &b| | |
if blk = Thread.current[:tilt_blk].last | |
blk.call(*a, &b) | |
else | |
raise LocalJumpError, "no block given" | |
end | |
end | |
class << self; remove_method :definer end | |
RUBY | |
end | |
end | |
end | |
module Helper | |
A = "A" | |
end | |
class Scope | |
include Template::CompileSite | |
include Helper | |
B = "B" | |
end | |
Template.new.compile(Scope, 'hello', <<-RUBY) | |
if locals.nil? | |
yield | |
else | |
[locals, A, B, yield, hello(nil) { 'Y2' }, yield].join(" ") | |
end | |
RUBY | |
Template::CompileSite.instance_method(:hello) | |
res = Scope.new.hello("L") { "Y1" } | |
if res == "L A B Y1 Y2 Y1" | |
puts "YOU DID IT!" | |
else | |
puts "Failed:", res | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
commit 4addd0777245f1618fca8bbc8114194ac8c5fb64 | |
Author: Magnus Holm <[email protected]> | |
Date: Wed Jul 21 22:47:13 2010 +0200 | |
Add support for constant access | |
diff --git a/lib/tilt.rb b/lib/tilt.rb | |
index e61efe9..c5e5bdf 100644 | |
--- a/lib/tilt.rb | |
+++ b/lib/tilt.rb | |
@@ -188,7 +188,7 @@ module Tilt | |
if scope.respond_to?(method_name) | |
scope.send(method_name, locals, &block) | |
else | |
- compile_template_method(method_name, locals) | |
+ compile_template_method(scope.class, method_name, locals) | |
scope.send(method_name, locals, &block) | |
end | |
else | |
@@ -279,18 +279,109 @@ module Tilt | |
digest = Digest::MD5.hexdigest(parts.join(':')) | |
"__tilt_#{digest}" | |
end | |
- | |
- def compile_template_method(method_name, locals) | |
- source, offset = precompiled(locals) | |
- offset += 1 | |
- CompileSite.module_eval <<-RUBY, eval_file, line - offset | |
- def #{method_name}(locals) | |
- #{source} | |
- end | |
- RUBY | |
- | |
- ObjectSpace.define_finalizer self, | |
- Template.compiled_template_method_remover(CompileSite, method_name) | |
+ | |
+ # Compiling the template is surprisingly hard because: | |
+ # 1) `yield` should work | |
+ # 2) Constants should be relative to the scope | |
+ # | |
+ # Example: | |
+ # | |
+ # class Scope | |
+ # include Tilt::CompileSite | |
+ # Foo = 1 | |
+ # end | |
+ # | |
+ # template = Tilt::ERBTemplate.new { '<%= Foo + yield %>' } | |
+ # template.render(Scope.new) { 1 } | |
+ if RUBY_VERSION < '1.9' | |
+ def compile_template_method(klass, method_name, locals) | |
+ source, offset = precompiled(locals) | |
+ offset += 2 | |
+ | |
+ # In 1.8, #class_eval with a string will change the constant | |
+ # lookup, while #class_eval with a block will not. Therefore, | |
+ # all constants in the source will be relative to `klass`. | |
+ # | |
+ # In 1.9 however, both versions of #class_eval changes the | |
+ # constant lookup, so this won't help here. | |
+ klass.class_eval <<-RUBY, eval_file, line - offset | |
+ ::Tilt::CompileSite.class_eval do | |
+ def #{method_name}(locals) | |
+ #{source} | |
+ end | |
+ end | |
+ RUBY | |
+ | |
+ ObjectSpace.define_finalizer self, | |
+ Template.compiled_template_method_remover(CompileSite, method_name) | |
+ end | |
+ else | |
+ def compile_template_method(klass, method_name, locals) | |
+ source, offset = precompiled(locals) | |
+ definer = "__tilt_definer_#{rand.to_s[2..-1]}" | |
+ offset += 4 | |
+ | |
+ # In 1.9, we'll have to do something a little more complex | |
+ # and ugly. First we'll have to do the #class_eval call | |
+ # in a separate method in order to not capture the template | |
+ # object inside the method we're defining. This will result | |
+ # in a leak, because the method holds a reference to the | |
+ # template object, and therefore the GC won't release it. | |
+ # | |
+ # We use #class_eval with a string in order to change the | |
+ # constant lookup, and #define_method in order to not change | |
+ # it *back* again to Tilt::CompileSite. | |
+ # | |
+ # However, any `yield` inside a block will call an outer method, | |
+ # and *not* the block passed to the block. Therefore we'll have to | |
+ # wrap the #define_method inside another method (a definer) which | |
+ # we call with a custom block. When the template calls `yield`, | |
+ # this is the actual block it's calling. | |
+ # | |
+ # When the method is called, right before any template-specific | |
+ # code is ran, we store the block given to #render in a thread | |
+ # local variable. In our custom block (which is invoked when the | |
+ # template calls `yield`) we then find the block again and invokes | |
+ # that instead. | |
+ # | |
+ # Because during the rendering of a template, there is a possibility | |
+ # that the same template is rendered again, we'll have to make sure | |
+ # the blocks aren't overwritten in the thread local variables. Here | |
+ # I'm storing it in an array which is popped when the rendering is | |
+ # done. Another choice would have been to randomly generate a | |
+ # variable and use that instead, but only a benchmark would tell | |
+ # us if it's actually any faster. | |
+ Template.custom_class_eval klass, <<-RUBY, eval_file, line - offset | |
+ def self.#{definer} | |
+ ::Tilt::CompileSite.send(:define_method, #{method_name.inspect}) do |locals, &blk| | |
+ (Thread.current[:tilt_blk] ||= []) << blk | |
+ begin | |
+ #{source} | |
+ ensure | |
+ Thread.current[:tilt_blk].pop | |
+ end | |
+ end | |
+ end | |
+ | |
+ #{definer} do |*a, &b| | |
+ if blk = Thread.current[:tilt_blk].last | |
+ blk.call(*a, &b) | |
+ else | |
+ raise LocalJumpError, "no block given" | |
+ end | |
+ end | |
+ | |
+ class << self; remove_method #{definer.inspect} end | |
+ RUBY | |
+ | |
+ ObjectSpace.define_finalizer self, | |
+ Template.compiled_template_method_remover(CompileSite, method_name) | |
+ end | |
+ | |
+ # | |
+ def self.custom_class_eval(klass, code, file, line) | |
+ klass.class_eval(code, file, line) | |
+ end | |
end | |
def self.compiled_template_method_remover(site, method_name) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/test/tilt_template_test.rb b/test/tilt_template_test.rb | |
index ec6743f..adf768d 100644 | |
--- a/test/tilt_template_test.rb | |
+++ b/test/tilt_template_test.rb | |
@@ -121,8 +121,13 @@ class TiltTemplateTest < Test::Unit::TestCase | |
assert inst.prepared? | |
end | |
+ module PersonHelper | |
+ CONSTANT2 = "Joe" | |
+ end | |
+ | |
class Person | |
CONSTANT = "Bob" | |
+ include PersonHelper | |
attr_accessor :name | |
def initialize(name) | |
@@ -146,6 +151,11 @@ class TiltTemplateTest < Test::Unit::TestCase | |
assert_equal "Hey Bob!", inst.render(Person.new("Joe")) | |
end | |
+ test "template which accesses an included constant" do | |
+ inst = SourceGeneratingMockTemplate.new { |t| 'Hey #{CONSTANT2}!' } | |
+ assert_equal "Hey Joe!", inst.render(Person.new("Joe")) | |
+ end | |
+ | |
class FastPerson < Person | |
include Tilt::CompileSite | |
end | |
@@ -156,4 +166,9 @@ class TiltTemplateTest < Test::Unit::TestCase | |
# inst = SourceGeneratingMockTemplate.new { |t| 'Hey #{CONSTANT}!' } | |
# assert_equal "Hey Bob!", inst.render(FastPerson.new("Joe")) | |
# end | |
+ # | |
+ # test "template which accesses an included constant with Tilt::CompileSite" do | |
+ # inst = SourceGeneratingMockTemplate.new { |t| 'Hey #{CONSTANT2}!' } | |
+ # assert_equal "Hey Joe!", inst.render(FastPerson.new("Joe")) | |
+ # end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment