-
-
Save judofyr/485811 to your computer and use it in GitHub Desktop.
## Ruby Quiz #666 | |
module CompileSite; end | |
def compile(name, source) | |
CompileSite.class_eval <<-RUBY | |
def #{name}(locals) | |
Thread.current[:tilt_vars] = [self, locals] | |
class << self | |
this, locals = Thread.current[:tilt_vars] | |
this.instance_eval do | |
#{source} | |
end | |
end | |
end | |
RUBY | |
end | |
def render(name, scope, locals, &blk) | |
scope.__send__(name, locals, &blk) | |
end | |
module Helper | |
A = "A" | |
end | |
class Scope | |
include CompileSite | |
include Helper | |
B = "B" | |
end | |
class ScopeB < Scope | |
B = 'C' | |
end | |
compile('hello', <<-RUBY) | |
if locals.nil? | |
yield | |
else | |
[locals, A, B, yield, render(:hello, self, nil) { 'Y2' }, yield, locals].join(" ") | |
end | |
RUBY | |
CompileSite.instance_method(:hello) | |
res = render(:hello, Scope.new, "L") { "Y1" } | |
res2 = render(:hello, ScopeB.new, "L") { "Y1" } | |
if res == "L A B Y1 Y2 Y1 L" and res2 == "L A C Y1 Y2 Y1 L" | |
puts "YOU DID IT!" | |
else | |
puts "Failed:", res, res2 | |
end |
As far as defining a method on each scope class, for the most part, in real world scenarios, that wouldn't bother me, it'd get defined for each 'controller' that renders a given template. To me that's relatively reasonable, although I can see for something that was partial heavy, this could bloat.
I think this latest solution using instance eval in the metaclass is the better one out of the lot. The problem is, it bugs out on JRuby, RBX, and 1.9.1. JRuby has issues with ScopeB, it looks up Scope::B instead of ScopeB::B. RBX and 1.9.1 raise local jump errors because the yield context is dropped in the metaclass. I get the feeling Evans going to hate this... ;-)
Interesting stuff :-)
I filed some bugs:
I should also mention that it currently defines different methods depending on the locals you pass in. It actually "unrolls" the locals so the source actually looks like this:
foo = locals[:foo]
bar = locals[:bar]
#{original_source}
So if we define the method directly on the scope, it means that a total of L * S methods will be defined (where L is the number of variations of lvars and S is the number of variations of scope).
Yeah, I had a feeling you'd simplified that from the original. That's fine... :-)
Good work on the bug reports!
JRuby is weird. This works:
class Base
class_eval <<-RUBY
def hello
class << self
yield
end
end
RUBY
end
p Base.new.hello { 123 }
But this does not:
class Base
#class_eval <<-RUBY
def hello
class << self
yield
end
end
#RUBY
end
p Base.new.hello { 123 }
Yay! The bugs in Rubinius are now closed :-)
awesome
Merged. Passes all tests on 1.8.7. I'm pulling down the latest 1.9.1 now. Review appreciated.
(I've updated the gist so it's closer to the original Tilt code)
You'll have to look at this in the context of template engines:
A fast template engine will compile the template into plain Ruby. For instance:
Hello <%= world %>
could be compiled into"Hello #{world}"
. In order to evaluate such a template you could have something like this:This passes every test above. BUT: it's quite slow because Ruby has to parse the string every time. This can be fixed by defining it as method instead (and then render will call that method). But we still want it to be able to pass in any scope you want:
render("hello")
,render(Request.new)
andrender(Object.new)
should still work. In order to do that we had two choices: define the method under Object (which is available everywhere) or define the method under Tilt::CompileSite (which you can include if you want fast rendering). And we don't want to pollute Object with tons of methods.Currently, render basically looks like this:
This means that if you simply
include Tilt::CompileSite
in your class,render(YourClass.new)
will evaluate several times faster.So even though we define the method under Tilt::CompileSite, we want it to work like it was defined under whatever scope you pass in to #render. My first patch to fix the constant lookup wasn't quite optimal, because it ran the code under the constant scope of the first render call, so
render(Foo.new) + render(Bar.new)
would both use Foo's constant scope. It wasn't perfect, but it was certainly better than having constant lookup relative to Tilt::CompileSite.Your suggestion to define the method under each scope has a few disadvantages:
Object.new
it will actually define the method on Object and thus pollute the namespace.