curlybars 1.12.0 → 1.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ee481734b28469381f2b97ae1bdfd7bea4f53fd2d8cebef0ca7cd1630bc61d2
4
- data.tar.gz: 5444467d66e9991ebb7b43c8cbb71b4b066e3d5c032d5a7fe305a1bd7e9a8cfc
3
+ metadata.gz: 2ef985c46340022e337318774e4fcf8f36a1f0f7fc6d87788d70ffa3b214f16f
4
+ data.tar.gz: 1bd1f9195be76418bc0b416f6466aa73809f39037d93ccdde901d90ffca14baa
5
5
  SHA512:
6
- metadata.gz: 7998d467492215867831a81d15f94b4b0748e02e007711e9772c2c632bfc40554d997e9b6762690ff39b04e7f6a56efcb2dee8ab2be88107f5e8e47735ddc09b
7
- data.tar.gz: b647184f3f5466d4278193aa675295a27aca3f90cc3137a0782562192aed7ec2f7a00170f5479426e551e0571f7592ba16262ada7005e980881359d492624d45
6
+ metadata.gz: 443143ccfb796e5cc14f4cb4bf2f5c1958b8de9b271c95f8fdfccc07caa169d520e4563e70b5111109f6d914388d18c8c6bd519f837524371f235cb375b10389
7
+ data.tar.gz: fc72c19b12e2084f7e9dab5a01c51d6c6f7ab1614dde449f635356f40fc0841ef3305fd2fb6169763c6333e66324e3486d38f396516325794597b8184801e84a
@@ -9,7 +9,7 @@ module Curlybars
9
9
 
10
10
  error_line = source.split("\n")[line_number - 1]
11
11
  before_error = error_line.first(line_offset).last(10)
12
- after_error = error_line[line_offset + 1..].first(10)
12
+ after_error = error_line[(line_offset + 1)..].first(10)
13
13
  error = error_line[line_offset]
14
14
 
15
15
  details = [before_error, error, after_error]
@@ -16,7 +16,7 @@ module Curlybars
16
16
 
17
17
  error_line = source.split("\n")[line_number - 1]
18
18
  before_error = error_line.first(line_offset).last(10)
19
- after_error = error_line[line_offset + length..].first(10)
19
+ after_error = error_line[(line_offset + length)..].first(10)
20
20
  error = error_line.slice(line_offset, length)
21
21
 
22
22
  details = [before_error, error, after_error]
@@ -51,10 +51,10 @@ module Curlybars
51
51
  r(/'(.*?)'/, :curly) { [:LITERAL, match[1].inspect] }
52
52
  r(/"(.*?)"/, :curly) { [:LITERAL, match[1].inspect] }
53
53
 
54
- r(/@((?:\.\.\/)*#{IDENTIFIER})/, :curly) { |variable| [:VARIABLE, match[1]] }
54
+ r(/@((?:\.\.\/)*#{IDENTIFIER})/o, :curly) { |variable| [:VARIABLE, match[1]] }
55
55
 
56
- r(/(#{IDENTIFIER})\s*=/, :curly) { [:KEY, match[1]] }
57
- r(/(?:\.\.\/)*(?:#{IDENTIFIER}\.)*#{IDENTIFIER}/, :curly) { |name| [:PATH, name] }
56
+ r(/(#{IDENTIFIER})\s*=/o, :curly) { [:KEY, match[1]] }
57
+ r(/(?:\.\.\/)*(?:#{IDENTIFIER}\.)*#{IDENTIFIER}/o, :curly) { |name| [:PATH, name] }
58
58
 
59
59
  r(/\s/, :curly)
60
60
 
@@ -87,15 +87,12 @@ module Curlybars
87
87
  end
88
88
 
89
89
  def resolve(branches)
90
- @value ||= begin
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
90
+ if !defined?(@value) && Curlybars.global_helpers_dependency_tree.key?(path.to_sym)
91
+ dep_node = Curlybars.global_helpers_dependency_tree[path.to_sym]
92
+ @value = dep_node.nil? ? :helper : [:helper, dep_node]
93
+ end
98
94
 
95
+ @value ||= begin
99
96
  path_split_by_slashes = path.split('/')
100
97
  backward_steps_on_branches = path_split_by_slashes.count - 1
101
98
  base_tree_position = branches.length - backward_steps_on_branches
@@ -108,10 +105,10 @@ module Curlybars
108
105
  dotted_path_side = path_split_by_slashes.last
109
106
 
110
107
  offset_adjustment = 0
111
- dotted_path_side.split(/\./).map(&:to_sym).inject(base_tree) do |sub_tree, step|
108
+ dotted_path_side.split(".").map(&:to_sym).inject(base_tree) do |sub_tree, step|
112
109
  if step == :this
113
110
  next sub_tree
114
- elsif step == :length && (sub_tree.is_a?(Array) && sub_tree.first.is_a?(Hash))
111
+ elsif step == :length && sub_tree.is_a?(Array) && sub_tree.first.is_a?(Hash)
115
112
  next nil # :length is synthesised leaf
116
113
  elsif !(sub_tree.is_a?(Hash) && sub_tree.key?(step))
117
114
  message = step.to_s == path ? "'#{path}' does not exist" : "not possible to access `#{step}` in `#{path}`"
@@ -0,0 +1,112 @@
1
+ module Curlybars
2
+ class PathFinder
3
+ def initialize(ast)
4
+ @ast = ast
5
+ @matches = []
6
+ end
7
+
8
+ def find(target_path)
9
+ @matches = []
10
+ @target_segments = normalize_path(target_path)
11
+ traverse(@ast.template, [])
12
+ @matches
13
+ end
14
+
15
+ private
16
+
17
+ def normalize_path(path)
18
+ return path if path.is_a?(Array)
19
+
20
+ path.to_s.split('.')
21
+ end
22
+
23
+ def traverse(node, context_stack)
24
+ return unless node
25
+
26
+ case node
27
+ when Curlybars::Node::Template
28
+ node.items.each { |item| traverse(item, context_stack) }
29
+ when Curlybars::Node::Item
30
+ traverse(node.item, context_stack)
31
+ when Curlybars::Node::Path
32
+ check_path_match(node, context_stack)
33
+ when Curlybars::Node::Output
34
+ traverse(node.value, context_stack)
35
+ when Curlybars::Node::Partial
36
+ traverse(node.path, context_stack)
37
+ when Curlybars::Node::SubExpression
38
+ traverse(node.helper, context_stack)
39
+ node.arguments&.each { |arg| traverse(arg, context_stack) }
40
+ node.options&.each { |opt| traverse(opt.expression, context_stack) }
41
+ when Curlybars::Node::BlockHelperElse
42
+ handle_block_helper(node, context_stack)
43
+ when Curlybars::Node::IfElse, Curlybars::Node::UnlessElse
44
+ handle_conditional(node, context_stack)
45
+ when Curlybars::Node::EachElse
46
+ handle_each(node, context_stack)
47
+ when Curlybars::Node::WithElse
48
+ handle_with(node, context_stack)
49
+ end
50
+ end
51
+
52
+ def check_path_match(path_node, context_stack)
53
+ path_segments = path_node.path.split('.')
54
+ resolved_segments = resolve_path(path_segments, context_stack)
55
+
56
+ @matches << path_node if resolved_segments == @target_segments
57
+ end
58
+
59
+ def resolve_path(path_segments, context_stack)
60
+ full_path = path_segments.join('.')
61
+ stack = context_stack.dup
62
+
63
+ # Handle ../ parent navigation
64
+ while full_path.start_with?('../')
65
+ full_path = full_path[3..]
66
+ stack.pop unless stack.empty?
67
+ end
68
+
69
+ remaining_segments = full_path.empty? ? [] : full_path.split('.')
70
+ stack + remaining_segments
71
+ end
72
+
73
+ def handle_block_helper(node, context_stack)
74
+ traverse(node.helper, context_stack)
75
+ traverse(node.helper_template, context_stack)
76
+ traverse(node.else_template, context_stack) if node.else_template
77
+ node.arguments&.each { |arg| traverse(arg, context_stack) }
78
+ node.options&.each { |opt| traverse(opt.expression, context_stack) }
79
+ end
80
+
81
+ def handle_conditional(node, context_stack)
82
+ traverse(node.expression, context_stack)
83
+
84
+ if node.is_a?(Curlybars::Node::IfElse)
85
+ traverse(node.if_template, context_stack)
86
+ elsif node.is_a?(Curlybars::Node::UnlessElse)
87
+ traverse(node.unless_template, context_stack)
88
+ end
89
+ traverse(node.else_template, context_stack) if node.else_template
90
+ end
91
+
92
+ def handle_each(node, context_stack)
93
+ traverse(node.path, context_stack)
94
+
95
+ # EachElse changes context to the item being iterated
96
+ collection_path = node.path.respond_to?(:subexpression?) && node.path.subexpression? ? node.path.helper : node.path
97
+ new_context = context_stack + collection_path.path.split('.')
98
+ traverse(node.each_template, new_context)
99
+ traverse(node.else_template, context_stack) if node.else_template
100
+ end
101
+
102
+ def handle_with(node, context_stack)
103
+ traverse(node.path, context_stack)
104
+
105
+ # WithElse changes context to the specified path
106
+ presenter_path = node.path.respond_to?(:subexpression?) && node.path.subexpression? ? node.path.helper : node.path
107
+ new_context = context_stack + presenter_path.path.split('.')
108
+ traverse(node.with_template, new_context)
109
+ traverse(node.else_template, context_stack) if node.else_template
110
+ end
111
+ end
112
+ end
@@ -60,7 +60,7 @@ module Curlybars
60
60
  end
61
61
  end
62
62
 
63
- instance_variable_set("@#{name}", value)
63
+ instance_variable_set(:"@#{name}", value)
64
64
  end
65
65
  end
66
66
 
@@ -276,8 +276,8 @@ module Curlybars
276
276
  # Delegates private method calls to the current view context.
277
277
  #
278
278
  # The view context, an instance of ActionView::Base, is set by Rails.
279
- def method_missing(method, *args, **kwargs, &block)
280
- @_context.public_send(method, *args, **kwargs, &block)
279
+ def method_missing(method, ...)
280
+ @_context.public_send(method, ...)
281
281
  end
282
282
 
283
283
  # Tells ruby (and developers) what methods we can accept.
@@ -140,15 +140,15 @@ module Curlybars
140
140
  check_context_is_hash_or_enum_of_presenters(collection, path, position)
141
141
  if collection.is_a?(Hash)
142
142
  collection
143
- elsif collection.respond_to? :each_with_index
144
- collection.each_with_index.map { |value, index| [index, value] }.to_h
143
+ elsif collection.respond_to?(:each_with_index)
144
+ collection.each_with_index.to_h { |value, index| [index, value] }
145
145
  else
146
146
  raise "Collection is not coerceable to hash"
147
147
  end
148
148
  end
149
149
 
150
150
  def presenter?(context)
151
- context.respond_to? :allows_method?
151
+ context.respond_to?(:allows_method?)
152
152
  end
153
153
 
154
154
  def presenter_collection?(collection)
@@ -180,12 +180,12 @@ module Curlybars
180
180
 
181
181
  attr_reader :contexts, :variables, :cached_calls, :file_name, :global_helpers, :start_time, :timeout, :cache
182
182
 
183
- def instrument(meth, &block)
183
+ def instrument(meth, &)
184
184
  # Instruments only callables that give enough details (eg. methods)
185
185
  return yield unless meth.respond_to?(:name) && meth.respond_to?(:owner)
186
186
 
187
187
  payload = { presenter: meth.owner, method: meth.name }
188
- ActiveSupport::Notifications.instrument("call_to_presenter.curlybars", payload, &block)
188
+ ActiveSupport::Notifications.instrument("call_to_presenter.curlybars", payload, &)
189
189
  end
190
190
 
191
191
  def arguments_for_signature(helper, arguments, options)
@@ -43,6 +43,9 @@ module Curlybars
43
43
 
44
44
  private
45
45
 
46
+ LEADING_ENCODING_REGEXP = /\A#{ActionView::ENCODING_FLAG}/
47
+ private_constant :LEADING_ENCODING_REGEXP
48
+
46
49
  def compile(template, source)
47
50
  # Template is empty, so there's no need to initialize a presenter.
48
51
  return %("") if source.empty?
@@ -55,7 +58,7 @@ module Curlybars
55
58
  # For security reason, we strip the encoding directive in order to avoid
56
59
  # potential issues when rendering the template in another character
57
60
  # encoding.
58
- safe_source = source.gsub(/\A#{ActionView::ENCODING_FLAG}/, '')
61
+ safe_source = source.sub(LEADING_ENCODING_REGEXP, '')
59
62
 
60
63
  source = Curlybars.compile(safe_source, template.identifier)
61
64
 
@@ -84,9 +87,9 @@ module Curlybars
84
87
  RUBY
85
88
  end
86
89
 
87
- def instrument(template, &block)
90
+ def instrument(template, &)
88
91
  payload = { path: template.virtual_path }
89
- ActiveSupport::Notifications.instrument("compile.curlybars", payload, &block)
92
+ ActiveSupport::Notifications.instrument("compile.curlybars", payload, &)
90
93
  end
91
94
  end
92
95
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Curlybars
4
- VERSION = '1.12.0'
4
+ VERSION = '1.13.0'
5
5
  end
@@ -20,7 +20,7 @@ module Curlybars
20
20
  return unless class_name.start_with?('Curlybars::Node')
21
21
 
22
22
  method_name = class_name.demodulize.underscore
23
- send("visit_#{method_name}", node)
23
+ send(:"visit_#{method_name}", node)
24
24
  end
25
25
 
26
26
  def visit_block_helper_else(node)
data/lib/curlybars.rb CHANGED
@@ -63,8 +63,8 @@ module Curlybars
63
63
  # identifier - The the file name of the template being checked (defaults to `nil`).
64
64
  #
65
65
  # Returns true if the template is valid, false otherwise.
66
- def valid?(presenter_class, source, identifier = nil, **options)
67
- errors = validate(presenter_class, source, identifier, **options)
66
+ def valid?(presenter_class, source, identifier = nil, **)
67
+ errors = validate(presenter_class, source, identifier, **)
68
68
  errors.empty?
69
69
  end
70
70
 
@@ -78,6 +78,20 @@ module Curlybars
78
78
  visitor.accept(tree)
79
79
  end
80
80
 
81
+ # Find all path nodes in the AST that resolve to a given path.
82
+ # Takes into account contextual scope from block helpers like #with, #each, etc.
83
+ #
84
+ # target_path - The path String to search for (e.g., "user.name" or "user.organizations.id").
85
+ # source - The source HBS String used to generate an AST.
86
+ # identifier - The the file name of the template being checked (defaults to `nil`).
87
+ #
88
+ # Returns an Array of Curlybars::Node::Path instances that match the target path.
89
+ def find(target_path, source, identifier = nil)
90
+ tree = ast(transformed_source(source), identifier, run_processors: true)
91
+ finder = Curlybars::PathFinder.new(tree)
92
+ finder.find(target_path)
93
+ end
94
+
81
95
  def global_helpers_dependency_tree
82
96
  @global_helpers_dependency_tree ||= begin
83
97
  classes = Curlybars.configuration.global_helpers_provider_classes
@@ -139,3 +153,4 @@ require 'curlybars/railtie' if defined?(Rails)
139
153
  require 'curlybars/presenter'
140
154
  require 'curlybars/method_whitelist'
141
155
  require 'curlybars/visitor'
156
+ require 'curlybars/path_finder'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curlybars
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.0
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Libo Cannici
@@ -12,10 +12,9 @@ authors:
12
12
  - Andreas Garnæs
13
13
  - Augusto Silva
14
14
  - Attila Večerek
15
- autorequire:
16
15
  bindir: bin
17
16
  cert_chain: []
18
- date: 2024-07-10 00:00:00.000000000 Z
17
+ date: 1980-01-02 00:00:00.000000000 Z
19
18
  dependencies:
20
19
  - !ruby/object:Gem::Dependency
21
20
  name: actionpack
@@ -23,28 +22,28 @@ dependencies:
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '6.0'
25
+ version: '7.2'
27
26
  type: :runtime
28
27
  prerelease: false
29
28
  version_requirements: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: '6.0'
32
+ version: '7.2'
34
33
  - !ruby/object:Gem::Dependency
35
34
  name: activesupport
36
35
  requirement: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
- version: '6.0'
39
+ version: '7.2'
41
40
  type: :runtime
42
41
  prerelease: false
43
42
  version_requirements: !ruby/object:Gem::Requirement
44
43
  requirements:
45
44
  - - ">="
46
45
  - !ruby/object:Gem::Version
47
- version: '6.0'
46
+ version: '7.2'
48
47
  - !ruby/object:Gem::Dependency
49
48
  name: ffi
50
49
  requirement: !ruby/object:Gem::Requirement
@@ -73,132 +72,6 @@ dependencies:
73
72
  - - ">="
74
73
  - !ruby/object:Gem::Version
75
74
  version: '0'
76
- - !ruby/object:Gem::Dependency
77
- name: bundler
78
- requirement: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- type: :development
84
- prerelease: false
85
- version_requirements: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- - !ruby/object:Gem::Dependency
91
- name: byebug
92
- requirement: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- type: :development
98
- prerelease: false
99
- version_requirements: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- - !ruby/object:Gem::Dependency
105
- name: railties
106
- requirement: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '6.0'
111
- type: :development
112
- prerelease: false
113
- version_requirements: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '6.0'
118
- - !ruby/object:Gem::Dependency
119
- name: rake
120
- requirement: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- type: :development
126
- prerelease: false
127
- version_requirements: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- - !ruby/object:Gem::Dependency
133
- name: rspec-rails
134
- requirement: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- type: :development
140
- prerelease: false
141
- version_requirements: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- - !ruby/object:Gem::Dependency
147
- name: rubocop
148
- requirement: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - "~>"
151
- - !ruby/object:Gem::Version
152
- version: '1.57'
153
- type: :development
154
- prerelease: false
155
- version_requirements: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - "~>"
158
- - !ruby/object:Gem::Version
159
- version: '1.57'
160
- - !ruby/object:Gem::Dependency
161
- name: rubocop-performance
162
- requirement: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - "~>"
165
- - !ruby/object:Gem::Version
166
- version: '1.19'
167
- type: :development
168
- prerelease: false
169
- version_requirements: !ruby/object:Gem::Requirement
170
- requirements:
171
- - - "~>"
172
- - !ruby/object:Gem::Version
173
- version: '1.19'
174
- - !ruby/object:Gem::Dependency
175
- name: rubocop-rake
176
- requirement: !ruby/object:Gem::Requirement
177
- requirements:
178
- - - "~>"
179
- - !ruby/object:Gem::Version
180
- version: '0.6'
181
- type: :development
182
- prerelease: false
183
- version_requirements: !ruby/object:Gem::Requirement
184
- requirements:
185
- - - "~>"
186
- - !ruby/object:Gem::Version
187
- version: '0.6'
188
- - !ruby/object:Gem::Dependency
189
- name: rubocop-rspec
190
- requirement: !ruby/object:Gem::Requirement
191
- requirements:
192
- - - "~>"
193
- - !ruby/object:Gem::Version
194
- version: '2.25'
195
- type: :development
196
- prerelease: false
197
- version_requirements: !ruby/object:Gem::Requirement
198
- requirements:
199
- - - "~>"
200
- - !ruby/object:Gem::Version
201
- version: '2.25'
202
75
  description: |-
203
76
  A view layer for your Rails apps that separates structure and logic, using Handlebars templates.
204
77
  Strongly inspired by Curly Template gem by Daniel Schierbeck.
@@ -239,6 +112,7 @@ files:
239
112
  - lib/curlybars/node/variable.rb
240
113
  - lib/curlybars/node/with_else.rb
241
114
  - lib/curlybars/parser.rb
115
+ - lib/curlybars/path_finder.rb
242
116
  - lib/curlybars/position.rb
243
117
  - lib/curlybars/presenter.rb
244
118
  - lib/curlybars/processor/tilde.rb
@@ -253,7 +127,6 @@ homepage: https://github.com/zendesk/curlybars
253
127
  licenses:
254
128
  - Apache-2.0
255
129
  metadata: {}
256
- post_install_message:
257
130
  rdoc_options:
258
131
  - "--charset=UTF-8"
259
132
  require_paths:
@@ -262,15 +135,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
262
135
  requirements:
263
136
  - - ">="
264
137
  - !ruby/object:Gem::Version
265
- version: '3.0'
138
+ version: '3.2'
266
139
  required_rubygems_version: !ruby/object:Gem::Requirement
267
140
  requirements:
268
141
  - - ">="
269
142
  - !ruby/object:Gem::Version
270
143
  version: '0'
271
144
  requirements: []
272
- rubygems_version: 3.5.11
273
- signing_key:
145
+ rubygems_version: 3.6.9
274
146
  specification_version: 4
275
147
  summary: Create your views using Handlebars templates!
276
148
  test_files: []