temple 0.2.0 → 0.3.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.
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',