temple 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.yardopts +2 -1
  2. data/CHANGES +63 -0
  3. data/EXPRESSIONS.md +250 -0
  4. data/README.md +24 -12
  5. data/lib/temple.rb +34 -18
  6. data/lib/temple/engine.rb +11 -7
  7. data/lib/temple/erb/engine.rb +5 -3
  8. data/lib/temple/erb/parser.rb +5 -2
  9. data/lib/temple/erb/template.rb +11 -0
  10. data/lib/temple/erb/trimming.rb +9 -1
  11. data/lib/temple/filter.rb +2 -0
  12. data/lib/temple/filters/control_flow.rb +43 -0
  13. data/lib/temple/filters/dynamic_inliner.rb +29 -32
  14. data/lib/temple/filters/eraser.rb +22 -0
  15. data/lib/temple/filters/escapable.rb +39 -0
  16. data/lib/temple/filters/multi_flattener.rb +4 -1
  17. data/lib/temple/filters/static_merger.rb +11 -10
  18. data/lib/temple/filters/validator.rb +15 -0
  19. data/lib/temple/generators.rb +41 -100
  20. data/lib/temple/grammar.rb +56 -0
  21. data/lib/temple/hash.rb +48 -0
  22. data/lib/temple/html/dispatcher.rb +10 -4
  23. data/lib/temple/html/fast.rb +50 -38
  24. data/lib/temple/html/filter.rb +8 -0
  25. data/lib/temple/html/pretty.rb +25 -14
  26. data/lib/temple/mixins/dispatcher.rb +103 -0
  27. data/lib/temple/{mixins.rb → mixins/engine_dsl.rb} +10 -95
  28. data/lib/temple/mixins/grammar_dsl.rb +166 -0
  29. data/lib/temple/mixins/options.rb +28 -0
  30. data/lib/temple/mixins/template.rb +25 -0
  31. data/lib/temple/templates.rb +2 -0
  32. data/lib/temple/utils.rb +11 -57
  33. data/lib/temple/version.rb +1 -1
  34. data/test/filters/test_control_flow.rb +92 -0
  35. data/test/filters/test_dynamic_inliner.rb +7 -7
  36. data/test/filters/test_eraser.rb +55 -0
  37. data/test/filters/{test_escape_html.rb → test_escapable.rb} +13 -6
  38. data/test/filters/test_multi_flattener.rb +1 -1
  39. data/test/filters/test_static_merger.rb +3 -3
  40. data/test/helper.rb +8 -0
  41. data/test/html/test_fast.rb +42 -57
  42. data/test/html/test_pretty.rb +10 -7
  43. data/test/mixins/test_dispatcher.rb +31 -0
  44. data/test/mixins/test_grammar_dsl.rb +86 -0
  45. data/test/test_engine.rb +73 -57
  46. data/test/test_erb.rb +0 -7
  47. data/test/test_filter.rb +26 -0
  48. data/test/test_generator.rb +57 -36
  49. data/test/test_grammar.rb +52 -0
  50. data/test/test_hash.rb +39 -0
  51. data/test/test_utils.rb +11 -38
  52. metadata +34 -10
  53. data/lib/temple/filters/debugger.rb +0 -26
  54. data/lib/temple/filters/escape_html.rb +0 -33
data/lib/temple/engine.rb CHANGED
@@ -20,12 +20,14 @@ module Temple
20
20
  #
21
21
  # class SpecialEngine < MyEngine
22
22
  # append MyCodeOptimizer
23
- # replace Temple::Generators::ArrayBuffer, Temple::Generators::RailsOutputBuffer
23
+ # before :ArrayBuffer, Temple::Filters::Validator
24
+ # replace :ArrayBuffer, Temple::Generators::RailsOutputBuffer
24
25
  # end
25
26
  #
26
27
  # engine = MyEngine.new(:strict => "For MyParser")
27
28
  # engine.call(something)
28
29
  #
30
+ # @api public
29
31
  class Engine
30
32
  include Mixins::Options
31
33
  include Mixins::EngineDSL
@@ -40,24 +42,26 @@ module Temple
40
42
  def initialize(o = {})
41
43
  super
42
44
  @chain = self.class.chain.dup
43
- yield(self) if block_given?
44
45
  [*options[:chain]].compact.each {|block| block.call(self) }
45
- @chain = build_chain
46
46
  end
47
47
 
48
48
  def call(input)
49
- chain.inject(input) {|m, e| e.call(m) }
49
+ call_chain.inject(input) {|m, e| e.call(m) }
50
50
  end
51
51
 
52
52
  protected
53
53
 
54
- def build_chain
55
- chain.map do |e|
54
+ def chain_modified!
55
+ @call_chain = nil
56
+ end
57
+
58
+ def call_chain
59
+ @call_chain ||= @chain.map do |e|
56
60
  name, filter, option_filter, local_options = e
57
61
  case filter
58
62
  when Class
59
63
  filtered_options = Hash[*option_filter.select {|k| options.include?(k) }.map {|k| [k, options[k]] }.flatten]
60
- filter.new(Utils::ImmutableHash.new(local_options, filtered_options))
64
+ filter.new(ImmutableHash.new(local_options, filtered_options))
61
65
  when UnboundMethod
62
66
  filter.bind(self)
63
67
  else
@@ -1,11 +1,13 @@
1
1
  module Temple
2
2
  module ERB
3
+ # Example ERB engine implementation
4
+ #
5
+ # @api public
3
6
  class Engine < Temple::Engine
4
- use Temple::ERB::Parser, :auto_escape
5
- filter :EscapeHTML, :use_html_safe
7
+ use Temple::ERB::Parser
6
8
  use Temple::ERB::Trimming, :trim_mode
9
+ filter :Escapable, :use_html_safe, :disable_escape
7
10
  filter :MultiFlattener
8
- filter :StaticMerger
9
11
  filter :DynamicInliner
10
12
  generator :ArrayBuffer
11
13
  end
@@ -1,5 +1,8 @@
1
1
  module Temple
2
2
  module ERB
3
+ # Example ERB parser
4
+ #
5
+ # @api public
3
6
  class Parser
4
7
  include Mixins::Options
5
8
 
@@ -25,9 +28,9 @@ module Temple
25
28
  when '#'
26
29
  code.count("\n").times { result << [:newline] }
27
30
  when /=/
28
- result << [:escape, indicator.length <= 1 && options[:auto_escape], [:dynamic, code]]
31
+ result << [:escape, indicator.size <= 1, [:dynamic, code]]
29
32
  else
30
- result << [:block, code]
33
+ result << [:code, code]
31
34
  end
32
35
  end
33
36
  end
@@ -0,0 +1,11 @@
1
+ module Temple
2
+ # ERB example implementation
3
+ #
4
+ # Example usage:
5
+ # Temple::ERB::Template.new { "<%= 'Hello, world!' %>" }.render
6
+ #
7
+ module ERB
8
+ # ERB Template class
9
+ Template = Temple::Templates::Tilt(Engine)
10
+ end
11
+ end
@@ -1,5 +1,11 @@
1
1
  module Temple
2
2
  module ERB
3
+ # ERB trimming
4
+ # Set option :trim_mode to
5
+ # <> - omit newline for lines starting with <% and ending in %>
6
+ # > - omit newline for lines ending in %>
7
+ #
8
+ # @api public
3
9
  class Trimming < Filter
4
10
  def on_multi(*exps)
5
11
  case options[:trim_mode]
@@ -21,8 +27,10 @@ module Temple
21
27
  [:multi, *exps]
22
28
  end
23
29
 
30
+ protected
31
+
24
32
  def code?(exp)
25
- exp[0] == :dynamic || exp[0] == :block
33
+ exp[0] == :escape || exp[0] == :code
26
34
  end
27
35
 
28
36
  def static?(exp)
data/lib/temple/filter.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  module Temple
2
+ # Temple base filter
3
+ # @api public
2
4
  class Filter
3
5
  include Utils
4
6
  include Mixins::Dispatcher
@@ -0,0 +1,43 @@
1
+ module Temple
2
+ module Filters
3
+ # Control flow filter which processes [:if, condition, yes-exp, no-exp]
4
+ # and [:block, code, content] expressions.
5
+ # This is useful for ruby code generation with lots of conditionals.
6
+ #
7
+ # @api public
8
+ class ControlFlow < Filter
9
+ def on_if(condition, yes, no = nil)
10
+ result = [:multi, [:code, "if #{condition}"], compile(yes)]
11
+ while no && no.first == :if
12
+ result << [:code, "elsif #{no[1]}"] << compile(no[2])
13
+ no = no[3]
14
+ end
15
+ result << [:code, 'else'] << compile(no) if no
16
+ result << [:code, 'end']
17
+ result
18
+ end
19
+
20
+ def on_case(arg, *cases)
21
+ result = [:multi, [:code, arg ? "case (#{arg})" : 'case']]
22
+ cases.map do |c|
23
+ condition, *exps = c
24
+ result << [:code, condition == :else ? 'else' : "when #{condition}"]
25
+ exps.each {|e| result << compile(e) }
26
+ end
27
+ result << [:code, 'end']
28
+ result
29
+ end
30
+
31
+ def on_cond(*cases)
32
+ on_case(nil, *cases)
33
+ end
34
+
35
+ def on_block(code, exp)
36
+ [:multi,
37
+ [:code, code],
38
+ compile(exp),
39
+ [:code, 'end']]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,24 +1,24 @@
1
1
  module Temple
2
2
  module Filters
3
3
  # Inlines several static/dynamic into a single dynamic.
4
+ #
5
+ # @api public
4
6
  class DynamicInliner < Filter
5
7
  def on_multi(*exps)
6
- res = [:multi]
8
+ result = [:multi]
7
9
  curr = nil
8
10
  prev = []
9
11
  state = :looking
10
12
 
11
- # We add a noop because we need to do some cleanup at the end too.
12
- (exps + [:noop]).each do |exp|
13
- head, arg = exp
13
+ exps.each do |exp|
14
+ type, arg = exp
14
15
 
15
- case head
16
+ case type
16
17
  when :newline
17
- case state
18
- when :looking
18
+ if state == :looking
19
19
  # We haven't found any static/dynamic, so let's just add it
20
- res << exp
21
- when :single, :several
20
+ result << exp
21
+ else
22
22
  # We've found something, so let's make sure the generated
23
23
  # dynamic contains a newline by escaping a newline and
24
24
  # starting a new string:
@@ -31,43 +31,40 @@ module Temple
31
31
  when :dynamic, :static
32
32
  case state
33
33
  when :looking
34
- # Found a single static/dynamic. We don't want to turn this
34
+ # Found a single static/dynamic. We don't want to turn this
35
35
  # into a dynamic yet. Instead we store it, and if we find
36
36
  # another one, we add both then.
37
37
  state = :single
38
38
  prev = [exp]
39
- curr = [:dynamic, '"' + send(head, arg)]
39
+ curr = [:dynamic, '"']
40
40
  when :single
41
- # Yes! We found another one. Append the content to the current
42
- # dynamic and add it to the result.
43
- curr[1] << send(head, arg)
44
- res << curr
41
+ # Yes! We found another one. Add the current dynamic to the result.
45
42
  state = :several
46
- when :several
47
- # Yet another dynamic/single. Just add it now.
48
- curr[1] << send(head, arg)
43
+ result << curr
49
44
  end
45
+ curr[1] << (type == :static ? arg.inspect[1..-2] : "\#{#{arg}}")
50
46
  else
51
- # We need to add the closing quote.
52
- curr[1] << '"' unless state == :looking
53
- # If we found a single exp last time, let's add it.
54
- res.concat(prev) if state == :single
55
- # Compile the current exp (unless it's the noop)
56
- res << compile(exp) unless head == :noop
47
+ if state != :looking
48
+ # We need to add the closing quote.
49
+ curr[1] << '"'
50
+ # If we found a single exp last time, let's add it.
51
+ result.concat(prev) if state == :single
52
+ end
53
+ # Compile the current exp
54
+ result << compile(exp)
57
55
  # Now we're looking for more!
58
56
  state = :looking
59
57
  end
60
58
  end
61
59
 
62
- res
63
- end
64
-
65
- def static(str)
66
- str.inspect[1..-2]
67
- end
60
+ if state != :looking
61
+ # We need to add the closing quote.
62
+ curr[1] << '"'
63
+ # If we found a single exp last time, let's add it.
64
+ result.concat(prev) if state == :single
65
+ end
68
66
 
69
- def dynamic(str)
70
- '#{%s}' % str
67
+ result
71
68
  end
72
69
  end
73
70
  end
@@ -0,0 +1,22 @@
1
+ module Temple
2
+ module Filters
3
+ # Erase expressions with a certain type
4
+ #
5
+ # @api public
6
+ class Eraser < Filter
7
+ # [] is the empty type => keep all
8
+ default_options[:keep] = [[]]
9
+
10
+ def compile(exp)
11
+ exp.first == :multi || (do?(:keep, exp) && !do?(:erase, exp)) ?
12
+ super(exp) : [:multi]
13
+ end
14
+
15
+ protected
16
+
17
+ def do?(list, exp)
18
+ options[list].to_a.map {|type| [*type] }.any? {|type| exp[0,type.size] == type }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ module Temple
2
+ module Filters
3
+ # Escape dynamic or static expressions.
4
+ # This filter must be used after Temple::HTML::* and before the generators.
5
+ # It can be enclosed with Temple::Filters::DynamicInliner filters to
6
+ # reduce calls to Temple::Utils#escape_html.
7
+ #
8
+ # @api public
9
+ class Escapable < Filter
10
+ # Activate the usage of html_safe? if it is available (for Rails 3 for example)
11
+ set_default_options :use_html_safe => ''.respond_to?(:html_safe?),
12
+ :disable_escape => false
13
+
14
+ def initialize(opts = {})
15
+ super
16
+ @escape_code = options[:escape_code] ||
17
+ "Temple::Utils.escape_html#{options[:use_html_safe] ? '_safe' : ''}((%s))"
18
+ @escaper = eval("proc {|v| #{@escape_code % 'v'} }")
19
+ @escape = false
20
+ end
21
+
22
+ def on_escape(flag, exp)
23
+ old = @escape
24
+ @escape = flag && !options[:disable_escape]
25
+ compile(exp)
26
+ ensure
27
+ @escape = old
28
+ end
29
+
30
+ def on_static(value)
31
+ [:static, @escape ? @escaper[value] : value]
32
+ end
33
+
34
+ def on_dynamic(value)
35
+ [:dynamic, @escape ? @escape_code % value : value]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,9 +1,12 @@
1
1
  module Temple
2
2
  module Filters
3
+ # Flattens nested multi expressions
4
+ #
5
+ # @api public
3
6
  class MultiFlattener < Filter
4
7
  def on_multi(*exps)
5
8
  # If the multi contains a single element, just return the element
6
- return compile(exps.first) if exps.length == 1
9
+ return compile(exps.first) if exps.size == 1
7
10
  result = [:multi]
8
11
 
9
12
  exps.each do |exp|
@@ -10,27 +10,28 @@ module Temple
10
10
  #
11
11
  # [:multi,
12
12
  # [:static, "Hello World!"]]
13
+ #
14
+ # @api public
13
15
  class StaticMerger < Filter
14
16
  def on_multi(*exps)
15
- res = [:multi]
16
- curr = nil
17
- state = :looking
17
+ result = [:multi]
18
+ text = nil
18
19
 
19
20
  exps.each do |exp|
20
21
  if exp.first == :static
21
- if state == :looking
22
- res << [:static, (curr = exp[1].dup)]
23
- state = :static
22
+ if text
23
+ text << exp.last
24
24
  else
25
- curr << exp[1]
25
+ text = exp.last.dup
26
+ result << [:static, text]
26
27
  end
27
28
  else
28
- res << compile(exp)
29
- state = :looking unless exp.first == :newline
29
+ result << compile(exp)
30
+ text = nil unless exp.first == :newline
30
31
  end
31
32
  end
32
33
 
33
- res
34
+ result
34
35
  end
35
36
  end
36
37
  end
@@ -0,0 +1,15 @@
1
+ module Temple
2
+ module Filters
3
+ # Validates temple expression with given grammar
4
+ #
5
+ # @api public
6
+ class Validator < Filter
7
+ default_options[:grammar] = Temple::Grammar
8
+
9
+ def compile(exp)
10
+ options[:grammar].validate!(exp)
11
+ exp
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,101 +1,56 @@
1
1
  module Temple
2
- # == The Core Abstraction
2
+ # Exception raised if invalid temple expression is found
3
3
  #
4
- # The core abstraction is what every template evetually should be compiled
5
- # to. Currently it consists of four essential and two convenient types:
6
- # multi, static, dynamic, block, newline and capture.
7
- #
8
- # When compiling, there's two different strings we'll have to think about.
9
- # First we have the generated code. This is what your engine (from Temple's
10
- # point of view) spits out. If you construct this carefully enough, you can
11
- # make exceptions report correct line numbers, which is very convenient.
12
- #
13
- # Then there's the result. This is what your engine (from the user's point
14
- # of view) spits out. It's what happens if you evaluate the generated code.
15
- #
16
- # === [:multi, *sexp]
17
- #
18
- # Multi is what glues everything together. It's simply a sexp which combines
19
- # several others sexps:
20
- #
21
- # [:multi,
22
- # [:static, "Hello "],
23
- # [:dynamic, "@world"]]
24
- #
25
- # === [:static, string]
26
- #
27
- # Static indicates that the given string should be appended to the result.
28
- #
29
- # Example:
30
- #
31
- # [:static, "Hello World"]
32
- # # is the same as:
33
- # _buf << "Hello World"
34
- #
35
- # [:static, "Hello \n World"]
36
- # # is the same as
37
- # _buf << "Hello\nWorld"
38
- #
39
- # === [:dynamic, ruby]
40
- #
41
- # Dynamic indicates that the given Ruby code should be evaluated and then
42
- # appended to the result.
43
- #
44
- # The Ruby code must be a complete expression in the sense that you can pass
45
- # it to eval() and it would not raise SyntaxError.
46
- #
47
- # === [:block, ruby]
48
- #
49
- # Block indicates that the given Ruby code should be evaluated, and may
50
- # change the control flow. Any \n causes a newline in the generated code.
51
- #
52
- # === [:newline]
53
- #
54
- # Newline causes a newline in the generated code, but not in the result.
55
- #
56
- # === [:capture, variable_name, sexp]
57
- #
58
- # Evaluates the Sexp using the rules above, but instead of appending to the
59
- # result, it sets the content to the variable given.
60
- #
61
- # Example:
4
+ # @api public
5
+ class InvalidExpression < RuntimeError
6
+ end
7
+
8
+ # Abstract generator base class
9
+ # Generators should inherit this class and
10
+ # compile the Core Abstraction to ruby code.
62
11
  #
63
- # [:multi,
64
- # [:static, "Some content"],
65
- # [:capture, "foo", [:static, "More content"]],
66
- # [:dynamic, "foo.downcase"]]
67
- # # is the same as:
68
- # _buf << "Some content"
69
- # foo = "More content"
70
- # _buf << foo.downcase
12
+ # @api public
71
13
  class Generator
72
14
  include Mixins::Options
73
15
 
74
16
  default_options[:buffer] = '_buf'
75
17
 
76
18
  def call(exp)
77
- [preamble, compile(exp), postamble].join(' ; ')
19
+ [preamble, compile(exp), postamble].join('; ')
78
20
  end
79
21
 
80
22
  def compile(exp)
81
23
  type, *args = exp
82
- if respond_to?("on_#{type}")
83
- send("on_#{type}", *args)
24
+ method = "on_#{type}"
25
+ if respond_to?(method)
26
+ send(method, *args)
84
27
  else
85
- raise "Generator supports only core expressions - found #{exp.inspect}"
28
+ raise InvalidExpression, "Generator supports only core expressions - found #{exp.inspect}"
86
29
  end
87
30
  end
88
31
 
89
32
  def on_multi(*exp)
90
- exp.map { |e| compile(e) }.join(' ; ')
33
+ exp.map {|e| compile(e) }.join('; ')
91
34
  end
92
35
 
93
36
  def on_newline
94
37
  "\n"
95
38
  end
96
39
 
97
- def on_capture(name, block)
98
- options[:capture_generator].new(:buffer => name).call(block)
40
+ def on_capture(name, exp)
41
+ options[:capture_generator].new(:buffer => name).call(exp)
42
+ end
43
+
44
+ def on_static(text)
45
+ concat(text.inspect)
46
+ end
47
+
48
+ def on_dynamic(code)
49
+ concat(code)
50
+ end
51
+
52
+ def on_code(code)
53
+ code
99
54
  end
100
55
 
101
56
  protected
@@ -115,36 +70,24 @@ module Temple
115
70
  # _buf = []
116
71
  # _buf << "static"
117
72
  # _buf << dynamic
118
- # block do
119
- # _buf << "more static"
120
- # end
121
73
  # _buf.join
122
- class ArrayBuffer < Generator
74
+ #
75
+ # @api public
76
+ class Array < Generator
123
77
  def preamble
124
78
  "#{buffer} = []"
125
79
  end
126
80
 
127
81
  def postamble
128
- "#{buffer} = #{buffer}.join"
129
- end
130
-
131
- def on_static(text)
132
- concat(text.inspect)
133
- end
134
-
135
- def on_dynamic(code)
136
- concat(code)
137
- end
138
-
139
- def on_block(code)
140
- code
82
+ buffer
141
83
  end
142
84
  end
143
85
 
144
- # Just like ArrayBuffer, but doesn't call #join on the array.
145
- class Array < ArrayBuffer
86
+ # Just like Array, but calls #join on the array.
87
+ # @api public
88
+ class ArrayBuffer < Array
146
89
  def postamble
147
- buffer
90
+ "#{buffer} = #{buffer}.join"
148
91
  end
149
92
  end
150
93
 
@@ -153,10 +96,9 @@ module Temple
153
96
  # _buf = ''
154
97
  # _buf << "static"
155
98
  # _buf << dynamic.to_s
156
- # block do
157
- # _buf << "more static"
158
- # end
159
99
  # _buf
100
+ #
101
+ # @api public
160
102
  class StringBuffer < Array
161
103
  def preamble
162
104
  "#{buffer} = ''"
@@ -172,10 +114,9 @@ module Temple
172
114
  # @output_buffer = ActionView::OutputBuffer
173
115
  # @output_buffer.safe_concat "static"
174
116
  # @output_buffer.safe_concat dynamic.to_s
175
- # block do
176
- # @output_buffer << "more static"
177
- # end
178
117
  # @output_buffer
118
+ #
119
+ # @api public
179
120
  class RailsOutputBuffer < StringBuffer
180
121
  set_default_options :buffer_class => 'ActionView::OutputBuffer',
181
122
  :buffer => '@output_buffer',