Skip to content

Instantly share code, notes, and snippets.

@judofyr
Created July 21, 2010 20:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save judofyr/485111 to your computer and use it in GitHub Desktop.
Save judofyr/485111 to your computer and use it in GitHub Desktop.
# 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
commit 4addd0777245f1618fca8bbc8114194ac8c5fb64
Author: Magnus Holm <judofyr@gmail.com>
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)
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