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
@@ -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