curlybars 0.9.13

Sign up to get free protection for your applications and to get access to all the features.
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