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,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