curly-templates 1.0.1 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,8 @@
1
1
  require 'curly/scanner'
2
+ require 'curly/component_compiler'
3
+ require 'curly/component_parser'
2
4
  require 'curly/error'
3
- require 'curly/invalid_reference'
5
+ require 'curly/invalid_component'
4
6
  require 'curly/incorrect_ending_error'
5
7
  require 'curly/incomplete_block_error'
6
8
 
@@ -9,7 +11,7 @@ module Curly
9
11
  # Compiles Curly templates into executable Ruby code.
10
12
  #
11
13
  # A template must be accompanied by a presenter class. This class defines the
12
- # references that are valid within the template.
14
+ # components that are valid within the template.
13
15
  #
14
16
  class Compiler
15
17
  # Compiles a Curly template to Ruby code.
@@ -17,7 +19,7 @@ module Curly
17
19
  # template - The template String that should be compiled.
18
20
  # presenter_class - The presenter Class.
19
21
  #
20
- # Raises InvalidReference if the template contains a reference that is not
22
+ # Raises InvalidComponent if the template contains a component that is not
21
23
  # allowed.
22
24
  # Raises IncorrectEndingError if a conditional block is not ended in the
23
25
  # correct order - the most recent block must be ended first.
@@ -28,7 +30,7 @@ module Curly
28
30
  end
29
31
 
30
32
  # Whether the Curly template is valid. This includes whether all
31
- # references are available on the presenter class.
33
+ # components are available on the presenter class.
32
34
  #
33
35
  # template - The template String that should be validated.
34
36
  # presenter_class - The presenter Class.
@@ -42,10 +44,11 @@ module Curly
42
44
  false
43
45
  end
44
46
 
45
- attr_reader :template, :presenter_class
47
+ attr_reader :template
46
48
 
47
49
  def initialize(template, presenter_class)
48
- @template, @presenter_class = template, presenter_class
50
+ @template = template
51
+ @presenter_classes = [presenter_class]
49
52
  end
50
53
 
51
54
  def compile
@@ -67,6 +70,7 @@ module Curly
67
70
 
68
71
  <<-RUBY
69
72
  buffer = ActiveSupport::SafeBuffer.new
73
+ presenters = []
70
74
  #{parts.join("\n")}
71
75
  buffer
72
76
  RUBY
@@ -74,74 +78,100 @@ module Curly
74
78
 
75
79
  private
76
80
 
77
- def compile_block_start(reference)
78
- compile_conditional_block "if", reference
81
+ def presenter_class
82
+ @presenter_classes.last
79
83
  end
80
84
 
81
- def compile_inverse_block_start(reference)
82
- compile_conditional_block "unless", reference
85
+ def compile_conditional_block_start(component)
86
+ compile_conditional_block "if", component
83
87
  end
84
88
 
85
- def compile_conditional_block(keyword, reference)
86
- m = reference.match(/\A(.+?)(?:\.(.+))?\?\z/)
87
- method, argument = "#{m[1]}?", m[2]
89
+ def compile_inverse_conditional_block_start(component)
90
+ compile_conditional_block "unless", component
91
+ end
88
92
 
89
- @blocks.push reference
93
+ def compile_collection_block_start(component)
94
+ name, identifier, attributes = ComponentParser.parse(component)
95
+ method_call = ComponentCompiler.compile_component(presenter_class, name, identifier, attributes)
90
96
 
91
- unless presenter_class.method_available?(method.to_sym)
92
- raise Curly::InvalidReference.new(method.to_sym)
93
- end
97
+ as = name.singularize
98
+ counter = "#{as}_counter"
94
99
 
95
- if presenter_class.instance_method(method).arity == 1
96
- <<-RUBY
97
- #{keyword} presenter.#{method}(#{argument.inspect})
98
- RUBY
99
- else
100
- <<-RUBY
101
- #{keyword} presenter.#{method}
102
- RUBY
100
+ begin
101
+ item_presenter_class = presenter_class.presenter_for_name(as)
102
+ rescue NameError
103
+ raise Curly::Error,
104
+ "cannot enumerate `#{component}`, could not find matching presenter class"
103
105
  end
106
+
107
+ push_block(name, identifier)
108
+ @presenter_classes.push(item_presenter_class)
109
+
110
+ <<-RUBY
111
+ presenters << presenter
112
+ items = Array(#{method_call})
113
+ items.each_with_index do |item, index|
114
+ item_options = options.merge(:#{as} => item, :#{counter} => index + 1)
115
+ presenter = #{item_presenter_class}.new(self, item_options)
116
+ RUBY
104
117
  end
105
118
 
106
- def compile_block_end(reference)
107
- last_block = @blocks.pop
119
+ def compile_conditional_block(keyword, component)
120
+ name, identifier, attributes = ComponentParser.parse(component)
121
+ method_call = ComponentCompiler.compile_conditional(presenter_class, name, identifier, attributes)
108
122
 
109
- unless last_block == reference
110
- raise Curly::IncorrectEndingError.new(reference, last_block)
111
- end
123
+ push_block(name, identifier)
124
+
125
+ <<-RUBY
126
+ #{keyword} #{method_call}
127
+ RUBY
128
+ end
129
+
130
+ def compile_conditional_block_end(component)
131
+ validate_block_end(component)
112
132
 
113
133
  <<-RUBY
114
134
  end
115
135
  RUBY
116
136
  end
117
137
 
118
- def compile_reference(reference)
119
- method, argument = reference.split(".", 2)
138
+ def compile_collection_block_end(component)
139
+ @presenter_classes.pop
140
+ validate_block_end(component)
120
141
 
121
- unless presenter_class.method_available?(method.to_sym)
122
- raise Curly::InvalidReference.new(method.to_sym)
123
- end
142
+ <<-RUBY
143
+ end
144
+ presenter = presenters.pop
145
+ RUBY
146
+ end
124
147
 
125
- if presenter_class.instance_method(method).arity == 1
126
- # The method accepts a single argument -- pass it in.
127
- code = <<-RUBY
128
- presenter.#{method}(#{argument.inspect}) {|*args| yield(*args) }
129
- RUBY
130
- else
131
- code = <<-RUBY
132
- presenter.#{method} {|*args| yield(*args) }
133
- RUBY
134
- end
148
+ def compile_component(component)
149
+ name, identifier, attributes = ComponentParser.parse(component)
150
+ method_call = ComponentCompiler.compile_component(presenter_class, name, identifier, attributes)
151
+ code = "#{method_call} {|*args| yield(*args) }"
135
152
 
136
- 'buffer.concat(%s.to_s)' % code.strip
153
+ "buffer.concat(#{code.strip}.to_s)"
137
154
  end
138
155
 
139
156
  def compile_text(text)
140
- 'buffer.safe_concat(%s)' % text.inspect
157
+ "buffer.safe_concat(#{text.inspect})"
141
158
  end
142
159
 
143
160
  def compile_comment(comment)
144
161
  "" # Replace the content with an empty string.
145
162
  end
163
+
164
+ def validate_block_end(component)
165
+ name, identifier, attributes = ComponentParser.parse(component)
166
+ last_block = @blocks.pop
167
+
168
+ unless last_block == [name, identifier]
169
+ raise Curly::IncorrectEndingError.new([name, identifier], last_block)
170
+ end
171
+ end
172
+
173
+ def push_block(name, identifier)
174
+ @blocks.push([name, identifier])
175
+ end
146
176
  end
147
177
  end
@@ -0,0 +1,119 @@
1
+ module Curly
2
+ class ComponentCompiler
3
+ attr_reader :presenter_class, :method
4
+
5
+ def initialize(presenter_class, method)
6
+ @presenter_class, @method = presenter_class, method
7
+ end
8
+
9
+ def self.compile_component(presenter_class, method, argument, attributes)
10
+ new(presenter_class, method).compile(argument, attributes)
11
+ end
12
+
13
+ def self.compile_conditional(presenter_class, method, argument, attributes)
14
+ if argument && argument.end_with?("?")
15
+ method += "?"
16
+ argument = argument[0..-2]
17
+ end
18
+
19
+ unless method.end_with?("?")
20
+ raise Curly::Error, "conditional components must end with `?`"
21
+ end
22
+
23
+ new(presenter_class, method).compile(argument, attributes)
24
+ end
25
+
26
+ def compile(argument, attributes = {})
27
+ unless presenter_class.component_available?(method)
28
+ raise Curly::InvalidComponent.new(method)
29
+ end
30
+
31
+ validate_attributes(attributes)
32
+
33
+ code = "presenter.#{method}("
34
+
35
+ append_positional_argument(code, argument)
36
+ append_keyword_arguments(code, argument, attributes)
37
+
38
+ code << ")"
39
+ end
40
+
41
+ private
42
+
43
+ def append_positional_argument(code, argument)
44
+ if required_identifier?
45
+ if argument.nil?
46
+ raise Curly::Error, "`#{method}` requires an identifier"
47
+ end
48
+
49
+ code << argument.inspect
50
+ elsif optional_identifier?
51
+ code << argument.inspect unless argument.nil?
52
+ elsif invalid_signature?
53
+ raise Curly::Error, "`#{method}` is not a valid component method"
54
+ elsif !argument.nil?
55
+ raise Curly::Error, "`#{method}` does not take an identifier"
56
+ end
57
+ end
58
+
59
+ def append_keyword_arguments(code, argument, attributes)
60
+ keyword_argument_string = build_keyword_argument_string(attributes)
61
+
62
+ unless keyword_argument_string.empty?
63
+ code << ", " unless argument.nil?
64
+ code << keyword_argument_string
65
+ end
66
+ end
67
+
68
+ def invalid_signature?
69
+ positional_params = param_types.select {|type| [:req, :opt].include?(type) }
70
+ positional_params.size > 1
71
+ end
72
+
73
+ def required_identifier?
74
+ param_types.include?(:req)
75
+ end
76
+
77
+ def optional_identifier?
78
+ param_types.include?(:opt)
79
+ end
80
+
81
+ def build_keyword_argument_string(kwargs)
82
+ kwargs.map {|name, value| "#{name}: #{value.inspect}" }.join(", ")
83
+ end
84
+
85
+ def validate_attributes(kwargs)
86
+ kwargs.keys.each do |key|
87
+ unless attribute_names.include?(key)
88
+ raise Curly::Error, "`#{method}` does not allow attribute `#{key}`"
89
+ end
90
+ end
91
+
92
+ required_attribute_names.each do |key|
93
+ unless kwargs.key?(key)
94
+ raise Curly::Error, "`#{method}` is missing the required attribute `#{key}`"
95
+ end
96
+ end
97
+ end
98
+
99
+ def params
100
+ @params ||= presenter_class.instance_method(method).parameters
101
+ end
102
+
103
+ def param_types
104
+ params.map(&:first)
105
+ end
106
+
107
+ def attribute_names
108
+ @attribute_names ||= params.
109
+ select {|type, name| [:key, :keyreq].include?(type) }.
110
+ map {|type, name| name.to_s }
111
+ end
112
+
113
+ def required_attribute_names
114
+ @required_attribute_names ||= params.
115
+ select {|type, name| type == :keyreq }.
116
+ map {|type, name| name.to_s }
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,13 @@
1
+ require 'curly/attribute_parser'
2
+
3
+ module Curly
4
+ class ComponentParser
5
+ def self.parse(component)
6
+ first, rest = component.split(/\s+/, 2)
7
+ name, identifier = first.split(".", 2)
8
+ attributes = AttributeParser.parse(rest)
9
+
10
+ [name, identifier, attributes]
11
+ end
12
+ end
13
+ end
@@ -1,11 +1,11 @@
1
1
  module Curly
2
2
  class IncompleteBlockError < Error
3
- def initialize(reference)
4
- @reference = reference
3
+ def initialize(component)
4
+ @component = component
5
5
  end
6
6
 
7
7
  def message
8
- "error compiling `{{##{@reference}}}`: conditional block must be terminated with `{{/#{@reference}}}}`"
8
+ "error compiling `{{##{@component}}}`: conditional block must be terminated with `{{/#{@component}}}}`"
9
9
  end
10
10
  end
11
11
  end
@@ -1,11 +1,25 @@
1
1
  module Curly
2
2
  class IncorrectEndingError < Error
3
- def initialize(reference, last_block)
4
- @reference, @last_block = reference, last_block
3
+ def initialize(actual_block, expected_block)
4
+ @actual_block, @expected_block = actual_block, expected_block
5
5
  end
6
6
 
7
7
  def message
8
- "error compiling `{{##{@last_block}}}`: expected `{{/#{@last_block}}}`, got `{{/#{@reference}}}`"
8
+ "compilation error: expected `{{/#{expected_block}}}`, got `{{/#{actual_block}}}`"
9
+ end
10
+
11
+ private
12
+
13
+ def actual_block
14
+ present_block(@actual_block)
15
+ end
16
+
17
+ def expected_block
18
+ present_block(@expected_block)
19
+ end
20
+
21
+ def present_block(block)
22
+ block.compact.join(".")
9
23
  end
10
24
  end
11
25
  end
@@ -0,0 +1,13 @@
1
+ module Curly
2
+ class InvalidComponent < Error
3
+ attr_reader :component
4
+
5
+ def initialize(component)
6
+ @component = component
7
+ end
8
+
9
+ def message
10
+ "invalid component `{{#{component}}}'"
11
+ end
12
+ end
13
+ end
@@ -6,7 +6,7 @@ module Curly
6
6
  # form of simple strings. Each public instance method on the presenter class
7
7
  # can be referenced in a template. When a template is evaluated with a
8
8
  # presenter, the referenced methods will be called with no arguments, and
9
- # the returned strings inserted in place of the references in the template.
9
+ # the returned strings inserted in place of the components in the template.
10
10
  #
11
11
  # Note that strings that are not HTML safe will be escaped.
12
12
  #
@@ -47,7 +47,7 @@ module Curly
47
47
  self.class.presented_names.each do |name|
48
48
  value = options.fetch(name) do
49
49
  default_values.fetch(name) do
50
- raise ArgumentError.new("required parameter `#{name}` missing")
50
+ raise ArgumentError.new("required identifier `#{name}` missing")
51
51
  end
52
52
  end
53
53
 
@@ -152,9 +152,28 @@ module Curly
152
152
  end
153
153
  end
154
154
 
155
- # Whether a method is available to templates rendered with the presenter.
155
+ def presenter_for_name(name)
156
+ namespace = to_s.split("::")
157
+ class_name = name.camelcase << "Presenter"
158
+
159
+ # Because Rails' autoloading mechanism doesn't work properly with
160
+ # namespace we need to loop through the namespace ourselves. Ideally,
161
+ # `X::Y.const_get("Z")` would autoload `X::Z`, but only `X::Y::Z` is
162
+ # attempted by Rails. This sucks, and hopefully we can find a better
163
+ # solution in the future.
164
+ begin
165
+ full_name = namespace.join("::") << "::" << class_name
166
+ const_get(full_name)
167
+ rescue NameError
168
+ raise if namespace.empty?
169
+ namespace.pop
170
+ retry
171
+ end
172
+ end
173
+
174
+ # Whether a component is available to templates rendered with the presenter.
156
175
  #
157
- # Templates can reference "variables", which are simply methods defined on
176
+ # Templates have components which correspond with methods defined on
158
177
  # the presenter. By default, only public instance methods can be
159
178
  # referenced, and any method defined on Curly::Presenter itself cannot be
160
179
  # referenced. This means that methods such as `#cache_key` and #inspect are
@@ -162,19 +181,20 @@ module Curly
162
181
  #
163
182
  # This policy can be changed by overriding this method in your presenters.
164
183
  #
165
- # method - The Symbol name of the method.
184
+ # name - The String name of the component.
166
185
  #
167
186
  # Returns true if the method can be referenced by a template,
168
187
  # false otherwise.
169
- def method_available?(method)
170
- available_methods.include?(method)
188
+ def component_available?(name)
189
+ available_components.include?(name)
171
190
  end
172
191
 
173
- # A list of methods available to templates rendered with the presenter.
192
+ # A list of components available to templates rendered with the presenter.
174
193
  #
175
- # Returns an Array of Symbol method names.
176
- def available_methods
177
- public_instance_methods - Curly::Presenter.public_instance_methods
194
+ # Returns an Array of String component names.
195
+ def available_components
196
+ methods = public_instance_methods - Curly::Presenter.public_instance_methods
197
+ methods.map(&:to_s)
178
198
  end
179
199
 
180
200
  # The set of view paths that the presenter depends on.