curlybars 1.3.1 → 1.7.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 (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