curly-templates 1.0.1 → 2.0.0.beta1

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