2007年 10月 31日

n行で書くテンプレートエンジン

io のやつは 50 行ぐらいだった。いい線じゃないですか!! 正規表現ないのに!!! (あるけど標準じゃない)

とかはどうでもよくて、Ruby で 40 行で ERB に似てるテンプレートエンジン (たぶんすごいだめなバグがある気がする)

class MiniERB
	module Util
		ESCAPES = { "&" => "&amp;", "<" => "&lt;" , ">" => "&gt;"}

		def escape(str)
			str.to_s.gsub(/[#{ESCAPES.keys.join}]/o) {|i| ESCAPES[i] }
		end
		module_function :escape
	end

	attr_reader :src

	def initialize(template)
		@src = compile(template)
	end

	def compile(s)
		ret = "_tt_string = ''¥n"
		while m = s.match(/<%([^¥s]*)/)
			ret << "_tt_string << #{m.pre_match.dump}¥n"
			code = m.post_match.match(/(.*?)%>/)
			s = code.post_match
			code = code[1]
			case m[1]
			when "h"
				ret << "_tt_string << MiniERB::Util.escape(#{code})¥n"
			when "="
				ret << "_tt_string << (#{code}).to_s¥n"
			else
				ret << "#{code}¥n"
			end
		end
		ret << "_tt_string << #{s.dump}"
		ret
	end

	def result(binding=TOPLEVEL_BINDING)
		eval(@src, binding)
	end
end

sample

require "time"

entries = [
	{
		:title => "hoge<<<&>>>>>>>>>>",
		:body => "a<a href='aa'>a</a>a",
		:date => Time.now
	},
	{
		:title => "fuaaaaa<<<",
		:body => "hehe, Nice boat.",
		:date => Time.now
	},
]

puts MiniERB.new(DATA.read).result(binding)

__END__
<% entries.each do |entry| %>
<div class="entry">
	<h2><%h entry[:title] %></h2>
	<div class="body"><%= entry[:body] %></div>
	<div class="info">Time: <%= entry[:date].xmlschema %></div>
</div>
<% end %>

%h で htmlescape 済みの。 = で生のデータうめこみ。

できるだけ式展開するバージョン (早くなると思われる) 同じく 40 行

class MiniERBExpressionExpantion
	module Util
		ESCAPES = { "&" => "&amp;", "<" => "&lt;" , ">" => "&gt;"}

		def escape(str)
			str.to_s.gsub(/[#{ESCAPES.keys.join}]/o) {|i| ESCAPES[i] }
		end
		module_function :escape
	end

	attr_reader :src

	def initialize(template)
		@src = compile(template)
	end

	def compile(s)
		ret = "_tt_string = ¥""
		while m = s.match(/<%([^¥s]*)/)
			ret << "¥#{#{m.pre_match.dump}}"
			code = m.post_match.match(/(.*?)%>/)
			s = code.post_match
			code = code[1]
			case m[1]
			when "h"
				ret << #{MiniERB::Util.escape(#{code})}"
			when "="
				ret << #{(#{code}).to_s}"
			else
				ret << "¥"¥n#{code}¥n _tt_string << ¥""
			end
		end
		ret << "¥" << #{s.dump}"
		ret
	end

	def result(binding=TOPLEVEL_BINDING)
		eval(@src, binding)
	end
end


ベンチマーク

require "time"
data = DATA.read
entries = [
	{
		:title => "hoge<<<&>>>>>>>>>>",
		:body => "a<a href='aa'>a</a>a",
		:date => Time.now
	},
	{
		:title => "fuaaaaa<<<",
		:body => "hehe, Nice boat.",
		:date => Time.now
	},
]

require "benchmark"
Benchmark.bmbm do |x|
	x.report("concat") { 100000.times {
		MiniERB.new(data).result(binding)
	} }
	x.report("expexp") { 100000.times {
		MiniERBExpressionExpantion.new(data).result(binding)
	} }
end

__END__
<% entries.each do |entry| %>
<div class="entry">
	<h2><%h entry[:title] %></h2>
	<div class="body"><%= entry[:body] %></div>
	<div class="info">Time: <%= entry[:date].xmlschema %></div>
</div>
<% end %>

Mac 1.8.2 / 1.83GHz CoreDuo

ruby 1.8.2 (2004-12-25) [universal-darwin8.0]
Rehearsal ------------------------------------------
concat  35.230000   0.240000  35.470000 ( 36.472992)
expexp  33.690000   0.220000  33.910000 ( 35.208808)
-------------------------------- total: 69.380000sec

             user     system      total        real
concat  35.220000   0.210000  35.430000 ( 36.348813)
expexp  33.570000   0.180000  33.750000 ( 34.598484)

ubuntu LTS 1.8.4 / Intel(R) Celeron(R) CPU 2.66GHz

ruby 1.8.4 (2005-12-24) [i486-linux]
Rehearsal ------------------------------------------
concat  37.250000   1.040000  38.290000 ( 38.371623)
expexp  36.270000   1.000000  37.270000 ( 42.917439)
-------------------------------- total: 75.560000sec

             user     system      total        real
concat  37.110000   0.980000  38.090000 ( 38.109001)
expexp  35.650000   1.030000  36.680000 ( 36.799591)

ubuntu LTS 1.9.0

ruby 1.9.0 (2007-09-24 patchlevel 0) [i686-linux]
Rehearsal ------------------------------------------
concat  37.910000   0.060000  37.970000 ( 38.100921)
expexp  35.910000   0.010000  35.920000 ( 35.944483)
-------------------------------- total: 73.890000sec

             user     system      total        real
concat  37.870000   0.010000  37.880000 ( 37.909874)
expexp  36.040000   0.020000  36.060000 ( 37.622068)

>

いちおうはやくなったようだ。誤差の範囲すぎる。

(あたまわるくて意味ないベンチした)