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.
- checksums.yaml +4 -4
- data/lib/curlybars.rb +0 -1
- data/lib/curlybars/configuration.rb +1 -9
- data/lib/curlybars/error/base.rb +2 -0
- data/lib/curlybars/lexer.rb +3 -0
- data/lib/curlybars/method_whitelist.rb +16 -11
- data/lib/curlybars/node/block_helper_else.rb +1 -0
- data/lib/curlybars/node/if_else.rb +1 -1
- data/lib/curlybars/node/path.rb +6 -0
- data/lib/curlybars/node/sub_expression.rb +62 -0
- data/lib/curlybars/node/unless_else.rb +1 -1
- data/lib/curlybars/parser.rb +11 -0
- data/lib/curlybars/processor/tilde.rb +3 -0
- data/lib/curlybars/rendering_support.rb +9 -1
- data/lib/curlybars/template_handler.rb +18 -6
- data/lib/curlybars/version.rb +1 -1
- data/lib/curlybars/visitor.rb +6 -0
- data/spec/acceptance/application_layout_spec.rb +2 -2
- data/spec/acceptance/collection_blocks_spec.rb +1 -1
- data/spec/acceptance/global_helper_spec.rb +1 -1
- data/spec/curlybars/lexer_spec.rb +25 -2
- data/spec/curlybars/method_whitelist_spec.rb +15 -0
- data/spec/curlybars/rendering_support_spec.rb +4 -9
- data/spec/curlybars/template_handler_spec.rb +33 -30
- data/spec/integration/cache_spec.rb +20 -18
- data/spec/integration/node/block_helper_else_spec.rb +0 -2
- data/spec/integration/node/each_else_spec.rb +0 -2
- data/spec/integration/node/each_spec.rb +0 -2
- data/spec/integration/node/helper_spec.rb +12 -2
- data/spec/integration/node/if_else_spec.rb +0 -2
- data/spec/integration/node/if_spec.rb +2 -4
- data/spec/integration/node/output_spec.rb +0 -2
- data/spec/integration/node/partial_spec.rb +0 -2
- data/spec/integration/node/path_spec.rb +0 -2
- data/spec/integration/node/root_spec.rb +0 -2
- data/spec/integration/node/sub_expression_spec.rb +426 -0
- data/spec/integration/node/template_spec.rb +0 -2
- data/spec/integration/node/unless_else_spec.rb +2 -4
- data/spec/integration/node/unless_spec.rb +0 -2
- data/spec/integration/node/with_spec.rb +0 -2
- data/spec/integration/processors_spec.rb +0 -1
- data/spec/integration/visitor_spec.rb +13 -5
- metadata +47 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2be6d03409f5c575fb33c7ba4ed74117420fd22ddca8272005c5f23e459a0d8e
|
4
|
+
data.tar.gz: ad11b01849c9d28a70b2ae2260615665cc2dc3ca3c380b9dfbc2ab9c91bf1de3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba6035a72fd5cf75120cc3adfc663192dae68d75132887b801bd29a53c4b655f6be09ed6b8e4a2ee497e708172c38db19258215b5f3b1a9b788371db20eeac3c
|
7
|
+
data.tar.gz: 83961d2f0ef03848c1ffb3feb9db859c866ff90e74d173f116b16c3af5ff4eac710e934cd7b3bb5039749f05169bd4f95e3f19fc208163f3333a6d66ba7261df
|
data/lib/curlybars.rb
CHANGED
@@ -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 = ''
|
data/lib/curlybars/error/base.rb
CHANGED
@@ -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
|
data/lib/curlybars/lexer.rb
CHANGED
@@ -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
|
-
|
18
|
+
@method_whitelist_allowed_methods ||= begin
|
19
|
+
methods_list = methods_without_type + methods_with_type.keys
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
25
|
+
methods_list += more_methods
|
26
|
+
methods_list += more_methods_with_type.keys
|
27
|
+
end
|
26
28
|
|
27
|
-
|
29
|
+
contextual_block&.call(self, method_adder)
|
28
30
|
|
29
|
-
|
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.
|
51
|
-
|
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
|
data/lib/curlybars/node/path.rb
CHANGED
@@ -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
|
data/lib/curlybars/parser.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
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
|
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 =
|
70
|
+
safe_source = source.gsub(/\A#{ActionView::ENCODING_FLAG}/, '')
|
59
71
|
|
60
72
|
source = Curlybars.compile(safe_source, template.identifier)
|
61
73
|
|
data/lib/curlybars/version.rb
CHANGED
data/lib/curlybars/visitor.rb
CHANGED
@@ -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(
|
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(
|
34
|
+
expect(body).to eq(<<~HTML)
|
35
35
|
<html>
|
36
36
|
<head>
|
37
37
|
<title>Dummy app</title>
|