curlybars 0.9.13

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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/lib/curlybars.rb +108 -0
  3. data/lib/curlybars/configuration.rb +41 -0
  4. data/lib/curlybars/dependency_tracker.rb +8 -0
  5. data/lib/curlybars/error/base.rb +18 -0
  6. data/lib/curlybars/error/compile.rb +11 -0
  7. data/lib/curlybars/error/lex.rb +22 -0
  8. data/lib/curlybars/error/parse.rb +41 -0
  9. data/lib/curlybars/error/presenter/not_found.rb +23 -0
  10. data/lib/curlybars/error/render.rb +11 -0
  11. data/lib/curlybars/error/validate.rb +18 -0
  12. data/lib/curlybars/lexer.rb +60 -0
  13. data/lib/curlybars/method_whitelist.rb +69 -0
  14. data/lib/curlybars/node/block_helper_else.rb +108 -0
  15. data/lib/curlybars/node/boolean.rb +24 -0
  16. data/lib/curlybars/node/each_else.rb +69 -0
  17. data/lib/curlybars/node/if_else.rb +33 -0
  18. data/lib/curlybars/node/item.rb +31 -0
  19. data/lib/curlybars/node/literal.rb +28 -0
  20. data/lib/curlybars/node/option.rb +25 -0
  21. data/lib/curlybars/node/output.rb +24 -0
  22. data/lib/curlybars/node/partial.rb +24 -0
  23. data/lib/curlybars/node/path.rb +137 -0
  24. data/lib/curlybars/node/root.rb +29 -0
  25. data/lib/curlybars/node/string.rb +24 -0
  26. data/lib/curlybars/node/template.rb +32 -0
  27. data/lib/curlybars/node/text.rb +24 -0
  28. data/lib/curlybars/node/unless_else.rb +33 -0
  29. data/lib/curlybars/node/variable.rb +34 -0
  30. data/lib/curlybars/node/with_else.rb +54 -0
  31. data/lib/curlybars/parser.rb +183 -0
  32. data/lib/curlybars/position.rb +7 -0
  33. data/lib/curlybars/presenter.rb +288 -0
  34. data/lib/curlybars/processor/tilde.rb +31 -0
  35. data/lib/curlybars/processor/token_factory.rb +9 -0
  36. data/lib/curlybars/railtie.rb +18 -0
  37. data/lib/curlybars/rendering_support.rb +222 -0
  38. data/lib/curlybars/safe_buffer.rb +11 -0
  39. data/lib/curlybars/template_handler.rb +93 -0
  40. data/lib/curlybars/version.rb +3 -0
  41. data/spec/acceptance/application_layout_spec.rb +60 -0
  42. data/spec/acceptance/collection_blocks_spec.rb +28 -0
  43. data/spec/acceptance/global_helper_spec.rb +25 -0
  44. data/spec/curlybars/configuration_spec.rb +57 -0
  45. data/spec/curlybars/error/base_spec.rb +41 -0
  46. data/spec/curlybars/error/compile_spec.rb +19 -0
  47. data/spec/curlybars/error/lex_spec.rb +25 -0
  48. data/spec/curlybars/error/parse_spec.rb +74 -0
  49. data/spec/curlybars/error/render_spec.rb +19 -0
  50. data/spec/curlybars/error/validate_spec.rb +19 -0
  51. data/spec/curlybars/lexer_spec.rb +466 -0
  52. data/spec/curlybars/method_whitelist_spec.rb +168 -0
  53. data/spec/curlybars/processor/tilde_spec.rb +60 -0
  54. data/spec/curlybars/rendering_support_spec.rb +426 -0
  55. data/spec/curlybars/safe_buffer_spec.rb +46 -0
  56. data/spec/curlybars/template_handler_spec.rb +222 -0
  57. data/spec/integration/cache_spec.rb +124 -0
  58. data/spec/integration/comment_spec.rb +60 -0
  59. data/spec/integration/exception_spec.rb +31 -0
  60. data/spec/integration/node/block_helper_else_spec.rb +422 -0
  61. data/spec/integration/node/each_else_spec.rb +204 -0
  62. data/spec/integration/node/each_spec.rb +291 -0
  63. data/spec/integration/node/escape_spec.rb +27 -0
  64. data/spec/integration/node/helper_spec.rb +176 -0
  65. data/spec/integration/node/if_else_spec.rb +129 -0
  66. data/spec/integration/node/if_spec.rb +143 -0
  67. data/spec/integration/node/output_spec.rb +68 -0
  68. data/spec/integration/node/partial_spec.rb +66 -0
  69. data/spec/integration/node/path_spec.rb +286 -0
  70. data/spec/integration/node/root_spec.rb +15 -0
  71. data/spec/integration/node/template_spec.rb +86 -0
  72. data/spec/integration/node/unless_else_spec.rb +129 -0
  73. data/spec/integration/node/unless_spec.rb +130 -0
  74. data/spec/integration/node/with_spec.rb +116 -0
  75. data/spec/integration/processor/tilde_spec.rb +38 -0
  76. data/spec/integration/processors_spec.rb +30 -0
  77. metadata +358 -0
@@ -0,0 +1,31 @@
1
+ module Curlybars
2
+ module Processor
3
+ class Tilde
4
+ extend TokenFactory
5
+
6
+ class << self
7
+ def process!(tokens, identifier)
8
+ tokens.each_with_index do |token, index|
9
+ case token.type
10
+ when :TILDE_START
11
+ tokens[index] = create_token(:START, token.value, token.position)
12
+ next if index == 0
13
+ strip_token_if_text(tokens, index - 1, :rstrip)
14
+ when :TILDE_END
15
+ tokens[index] = create_token(:END, token.value, token.position)
16
+ next if index == (tokens.length - 1)
17
+ strip_token_if_text(tokens, index + 1, :lstrip)
18
+ end
19
+ end
20
+ end
21
+
22
+ def strip_token_if_text(tokens, index, strip_method)
23
+ token = tokens[index]
24
+ return if token.type != :TEXT
25
+ stripped_value = token.value.public_send(strip_method)
26
+ tokens[index] = create_token(token.type, stripped_value, token.position)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ module Curlybars
2
+ module Processor
3
+ module TokenFactory
4
+ def create_token(type, value, position)
5
+ RLTK::Token.new(type, value, position)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ require 'curlybars/template_handler'
2
+ require 'curlybars/dependency_tracker'
3
+
4
+ module Curlybars
5
+ class Railtie < Rails::Railtie
6
+ initializer 'curlybars.initialize_template_handler' do
7
+ ActionView::Template.register_template_handler(:hbs, Curlybars::TemplateHandler)
8
+ end
9
+
10
+ if defined?(CacheDigests::DependencyTracker)
11
+ CacheDigests::DependencyTracker.register_tracker :hbs, Curlybars::DependencyTracker
12
+ end
13
+
14
+ if defined?(ActionView::DependencyTracker)
15
+ ActionView::DependencyTracker.register_tracker :hbs, Curlybars::DependencyTracker
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,222 @@
1
+ module Curlybars
2
+ class RenderingSupport
3
+ def initialize(timeout, contexts, variables, file_name, global_helpers_providers = [], cache = ->(key, &block) { block.call })
4
+ @timeout = timeout
5
+ @start_time = Time.now
6
+
7
+ @contexts = contexts
8
+ @variables = variables
9
+ @file_name = file_name
10
+ @cached_calls = {}
11
+ @cache = cache
12
+
13
+ @global_helpers = {}
14
+
15
+ global_helpers_providers.each do |provider|
16
+ provider.allowed_methods.each do |global_helper_name|
17
+ symbol = global_helper_name.to_sym
18
+ @global_helpers[symbol] = provider.method(symbol)
19
+ end
20
+ end
21
+ end
22
+
23
+ def check_timeout!
24
+ return unless timeout.present?
25
+ return unless (Time.now - start_time) > timeout
26
+ message = "Rendering took too long (> #{timeout} seconds)"
27
+ raise ::Curlybars::Error::Render.new('timeout', message, nil)
28
+ end
29
+
30
+ def check_context_is_presenter(context, path, position)
31
+ return if presenter?(context)
32
+ message = "`#{path}` is not a context type object"
33
+ raise Curlybars::Error::Render.new('context_is_not_a_presenter', message, position)
34
+ end
35
+
36
+ def check_context_is_hash_or_enum_of_presenters(collection, path, position)
37
+ return if presenter_collection?(collection)
38
+
39
+ message = "`#{path}` is not an array of presenters or a hash of such"
40
+ raise Curlybars::Error::Render.new('context_is_not_an_array_of_presenters', message, position)
41
+ end
42
+
43
+ def to_bool(condition)
44
+ condition != false &&
45
+ condition != [] &&
46
+ condition != {} &&
47
+ condition != 0 &&
48
+ condition != '' &&
49
+ !condition.nil?
50
+ end
51
+
52
+ def variable(variable_path, position)
53
+ check_traverse_not_too_deep(variable_path, position)
54
+
55
+ variable_split_by_slashes = variable_path.split('/')
56
+ variable = variable_split_by_slashes.last.to_sym
57
+ backward_steps_on_variables = variable_split_by_slashes.count - 1
58
+ variables_position = variables.length - backward_steps_on_variables
59
+ scope = variables.first(variables_position).reverse.find do |vars|
60
+ vars.key? variable
61
+ end
62
+ return scope[variable] if scope
63
+ end
64
+
65
+ def path(path, position)
66
+ return global_helpers[path.to_sym] if global_helpers.key?(path.to_sym)
67
+
68
+ check_traverse_not_too_deep(path, position)
69
+
70
+ path_split_by_slashes = path.split('/')
71
+ backward_steps_on_contexts = path_split_by_slashes.count - 1
72
+ base_context_position = contexts.length - backward_steps_on_contexts
73
+
74
+ return -> {} unless base_context_position > 0
75
+
76
+ base_context_index = base_context_position - 1
77
+ base_context = contexts[base_context_index]
78
+
79
+ dotted_path_side = path_split_by_slashes.last
80
+ chain = dotted_path_side.split('.')
81
+ method_to_return = chain.pop
82
+
83
+ resolved = chain.inject(base_context) do |context, meth|
84
+ next context if meth == 'this'
85
+ next context.count if meth == 'length' && presenter_collection?(context)
86
+ raise_if_not_traversable(context, meth, position)
87
+ outcome = instrument(context.method(meth)) { context.public_send(meth) }
88
+ return -> {} if outcome.nil?
89
+ outcome
90
+ end
91
+
92
+ return -> { resolved } if method_to_return == 'this'
93
+
94
+ if method_to_return == 'length' && presenter_collection?(resolved)
95
+ return -> { resolved.count }
96
+ end
97
+
98
+ raise_if_not_traversable(resolved, method_to_return, position)
99
+ resolved.method(method_to_return.to_sym)
100
+ end
101
+
102
+ def cached_call(meth)
103
+ return cached_calls[meth] if cached_calls.key? meth
104
+ instrument(meth) { cached_calls[meth] = meth.call }
105
+ end
106
+
107
+ def call(helper, helper_path, helper_position, arguments, options, &block)
108
+ parameters = helper.parameters
109
+ parameter_types = parameters.map(&:first)
110
+
111
+ # parameters has value [[:rest]] when the presenter is using method_missing to catch all calls
112
+ has_invalid_parameters = parameter_types.map { |type| type != :req }.any? && parameter_types != [:rest]
113
+ if has_invalid_parameters
114
+ source_location = helper.source_location
115
+
116
+ file_path = source_location ? source_location.first : "n/a"
117
+ line_number = source_location ? helper.source_location.last : "n/a"
118
+
119
+ message = "#{file_path}:#{line_number} - `#{helper_path}` bad signature "
120
+ message << "for #{helper} - helpers must have only required parameters"
121
+ raise Curlybars::Error::Render.new('invalid_helper_signature', message, helper_position)
122
+ end
123
+
124
+ instrument(helper) do
125
+ helper.call(*arguments_for_signature(helper, arguments, options), &block)
126
+ end
127
+ end
128
+
129
+ def position(line_number, line_offset)
130
+ Curlybars::Position.new(file_name, line_number, line_offset)
131
+ end
132
+
133
+ def coerce_to_hash!(collection, path, position)
134
+ check_context_is_hash_or_enum_of_presenters(collection, path, position)
135
+ if collection.is_a?(Hash)
136
+ collection
137
+ elsif collection.respond_to? :each_with_index
138
+ collection.each_with_index.map { |value, index| [index, value] }.to_h
139
+ else
140
+ raise "Collection is not coerceable to hash"
141
+ end
142
+ end
143
+
144
+ def presenter?(context)
145
+ context.respond_to? :allows_method?
146
+ end
147
+
148
+ def presenter_collection?(collection)
149
+ collection = collection.values if collection.is_a?(Hash)
150
+
151
+ collection.respond_to?(:each) && collection.all? do |presenter|
152
+ presenter?(presenter)
153
+ end
154
+ end
155
+
156
+ def optional_presenter_cache(presenter, template_cache_key, buffer)
157
+ presenter_cache_key = presenter.respond_to?(:cache_key) ? presenter.cache_key : nil
158
+
159
+ if presenter_cache_key
160
+ cache_key = "#{presenter_cache_key}/#{template_cache_key}"
161
+
162
+ buffer << cache.call(cache_key) do
163
+ # Output from the block must be isolated from the main output buffer
164
+ SafeBuffer.new.tap do |cache_buffer|
165
+ yield cache_buffer
166
+ end
167
+ end
168
+ else
169
+ yield buffer
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ attr_reader :contexts, :variables, :cached_calls, :file_name, :global_helpers, :start_time, :timeout, :cache
176
+
177
+ def instrument(meth, &block)
178
+ # Instruments only callables that give enough details (eg. methods)
179
+ return yield unless meth.respond_to?(:name) && meth.respond_to?(:owner)
180
+
181
+ payload = { presenter: meth.owner, method: meth.name }
182
+ ActiveSupport::Notifications.instrument("call_to_presenter.curlybars", payload, &block)
183
+ end
184
+
185
+ def arguments_for_signature(helper, arguments, options)
186
+ return [] if helper.parameters.empty?
187
+ return arguments if helper.parameters.map(&:first) == [:rest]
188
+
189
+ number_of_parameters_available_for_arguments = helper.parameters.length - 1
190
+ arguments_that_can_fit = arguments.first(number_of_parameters_available_for_arguments)
191
+ nil_padding_length = number_of_parameters_available_for_arguments - arguments_that_can_fit.length
192
+ nil_padding = Array.new(nil_padding_length)
193
+
194
+ [arguments_that_can_fit, nil_padding, options].flatten(1)
195
+ end
196
+
197
+ def raise_if_not_traversable(context, meth, position)
198
+ check_context_is_presenter(context, meth, position)
199
+ check_context_allows_method(context, meth, position)
200
+ check_context_has_method(context, meth, position)
201
+ end
202
+
203
+ def check_context_allows_method(context, meth, position)
204
+ return if context.allows_method?(meth.to_sym)
205
+ message = "`#{meth}` is not available - "
206
+ message += "add `allow_methods :#{meth}` to #{context.class} to allow this path"
207
+ raise Curlybars::Error::Render.new('unallowed_path', message, position, meth: meth.to_sym)
208
+ end
209
+
210
+ def check_context_has_method(context, meth, position)
211
+ return if context.respond_to?(meth.to_sym)
212
+ message = "`#{meth}` is not available in #{context.class}"
213
+ raise Curlybars::Error::Render.new('unallowed_path', message, position)
214
+ end
215
+
216
+ def check_traverse_not_too_deep(traverse, position)
217
+ return unless traverse.count('.') > Curlybars.configuration.traversing_limit
218
+ message = "`#{traverse}` too deep"
219
+ raise Curlybars::Error::Render.new('traverse_too_deep', message, position)
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,11 @@
1
+ module Curlybars
2
+ class SafeBuffer < ActiveSupport::SafeBuffer
3
+ def concat(buffer)
4
+ if (length + buffer.length) > Curlybars.configuration.output_limit
5
+ message = "Output too long (> %s bytes)" % Curlybars.configuration.output_limit
6
+ raise Curlybars::Error::Render.new('output_too_long', message, nil)
7
+ end
8
+ super
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,93 @@
1
+ require 'active_support'
2
+ require 'action_view'
3
+ require 'curlybars'
4
+ require 'curlybars/error/presenter/not_found'
5
+
6
+ module Curlybars
7
+ class TemplateHandler
8
+ class << self
9
+ # Handles a Curlybars template, compiling it to Ruby code. The code will be
10
+ # evaluated in the context of an ActionView::Base instance, having access
11
+ # to a number of variables.
12
+ #
13
+ # template - The ActionView::Template template that should be compiled.
14
+ #
15
+ # Returns a String containing the Ruby code representing the template.
16
+ def call(template)
17
+ instrument(template) do
18
+ compile(template)
19
+ end
20
+ end
21
+
22
+ def cache_if_key_is_not_nil(context, presenter)
23
+ key = presenter.cache_key
24
+ if key.present?
25
+ presenter_key = if presenter.class.respond_to?(:cache_key)
26
+ presenter.class.cache_key
27
+ end
28
+
29
+ cache_options = presenter.cache_options || {}
30
+ cache_options[:expires_in] ||= presenter.cache_duration
31
+
32
+ # Curlybars doesn't allow Rails to handle the template digest.
33
+ # So, we disable it.
34
+ cache_options[:skip_digest] = true
35
+
36
+ context.cache([key, presenter_key].compact, cache_options) do
37
+ yield
38
+ end
39
+ else
40
+ yield
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def compile(template)
47
+ # Template is empty, so there's no need to initialize a presenter.
48
+ return %("") if template.source.empty?
49
+
50
+ path = template.virtual_path
51
+ presenter_class = Curlybars::Presenter.presenter_for_path(path)
52
+
53
+ raise Curlybars::Error::Presenter::NotFound.new(path) if presenter_class.nil?
54
+
55
+ # For security reason, we strip the encoding directive in order to avoid
56
+ # potential issues when rendering the template in another character
57
+ # encoding.
58
+ safe_source = template.source.gsub(/\A#{ActionView::ENCODING_FLAG}/, '')
59
+
60
+ source = Curlybars.compile(safe_source, template.identifier)
61
+
62
+ <<-RUBY
63
+ if local_assigns.empty?
64
+ options = assigns
65
+ else
66
+ options = local_assigns
67
+ end
68
+
69
+ provider_classes = ::Curlybars.configuration.global_helpers_provider_classes
70
+ global_helpers_providers = provider_classes.map { |klass| klass.new(self) }
71
+
72
+ presenter = ::#{presenter_class}.new(self, options)
73
+ presenter.setup!
74
+
75
+ @output_buffer = output_buffer || ::ActiveSupport::SafeBuffer.new
76
+
77
+ ::Curlybars::TemplateHandler.cache_if_key_is_not_nil(self, presenter) do
78
+ safe_concat begin
79
+ #{source}
80
+ end
81
+ end
82
+
83
+ @output_buffer
84
+ RUBY
85
+ end
86
+
87
+ def instrument(template, &block)
88
+ payload = { path: template.virtual_path }
89
+ ActiveSupport::Notifications.instrument("compile.curlybars", payload, &block)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,3 @@
1
+ module Curlybars
2
+ VERSION = '0.9.13'.freeze
3
+ end
@@ -0,0 +1,60 @@
1
+ describe "Using Curlybars for the application layout", type: :request do
2
+ example "A simple layout view in Curlybars" do
3
+ get '/'
4
+
5
+ expect(body).to eq(<<-HTML.strip_heredoc)
6
+ <html>
7
+ <head>
8
+ <title>Dummy app</title>
9
+ </head>
10
+ <body>
11
+ <h1>Dashboard</h1>
12
+ <p>Hello, World!</p>
13
+ <p>Welcome!</p>
14
+
15
+ </body>
16
+ </html>
17
+ HTML
18
+ end
19
+
20
+ describe "Curlybars" do
21
+ before do
22
+ Curlybars.configure do |config|
23
+ config.presenters_namespace = 'curlybars_presenters'
24
+ end
25
+ end
26
+
27
+ after do
28
+ Curlybars.reset
29
+ end
30
+
31
+ example "A simple layout view in Curlybars with html safe logic" do
32
+ get '/articles/1'
33
+
34
+ expect(body).to eq(<<-HTML.strip_heredoc)
35
+ <html>
36
+ <head>
37
+ <title>Dummy app</title>
38
+ </head>
39
+ <body>
40
+ <p>Hi Admin</p>
41
+
42
+ <h1>Article: The Prince</h1>
43
+
44
+ <p>This is <strong>important</strong>!</p>
45
+
46
+ <p>&lt;script&gt;alert(&#39;bad&#39;)&lt;/script&gt;</p>
47
+
48
+
49
+ <p>
50
+ author Nicolò
51
+ <img src="http://example.com/foo.png" />
52
+ </p>
53
+
54
+
55
+ </body>
56
+ </html>
57
+ HTML
58
+ end
59
+ end
60
+ end