judofyr / Parkaby
Programming Languages
~ ~ NOTE~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
I'm just pushing the latest changes out before I'll go on holiday, so at the moment it's not working 100% correctly.
If you still want to play around you could: 1) git checkout 0797b2a253e40103c8132374996c1dc9ed5aa4aa (old version) 2) clone SexpTemplate and SexpBuilder (see github.com/judofyr) and put them in the load path at the top of lib/parkaby.rb
I'll release it as a gem when I'll get back home.
//Magnus Holm
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
= Parkaby, ParseTree meets Markaby
In the beginning God created ERB. And ERB was waste and void;
and darkness was upon the face of the deep: and the Spirit of
God moved upon the face of the templates. And God said, Let
there be Ruby: and there was Markaby.
~~ Genesis 1:1-3, The Template Engine Bible
Ruby is nice. I think that's something we all can agree on. When you suddenly need to output some HTML in the middle of your app, it's very convenient to continue writing Ruby, instead of switching to String and interpolation. That's where Markaby comes in:
mab { html { head { title "happy title" } body { h1 "happy heading" a "a link", "href" => "url" } } }
Of course, we all know that Ruby is slow. Needless to say, that makes Markaby slow too. Not even Tagz, Ara T Howard's fast Markaby-clone, can't stand a chance against Erubis and Haml. It's time we fight back. Let's show them that pure, readable Ruby can get on par with percent signs and forced indentations:
Parkaby { html { head { title "happy title" } body { h1 "happy heading" a "a link", "href" => "url" } } }
See, no changes at all, but take a look at the benchmark:
~> ruby bench/run.rb simple 10000
user system total real
Erubis 0.030000 0.000000 0.030000 ( 0.022264) Haml 0.110000 0.000000 0.110000 ( 0.117887) Parkaby (def_method) 0.130000 0.000000 0.130000 ( 0.135996) Parkaby (render) 0.150000 0.010000 0.160000 ( 0.150680) Parkaby (inline) 0.970000 0.000000 0.970000 ( 0.988010) Tagz 3.250000 0.040000 3.290000 ( 3.400699) Markaby 12.610000 0.140000 12.750000 ( 13.067794)
Okay, with all respect: this is a really crappy benchmark. Luckily, the Haml guys have written a very nasty template:
~> ruby bench/run.rb nasty 500
user system total real
Erubis 0.190000 0.010000 0.200000 ( 0.198487) Parkaby (def_method) 0.350000 0.000000 0.350000 ( 0.363106) Parkaby (render) 0.360000 0.010000 0.370000 ( 0.365007) Parkaby (inline) 0.570000 0.000000 0.570000 ( 0.614286) Haml 2.490000 0.030000 2.520000 ( 2.620025) Tagz 5.100000 0.060000 5.160000 ( 5.394778) Markaby 7.220000 0.090000 7.310000 ( 7.630588)
That's more like it!
It's still not a truly fair comparision though. Both Parkaby, Tagz and Markaby escapes all input, while in Erubis and Haml you'll need to explicitly mark it where needed. In the nasty template, nearly nothing needs to be escaped, so Parkaby, Tagz and Markaby are doing a lot of escaping for nothing!
~ ~ UPDATE ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
Nathan Weizenbaum reports in: "You say in the Parkaby docs that in Haml you need to manually mark escaping, but that's not true. Haml supports an option (:escape_html) that makes it default to escaping all input."
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
In a real-life scenario you want to escape nearly everything which comes from the user, so I'm still looking for a better template to run benchmarks on.
= Synopsis
require 'lib/parkaby'
before = self
inline
Parkaby {
self == before # => true
self << "<!DOCTYPE html>"
# or: text "<!DOCTYPE html>"
html {
head { something } # tag unless self.respond_to?(:head)
tag.head { something } # force a tag
self.head { something } # force a method call
span "<em>Escape!</em>" # => "<span><em>Escape!</em></span>"
span { "<em>No escape!</em>" } # => "<span><em>No escape!</em></span>"
div {
strong "Ruby!" # You can't mix tags
"Silent." # and return values in blocks
} # => "<div><strong>Ruby!</strong></div>"
a "something", :href => 'http://google.com'
a(:href => 'http://google.com') { "something else" }
}
} # => lots of html
Using Parkaby::Template
temp = Parkaby::Template.string('strong self')
= Parkaby::Template.block { strong self }
ctemp = temp.compile(Helpers) ctemp.render("Ruby!") # => "Ruby!"
temp.def_method(String, :strong)
"Ruby!".strong # => "Ruby!"
= More
The easiest way to use Parkaby is simply to pass in a block:
Parkaby { html { ... } } # => "..."
Let's however take a look at how to use Parkaby::Template:
temp = Parkaby::Template.string('html { ... }') temp = Parkaby::Template.block { html { ... } }
After you got a Parkaby::Template, you'll have to compile it to a Parkaby::CompiledTemplate. There are two types of a compiled template: One that stores the template as a string, and one that stores it as a proc. The latter is quite a lot faster the former, but if you're going to evaluate the template under a binding, you'll have to use a string.
You also have to give it a helper object when you compile it. This makes sure it won't turn methods into HTML-blocks if they exist on the helper object. (You can however always prepend the method with "self." to force a method call.)
ctemp = temp.compile_as_string(helper || binding) ctemp = temp.compile_as_proc(helper || binding)
temp.compile(binding) == temp.compile_as_string(binding) temp.compile(helper) == temp.compile_as_proc(helper)
After you've compiled it, you just call #render with the object that you want
to be self
, or the binding it should be called with. Notice that both
compile_as_proc and compile_as_string supports both bindings and regular
objects as arguments (same goes for their #render). They're smart enough to
convert it the way they want it.
You can also use Template#def_method to define it as a method on an object:
temp.def_method(String, :cool) "awesome".cool
obj = Object.new temp.def_method(obj, :cool) obj.cool
Use :instance_eval to make it a class method on a class
temp.def_method(String, :cool, :instance_eval) String.cool
= How
It's quite obvious that Parkaby is fast. How? The secret ingredient is: ParseTree! ParseTree turns this:
html { head { title "happy title" } body { h1 "happy heading" a "a link", "href" => "url" } }
into this:
s(:iter, s(:call, nil, :html, s(:arglist)), nil, s(:block, s(:iter, s(:call, nil, :head, s(:arglist)), nil, s(:call, nil, :title, s(:arglist, s(:str, "happy title")))), s(:iter, s(:call, nil, :body, s(:arglist)), nil, s(:block, s(:call, nil, :h1, s(:arglist, s(:str, "happy heading"))), s(:call, nil, :a, s(:arglist, s(:str, "a link"), s(:hash, s(:str, "href"), s(:str, "url"))))))))
then Parkaby::Processor turns it into this:
s(:parkaby,
:begin,
s(:parkaby,
:blocktag,
:html,
s(:block,
s(:parkaby,
:blocktag,
:head,
s(:parkaby, :tag, :title, s(:str, "happy title"), nil),
nil),
s(:parkaby,
:blocktag,
:body,
s(:block,
s(:parkaby, :tag, :h1, s(:str, "happy heading"), nil),
s(:parkaby,
:tag,
:a,
s(:str, "a link"),
s(:hash, s(:str, "href"), s(:str, "url")))),
nil)),
nil))
and Parkaby::Generator turns that into this:
_parkaby_buffer = [_parkaby_current = []] _parkaby_current << "" _parkaby_buffer << (_parkaby_current = []) _parkaby_value = begin (_parkaby_current << "
" _parkaby_buffer << (_parkaby_current = []) _parkaby_value = begin _parkaby_current << "<title>happy title</title>" end _parkaby_current << _parkaby_value if _parkaby_current.empty? _parkaby_current << '' _parkaby_current << "" _parkaby_buffer << (_parkaby_current = []) _parkaby_value = begin (_parkaby_current << "" _parkaby_current << "<a href="url">a link" ) end _parkaby_current << _parkaby_value if _parkaby_current.empty? _parkaby_current << '' ) end _parkaby_current << _parkaby_value if _parkaby_current.empty? _parkaby_current << '' _parkaby_buffer.join happy heading
In this specific example you can clearly see that we still have quite a few optimizations left until it's perfect, but it's still 23 times faster than Markaby's:
mab { html { head { title "happy title" } body { h1 "happy heading" a "a link", "href" => "url" } } }
= Notes
- Parkaby doesn't change
self
at all. - It's smart (maybe too smart):
- Parkaby { li "I", "got", "three arguments" } # => NoMethodError
- Parkaby { li({:hash => :fist}, "content after")} # => NoMethodError
- Parkaby { self.li } # => NoMethodError
- Parkaby { li } # => "
- "
- def li end; Parkaby { li } # => ""
follow.some_method
it fetches the method definition and inlines it in the main template.
Yes, this is very experimental and I don't think anyone is ever going to use
it.