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
@@ -0,0 +1,56 @@
1
+ module Temple
2
+ # Temple expression grammar which can be used to validate Temple expressions.
3
+ #
4
+ # Example:
5
+ # Temple::Grammar.match? [:static, 'Valid Temple Expression']
6
+ # Temple::Grammar.validate! [:multi, 'Invalid Temple Expression']
7
+ #
8
+ # See {file:EXPRESSIONS.md Expression documentation}.
9
+ #
10
+ # @api public
11
+ module Grammar
12
+ extend Mixins::GrammarDSL
13
+
14
+ Expression <<
15
+ # Core abstraction
16
+ [:multi, 'Expression*'] |
17
+ [:static, String] |
18
+ [:dynamic, String] |
19
+ [:code, String] |
20
+ [:capture, String, Expression] |
21
+ [:newline] |
22
+ # Control flow abstraction
23
+ [:if, String, Expression, 'Expression?'] |
24
+ [:block, String, Expression] |
25
+ [:case, String, 'Case*'] |
26
+ [:cond, 'Case*'] |
27
+ # Escape abstraction
28
+ [:escape, Bool, Expression] |
29
+ # HTML abstraction
30
+ [:html, :doctype, String] |
31
+ [:html, :comment, Expression] |
32
+ [:html, :tag, HTMLIdentifier, HTMLAttrs, 'Expression?']
33
+
34
+ EmptyExp <<
35
+ [:newline] | [:multi, 'EmptyExp*']
36
+
37
+ HTMLAttrs <<
38
+ Expression | [:html, :attrs, 'HTMLAttr*']
39
+
40
+ HTMLAttr <<
41
+ [:html, :attr, HTMLIdentifier, Expression]
42
+
43
+ HTMLIdentifier <<
44
+ Symbol | String
45
+
46
+ Case <<
47
+ [Condition, 'Expression*']
48
+
49
+ Condition <<
50
+ String | :else
51
+
52
+ Bool <<
53
+ true | false
54
+
55
+ end
56
+ end
@@ -0,0 +1,48 @@
1
+ module Temple
2
+ # Immutable hash class which supports hash merging
3
+ # @api public
4
+ class ImmutableHash
5
+ include Enumerable
6
+
7
+ def initialize(*hash)
8
+ @hash = hash.compact
9
+ end
10
+
11
+ def include?(key)
12
+ @hash.any? {|h| h.include?(key) }
13
+ end
14
+
15
+ def [](key)
16
+ @hash.each {|h| return h[key] if h.include?(key) }
17
+ nil
18
+ end
19
+
20
+ def each
21
+ keys.each {|k| yield(k, self[k]) }
22
+ end
23
+
24
+ def keys
25
+ @hash.inject([]) {|keys, h| keys += h.keys }.uniq
26
+ end
27
+
28
+ def values
29
+ keys.map {|k| self[k] }
30
+ end
31
+ end
32
+
33
+ # Mutable hash class which supports hash merging
34
+ # @api public
35
+ class MutableHash < ImmutableHash
36
+ def initialize(*hash)
37
+ super({}, *hash)
38
+ end
39
+
40
+ def []=(key, value)
41
+ @hash.first[key] = value
42
+ end
43
+
44
+ def update(hash)
45
+ @hash.first.update(hash)
46
+ end
47
+ end
48
+ end
@@ -1,16 +1,22 @@
1
1
  module Temple
2
2
  module HTML
3
+ # @api private
3
4
  module Dispatcher
4
- def on_html_staticattrs(*attrs)
5
- [:html, :staticattrs, *attrs.map {|k,v| [k, compile(v)] }]
5
+ def on_html_attrs(*attrs)
6
+ [:html, :attrs, *attrs.map {|a| compile(a) }]
7
+ end
8
+
9
+ def on_html_attr(name, content)
10
+ [:html, :attr, name, compile(content)]
6
11
  end
7
12
 
8
13
  def on_html_comment(content)
9
14
  [:html, :comment, compile(content)]
10
15
  end
11
16
 
12
- def on_html_tag(name, attrs, closed, content)
13
- [:html, :tag, name, compile(attrs), closed, compile(content)]
17
+ def on_html_tag(name, attrs, content = nil)
18
+ result = [:html, :tag, name, compile(attrs)]
19
+ content ? (result << compile(content)) : result
14
20
  end
15
21
  end
16
22
  end
@@ -1,8 +1,7 @@
1
1
  module Temple
2
2
  module HTML
3
+ # @api public
3
4
  class Fast < Filter
4
- temple_dispatch :html
5
-
6
5
  XHTML_DOCTYPES = {
7
6
  '1.1' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
8
7
  '5' => '<!DOCTYPE html>',
@@ -23,14 +22,14 @@ module Temple
23
22
  set_default_options :format => :xhtml,
24
23
  :attr_wrapper => "'",
25
24
  :autoclose => %w[meta img link br hr input area param col base],
26
- :id_delimiter => '_'
25
+ :attr_delimiter => {'id' => '_', 'class' => ' '}
27
26
 
28
- def initialize(options = {})
27
+ def initialize(opts = {})
29
28
  super
30
29
  # html5 is now called html only
31
- @options[:format] = :html5 if @options[:format] == :html
32
- unless [:xhtml, :html4, :html5].include?(@options[:format])
33
- raise "Invalid format #{@options[:format].inspect}"
30
+ options[:format] = :html5 if options[:format] == :html
31
+ unless [:xhtml, :html4, :html5].include?(options[:format])
32
+ raise "Invalid format #{options[:format].inspect}"
34
33
  end
35
34
  end
36
35
 
@@ -73,58 +72,69 @@ module Temple
73
72
  [:static, '-->']]
74
73
  end
75
74
 
76
- def on_html_tag(name, attrs, closed, content)
77
- closed ||= options[:autoclose].include?(name)
78
- raise "Closed tag #{name} has content" if closed && !empty_exp?(content)
75
+ def on_html_tag(name, attrs, content = nil)
76
+ name = name.to_s
77
+ closed = !content || (empty_exp?(content) && options[:autoclose].include?(name))
79
78
  result = [:multi, [:static, "<#{name}"], compile(attrs)]
80
- result << [:static, ' /'] if closed && xhtml?
81
- result << [:static, '>'] << compile(content)
79
+ result << [:static, (closed && xhtml? ? ' /' : '') + '>']
80
+ result << compile(content) if content
82
81
  result << [:static, "</#{name}>"] if !closed
83
82
  result
84
83
  end
85
84
 
86
- def on_html_staticattrs(*attrs)
85
+ def on_html_attrs(*attrs)
87
86
  result = {}
88
- attrs.each do |name, value|
89
- if result[name] && %w(class id).include?(name)
90
- raise 'Multiple id attributes specified, but id concatenation disabled' if name == 'id' && !options[:id_delimiter]
91
- result[name] = [:multi,
92
- result[name],
93
- [:static, (name == 'class' ? ' ' : options[:id_delimiter])],
94
- value]
87
+ attrs.each do |attr|
88
+ raise(InvalidExpression, 'Attribute is not a html attr') if attr[0] != :html || attr[1] != :attr
89
+ name, value = attr[2].to_s, attr[3]
90
+ next if empty_exp?(value)
91
+ if result[name]
92
+ delimiter = options[:attr_delimiter][name]
93
+ raise "Multiple #{name} attributes specified" unless delimiter
94
+ if contains_static?(value)
95
+ result[name] = [:html, :attr, name,
96
+ [:multi,
97
+ result[name][3],
98
+ [:static, delimiter],
99
+ value]]
100
+ else
101
+ tmp = unique_name
102
+ result[name] = [:html, :attr, name,
103
+ [:multi,
104
+ result[name][3],
105
+ [:capture, tmp, value],
106
+ [:if, "!#{tmp}.empty?",
107
+ [:multi,
108
+ [:static, delimiter],
109
+ [:dynamic, tmp]]]]]
110
+ end
95
111
  else
96
- result[name] = value
112
+ result[name] = attr
97
113
  end
98
114
  end
99
- result.sort.inject([:multi]) do |list, (name, value)|
100
- list << compile_attribute(name, value)
101
- end
115
+ [:multi, *result.sort.map {|name,attr| compile(attr) }]
102
116
  end
103
117
 
104
- protected
105
-
106
- def compile_attribute(name, value)
118
+ def on_html_attr(name, value)
107
119
  if empty_exp?(value)
108
- [:multi]
120
+ compile(value)
109
121
  elsif contains_static?(value)
110
122
  attribute(name, value)
111
123
  else
112
- tmp = tmp_var(:htmlattr)
124
+ tmp = unique_name
113
125
  [:multi,
114
- [:capture, tmp, value],
115
- [:block, "unless #{tmp}.empty?"],
116
- attribute(name, [:dynamic, tmp]),
117
- [:block, 'end']]
126
+ [:capture, tmp, compile(value)],
127
+ [:if, "!#{tmp}.empty?",
128
+ attribute(name, [:dynamic, tmp])]]
118
129
  end
119
130
  end
120
131
 
132
+ protected
133
+
121
134
  def attribute(name, value)
122
135
  [:multi,
123
- [:static, ' '],
124
- [:static, name],
125
- [:static, '='],
126
- [:static, options[:attr_wrapper]],
127
- value,
136
+ [:static, " #{name}=#{options[:attr_wrapper]}"],
137
+ compile(value),
128
138
  [:static, options[:attr_wrapper]]]
129
139
  end
130
140
 
@@ -132,6 +142,8 @@ module Temple
132
142
  case exp[0]
133
143
  when :multi
134
144
  exp[1..-1].any? {|e| contains_static?(e) }
145
+ when :escape
146
+ contains_static?(exp[2])
135
147
  when :static
136
148
  true
137
149
  else
@@ -0,0 +1,8 @@
1
+ module Temple
2
+ module HTML
3
+ # @api public
4
+ class Filter < Temple::Filter
5
+ include Dispatcher
6
+ end
7
+ end
8
+ end
@@ -1,12 +1,13 @@
1
1
  module Temple
2
2
  module HTML
3
+ # @api public
3
4
  class Pretty < Fast
4
5
  set_default_options :indent => ' ',
5
6
  :pretty => true,
6
7
  :indent_tags => %w(base body dd div dl dt fieldset form head h1 h2 h3
7
8
  h4 h5 h6 hr html img input li link meta ol p script
8
9
  style table tbody td tfoot th thead title tr ul).freeze,
9
- :pre_tags => %w(pre textarea).freeze
10
+ :pre_tags => %w(code pre textarea).freeze
10
11
 
11
12
  def initialize(opts = {})
12
13
  super
@@ -22,15 +23,23 @@ module Temple
22
23
 
23
24
  def on_static(content)
24
25
  if @pretty
25
- content = Utils.indent(content, indent, @pre_tags)
26
+ content.gsub!("\n", indent) if @pre_tags !~ content
26
27
  @last = content.sub!(/\r?\n\s*$/, ' ') ? nil : :noindent
27
28
  end
28
29
  [:static, content]
29
30
  end
30
31
 
31
- def on_dynamic(content)
32
- @last = :noindent
33
- [:dynamic, @pretty ? "Temple::Utils.indent((#{content}), #{indent.inspect}, _temple_pre_tags)" : content]
32
+ def on_dynamic(code)
33
+ if @pretty
34
+ @last = :noindent
35
+ tmp = unique_name
36
+ [:multi,
37
+ [:code, "#{tmp} = (#{code}).to_s"],
38
+ [:code, "#{tmp}.gsub!(\"\\n\", #{indent.inspect}) if #{@pre_tags_name} !~ #{tmp}"],
39
+ [:dynamic, tmp]]
40
+ else
41
+ [:dynamic, code]
42
+ end
34
43
  end
35
44
 
36
45
  def on_html_doctype(type)
@@ -44,21 +53,22 @@ module Temple
44
53
  [:multi, [:static, indent], super]
45
54
  end
46
55
 
47
- def on_html_tag(name, attrs, closed, content)
56
+ def on_html_tag(name, attrs, content = nil)
48
57
  return super unless @pretty
49
58
 
50
- closed ||= options[:autoclose].include?(name)
51
- raise "Closed tag #{name} has content" if closed && !empty_exp?(content)
59
+ name = name.to_s
60
+ closed = !content || (empty_exp?(content) && options[:autoclose].include?(name))
52
61
 
53
62
  @pretty = false
54
63
  result = [:multi, [:static, "#{tag_indent(name)}<#{name}"], compile(attrs)]
55
- result << [:static, ' /'] if closed && xhtml?
56
- result << [:static, '>']
64
+ result << [:static, (closed && xhtml? ? ' /' : '') + '>']
57
65
 
58
66
  @pretty = !options[:pre_tags].include?(name)
59
- @indent += 1
60
- result << compile(content)
61
- @indent -= 1
67
+ if content
68
+ @indent += 1
69
+ result << compile(content)
70
+ @indent -= 1
71
+ end
62
72
 
63
73
  result << [:static, "#{tag_indent(name)}</#{name}>"] if !closed
64
74
  @pretty = true
@@ -68,7 +78,8 @@ module Temple
68
78
  protected
69
79
 
70
80
  def preamble
71
- [:block, "_temple_pre_tags = /#{@pre_tags.source}/"]
81
+ @pre_tags_name = unique_name
82
+ [:code, "#{@pre_tags_name} = /#{@pre_tags.source}/"]
72
83
  end
73
84
 
74
85
  # Return indentation if not in pre tag
@@ -0,0 +1,103 @@
1
+ module Temple
2
+ module Mixins
3
+ # @api private
4
+ module CoreDispatcher
5
+ def on_multi(*exps)
6
+ multi = [:multi]
7
+ exps.each {|exp| multi << compile(exp) }
8
+ multi
9
+ end
10
+
11
+ def on_capture(name, exp)
12
+ [:capture, name, compile(exp)]
13
+ end
14
+ end
15
+
16
+ # @api private
17
+ module EscapeDispatcher
18
+ def on_escape(flag, exp)
19
+ [:escape, flag, compile(exp)]
20
+ end
21
+ end
22
+
23
+ # @api private
24
+ module ControlFlowDispatcher
25
+ def on_if(condition, *cases)
26
+ [:if, condition, *cases.compact.map {|e| compile(e) }]
27
+ end
28
+
29
+ def on_case(arg, *cases)
30
+ [:case, arg, *cases.map {|condition, exp| [condition, compile(exp)] }]
31
+ end
32
+
33
+ def on_block(code, content)
34
+ [:block, code, compile(content)]
35
+ end
36
+
37
+ def on_cond(*cases)
38
+ [:cond, *cases.map {|condition, exp| [condition, compile(exp)] }]
39
+ end
40
+ end
41
+
42
+ # @api private
43
+ module CompiledDispatcher
44
+ def call(exp)
45
+ compile(exp)
46
+ end
47
+
48
+ def compile(exp)
49
+ dispatcher(exp)
50
+ end
51
+
52
+ private
53
+
54
+ def case_statement(types)
55
+ code = "type, *args = args\ncase type\n"
56
+ types.each do |name, method|
57
+ code << "when #{name.to_sym.inspect}\n" <<
58
+ (Hash === method ? case_statement(method) : "#{method}(*args)\n")
59
+ end
60
+ code << "else\nexp\nend\n"
61
+ end
62
+
63
+ def dispatcher(exp)
64
+ replace_dispatcher(exp)
65
+ end
66
+
67
+ def replace_dispatcher(exp)
68
+ types = {}
69
+ self.class.instance_methods.each do |method|
70
+ next if method.to_s !~ /^on_(.*)$/
71
+ method_types = $1.split('_')
72
+ (0...method_types.size).inject(types) do |tmp, i|
73
+ raise "Invalid temple dispatcher #{method}" unless Hash === tmp
74
+ if i == method_types.size - 1
75
+ tmp[method_types[i]] = method
76
+ else
77
+ tmp[method_types[i]] ||= {}
78
+ end
79
+ end
80
+ end
81
+ self.class.class_eval %{
82
+ def dispatcher(exp)
83
+ if self.class == #{self.class}
84
+ args = exp
85
+ #{case_statement(types)}
86
+ else
87
+ replace_dispatcher(exp)
88
+ end
89
+ end
90
+ }
91
+ dispatcher(exp)
92
+ end
93
+ end
94
+
95
+ # @api private
96
+ module Dispatcher
97
+ include CompiledDispatcher
98
+ include CoreDispatcher
99
+ include EscapeDispatcher
100
+ include ControlFlowDispatcher
101
+ end
102
+ end
103
+ end