curlybars 1.3.0 → 1.6.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/curlybars.rb +0 -1
  3. data/lib/curlybars/configuration.rb +1 -9
  4. data/lib/curlybars/error/base.rb +2 -0
  5. data/lib/curlybars/lexer.rb +3 -0
  6. data/lib/curlybars/method_whitelist.rb +16 -11
  7. data/lib/curlybars/node/block_helper_else.rb +1 -0
  8. data/lib/curlybars/node/if_else.rb +1 -1
  9. data/lib/curlybars/node/path.rb +6 -0
  10. data/lib/curlybars/node/sub_expression.rb +62 -0
  11. data/lib/curlybars/node/unless_else.rb +1 -1
  12. data/lib/curlybars/parser.rb +11 -0
  13. data/lib/curlybars/processor/tilde.rb +3 -0
  14. data/lib/curlybars/rendering_support.rb +9 -1
  15. data/lib/curlybars/template_handler.rb +18 -6
  16. data/lib/curlybars/version.rb +1 -1
  17. data/lib/curlybars/visitor.rb +6 -0
  18. data/spec/acceptance/application_layout_spec.rb +2 -2
  19. data/spec/acceptance/collection_blocks_spec.rb +1 -1
  20. data/spec/acceptance/global_helper_spec.rb +1 -1
  21. data/spec/curlybars/lexer_spec.rb +25 -2
  22. data/spec/curlybars/method_whitelist_spec.rb +15 -0
  23. data/spec/curlybars/rendering_support_spec.rb +4 -9
  24. data/spec/curlybars/template_handler_spec.rb +33 -30
  25. data/spec/integration/cache_spec.rb +20 -18
  26. data/spec/integration/node/block_helper_else_spec.rb +0 -2
  27. data/spec/integration/node/each_else_spec.rb +0 -2
  28. data/spec/integration/node/each_spec.rb +0 -2
  29. data/spec/integration/node/helper_spec.rb +12 -2
  30. data/spec/integration/node/if_else_spec.rb +0 -2
  31. data/spec/integration/node/if_spec.rb +2 -4
  32. data/spec/integration/node/output_spec.rb +0 -2
  33. data/spec/integration/node/partial_spec.rb +0 -2
  34. data/spec/integration/node/path_spec.rb +0 -2
  35. data/spec/integration/node/root_spec.rb +0 -2
  36. data/spec/integration/node/sub_expression_spec.rb +426 -0
  37. data/spec/integration/node/template_spec.rb +0 -2
  38. data/spec/integration/node/unless_else_spec.rb +2 -4
  39. data/spec/integration/node/unless_spec.rb +0 -2
  40. data/spec/integration/node/with_spec.rb +0 -2
  41. data/spec/integration/processors_spec.rb +0 -1
  42. data/spec/integration/visitor_spec.rb +13 -5
  43. metadata +47 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c5f1fe84620e10957d38e663d3883158c00b6db25395c61e9628ddd3d19d110
4
- data.tar.gz: 5eac82875ef4fca6a4c0aa728626615c643089ed6cc24de91232d793753f8f5b
3
+ metadata.gz: 2be6d03409f5c575fb33c7ba4ed74117420fd22ddca8272005c5f23e459a0d8e
4
+ data.tar.gz: ad11b01849c9d28a70b2ae2260615665cc2dc3ca3c380b9dfbc2ab9c91bf1de3
5
5
  SHA512:
6
- metadata.gz: 3697082fceb22d1d868d4de0fcf1054d8facc0e8dee7da0e58704bf34bb303a0cd287b460baa5d40ffcbf17ed7d0e3235643b7d855e57f821ed63223f1d737e3
7
- data.tar.gz: dfba149b55705dbc9567a763dbc12b3514296a43ec576a683642f83f727d6d7ec4c49ffc514a4c2a9a4d1ec91740365dd144d2a87bde96d59b2632b8c24ed4df
6
+ metadata.gz: ba6035a72fd5cf75120cc3adfc663192dae68d75132887b801bd29a53c4b655f6be09ed6b8e4a2ee497e708172c38db19258215b5f3b1a9b788371db20eeac3c
7
+ data.tar.gz: 83961d2f0ef03848c1ffb3feb9db859c866ff90e74d173f116b16c3af5ff4eac710e934cd7b3bb5039749f05169bd4f95e3f19fc208163f3333a6d66ba7261df
@@ -118,7 +118,6 @@ require 'curlybars/rendering_support'
118
118
  require 'curlybars/parser'
119
119
  require 'curlybars/position'
120
120
  require 'curlybars/lexer'
121
- require 'curlybars/parser'
122
121
  require 'curlybars/processor/token_factory'
123
122
  require 'curlybars/processor/tilde'
124
123
  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
@@ -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 }
@@ -1,5 +1,6 @@
1
1
  module Curlybars
2
2
  module MethodWhitelist
3
+ # rubocop:disable Style/SoleNestedConditional
3
4
  def allow_methods(*methods_without_type, **methods_with_type, &contextual_block)
4
5
  methods_with_type_validator = lambda do |methods_to_validate|
5
6
  methods_to_validate.each do |(method_name, type)|
@@ -14,19 +15,21 @@ module Curlybars
14
15
  methods_with_type_validator.call(methods_with_type)
15
16
 
16
17
  define_method(:allowed_methods) do
17
- methods_list = methods_without_type + methods_with_type.keys
18
+ @method_whitelist_allowed_methods ||= begin
19
+ methods_list = methods_without_type + methods_with_type.keys
18
20
 
19
- # Adds methods to the list of allowed methods
20
- method_adder = lambda do |*more_methods, **more_methods_with_type|
21
- methods_with_type_validator.call(more_methods_with_type)
21
+ # Adds methods to the list of allowed methods
22
+ method_adder = lambda do |*more_methods, **more_methods_with_type|
23
+ methods_with_type_validator.call(more_methods_with_type)
22
24
 
23
- methods_list += more_methods
24
- methods_list += more_methods_with_type.keys
25
- end
25
+ methods_list += more_methods
26
+ methods_list += more_methods_with_type.keys
27
+ end
26
28
 
27
- contextual_block&.call(self, method_adder)
29
+ contextual_block&.call(self, method_adder)
28
30
 
29
- defined?(super) ? super() + methods_list : methods_list
31
+ defined?(super) ? super() + methods_list : methods_list
32
+ end
30
33
  end
31
34
 
32
35
  define_singleton_method(:methods_schema) do |context = nil|
@@ -47,8 +50,8 @@ module Curlybars
47
50
  memo[method] = nil
48
51
  end
49
52
 
50
- methods_with_type_resolved = all_methods_with_type.each_with_object({}) do |(method_name, type), memo|
51
- memo[method_name] = if type.respond_to?(:call)
53
+ methods_with_type_resolved = all_methods_with_type.transform_values do |type|
54
+ if type.respond_to?(:call)
52
55
  type.call(context)
53
56
  else
54
57
  type
@@ -63,6 +66,7 @@ module Curlybars
63
66
  # Included modules
64
67
  included_modules.each do |mod|
65
68
  next unless mod.respond_to?(:methods_schema)
69
+
66
70
  schema.merge!(mod.methods_schema(context))
67
71
  end
68
72
 
@@ -88,6 +92,7 @@ module Curlybars
88
92
  allowed_methods.include?(method)
89
93
  end
90
94
  end
95
+ # rubocop:enable Style/SoleNestedConditional
91
96
 
92
97
  def self.extended(base)
93
98
  # define a default of no method allowed
@@ -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
@@ -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
  ]
@@ -109,26 +109,32 @@ module Curlybars
109
109
  case check_type
110
110
  when :presenter
111
111
  return if presenter?(branches)
112
+
112
113
  message = "`#{path}` must resolve to a presenter"
113
114
  raise Curlybars::Error::Validate.new('not_a_presenter', message, position)
114
115
  when :presenter_collection
115
116
  return if presenter_collection?(branches)
117
+
116
118
  message = "`#{path}` must resolve to a collection of presenters"
117
119
  raise Curlybars::Error::Validate.new('not_a_presenter_collection', message, position)
118
120
  when :leaf
119
121
  return if leaf?(branches)
122
+
120
123
  message = "`#{path}` cannot resolve to a component"
121
124
  raise Curlybars::Error::Validate.new('not_a_leaf', message, position)
122
125
  when :partial
123
126
  return if partial?(branches)
127
+
124
128
  message = "`#{path}` cannot resolve to a partial"
125
129
  raise Curlybars::Error::Validate.new('not_a_partial', message, position)
126
130
  when :helper
127
131
  return if helper?(branches)
132
+
128
133
  message = "`#{path}` cannot resolve to a helper"
129
134
  raise Curlybars::Error::Validate.new('not_a_helper', message, position)
130
135
  when :not_helper
131
136
  return unless helper?(branches)
137
+
132
138
  message = "`#{path}` resolves to a helper"
133
139
  raise Curlybars::Error::Validate.new('is_a_helper', message, position)
134
140
  when :anything
@@ -0,0 +1,62 @@
1
+ module Curlybars
2
+ module Node
3
+ SubExpression = Struct.new(:helper, :arguments, :options, :position) do
4
+ def compile
5
+ compiled_arguments = arguments.map do |argument|
6
+ "arguments.push(rendering.cached_call(#{argument.compile}))"
7
+ end.join("\n")
8
+
9
+ compiled_options = options.map do |option|
10
+ "options.merge!(#{option.compile})"
11
+ end.join("\n")
12
+
13
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
14
+ # outputted by this node.
15
+ <<-RUBY
16
+ ::Module.new do
17
+ def self.exec(contexts, rendering)
18
+ rendering.check_timeout!
19
+
20
+ -> {
21
+ options = ::ActiveSupport::HashWithIndifferentAccess.new
22
+ #{compiled_options}
23
+
24
+ arguments = []
25
+ #{compiled_arguments}
26
+
27
+ helper = #{helper.compile}
28
+ helper_position = rendering.position(#{helper.position.line_number},
29
+ #{helper.position.line_offset})
30
+
31
+ options[:this] = contexts.last
32
+
33
+ rendering.call(helper, #{helper.path.inspect}, helper_position,
34
+ arguments, options)
35
+ }
36
+ end
37
+ end.exec(contexts, rendering)
38
+ RUBY
39
+ end
40
+
41
+ def validate_as_value(branches, check_type: :anything)
42
+ validate(branches, check_type: check_type)
43
+ end
44
+
45
+ def validate(branches, check_type: :anything)
46
+ [
47
+ helper.validate(branches, check_type: :helper),
48
+ arguments.map { |argument| argument.validate_as_value(branches) },
49
+ options.map { |option| option.validate(branches) }
50
+ ]
51
+ end
52
+
53
+ def cache_key
54
+ [
55
+ helper,
56
+ arguments,
57
+ options
58
+ ].flatten.map(&:cache_key).push(self.class.name).join("/")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -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
  unless_template.validate(branches),
20
20
  else_template.validate(branches)
21
21
  ]
@@ -15,6 +15,7 @@ require 'curlybars/node/block_helper_else'
15
15
  require 'curlybars/node/option'
16
16
  require 'curlybars/node/partial'
17
17
  require 'curlybars/node/output'
18
+ require 'curlybars/node/sub_expression'
18
19
 
19
20
  module Curlybars
20
21
  class Parser < RLTK::Parser
@@ -155,6 +156,7 @@ module Curlybars
155
156
  production(:expression) do
156
157
  clause('value') { |value| value }
157
158
  clause('path') { |path| path }
159
+ clause('subexpression') { |subexpression| subexpression }
158
160
  end
159
161
 
160
162
  production(:value) do
@@ -164,6 +166,15 @@ module Curlybars
164
166
 
165
167
  production(:path, 'PATH') { |path| Node::Path.new(path, pos(0)) }
166
168
 
169
+ production(:subexpression, 'LPAREN .path .expressions? .options? RPAREN') do |helper, arguments, options|
170
+ Node::SubExpression.new(
171
+ helper,
172
+ arguments || [],
173
+ options || [],
174
+ pos(0)
175
+ )
176
+ end
177
+
167
178
  finalize
168
179
 
169
180
  VOID = Class.new do
@@ -10,10 +10,12 @@ module Curlybars
10
10
  when :TILDE_START
11
11
  tokens[index] = create_token(:START, token.value, token.position)
12
12
  next if index == 0
13
+
13
14
  strip_token_if_text(tokens, index - 1, :rstrip)
14
15
  when :TILDE_END
15
16
  tokens[index] = create_token(:END, token.value, token.position)
16
17
  next if index == (tokens.length - 1)
18
+
17
19
  strip_token_if_text(tokens, index + 1, :lstrip)
18
20
  end
19
21
  end
@@ -22,6 +24,7 @@ module Curlybars
22
24
  def strip_token_if_text(tokens, index, strip_method)
23
25
  token = tokens[index]
24
26
  return if token.type != :TEXT
27
+
25
28
  stripped_value = token.value.public_send(strip_method)
26
29
  tokens[index] = create_token(token.type, stripped_value, token.position)
27
30
  end
@@ -23,12 +23,14 @@ module Curlybars
23
23
  def check_timeout!
24
24
  return unless timeout.present?
25
25
  return unless (Time.now - start_time) > timeout
26
+
26
27
  message = "Rendering took too long (> #{timeout} seconds)"
27
28
  raise ::Curlybars::Error::Render.new('timeout', message, nil)
28
29
  end
29
30
 
30
31
  def check_context_is_presenter(context, path, position)
31
32
  return if presenter?(context)
33
+
32
34
  message = "`#{path}` is not a context type object"
33
35
  raise Curlybars::Error::Render.new('context_is_not_a_presenter', message, position)
34
36
  end
@@ -83,9 +85,11 @@ module Curlybars
83
85
  resolved = chain.inject(base_context) do |context, meth|
84
86
  next context if meth == 'this'
85
87
  next context.count if meth == 'length' && presenter_collection?(context)
88
+
86
89
  raise_if_not_traversable(context, meth, position)
87
90
  outcome = instrument(context.method(meth)) { context.public_send(meth) }
88
91
  return -> {} if outcome.nil?
92
+
89
93
  outcome
90
94
  end
91
95
 
@@ -101,7 +105,8 @@ module Curlybars
101
105
 
102
106
  def cached_call(meth)
103
107
  return cached_calls[meth] if cached_calls.key? meth
104
- instrument(meth) { cached_calls[meth] = meth.call }
108
+
109
+ instrument(meth) { cached_calls[meth] = meth.call(*arguments_for_signature(meth, [], {})) }
105
110
  end
106
111
 
107
112
  def call(helper, helper_path, helper_position, arguments, options, &block)
@@ -202,6 +207,7 @@ module Curlybars
202
207
 
203
208
  def check_context_allows_method(context, meth, position)
204
209
  return if context.allows_method?(meth.to_sym)
210
+
205
211
  message = "`#{meth}` is not available - "
206
212
  message += "add `allow_methods :#{meth}` to #{context.class} to allow this path"
207
213
  raise Curlybars::Error::Render.new('unallowed_path', message, position, meth: meth.to_sym)
@@ -209,12 +215,14 @@ module Curlybars
209
215
 
210
216
  def check_context_has_method(context, meth, position)
211
217
  return if context.respond_to?(meth.to_sym)
218
+
212
219
  message = "`#{meth}` is not available in #{context.class}"
213
220
  raise Curlybars::Error::Render.new('unallowed_path', message, position)
214
221
  end
215
222
 
216
223
  def check_traverse_not_too_deep(traverse, position)
217
224
  return unless traverse.count('.') > Curlybars.configuration.traversing_limit
225
+
218
226
  message = "`#{traverse}` too deep"
219
227
  raise Curlybars::Error::Render.new('traverse_too_deep', message, position)
220
228
  end
@@ -13,9 +13,17 @@ module Curlybars
13
13
  # template - The ActionView::Template template that should be compiled.
14
14
  #
15
15
  # Returns a String containing the Ruby code representing the template.
16
- def call(template)
17
- instrument(template) do
18
- compile(template)
16
+ if ActionView::VERSION::MAJOR < 6
17
+ def call(template)
18
+ instrument(template) do
19
+ compile_for_actionview5(template)
20
+ end
21
+ end
22
+ else
23
+ def call(template, source)
24
+ instrument(template) do
25
+ compile(template, source)
26
+ end
19
27
  end
20
28
  end
21
29
 
@@ -43,9 +51,13 @@ module Curlybars
43
51
 
44
52
  private
45
53
 
46
- def compile(template)
54
+ def compile_for_actionview5(template)
55
+ compile(template, template.source)
56
+ end
57
+
58
+ def compile(template, source)
47
59
  # Template is empty, so there's no need to initialize a presenter.
48
- return %("") if template.source.empty?
60
+ return %("") if source.empty?
49
61
 
50
62
  path = template.virtual_path
51
63
  presenter_class = Curlybars::Presenter.presenter_for_path(path)
@@ -55,7 +67,7 @@ module Curlybars
55
67
  # For security reason, we strip the encoding directive in order to avoid
56
68
  # potential issues when rendering the template in another character
57
69
  # encoding.
58
- safe_source = template.source.gsub(/\A#{ActionView::ENCODING_FLAG}/, '')
70
+ safe_source = source.gsub(/\A#{ActionView::ENCODING_FLAG}/, '')
59
71
 
60
72
  source = Curlybars.compile(safe_source, template.identifier)
61
73
 
@@ -1,3 +1,3 @@
1
1
  module Curlybars
2
- VERSION = '1.3.0'.freeze
2
+ VERSION = '1.6.0'.freeze
3
3
  end
@@ -75,6 +75,12 @@ module Curlybars
75
75
  def visit_string(_node)
76
76
  end
77
77
 
78
+ def visit_sub_expression(node)
79
+ visit(node.helper)
80
+ node.arguments.each { |argument| visit(argument) }
81
+ node.options.each { |option| visit(option) }
82
+ end
83
+
78
84
  def visit_template(node)
79
85
  node.items.each { |item| visit(item) }
80
86
  end
@@ -2,7 +2,7 @@ describe "Using Curlybars for the application layout", type: :request do
2
2
  example "A simple layout view in Curlybars" do
3
3
  get '/'
4
4
 
5
- expect(body).to eq(<<-HTML.strip_heredoc)
5
+ expect(body).to eq(<<~HTML)
6
6
  <html>
7
7
  <head>
8
8
  <title>Dummy app</title>
@@ -31,7 +31,7 @@ describe "Using Curlybars for the application layout", type: :request do
31
31
  example "A simple layout view in Curlybars with html safe logic" do
32
32
  get '/articles/1'
33
33
 
34
- expect(body).to eq(<<-HTML.strip_heredoc)
34
+ expect(body).to eq(<<~HTML)
35
35
  <html>
36
36
  <head>
37
37
  <title>Dummy app</title>
@@ -8,7 +8,7 @@ describe "Collection blocks", type: :request do
8
8
  example "Rendering collections" do
9
9
  get '/categories'
10
10
 
11
- expect(body).to eq(<<-HTML.strip_heredoc)
11
+ expect(body).to eq(<<~HTML)
12
12
  <html>
13
13
  <head>
14
14
  <title>Dummy app</title>