temple 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|