curlybars 1.11.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: 75e0f6f2934a512ad3ba98455f3eab1492ddd131489322d4082a19cf9568b1cc
4
- data.tar.gz: 9e36b98d7c878e4083ca27f34a7daccc2eee4f39bb6e4e5040c186a806321f87
3
+ metadata.gz: 2ef985c46340022e337318774e4fcf8f36a1f0f7fc6d87788d70ffa3b214f16f
4
+ data.tar.gz: 1bd1f9195be76418bc0b416f6466aa73809f39037d93ccdde901d90ffca14baa
5
5
  SHA512:
6
- metadata.gz: 6aeb6a8ace0d10637dcc0a9be64fc23e150d1971afb01d4ce8c735d9740e6a1f1644c33cd662dbd3fbbaa9e2b8b34a2c76aa735d064eb08118dd54435c3eb5c9
7
- data.tar.gz: fd1289f763c7870e921120b849a1f1fa8de334c9998e8ca34fc4758384a83fc62ac86070b382b934f47961a4f0b7073c49ade00ebb77572b0d419d7e8a5d8677
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Curlybars
2
- VERSION = '1.11.0'.freeze
4
+ VERSION = '1.13.0'
3
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.11.0
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Libo Cannici
@@ -12,19 +12,15 @@ 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-02-27 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
22
21
  requirement: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '6.0'
27
- - - "<"
28
24
  - !ruby/object:Gem::Version
29
25
  version: '7.2'
30
26
  type: :runtime
@@ -32,9 +28,6 @@ dependencies:
32
28
  version_requirements: !ruby/object:Gem::Requirement
33
29
  requirements:
34
30
  - - ">="
35
- - !ruby/object:Gem::Version
36
- version: '6.0'
37
- - - "<"
38
31
  - !ruby/object:Gem::Version
39
32
  version: '7.2'
40
33
  - !ruby/object:Gem::Dependency
@@ -42,9 +35,6 @@ dependencies:
42
35
  requirement: !ruby/object:Gem::Requirement
43
36
  requirements:
44
37
  - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '6.0'
47
- - - "<"
48
38
  - !ruby/object:Gem::Version
49
39
  version: '7.2'
50
40
  type: :runtime
@@ -52,9 +42,6 @@ dependencies:
52
42
  version_requirements: !ruby/object:Gem::Requirement
53
43
  requirements:
54
44
  - - ">="
55
- - !ruby/object:Gem::Version
56
- version: '6.0'
57
- - - "<"
58
45
  - !ruby/object:Gem::Version
59
46
  version: '7.2'
60
47
  - !ruby/object:Gem::Dependency
@@ -85,138 +72,6 @@ dependencies:
85
72
  - - ">="
86
73
  - !ruby/object:Gem::Version
87
74
  version: '0'
88
- - !ruby/object:Gem::Dependency
89
- name: bundler
90
- requirement: !ruby/object:Gem::Requirement
91
- requirements:
92
- - - ">="
93
- - !ruby/object:Gem::Version
94
- version: '0'
95
- type: :development
96
- prerelease: false
97
- version_requirements: !ruby/object:Gem::Requirement
98
- requirements:
99
- - - ">="
100
- - !ruby/object:Gem::Version
101
- version: '0'
102
- - !ruby/object:Gem::Dependency
103
- name: byebug
104
- requirement: !ruby/object:Gem::Requirement
105
- requirements:
106
- - - ">="
107
- - !ruby/object:Gem::Version
108
- version: '0'
109
- type: :development
110
- prerelease: false
111
- version_requirements: !ruby/object:Gem::Requirement
112
- requirements:
113
- - - ">="
114
- - !ruby/object:Gem::Version
115
- version: '0'
116
- - !ruby/object:Gem::Dependency
117
- name: railties
118
- requirement: !ruby/object:Gem::Requirement
119
- requirements:
120
- - - ">="
121
- - !ruby/object:Gem::Version
122
- version: '6.0'
123
- - - "<"
124
- - !ruby/object:Gem::Version
125
- version: '7.2'
126
- type: :development
127
- prerelease: false
128
- version_requirements: !ruby/object:Gem::Requirement
129
- requirements:
130
- - - ">="
131
- - !ruby/object:Gem::Version
132
- version: '6.0'
133
- - - "<"
134
- - !ruby/object:Gem::Version
135
- version: '7.2'
136
- - !ruby/object:Gem::Dependency
137
- name: rake
138
- requirement: !ruby/object:Gem::Requirement
139
- requirements:
140
- - - ">="
141
- - !ruby/object:Gem::Version
142
- version: '0'
143
- type: :development
144
- prerelease: false
145
- version_requirements: !ruby/object:Gem::Requirement
146
- requirements:
147
- - - ">="
148
- - !ruby/object:Gem::Version
149
- version: '0'
150
- - !ruby/object:Gem::Dependency
151
- name: rspec-rails
152
- requirement: !ruby/object:Gem::Requirement
153
- requirements:
154
- - - ">="
155
- - !ruby/object:Gem::Version
156
- version: '0'
157
- type: :development
158
- prerelease: false
159
- version_requirements: !ruby/object:Gem::Requirement
160
- requirements:
161
- - - ">="
162
- - !ruby/object:Gem::Version
163
- version: '0'
164
- - !ruby/object:Gem::Dependency
165
- name: rubocop
166
- requirement: !ruby/object:Gem::Requirement
167
- requirements:
168
- - - "~>"
169
- - !ruby/object:Gem::Version
170
- version: '1.57'
171
- type: :development
172
- prerelease: false
173
- version_requirements: !ruby/object:Gem::Requirement
174
- requirements:
175
- - - "~>"
176
- - !ruby/object:Gem::Version
177
- version: '1.57'
178
- - !ruby/object:Gem::Dependency
179
- name: rubocop-performance
180
- requirement: !ruby/object:Gem::Requirement
181
- requirements:
182
- - - "~>"
183
- - !ruby/object:Gem::Version
184
- version: '1.19'
185
- type: :development
186
- prerelease: false
187
- version_requirements: !ruby/object:Gem::Requirement
188
- requirements:
189
- - - "~>"
190
- - !ruby/object:Gem::Version
191
- version: '1.19'
192
- - !ruby/object:Gem::Dependency
193
- name: rubocop-rake
194
- requirement: !ruby/object:Gem::Requirement
195
- requirements:
196
- - - "~>"
197
- - !ruby/object:Gem::Version
198
- version: '0.6'
199
- type: :development
200
- prerelease: false
201
- version_requirements: !ruby/object:Gem::Requirement
202
- requirements:
203
- - - "~>"
204
- - !ruby/object:Gem::Version
205
- version: '0.6'
206
- - !ruby/object:Gem::Dependency
207
- name: rubocop-rspec
208
- requirement: !ruby/object:Gem::Requirement
209
- requirements:
210
- - - "~>"
211
- - !ruby/object:Gem::Version
212
- version: '2.25'
213
- type: :development
214
- prerelease: false
215
- version_requirements: !ruby/object:Gem::Requirement
216
- requirements:
217
- - - "~>"
218
- - !ruby/object:Gem::Version
219
- version: '2.25'
220
75
  description: |-
221
76
  A view layer for your Rails apps that separates structure and logic, using Handlebars templates.
222
77
  Strongly inspired by Curly Template gem by Daniel Schierbeck.
@@ -257,6 +112,7 @@ files:
257
112
  - lib/curlybars/node/variable.rb
258
113
  - lib/curlybars/node/with_else.rb
259
114
  - lib/curlybars/parser.rb
115
+ - lib/curlybars/path_finder.rb
260
116
  - lib/curlybars/position.rb
261
117
  - lib/curlybars/presenter.rb
262
118
  - lib/curlybars/processor/tilde.rb
@@ -271,7 +127,6 @@ homepage: https://github.com/zendesk/curlybars
271
127
  licenses:
272
128
  - Apache-2.0
273
129
  metadata: {}
274
- post_install_message:
275
130
  rdoc_options:
276
131
  - "--charset=UTF-8"
277
132
  require_paths:
@@ -280,15 +135,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
280
135
  requirements:
281
136
  - - ">="
282
137
  - !ruby/object:Gem::Version
283
- version: '3.0'
138
+ version: '3.2'
284
139
  required_rubygems_version: !ruby/object:Gem::Requirement
285
140
  requirements:
286
141
  - - ">="
287
142
  - !ruby/object:Gem::Version
288
143
  version: '0'
289
144
  requirements: []
290
- rubygems_version: 3.5.3
291
- signing_key:
145
+ rubygems_version: 3.6.9
292
146
  specification_version: 4
293
147
  summary: Create your views using Handlebars templates!
294
148
  test_files: []