curlybars 1.3.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/curlybars.rb +8 -1
  3. data/lib/curlybars/configuration.rb +1 -9
  4. data/lib/curlybars/error/base.rb +2 -0
  5. data/lib/curlybars/generic.rb +36 -0
  6. data/lib/curlybars/lexer.rb +3 -0
  7. data/lib/curlybars/method_whitelist.rb +25 -3
  8. data/lib/curlybars/node/block_helper_else.rb +1 -0
  9. data/lib/curlybars/node/each_else.rb +6 -2
  10. data/lib/curlybars/node/if_else.rb +1 -1
  11. data/lib/curlybars/node/path.rb +58 -11
  12. data/lib/curlybars/node/sub_expression.rb +108 -0
  13. data/lib/curlybars/node/unless_else.rb +1 -1
  14. data/lib/curlybars/node/with_else.rb +6 -2
  15. data/lib/curlybars/parser.rb +39 -0
  16. data/lib/curlybars/processor/tilde.rb +3 -0
  17. data/lib/curlybars/rendering_support.rb +9 -1
  18. data/lib/curlybars/template_handler.rb +18 -6
  19. data/lib/curlybars/version.rb +1 -1
  20. data/lib/curlybars/visitor.rb +6 -0
  21. data/spec/acceptance/application_layout_spec.rb +2 -2
  22. data/spec/acceptance/collection_blocks_spec.rb +1 -1
  23. data/spec/acceptance/global_helper_spec.rb +1 -1
  24. data/spec/curlybars/lexer_spec.rb +25 -2
  25. data/spec/curlybars/method_whitelist_spec.rb +8 -4
  26. data/spec/curlybars/rendering_support_spec.rb +4 -9
  27. data/spec/curlybars/template_handler_spec.rb +33 -30
  28. data/spec/integration/cache_spec.rb +20 -18
  29. data/spec/integration/node/block_helper_else_spec.rb +0 -2
  30. data/spec/integration/node/each_else_spec.rb +208 -4
  31. data/spec/integration/node/each_spec.rb +0 -2
  32. data/spec/integration/node/helper_spec.rb +12 -2
  33. data/spec/integration/node/if_else_spec.rb +0 -2
  34. data/spec/integration/node/if_spec.rb +2 -4
  35. data/spec/integration/node/output_spec.rb +0 -2
  36. data/spec/integration/node/partial_spec.rb +0 -2
  37. data/spec/integration/node/path_spec.rb +0 -2
  38. data/spec/integration/node/root_spec.rb +0 -2
  39. data/spec/integration/node/sub_expression_spec.rb +426 -0
  40. data/spec/integration/node/template_spec.rb +0 -2
  41. data/spec/integration/node/unless_else_spec.rb +2 -4
  42. data/spec/integration/node/unless_spec.rb +0 -2
  43. data/spec/integration/node/with_spec.rb +66 -4
  44. data/spec/integration/processor/tilde_spec.rb +1 -1
  45. data/spec/integration/processors_spec.rb +4 -5
  46. data/spec/integration/visitor_spec.rb +13 -5
  47. metadata +49 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f83af0f1fc951b9a35dd68ae1b3bd181799031cf37913100574c65d98de5e870
4
- data.tar.gz: 954af845f827c80b1fced94ad10ed5f1bcaf99dc872f3b54435ab9e23c3d82e3
3
+ metadata.gz: ba4b2799fad2886edfd0bc1c4873d8da9ed80d8885a1868f4b0fa5797fb9e49c
4
+ data.tar.gz: ee1bfc5d500f2ceee63675c5188d915227f507de7895c6ed2f08e89ddcae122e
5
5
  SHA512:
6
- metadata.gz: f75d2a62e3c4dec26b01bf04928d624e70a0b807cf99c40ac0b5b6a03e421e5f296f6a5eaed609bb7bd4679bbe402ebfd0e8626c6f2527a8118afd71aa937522
7
- data.tar.gz: 748d32d09437bdf7bd02b5d607aee846cabd02ab015a837ec95ccc00e451099f21cf71f3b5a7cb305e8d4f8bfe18145b7401531db55cb8ba4fc0ad66906e259e
6
+ metadata.gz: 4233c4cd5ed833892d17603776a6db2abae2061cf72dfebe1add84bb7464e99b563cacee499fbcd447f59dd54a1ea3bf3b4aa18010526249aba2ed39a2310b96
7
+ data.tar.gz: f78754a1adc25de66302116338dd2abf9b4e9f8c2ae3439d1f74beda9065bb634c8fae64c19f65767be4d7389f1362559c0537f36bb438969e151bb93a47612f
data/lib/curlybars.rb CHANGED
@@ -77,6 +77,13 @@ module Curlybars
77
77
  visitor.accept(tree)
78
78
  end
79
79
 
80
+ def global_helpers_dependency_tree
81
+ @global_helpers_dependency_tree ||= begin
82
+ classes = Curlybars.configuration.global_helpers_provider_classes
83
+ classes.map(&:dependency_tree).inject({}, :merge)
84
+ end
85
+ end
86
+
80
87
  def cache
81
88
  @cache ||= ActiveSupport::Cache::MemoryStore.new
82
89
  end
@@ -117,8 +124,8 @@ require 'curlybars/configuration'
117
124
  require 'curlybars/rendering_support'
118
125
  require 'curlybars/parser'
119
126
  require 'curlybars/position'
127
+ require 'curlybars/generic'
120
128
  require 'curlybars/lexer'
121
- require 'curlybars/parser'
122
129
  require 'curlybars/processor/token_factory'
123
130
  require 'curlybars/processor/tilde'
124
131
  require 'curlybars/error/lex'
@@ -16,15 +16,7 @@ module Curlybars
16
16
  end
17
17
 
18
18
  class Configuration
19
- attr_accessor :presenters_namespace
20
- attr_accessor :nesting_limit
21
- attr_accessor :traversing_limit
22
- attr_accessor :output_limit
23
- attr_accessor :rendering_timeout
24
- attr_accessor :custom_processors
25
- attr_accessor :compiler_transformers
26
- attr_accessor :global_helpers_provider_classes
27
- attr_accessor :cache
19
+ attr_accessor :presenters_namespace, :nesting_limit, :traversing_limit, :output_limit, :rendering_timeout, :custom_processors, :compiler_transformers, :global_helpers_provider_classes, :cache
28
20
 
29
21
  def initialize
30
22
  @presenters_namespace = ''
@@ -8,8 +8,10 @@ module Curlybars
8
8
  @id = id
9
9
  @position = position
10
10
  @metadata = metadata
11
+
11
12
  return if position.nil?
12
13
  return if position.file_name.nil?
14
+
13
15
  location = "%s:%d:%d" % [position.file_name, position.line_number, position.line_offset]
14
16
  set_backtrace([location])
15
17
  end
@@ -0,0 +1,36 @@
1
+ require 'curlybars/method_whitelist'
2
+
3
+ module Curlybars
4
+ # A base class that can be used to signify that a helper's return type is a sort a generic.
5
+ #
6
+ # Examples
7
+ #
8
+ # class GlobalHelperProvider
9
+ # extend Curlybars::MethodWhitelist
10
+ #
11
+ # allow_methods slice: [:helper, [Curlybars::Generic]],
12
+ # translate: [:helper, Curlybars::Generic]
13
+ #
14
+ # def slice(collection, start, length, _)
15
+ # collection[start, length]
16
+ # end
17
+ #
18
+ # def translate(object, locale)
19
+ # object.translate(locale)
20
+ # end
21
+ # end
22
+ #
23
+ # {{#each (slice articles, 0, 5)}}
24
+ # Title: {{title}}
25
+ # Body: {{body}}
26
+ # {{/each}}
27
+ #
28
+ # {{#with (translate article "en-us")}}
29
+ # Title: {{title}}
30
+ # Body: {{body}}
31
+ # {{/with}}
32
+ #
33
+ class Generic
34
+ extend Curlybars::MethodWhitelist
35
+ end
36
+ end
@@ -32,6 +32,9 @@ module Curlybars
32
32
  r(/{{/) { push_state :curly; :START }
33
33
  r(/}}/, :curly) { pop_state; :END }
34
34
 
35
+ r(/\(/, :curly) { push_state :curly; :LPAREN }
36
+ r(/\)/, :curly) { pop_state; :RPAREN }
37
+
35
38
  r(/#/, :curly) { :HASH }
36
39
  r(/\//, :curly) { :SLASH }
37
40
  r(/>/, :curly) { :GT }
@@ -4,6 +4,8 @@ module Curlybars
4
4
  methods_with_type_validator = lambda do |methods_to_validate|
5
5
  methods_to_validate.each do |(method_name, type)|
6
6
  if type.is_a?(Array)
7
+ next if generic_or_collection_helper?(type)
8
+
7
9
  if type.size != 1 || !type.first.respond_to?(:dependency_tree)
8
10
  raise "Invalid allowed method syntax for `#{method_name}`. Collections must be of one presenter class"
9
11
  end
@@ -49,8 +51,8 @@ module Curlybars
49
51
  memo[method] = nil
50
52
  end
51
53
 
52
- methods_with_type_resolved = all_methods_with_type.each_with_object({}) do |(method_name, type), memo|
53
- memo[method_name] = if type.respond_to?(:call)
54
+ methods_with_type_resolved = all_methods_with_type.transform_values do |type|
55
+ if type.respond_to?(:call)
54
56
  type.call(context)
55
57
  else
56
58
  type
@@ -65,6 +67,7 @@ module Curlybars
65
67
  # Included modules
66
68
  included_modules.each do |mod|
67
69
  next unless mod.respond_to?(:methods_schema)
70
+
68
71
  schema.merge!(mod.methods_schema(context))
69
72
  end
70
73
 
@@ -79,7 +82,15 @@ module Curlybars
79
82
  memo[method_name] = if type.respond_to?(:dependency_tree)
80
83
  type.dependency_tree(context)
81
84
  elsif type.is_a?(Array)
82
- [type.first.dependency_tree(context)]
85
+ if type.first == :helper
86
+ if type.last.is_a?(Array)
87
+ [:helper, [type.last.first.dependency_tree(context)]]
88
+ else
89
+ [:helper, type.last.dependency_tree(context)]
90
+ end
91
+ else
92
+ [type.first.dependency_tree(context)]
93
+ end
83
94
  else
84
95
  type
85
96
  end
@@ -95,5 +106,16 @@ module Curlybars
95
106
  # define a default of no method allowed
96
107
  base.allow_methods
97
108
  end
109
+
110
+ private
111
+
112
+ def generic_or_collection_helper?(type)
113
+ return false unless type.size == 2
114
+ return false unless type.first == :helper
115
+ return true if type.last.respond_to?(:dependency_tree)
116
+ return false unless type.last.is_a?(Array) && type.last.size == 1
117
+
118
+ type.last.first.respond_to?(:dependency_tree)
119
+ end
98
120
  end
99
121
  end
@@ -100,6 +100,7 @@ module Curlybars
100
100
 
101
101
  def check_open_and_close_elements(helper, helperclose, error_class)
102
102
  return unless helper.path != helperclose.path
103
+
103
104
  message = "block `#{helper.path}` cannot be closed by `#{helperclose.path}`"
104
105
  raise error_class.new('closing_tag_mismatch', message, helperclose.position)
105
106
  end
@@ -11,7 +11,7 @@ module Curlybars
11
11
  position = rendering.position(#{position.line_number}, #{position.line_offset})
12
12
  template_cache_key = '#{each_template.cache_key}'
13
13
 
14
- collection = rendering.coerce_to_hash!(collection, #{path.path.inspect}, position)
14
+ collection = rendering.coerce_to_hash!(collection, #{collection_path.path.inspect}, position)
15
15
  collection.each.with_index.map do |key_and_presenter, index|
16
16
  rendering.check_timeout!
17
17
  rendering.optional_presenter_cache(key_and_presenter[1], template_cache_key, buffer) do |buffer|
@@ -37,7 +37,7 @@ module Curlybars
37
37
  end
38
38
 
39
39
  def validate(branches)
40
- resolved = path.resolve_and_check!(branches, check_type: :presenter_collection)
40
+ resolved = path.resolve_and_check!(branches, check_type: :collectionlike)
41
41
  sub_tree = resolved.first
42
42
 
43
43
  each_template_errors = begin
@@ -57,6 +57,10 @@ module Curlybars
57
57
  path_error
58
58
  end
59
59
 
60
+ def collection_path
61
+ path.subexpression? ? path.helper : path
62
+ end
63
+
60
64
  def cache_key
61
65
  [
62
66
  path,
@@ -15,7 +15,7 @@ module Curlybars
15
15
 
16
16
  def validate(branches)
17
17
  [
18
- expression.validate(branches, check_type: :not_helper),
18
+ expression.validate(branches),
19
19
  if_template.validate(branches),
20
20
  else_template.validate(branches)
21
21
  ]
@@ -35,11 +35,18 @@ module Curlybars
35
35
  resolve(branches).is_a?(Hash)
36
36
  end
37
37
 
38
- def presenter_collection?(branches)
39
- value = resolve(branches)
38
+ def presenter_value?(value)
39
+ value.is_a?(Hash)
40
+ end
41
+
42
+ def collection_value?(value)
40
43
  value.is_a?(Array) && value.first.is_a?(Hash)
41
44
  end
42
45
 
46
+ def presenter_collection?(branches)
47
+ collection_value?(resolve(branches))
48
+ end
49
+
43
50
  def leaf?(branches)
44
51
  value = resolve(branches)
45
52
  value.nil?
@@ -53,9 +60,41 @@ module Curlybars
53
60
  resolve(branches) == :helper
54
61
  end
55
62
 
63
+ def generic_helper?(branches)
64
+ value = resolve(branches)
65
+ value.is_a?(Array) &&
66
+ value.first == :helper &&
67
+ presenter_value?(value.last)
68
+ end
69
+
70
+ def generic_collection_helper?(branches)
71
+ value = resolve(branches)
72
+ value.is_a?(Array) &&
73
+ value.first == :helper &&
74
+ collection_value?(value.last)
75
+ end
76
+
77
+ def collectionlike?(branches)
78
+ presenter_collection?(branches) || generic_collection_helper?(branches)
79
+ end
80
+
81
+ def presenterlike?(branches)
82
+ presenter?(branches) || generic_helper?(branches)
83
+ end
84
+
85
+ def subexpression?
86
+ false
87
+ end
88
+
56
89
  def resolve(branches)
57
90
  @value ||= begin
58
- return :helper if global_helpers_dependency_tree.key?(path.to_sym)
91
+ if Curlybars.global_helpers_dependency_tree.key?(path.to_sym)
92
+ dep_node = Curlybars.global_helpers_dependency_tree[path.to_sym]
93
+
94
+ return :helper if dep_node.nil?
95
+
96
+ return [:helper, dep_node]
97
+ end
59
98
 
60
99
  path_split_by_slashes = path.split('/')
61
100
  backward_steps_on_branches = path_split_by_slashes.count - 1
@@ -97,38 +136,46 @@ module Curlybars
97
136
 
98
137
  private
99
138
 
100
- # TODO: extract me away
101
- def global_helpers_dependency_tree
102
- @global_helpers_dependency_tree ||= begin
103
- classes = Curlybars.configuration.global_helpers_provider_classes
104
- classes.map(&:dependency_tree).inject({}, :merge)
105
- end
106
- end
107
-
108
139
  def check_type_of(branches, check_type)
109
140
  case check_type
110
141
  when :presenter
111
142
  return if presenter?(branches)
143
+
112
144
  message = "`#{path}` must resolve to a presenter"
113
145
  raise Curlybars::Error::Validate.new('not_a_presenter', message, position)
146
+ when :presenterlike
147
+ return if presenterlike?(branches)
148
+
149
+ message = "`#{path}` must resolve to a presenter"
150
+ raise Curlybars::Error::Validate.new('not_presenterlike', message, position)
151
+ when :collectionlike
152
+ return if collectionlike?(branches)
153
+
154
+ message = "`#{path}` must resolve to a collection of presenters"
155
+ raise Curlybars::Error::Validate.new('not_collectionlike', message, position)
114
156
  when :presenter_collection
115
157
  return if presenter_collection?(branches)
158
+
116
159
  message = "`#{path}` must resolve to a collection of presenters"
117
160
  raise Curlybars::Error::Validate.new('not_a_presenter_collection', message, position)
118
161
  when :leaf
119
162
  return if leaf?(branches)
163
+
120
164
  message = "`#{path}` cannot resolve to a component"
121
165
  raise Curlybars::Error::Validate.new('not_a_leaf', message, position)
122
166
  when :partial
123
167
  return if partial?(branches)
168
+
124
169
  message = "`#{path}` cannot resolve to a partial"
125
170
  raise Curlybars::Error::Validate.new('not_a_partial', message, position)
126
171
  when :helper
127
172
  return if helper?(branches)
173
+
128
174
  message = "`#{path}` cannot resolve to a helper"
129
175
  raise Curlybars::Error::Validate.new('not_a_helper', message, position)
130
176
  when :not_helper
131
177
  return unless helper?(branches)
178
+
132
179
  message = "`#{path}` resolves to a helper"
133
180
  raise Curlybars::Error::Validate.new('is_a_helper', message, position)
134
181
  when :anything
@@ -0,0 +1,108 @@
1
+ module Curlybars
2
+ module Node
3
+ SubExpression = Struct.new(:helper, :arguments, :options, :position) do
4
+ def subexpression?
5
+ true
6
+ end
7
+
8
+ def compile
9
+ compiled_arguments = arguments.map do |argument|
10
+ "arguments.push(rendering.cached_call(#{argument.compile}))"
11
+ end.join("\n")
12
+
13
+ compiled_options = options.map do |option|
14
+ "options.merge!(#{option.compile})"
15
+ end.join("\n")
16
+
17
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
18
+ # outputted by this node.
19
+ <<-RUBY
20
+ ::Module.new do
21
+ def self.exec(contexts, rendering)
22
+ rendering.check_timeout!
23
+
24
+ -> {
25
+ options = ::ActiveSupport::HashWithIndifferentAccess.new
26
+ #{compiled_options}
27
+
28
+ arguments = []
29
+ #{compiled_arguments}
30
+
31
+ helper = #{helper.compile}
32
+ helper_position = rendering.position(#{helper.position.line_number},
33
+ #{helper.position.line_offset})
34
+
35
+ options[:this] = contexts.last
36
+
37
+ rendering.call(helper, #{helper.path.inspect}, helper_position,
38
+ arguments, options)
39
+ }
40
+ end
41
+ end.exec(contexts, rendering)
42
+ RUBY
43
+ end
44
+
45
+ def validate_as_value(branches, check_type: :anything)
46
+ validate(branches, check_type: check_type)
47
+ end
48
+
49
+ def validate(branches, check_type: :anything)
50
+ [
51
+ helper.validate(branches, check_type: :helper),
52
+ arguments.map { |argument| argument.validate_as_value(branches) },
53
+ options.map { |option| option.validate(branches) }
54
+ ]
55
+ end
56
+
57
+ def resolve_and_check!(branches, check_type: :collectionlike)
58
+ node = if arguments.first.is_a?(Curlybars::Node::SubExpression)
59
+ arguments.first
60
+ else
61
+ helper
62
+ end
63
+
64
+ type = node.resolve_and_check!(branches, check_type: check_type)
65
+
66
+ if helper?(type)
67
+ if generic_helper?(type)
68
+ is_collection = type.last.is_a?(Array)
69
+ return infer_generic_helper_type!(branches, is_collection: is_collection)
70
+ end
71
+
72
+ return type.last
73
+ end
74
+
75
+ type
76
+ end
77
+
78
+ def generic_helper?(type)
79
+ return false unless type.is_a?(Array)
80
+ return false unless type.size == 2
81
+ return false unless type.first == :helper
82
+
83
+ type.last == [{}] || type.last == {}
84
+ end
85
+
86
+ def helper?(type)
87
+ type.first == :helper
88
+ end
89
+
90
+ def infer_generic_helper_type!(branches, is_collection:)
91
+ if arguments.empty?
92
+ raise Curlybars::Error::Validate.new('missing_path', "'#{helper.path}' requires a collection as its first argument", helper.position)
93
+ end
94
+
95
+ check_type = is_collection ? :presenter_collection : :presenter
96
+ arguments.first.resolve_and_check!(branches, check_type: check_type)
97
+ end
98
+
99
+ def cache_key
100
+ [
101
+ helper,
102
+ arguments,
103
+ options
104
+ ].flatten.map(&:cache_key).push(self.class.name).join("/")
105
+ end
106
+ end
107
+ end
108
+ end