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,108 @@
1
+ require 'curlybars/error/compile'
2
+
3
+ module Curlybars
4
+ module Node
5
+ BlockHelperElse = Struct.new(:helper, :arguments, :options, :helper_template, :else_template, :helperclose, :position) do
6
+ def compile
7
+ check_open_and_close_elements(helper, helperclose, Curlybars::Error::Compile)
8
+
9
+ compiled_arguments = arguments.map do |argument|
10
+ "arguments.push(rendering.cached_call(#{argument.compile}))"
11
+ end.join("\n")
12
+
13
+ compiled_options = options.map do |option|
14
+ "options.merge!(#{option.compile})"
15
+ end.join("\n")
16
+
17
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
18
+ # outputted by this node.
19
+ <<-RUBY
20
+ options = ::ActiveSupport::HashWithIndifferentAccess.new
21
+ #{compiled_options}
22
+
23
+ arguments = []
24
+ #{compiled_arguments}
25
+
26
+ helper = #{helper.compile}
27
+ helper_position = rendering.position(#{helper.position.line_number},
28
+ #{helper.position.line_offset})
29
+
30
+ options[:fn] = ->(**vars) do
31
+ variables.push(vars.symbolize_keys)
32
+ outer_buffer = buffer
33
+ begin
34
+ buffer = ::Curlybars::SafeBuffer.new
35
+ #{helper_template.compile}
36
+ buffer
37
+ ensure
38
+ buffer = outer_buffer
39
+ variables.pop
40
+ end
41
+ end
42
+
43
+ options[:inverse] = ->(**vars) do
44
+ variables.push(vars.symbolize_keys)
45
+ outer_buffer = buffer
46
+ begin
47
+ buffer = ::Curlybars::SafeBuffer.new
48
+ #{else_template.compile}
49
+ buffer
50
+ ensure
51
+ buffer = outer_buffer
52
+ variables.pop
53
+ end
54
+ end
55
+
56
+ options[:this] = contexts.last
57
+
58
+ result = rendering.call(helper, #{helper.path.inspect}, helper_position,
59
+ arguments, options, &options[:fn])
60
+
61
+ unless rendering.presenter?(result) || rendering.presenter_collection?(result)
62
+ buffer.concat(result.to_s)
63
+ end
64
+ RUBY
65
+ end
66
+
67
+ def validate(branches)
68
+ check_open_and_close_elements(helper, helperclose, Curlybars::Error::Validate)
69
+
70
+ if helper.leaf?(branches)
71
+ if arguments.any? || options.any?
72
+ message = "#{helper.path} doesn't accept any arguments or options"
73
+ Curlybars::Error::Validate.new('invalid_signature', message, helper.position)
74
+ end
75
+ elsif helper.helper?(branches)
76
+ [
77
+ helper_template.validate(branches),
78
+ else_template.validate(branches),
79
+ arguments.map { |argument| argument.validate_as_value(branches) },
80
+ options.map { |option| option.validate(branches) }
81
+ ]
82
+ else
83
+ message = "#{helper.path} must be allowed as helper or leaf"
84
+ Curlybars::Error::Validate.new('invalid_block_helper', message, helper.position)
85
+ end
86
+ end
87
+
88
+ def cache_key
89
+ [
90
+ helper,
91
+ arguments,
92
+ options,
93
+ helper_template,
94
+ else_template,
95
+ helperclose
96
+ ].flatten.map(&:cache_key).push(self.class.name).join("/")
97
+ end
98
+
99
+ private
100
+
101
+ def check_open_and_close_elements(helper, helperclose, error_class)
102
+ return unless helper.path != helperclose.path
103
+ message = "block `#{helper.path}` cannot be closed by `#{helperclose.path}`"
104
+ raise error_class.new('closing_tag_mismatch', message, helperclose.position)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,24 @@
1
+ module Curlybars
2
+ module Node
3
+ Boolean = Struct.new(:boolean) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ ->() { #{boolean} }
9
+ RUBY
10
+ end
11
+
12
+ def validate(branches)
13
+ # Nothing to validate here.
14
+ end
15
+
16
+ def cache_key
17
+ [
18
+ boolean.to_s,
19
+ self.class.name
20
+ ].join("/")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,69 @@
1
+ module Curlybars
2
+ module Node
3
+ EachElse = Struct.new(:path, :each_template, :else_template, :position) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ collection = rendering.cached_call(#{path.compile})
9
+
10
+ if rendering.to_bool(collection)
11
+ position = rendering.position(#{position.line_number}, #{position.line_offset})
12
+ template_cache_key = '#{each_template.cache_key}'
13
+
14
+ collection = rendering.coerce_to_hash!(collection, #{path.path.inspect}, position)
15
+ collection.each.with_index.map do |key_and_presenter, index|
16
+ rendering.check_timeout!
17
+ rendering.optional_presenter_cache(key_and_presenter[1], template_cache_key, buffer) do |buffer|
18
+ begin
19
+ contexts.push(key_and_presenter[1])
20
+ variables.push({
21
+ index: index,
22
+ key: key_and_presenter[0],
23
+ first: index == 0,
24
+ last: index == (collection.length - 1),
25
+ })
26
+ #{each_template.compile}
27
+ ensure
28
+ variables.pop
29
+ contexts.pop
30
+ end
31
+ end
32
+ end
33
+ else
34
+ #{else_template.compile}
35
+ end
36
+ RUBY
37
+ end
38
+
39
+ def validate(branches)
40
+ resolved = path.resolve_and_check!(branches, check_type: :presenter_collection)
41
+ sub_tree = resolved.first
42
+
43
+ each_template_errors = begin
44
+ branches.push(sub_tree)
45
+ each_template.validate(branches)
46
+ ensure
47
+ branches.pop
48
+ end
49
+
50
+ else_template_errors = else_template.validate(branches)
51
+
52
+ [
53
+ each_template_errors,
54
+ else_template_errors
55
+ ]
56
+ rescue Curlybars::Error::Validate => path_error
57
+ path_error
58
+ end
59
+
60
+ def cache_key
61
+ [
62
+ path,
63
+ each_template,
64
+ else_template
65
+ ].map(&:cache_key).push(self.class.name).join("/")
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,33 @@
1
+ module Curlybars
2
+ module Node
3
+ IfElse = Struct.new(:expression, :if_template, :else_template) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ if rendering.to_bool(rendering.cached_call(#{expression.compile}))
9
+ #{if_template.compile}
10
+ else
11
+ #{else_template.compile}
12
+ end
13
+ RUBY
14
+ end
15
+
16
+ def validate(branches)
17
+ [
18
+ expression.validate(branches),
19
+ if_template.validate(branches),
20
+ else_template.validate(branches)
21
+ ]
22
+ end
23
+
24
+ def cache_key
25
+ [
26
+ expression,
27
+ if_template,
28
+ else_template
29
+ ].map(&:cache_key).push(self.class.name).join("/")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ module Curlybars
2
+ module Node
3
+ Item = Struct.new(:item) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ ::Module.new do
9
+ def self.exec(contexts, rendering, variables, buffer)
10
+ rendering.check_timeout!
11
+ #{item.compile}
12
+ end
13
+ end.exec(contexts, rendering, variables, buffer)
14
+ RUBY
15
+ end
16
+
17
+ def validate(branches)
18
+ catch(:skip_item_validation) do
19
+ item.validate(branches)
20
+ end
21
+ end
22
+
23
+ def cache_key
24
+ [
25
+ item.cache_key,
26
+ self.class.name
27
+ ].join("/")
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ module Curlybars
2
+ module Node
3
+ Literal = Struct.new(:literal) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ ->() { #{literal} }
9
+ RUBY
10
+ end
11
+
12
+ def validate(branches)
13
+ # Nothing to validate here.
14
+ end
15
+
16
+ def validate_as_value(branches)
17
+ # It is always a value.
18
+ end
19
+
20
+ def cache_key
21
+ [
22
+ literal.to_s,
23
+ self.class.name
24
+ ].join("/")
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ module Curlybars
2
+ module Node
3
+ Option = Struct.new(:key, :expression) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ { #{key.to_s.inspect} => rendering.cached_call(#{expression.compile}) }
9
+ RUBY
10
+ end
11
+
12
+ def validate(branches)
13
+ expression.validate_as_value(branches)
14
+ end
15
+
16
+ def cache_key
17
+ [
18
+ key,
19
+ expression.cache_key,
20
+ self.class.name
21
+ ].join("/")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module Curlybars
2
+ module Node
3
+ Output = Struct.new(:value) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ buffer.concat(rendering.cached_call(#{value.compile}).to_s)
9
+ RUBY
10
+ end
11
+
12
+ def validate(branches)
13
+ value.validate(branches)
14
+ end
15
+
16
+ def cache_key
17
+ [
18
+ value.cache_key,
19
+ self.class.name
20
+ ].join("/")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module Curlybars
2
+ module Node
3
+ Partial = Struct.new(:path) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ buffer.concat(rendering.cached_call(#{path.compile}).to_s)
9
+ RUBY
10
+ end
11
+
12
+ def validate(branches)
13
+ path.validate(branches, check_type: :partial)
14
+ end
15
+
16
+ def cache_key
17
+ [
18
+ path.cache_key,
19
+ self.class.name
20
+ ].join("/")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,137 @@
1
+ module Curlybars
2
+ module Node
3
+ Path = Struct.new(:path, :position) do
4
+ def compile
5
+ # NOTE: the following is a heredoc string, representing the ruby code fragment
6
+ # outputted by this node.
7
+ <<-RUBY
8
+ rendering.path(
9
+ #{path.inspect},
10
+ rendering.position(#{position.line_number}, #{position.line_offset})
11
+ )
12
+ RUBY
13
+ end
14
+
15
+ def validate_as_value(branches)
16
+ validate(branches, check_type: :leaf)
17
+ end
18
+
19
+ def validate(branches, check_type: :anything)
20
+ resolve_and_check!(branches, check_type: check_type)
21
+ []
22
+ rescue Curlybars::Error::Validate => path_error
23
+ path_error
24
+ end
25
+
26
+ def resolve_and_check!(branches, check_type: :anything)
27
+ value = resolve(branches)
28
+
29
+ check_type_of(branches, check_type)
30
+
31
+ value
32
+ end
33
+
34
+ def presenter?(branches)
35
+ resolve(branches).is_a?(Hash)
36
+ end
37
+
38
+ def presenter_collection?(branches)
39
+ value = resolve(branches)
40
+ value.is_a?(Array) && value.first.is_a?(Hash)
41
+ end
42
+
43
+ def leaf?(branches)
44
+ value = resolve(branches)
45
+ value.nil?
46
+ end
47
+
48
+ def partial?(branches)
49
+ resolve(branches) == :partial
50
+ end
51
+
52
+ def helper?(branches)
53
+ resolve(branches) == :helper
54
+ end
55
+
56
+ def resolve(branches)
57
+ @value ||= begin
58
+ return :helper if global_helpers_dependency_tree.key?(path.to_sym)
59
+
60
+ path_split_by_slashes = path.split('/')
61
+ backward_steps_on_branches = path_split_by_slashes.count - 1
62
+ base_tree_position = branches.length - backward_steps_on_branches
63
+
64
+ throw :skip_item_validation unless base_tree_position > 0
65
+
66
+ base_tree_index = base_tree_position - 1
67
+ base_tree = branches[base_tree_index]
68
+
69
+ dotted_path_side = path_split_by_slashes.last
70
+
71
+ offset_adjustment = 0
72
+ dotted_path_side.split(/\./).map(&:to_sym).inject(base_tree) do |sub_tree, step|
73
+ begin
74
+ if step == :this
75
+ next sub_tree
76
+ elsif step == :length && (sub_tree.is_a?(Array) && sub_tree.first.is_a?(Hash))
77
+ next nil # :length is synthesised leaf
78
+ elsif !(sub_tree.is_a?(Hash) && sub_tree.key?(step))
79
+ message = step.to_s == path ? "'#{path}' does not exist" : "not possible to access `#{step}` in `#{path}`"
80
+ raise Curlybars::Error::Validate.new('unallowed_path', message, position, offset_adjustment, path: path, step: step)
81
+ end
82
+
83
+ sub_tree[step]
84
+ ensure
85
+ offset_adjustment += step.length + 1 # '1' is the length of the dot
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def cache_key
92
+ [
93
+ path,
94
+ self.class.name
95
+ ].join("/")
96
+ end
97
+
98
+ private
99
+
100
+ # TODO: extract me away
101
+ def global_helpers_dependency_tree
102
+ @global_helpers_dependency_tree ||= begin
103
+ classes = Curlybars.configuration.global_helpers_provider_classes
104
+ classes.map(&:dependency_tree).inject({}, :merge)
105
+ end
106
+ end
107
+
108
+ def check_type_of(branches, check_type)
109
+ case check_type
110
+ when :presenter
111
+ return if presenter?(branches)
112
+ message = "`#{path}` must resolve to a presenter"
113
+ raise Curlybars::Error::Validate.new('not_a_presenter', message, position)
114
+ when :presenter_collection
115
+ return if presenter_collection?(branches)
116
+ message = "`#{path}` must resolve to a collection of presenters"
117
+ raise Curlybars::Error::Validate.new('not_a_presenter_collection', message, position)
118
+ when :leaf
119
+ return if leaf?(branches)
120
+ message = "`#{path}` cannot resolve to a component"
121
+ raise Curlybars::Error::Validate.new('not_a_leaf', message, position)
122
+ when :partial
123
+ return if partial?(branches)
124
+ message = "`#{path}` cannot resolve to a partial"
125
+ raise Curlybars::Error::Validate.new('not_a_partial', message, position)
126
+ when :helper
127
+ return if helper?(branches)
128
+ message = "`#{path}` cannot resolve to a helper"
129
+ raise Curlybars::Error::Validate.new('not_a_helper', message, position)
130
+ when :anything
131
+ else
132
+ raise "invalid type `#{check_type}`"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end