temple 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -0
- data/LICENSE +20 -0
- data/README.md +246 -0
- data/Rakefile +16 -16
- data/lib/temple.rb +7 -1
- data/lib/temple/core.rb +89 -41
- data/lib/temple/engine.rb +31 -9
- data/lib/temple/engines/erb.rb +93 -0
- data/lib/temple/filters/dynamic_inliner.rb +26 -6
- data/lib/temple/filters/escapable.rb +12 -5
- data/lib/temple/filters/multi_flattener.rb +27 -0
- data/lib/temple/filters/static_merger.rb +5 -1
- data/lib/temple/generator.rb +60 -6
- data/lib/temple/parsers/erb.rb +63 -15
- data/lib/temple/utils.rb +20 -0
- data/temple.gemspec +5 -40
- data/test/engines/hello.erb +4 -0
- data/test/engines/test_erb.rb +495 -0
- data/test/engines/test_erb_m17n.rb +132 -0
- data/test/filters/test_dynamic_inliner.rb +116 -0
- data/test/filters/test_escapable.rb +28 -0
- data/test/filters/test_static_merger.rb +45 -0
- data/test/helper.rb +21 -0
- data/test/test_generator.rb +122 -0
- metadata +33 -25
- data/README +0 -7
- data/VERSION +0 -1
- data/lib/temple/filters/mustache.rb +0 -70
- data/lib/temple/parsers/mustache.rb +0 -68
- data/spec/dynamic_inliner_spec.rb +0 -79
- data/spec/escapable_spec.rb +0 -24
- data/spec/spec_helper.rb +0 -15
- data/spec/static_merger_spec.rb +0 -27
- data/spec/temple_spec.rb +0 -5
data/lib/temple/engine.rb
CHANGED
@@ -1,13 +1,26 @@
|
|
1
1
|
module Temple
|
2
|
-
# An engine is simply a chain of compilers (that includes
|
3
|
-
# filters and
|
2
|
+
# An engine is simply a chain of compilers (that often includes a parser,
|
3
|
+
# some filters and a generator).
|
4
4
|
#
|
5
|
-
# class
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
5
|
+
# class MyEngine < Temple::Engine
|
6
|
+
# # First run MyParser, passing the :strict option
|
7
|
+
# use MyParser, :strict
|
8
|
+
#
|
9
|
+
# # Then a custom filter
|
10
|
+
# use MyFilter
|
11
|
+
#
|
12
|
+
# # Then some general optimizations filters
|
13
|
+
# filter :MultiFlattener
|
14
|
+
# filter :StaticMerger
|
15
|
+
# filter :DynamicInliner
|
16
|
+
#
|
17
|
+
# # Finally the generator
|
18
|
+
# generator :ArrayBuffer, :buffer
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# engine = MyEngine.new(:strict => "For MyParser")
|
22
|
+
# engine.compile(something)
|
23
|
+
#
|
11
24
|
class Engine
|
12
25
|
def self.filters
|
13
26
|
@filters ||= []
|
@@ -17,21 +30,30 @@ module Temple
|
|
17
30
|
filters << [filter, args, blk]
|
18
31
|
end
|
19
32
|
|
33
|
+
# Shortcut for <tt>use Temple::Parsers::parser</tt>
|
20
34
|
def self.parser(parser, *args, &blk)
|
21
35
|
use(Temple::Parsers.const_get(parser), *args, &blk)
|
22
36
|
end
|
23
37
|
|
38
|
+
# Shortcut for <tt>use Temple::Filters::parser</tt>
|
24
39
|
def self.filter(filter, *args, &blk)
|
25
40
|
use(Temple::Filters.const_get(filter), *args, &blk)
|
26
41
|
end
|
27
42
|
|
43
|
+
# Shortcut for <tt>use Temple::Generator::parser</tt>
|
28
44
|
def self.generator(compiler, *args, &blk)
|
29
45
|
use(Temple::Core.const_get(compiler), *args, &blk)
|
30
46
|
end
|
31
47
|
|
32
48
|
def initialize(options = {})
|
33
49
|
@chain = self.class.filters.map do |filter, args, blk|
|
34
|
-
|
50
|
+
opt = args.last.is_a?(Hash) ? args.pop : {}
|
51
|
+
opt = args.inject(opt) do |memo, ele|
|
52
|
+
memo[ele] = options[ele] if options.has_key?(ele)
|
53
|
+
memo
|
54
|
+
end
|
55
|
+
|
56
|
+
filter.new(opt, &blk)
|
35
57
|
end
|
36
58
|
end
|
37
59
|
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Temple
|
4
|
+
module Engines
|
5
|
+
# An engine which works in-place for ERB:
|
6
|
+
#
|
7
|
+
# require 'temple'
|
8
|
+
#
|
9
|
+
# template = Temple::Engine::ERB.new("<%= 1 + 1 %>")
|
10
|
+
# template.result # => "2"
|
11
|
+
class ERB < ::ERB
|
12
|
+
OriginalERB = ::ERB
|
13
|
+
Optimizers = [Filters::StaticMerger.new, Filters::DynamicInliner.new]
|
14
|
+
|
15
|
+
# The optional _filename_ argument passed to Kernel#eval when the ERB
|
16
|
+
# code is run
|
17
|
+
attr_accessor :filename
|
18
|
+
|
19
|
+
# The Ruby code generated by ERB
|
20
|
+
attr_reader :src
|
21
|
+
|
22
|
+
# The sexp generated by Temple
|
23
|
+
attr_reader :sexp
|
24
|
+
|
25
|
+
# The optimized sexp generated by Temple
|
26
|
+
attr_reader :optimized_sexp
|
27
|
+
|
28
|
+
# Sets the ERB constant to Temple::Engine::ERB
|
29
|
+
#
|
30
|
+
# Example:
|
31
|
+
#
|
32
|
+
# require 'temple'
|
33
|
+
# Temple::Engine::ERB.rock!
|
34
|
+
# ERB == Temple::Engine::ERB
|
35
|
+
def self.rock!
|
36
|
+
Object.send(:remove_const, :ERB)
|
37
|
+
Object.send(:const_set, :ERB, self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Sets the ERB constant back to regular ERB
|
41
|
+
#
|
42
|
+
# Example:
|
43
|
+
#
|
44
|
+
# require 'temple'
|
45
|
+
# original_erb = ERB
|
46
|
+
# Temple::Engine::ERB.rock!
|
47
|
+
# ERB.suck!
|
48
|
+
# ERB == original_erb
|
49
|
+
|
50
|
+
def self.suck!
|
51
|
+
Object.send(:remove_const, :ERB)
|
52
|
+
Object.send(:const_set, :ERB, OriginalERB)
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(str, safe_level = nil, trim_mode = nil, eoutvar = '_erbout', options = {})
|
56
|
+
@safe_level = safe_level
|
57
|
+
@trim_mode = trim_mode
|
58
|
+
@parser = Parsers::ERB.new(:trim_mode => @trim_mode)
|
59
|
+
|
60
|
+
@generator = options[:generator] || Core::ArrayBuffer
|
61
|
+
|
62
|
+
if @generator.is_a?(Class)
|
63
|
+
@generator = @generator.new(:buffer => eoutvar)
|
64
|
+
end
|
65
|
+
|
66
|
+
@sexp = @parser.compile(str)
|
67
|
+
@optimized_sexp = Optimizers.inject(@sexp) { |m, e| e.compile(m) }
|
68
|
+
@src = @generator.compile(@optimized_sexp)
|
69
|
+
|
70
|
+
if str.respond_to?(:encoding)
|
71
|
+
@enc = detect_magic_comment(str) || str.encoding
|
72
|
+
@src.insert(0, "#coding:#{@enc}\n")
|
73
|
+
@src << ".force_encoding(__ENCODING__)"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def percent?
|
78
|
+
@trim_mode.is_a?(String) and @trim_mode.include?("%")
|
79
|
+
end
|
80
|
+
|
81
|
+
def detect_magic_comment(s)
|
82
|
+
if /\A<%#(.*)%>/ =~ s or (percent? and /\A%#(.*)/ =~ s)
|
83
|
+
comment = $1
|
84
|
+
comment = $1 if comment[/-\*-\s*(.*?)\s*-*-$/]
|
85
|
+
if %r"coding\s*[=:]\s*([[:alnum:]\-_]+)" =~ comment
|
86
|
+
enc = $1.sub(/-(?:mac|dos|unix)/i, '')
|
87
|
+
enc = Encoding.find(enc)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -2,6 +2,10 @@ module Temple
|
|
2
2
|
module Filters
|
3
3
|
# Inlines several static/dynamic into a single dynamic.
|
4
4
|
class DynamicInliner
|
5
|
+
def initialize(options = {})
|
6
|
+
@options = {}
|
7
|
+
end
|
8
|
+
|
5
9
|
def compile(exp)
|
6
10
|
exp.first == :multi ? on_multi(*exp[1..-1]) : exp
|
7
11
|
end
|
@@ -9,21 +13,37 @@ module Temple
|
|
9
13
|
def on_multi(*exps)
|
10
14
|
res = [:multi]
|
11
15
|
curr = nil
|
12
|
-
prev =
|
16
|
+
prev = []
|
13
17
|
state = :looking
|
14
18
|
|
15
19
|
# We add a noop because we need to do some cleanup at the end too.
|
16
20
|
(exps + [:noop]).each do |exp|
|
17
21
|
head, arg = exp
|
18
|
-
|
19
|
-
|
22
|
+
|
23
|
+
case head
|
24
|
+
when :newline
|
25
|
+
case state
|
26
|
+
when :looking
|
27
|
+
# We haven't found any static/dynamic, so let's just add it
|
28
|
+
res << exp
|
29
|
+
when :single, :several
|
30
|
+
# We've found something, so let's make sure the generated
|
31
|
+
# dynamic contains a newline by escaping a newline and
|
32
|
+
# starting a new string:
|
33
|
+
#
|
34
|
+
# "Hello "\
|
35
|
+
# "#{@world}"
|
36
|
+
prev << exp
|
37
|
+
curr[1] << "\"\\\n\""
|
38
|
+
end
|
39
|
+
when :dynamic, :static
|
20
40
|
case state
|
21
41
|
when :looking
|
22
42
|
# Found a single static/dynamic. We don't want to turn this
|
23
43
|
# into a dynamic yet. Instead we store it, and if we find
|
24
44
|
# another one, we add both then.
|
25
45
|
state = :single
|
26
|
-
prev = exp
|
46
|
+
prev = [exp]
|
27
47
|
curr = [:dynamic, '"' + send(head, arg)]
|
28
48
|
when :single
|
29
49
|
# Yes! We found another one. Append the content to the current
|
@@ -39,7 +59,7 @@ module Temple
|
|
39
59
|
# We need to add the closing quote.
|
40
60
|
curr[1] << '"' unless state == :looking
|
41
61
|
# If we found a single exp last time, let's add it.
|
42
|
-
res
|
62
|
+
res.concat(prev) if state == :single
|
43
63
|
# Compile the current exp (unless it's the noop)
|
44
64
|
res << compile(exp) unless head == :noop
|
45
65
|
# Now we're looking for more!
|
@@ -51,7 +71,7 @@ module Temple
|
|
51
71
|
end
|
52
72
|
|
53
73
|
def static(str)
|
54
|
-
str
|
74
|
+
Generator.to_ruby(str)[1..-2]
|
55
75
|
end
|
56
76
|
|
57
77
|
def dynamic(str)
|
@@ -1,17 +1,24 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
1
3
|
module Temple
|
2
4
|
module Filters
|
3
5
|
class Escapable
|
4
6
|
def initialize(options = {})
|
5
|
-
@escaper = options[:escaper] || 'CGI.escapeHTML(%s.to_s)'
|
7
|
+
@escaper = options[:escaper] || 'CGI.escapeHTML((%s).to_s)'
|
6
8
|
end
|
7
9
|
|
8
10
|
def compile(exp)
|
9
11
|
return exp if !exp.is_a?(Enumerable) || exp.is_a?(String)
|
10
12
|
|
11
|
-
if
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
if is_escape?(exp)
|
14
|
+
case exp[1][0]
|
15
|
+
when :static
|
16
|
+
[:static, eval(@escaper % exp[1][1].inspect)]
|
17
|
+
when :dynamic, :block
|
18
|
+
[exp[1][0], @escaper % exp[1][1]]
|
19
|
+
else
|
20
|
+
raise "Escapable can only handle :static, :dynamic and :block for the moment."
|
21
|
+
end
|
15
22
|
else
|
16
23
|
exp.map { |e| compile(e) }
|
17
24
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Temple
|
2
|
+
module Filters
|
3
|
+
class MultiFlattener
|
4
|
+
def initialize(options = {})
|
5
|
+
@options = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def compile(exp)
|
9
|
+
return exp unless exp.first == :multi
|
10
|
+
# If the multi contains a single element, just return the element
|
11
|
+
return compile(exp[1]) if exp.length == 2
|
12
|
+
result = [:multi]
|
13
|
+
|
14
|
+
exp[1..-1].each do |e|
|
15
|
+
e = compile(e)
|
16
|
+
if e.first == :multi
|
17
|
+
result.concat(e[1..-1])
|
18
|
+
else
|
19
|
+
result << e
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -11,6 +11,10 @@ module Temple
|
|
11
11
|
# [:multi,
|
12
12
|
# [:static, "Hello World!"]]
|
13
13
|
class StaticMerger
|
14
|
+
def initialize(options = {})
|
15
|
+
@options = {}
|
16
|
+
end
|
17
|
+
|
14
18
|
def compile(exp)
|
15
19
|
exp.first == :multi ? on_multi(*exp[1..-1]) : exp
|
16
20
|
end
|
@@ -30,7 +34,7 @@ module Temple
|
|
30
34
|
end
|
31
35
|
else
|
32
36
|
res << compile(exp)
|
33
|
-
state = :looking
|
37
|
+
state = :looking unless exp.first == :newline
|
34
38
|
end
|
35
39
|
end
|
36
40
|
|
data/lib/temple/generator.rb
CHANGED
@@ -4,29 +4,83 @@ module Temple
|
|
4
4
|
:buffer => "_buf"
|
5
5
|
}
|
6
6
|
|
7
|
+
CONCATABLE = [:static, :dynamic]
|
8
|
+
|
7
9
|
def initialize(options = {})
|
8
10
|
@options = DEFAULT_OPTIONS.merge(options)
|
11
|
+
@in_multi = false
|
9
12
|
end
|
10
13
|
|
11
14
|
def compile(exp)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
res = send("on_#{exp.first}", *exp[1..-1])
|
16
|
+
|
17
|
+
if @in_multi && CONCATABLE.include?(exp.first)
|
18
|
+
concat(res)
|
19
|
+
else
|
20
|
+
res
|
21
|
+
end
|
17
22
|
end
|
18
23
|
|
19
24
|
def buffer(str = '')
|
20
25
|
@options[:buffer] + str
|
21
26
|
end
|
22
27
|
|
28
|
+
def self.to_ruby(str)
|
29
|
+
str.inspect.gsub(/(\\r)?\\n/m) do |str|
|
30
|
+
if $`[-1] == ?\\
|
31
|
+
str
|
32
|
+
elsif $1
|
33
|
+
"\\n"
|
34
|
+
else
|
35
|
+
"\n"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_ruby(str)
|
41
|
+
Generator.to_ruby(str)
|
42
|
+
end
|
43
|
+
|
23
44
|
# Sensible defaults
|
24
45
|
|
25
46
|
def preamble; '' end
|
26
47
|
def postamble; '' end
|
48
|
+
def concat(s) buffer " << (#{s})" end
|
27
49
|
|
28
50
|
def on_multi(*exp)
|
29
|
-
|
51
|
+
if @in_multi
|
52
|
+
exp.map { |e| compile(e) }
|
53
|
+
else
|
54
|
+
@in_multi = true
|
55
|
+
content = exp.map { |e| compile(e) }
|
56
|
+
content = [preamble, content, postamble].flatten
|
57
|
+
@in_multi = false
|
58
|
+
content
|
59
|
+
end.join(" ; ")
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_newline
|
63
|
+
"\n"
|
64
|
+
end
|
65
|
+
|
66
|
+
def on_capture(name, block)
|
67
|
+
unless @in_multi
|
68
|
+
# We always need the preamble/postamble in capture.
|
69
|
+
return compile([:multi, [:capture, name, block]])
|
70
|
+
end
|
71
|
+
|
72
|
+
@in_multi = false
|
73
|
+
prev_buffer, @options[:buffer] = @options[:buffer], name.to_s
|
74
|
+
content = compile(block)
|
75
|
+
@in_multi = true
|
76
|
+
|
77
|
+
if CONCATABLE.include?(block.first)
|
78
|
+
"#{name} = #{content}"
|
79
|
+
else
|
80
|
+
content
|
81
|
+
end
|
82
|
+
ensure
|
83
|
+
@options[:buffer] = prev_buffer
|
30
84
|
end
|
31
85
|
end
|
32
86
|
end
|
data/lib/temple/parsers/erb.rb
CHANGED
@@ -1,26 +1,74 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
1
3
|
module Temple
|
2
4
|
module Parsers
|
3
|
-
#
|
4
|
-
# It's crappy, but it works for now.
|
5
|
+
# Parses ERB exactly the same way as erb.rb.
|
5
6
|
class ERB
|
7
|
+
Compiler = ::ERB::Compiler
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@compiler = Compiler.new(options[:trim_mode])
|
11
|
+
end
|
12
|
+
|
6
13
|
def compile(src)
|
14
|
+
if src.respond_to?(:encoding) && src.encoding.dummy?
|
15
|
+
raise ArgumentError, "#{src.encoding} is not ASCII compatible"
|
16
|
+
end
|
17
|
+
|
7
18
|
result = [:multi]
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
19
|
+
|
20
|
+
content = ''
|
21
|
+
scanner = @compiler.make_scanner(src)
|
22
|
+
scanner.scan do |token|
|
23
|
+
next if token.nil?
|
24
|
+
next if token == ''
|
25
|
+
if scanner.stag.nil?
|
26
|
+
case token
|
27
|
+
when Compiler::PercentLine
|
28
|
+
result << [:static, content] if content.size > 0
|
29
|
+
content = ''
|
30
|
+
result << [:block, token.to_s.strip]
|
31
|
+
result << [:newline]
|
32
|
+
when :cr
|
33
|
+
result << [:newline]
|
34
|
+
when '<%', '<%=', '<%#'
|
35
|
+
scanner.stag = token
|
36
|
+
when "\n"
|
37
|
+
content << "\n"
|
38
|
+
result << [:static, content]
|
39
|
+
content = ''
|
40
|
+
when '<%%'
|
41
|
+
result << [:static, '<%']
|
42
|
+
else
|
43
|
+
result << [:static, token]
|
44
|
+
end
|
17
45
|
else
|
18
|
-
|
46
|
+
case token
|
47
|
+
when '%>'
|
48
|
+
case scanner.stag
|
49
|
+
when '<%'
|
50
|
+
if content[-1] == ?\n
|
51
|
+
content.chop!
|
52
|
+
result << [:block, content]
|
53
|
+
result << [:newline]
|
54
|
+
else
|
55
|
+
result << [:block, content]
|
56
|
+
end
|
57
|
+
when '<%='
|
58
|
+
result << [:dynamic, content]
|
59
|
+
when '<%#'
|
60
|
+
# nothing
|
61
|
+
end
|
62
|
+
scanner.stag = nil
|
63
|
+
content = ''
|
64
|
+
when '%%>'
|
65
|
+
content << '%>'
|
66
|
+
else
|
67
|
+
content << token
|
68
|
+
end
|
19
69
|
end
|
20
|
-
result << [head, text]
|
21
|
-
src = $'
|
22
70
|
end
|
23
|
-
|
71
|
+
|
24
72
|
result
|
25
73
|
end
|
26
74
|
end
|